diff --git a/bundled-addons/claude-connector/lib/clients.cjs b/bundled-addons/claude-connector/lib/clients.cjs index beede4b4..9b5c5c95 100644 --- a/bundled-addons/claude-connector/lib/clients.cjs +++ b/bundled-addons/claude-connector/lib/clients.cjs @@ -104,12 +104,53 @@ const claudeCode = { // ---- Claude Desktop ------------------------------------------------------ +// Pick the Claude Desktop config path, preferring whichever install is +// actually present on this box. +// • Classic installer / per-user: %APPDATA%\Claude\claude_desktop_config.json +// • Microsoft Store install: the Store virtualises AppData under +// %LOCALAPPDATA%\Packages\Claude_\LocalCache\Roaming\Claude\ +// — the Store app's "AppData\Roaming" is that folder, not the user's. +// If both exist (or neither), the classic path wins; register() creates +// a fresh config under whichever directory is already there. function desktopConfigPath(opts = {}) { const { e, platform, home } = env(opts); - if (platform === "win32") return path.join(e.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"); + if (platform === "win32") { + // A Microsoft Store install of Claude Desktop virtualises AppData under + // its package folder; the Store app never reads the classic %APPDATA%\ + // Claude\ path, so a config dropped there is invisible. Prefer the Store + // path whenever a Claude_ package is present. Fall back to classic + // otherwise. + try { + const packagesDir = path.join(e.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages"); + if (fs.existsSync(packagesDir)) { + const match = fs.readdirSync(packagesDir).find((n) => /^Claude_/i.test(n)); + if (match) return path.join(packagesDir, match, "LocalCache", "Roaming", "Claude", "claude_desktop_config.json"); + } + } catch {} + return path.join(e.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"); + } if (platform === "darwin") return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"); return path.join(e.XDG_CONFIG_HOME || path.join(home, ".config"), "Claude", "claude_desktop_config.json"); } +// A broader "is Claude Desktop installed" signal than just the config +// folder existing — the Store install may not have created the roaming +// folder yet, and the Program Files install leaves per-user folders empty +// until the first launch. Any of these says "yes, it is here". +function desktopInstalled(opts = {}) { + const { e, platform, home } = env(opts); + if (fs.existsSync(path.dirname(desktopConfigPath(opts)))) return true; + if (platform !== "win32") return false; + const packagesDir = path.join(e.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages"); + try { + if (fs.existsSync(packagesDir) && fs.readdirSync(packagesDir).some((n) => /^Claude_/i.test(n))) return true; + } catch {} + for (const p of [ + path.join(e.LOCALAPPDATA || path.join(home, "AppData", "Local"), "AnthropicClaude"), + path.join(e.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Programs", "Claude"), + path.join(e["PROGRAMFILES"] || "C:\\Program Files", "Claude"), + ]) { try { if (fs.existsSync(p)) return true; } catch {} } + return false; +} // Claude Desktop starts stdio servers only: it runs Theseus itself in Node // mode with the relay, which reads the address from Theseus's endpoint file. @@ -143,13 +184,17 @@ const claudeDesktop = { const cfg = readJson(file); const entry = cfg && cfg.mcpServers ? cfg.mcpServers[NAME] : null; return { - installed: fs.existsSync(path.dirname(file)), + installed: desktopInstalled(opts), configured: !!entry, current: !!params.url && sameDesktop(entry, desktopEntry(params)), }; }, async register(params, opts = {}) { const file = desktopConfigPath(opts); + // Create the config directory if needed — the Store install virtualises + // AppData so the Roaming\Claude folder may be absent until first write; + // Claude Desktop will pick up the file we drop next to it. + try { fs.mkdirSync(path.dirname(file), { recursive: true }); } catch {} if (!fs.existsSync(path.dirname(file))) throw new Error("Claude Desktop was not found on this computer (no Claude settings folder)"); editDesktopConfig(file, (cfg) => { if (!cfg.mcpServers || typeof cfg.mcpServers !== "object") cfg.mcpServers = {}; @@ -165,4 +210,4 @@ const claudeDesktop = { }, }; -module.exports = { claudeCode, claudeDesktop, findClaudeCli, claudeCodeConfigPath, desktopConfigPath, httpEntry, desktopEntry }; +module.exports = { claudeCode, claudeDesktop, findClaudeCli, claudeCodeConfigPath, desktopConfigPath, desktopInstalled, httpEntry, desktopEntry }; diff --git a/package.json b/package.json index c99be1ab..5e8a3e08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.125", + "version": "0.3.126", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode",