@akon-labs/nexus-cli 0.1.10 → 0.1.11-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/require-login.d.ts.map +1 -1
- package/dist/cli/require-login.js +2 -2
- package/dist/cli/require-login.js.map +1 -1
- package/dist/cli.js +1919 -1040
- package/dist/core/agent-session.d.ts +3 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +14 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +18 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/extensions/gitnexus/config.d.ts +15 -1
- package/dist/extensions/gitnexus/config.d.ts.map +1 -1
- package/dist/extensions/gitnexus/config.js +86 -2
- package/dist/extensions/gitnexus/config.js.map +1 -1
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +2 -2
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
- package/dist/extensions/gitnexus/observatory.js +2 -2
- package/dist/extensions/gitnexus/observatory.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +3 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/mcp/commands.d.ts +22 -2
- package/dist/extensions/mcp/commands.d.ts.map +1 -1
- package/dist/extensions/mcp/commands.js +201 -9
- package/dist/extensions/mcp/commands.js.map +1 -1
- package/dist/extensions/mcp/config.d.ts +2 -0
- package/dist/extensions/mcp/config.d.ts.map +1 -1
- package/dist/extensions/mcp/config.js +6 -1
- package/dist/extensions/mcp/config.js.map +1 -1
- package/dist/extensions/mcp/index.d.ts.map +1 -1
- package/dist/extensions/mcp/index.js +54 -5
- package/dist/extensions/mcp/index.js.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
- package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
- package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
- package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +2 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -6
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent-render.d.ts +3 -0
- package/dist/extensions/subagent-render.d.ts.map +1 -0
- package/dist/extensions/subagent-render.js +39 -0
- package/dist/extensions/subagent-render.js.map +1 -0
- package/dist/index.js +1951 -1072
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/rpc-entry.js +1919 -1040
- package/dist/utils/hidden-spawn.d.ts +29 -0
- package/dist/utils/hidden-spawn.d.ts.map +1 -0
- package/dist/utils/hidden-spawn.js +101 -0
- package/dist/utils/hidden-spawn.js.map +1 -0
- package/dist/vendor/ai.js +233 -233
- package/native/win32/build.mjs +95 -0
- package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
- package/native/win32/src/hidden-spawn.c +157 -0
- package/npm-shrinkwrap.json +2 -37
- package/package.json +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport {\n\tapplyNexusSubagentEnvAliases,\n\tisDelegatedWorkerProcess,\n\tisExternalDelegationMode,\n\tregisterDelegateTaskTool,\n} from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Nexus Agent must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tapplyNexusSubagentEnvAliases();\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\tisExternalDelegationMode() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport {\n\tapplyNexusSubagentEnvAliases,\n\tisDelegatedWorkerProcess,\n\tisExternalDelegationMode,\n\tregisterDelegateTaskTool,\n} from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { resolveGitNexusConfig, resolveHubUrl, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Nexus Agent must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tapplyNexusSubagentEnvAliases();\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\tisExternalDelegationMode() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = resolveHubUrl();\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observatory.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkClD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAiBxD;AAeD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBhF","sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"observatory.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkClD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAiBxD;AAeD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBhF","sourcesContent":["import { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { spawnWithoutConsole } from \"../../utils/hidden-spawn.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawnWithoutConsole(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { appendFileSync } from "node:fs";
|
|
3
2
|
import { graphRailStore } from "../../core/graph-rail-store.js";
|
|
4
3
|
import { observatoryCachePath } from "../../core/observatory-cache.js";
|
|
4
|
+
import { spawnWithoutConsole } from "../../utils/hidden-spawn.js";
|
|
5
5
|
import { GitNexusClient } from "./client.js";
|
|
6
6
|
function trace(message) {
|
|
7
7
|
if (!process.env.NEXUS_TRACE)
|
|
@@ -100,7 +100,7 @@ export async function fetchObservatory(client, repo) {
|
|
|
100
100
|
export function spawnObservatoryFetch(config, repo) {
|
|
101
101
|
try {
|
|
102
102
|
const script = buildFetchScript();
|
|
103
|
-
const child =
|
|
103
|
+
const child = spawnWithoutConsole(process.execPath, ["-e", script], {
|
|
104
104
|
detached: true,
|
|
105
105
|
stdio: "ignore",
|
|
106
106
|
env: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observatory.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawn(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"observatory.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { spawnWithoutConsole } from \"../../utils/hidden-spawn.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawnWithoutConsole(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AASnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAQ9C,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport subagentRenderExtension from \"./subagent-render.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.\n\t{ name: \"subagent-render\", factory: subagentRenderExtension, hidden: true },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
package/dist/extensions/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import gitnexusExtension from "./gitnexus/index.js";
|
|
2
2
|
import mcpExtension from "./mcp/index.js";
|
|
3
3
|
import subagentExtension from "./subagent/index.js";
|
|
4
|
+
import subagentRenderExtension from "./subagent-render.js";
|
|
4
5
|
import webSearchConcurrencyExtension from "./web-search-concurrency.js";
|
|
5
6
|
// llama.cpp support lives in ./llama and is kept in tree but not registered.
|
|
6
7
|
// Re-add `{ name: "llama.cpp", factory: llamaExtension, hidden: true }` to turn it back on.
|
|
@@ -9,6 +10,8 @@ export const builtInExtensions = [
|
|
|
9
10
|
{ name: "delegated-worker-runtime", factory: subagentExtension, hidden: true },
|
|
10
11
|
// No-op until the user writes an mcp.json; connecting nothing costs nothing.
|
|
11
12
|
{ name: "mcp", factory: mcpExtension },
|
|
13
|
+
// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.
|
|
14
|
+
{ name: "subagent-render", factory: subagentRenderExtension, hidden: true },
|
|
12
15
|
{ name: "web-search-concurrency", factory: webSearchConcurrencyExtension },
|
|
13
16
|
];
|
|
14
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC9E,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,uBAAuB,MAAM,sBAAsB,CAAC;AAC3D,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC9E,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,qGAAqG;IACrG,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC3E,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport subagentRenderExtension from \"./subagent-render.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t// Display-only override for bundled pi-subagents tools; harmless no-op if that package isn't loaded.\n\t{ name: \"subagent-render\", factory: subagentRenderExtension, hidden: true },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "../../core/extensions/types.ts";
|
|
2
2
|
import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
3
3
|
import type { McpExtensionState } from "./state.ts";
|
|
4
|
-
import { type McpAuthResult, type McpConfig } from "./types.ts";
|
|
4
|
+
import { type McpAuthResult, type McpConfig, type ServerEntry } from "./types.ts";
|
|
5
5
|
export declare function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
6
6
|
export declare function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
7
7
|
export declare function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void>;
|
|
8
8
|
export declare function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean>;
|
|
9
9
|
export declare function reconnectServers(state: McpExtensionState, ctx: ExtensionContext, targetServer?: string): Promise<void>;
|
|
10
|
-
export declare function authenticateServer(serverName: string, config: McpConfig, ctx: ExtensionContext, signal?: AbortSignal, runtime?: McpOAuthRuntime): Promise<McpAuthResult>;
|
|
10
|
+
export declare function authenticateServer(serverName: string, config: McpConfig, ctx: ExtensionContext, signal?: AbortSignal, runtime?: McpOAuthRuntime, nexus?: ExtensionAPI): Promise<McpAuthResult>;
|
|
11
11
|
export declare function logoutServer(serverName: string, state: McpExtensionState, ctx: ExtensionContext): Promise<{
|
|
12
12
|
ok: boolean;
|
|
13
13
|
message: string;
|
|
@@ -16,6 +16,25 @@ export declare function manageBearerToken(action: "set" | "remove" | "status", s
|
|
|
16
16
|
ok: boolean;
|
|
17
17
|
message: string;
|
|
18
18
|
}>;
|
|
19
|
+
type ParsedAddServer = {
|
|
20
|
+
ok: true;
|
|
21
|
+
name: string;
|
|
22
|
+
entry: ServerEntry;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive
|
|
29
|
+
* whitespace-split argument handling (no quoting support) rather than
|
|
30
|
+
* inventing a shell-quote parser just for this subcommand.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseAddServerArgs(argv: string[]): ParsedAddServer;
|
|
33
|
+
export declare const ADD_SERVER_USAGE: string;
|
|
34
|
+
/** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */
|
|
35
|
+
export declare function addCustomServer(cwd: string, name: string, entry: ServerEntry): {
|
|
36
|
+
path: string;
|
|
37
|
+
};
|
|
19
38
|
export interface PanelFlowResult {
|
|
20
39
|
configChanged: boolean;
|
|
21
40
|
}
|
|
@@ -24,4 +43,5 @@ export declare function openMcpSetup(state: McpExtensionState, nexus: ExtensionA
|
|
|
24
43
|
}): Promise<PanelFlowResult>;
|
|
25
44
|
export declare function openMcpPanel(state: McpExtensionState, nexus: ExtensionAPI, ctx: ExtensionContext, configOverridePath?: string, onDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>): Promise<PanelFlowResult>;
|
|
26
45
|
export declare function openMcpAuthPanel(state: McpExtensionState, nexus: ExtensionAPI, ctx: ExtensionContext, configOverridePath?: string): Promise<PanelFlowResult>;
|
|
46
|
+
export {};
|
|
27
47
|
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AA0BrF,OAAO,EAAgB,KAAK,eAAe,EAA6B,MAAM,oBAAoB,CAAC;AAWnG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EAGd,MAAM,YAAY,CAAC;AAoBpB,wBAAsB,UAAU,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C/F;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyChG;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA4ErH;AAED,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,EACrB,YAAY,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,kBAAkB,CACvC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,gBAAgB,EACrB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,aAAa,CAAC,CAyExB;AAED,wBAAsB,YAAY,CACjC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACnB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA8C3C;AA8BD,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,EACnC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,GAAG,EAAE,gBAAgB,GACnB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAwC3C;AAED,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,OAAO,CAAC;CACvB;AAuBD,wBAAsB,YAAY,CACjC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,IAAI,GAAE,OAAO,GAAG,OAAiB,EACjC,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5C,OAAO,CAAC,eAAe,CAAC,CAuE1B;AAqED,wBAAsB,YAAY,CACjC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,0BAA0B,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClG,OAAO,CAAC,eAAe,CAAC,CA4E1B;AAED,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,gBAAgB,EACrB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,eAAe,CAAC,CA2D1B","sourcesContent":["import type { OverlayHandle } from \"@earendil-works/pi-tui\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport {\n\tensureCompatibilityImports,\n\tgetMcpDiscoverySummary,\n\tgetMcpStandardConfigSummary,\n\tgetProjectConfigPath,\n\tgetServerProvenance,\n\ttype KnownServerPreset,\n\tpreviewCompatibilityImports,\n\tpreviewSharedServerEntry,\n\tpreviewStarterProjectConfig,\n\twriteDirectToolsConfig,\n\twriteSharedServerEntry,\n\twriteStarterProjectConfig,\n} from \"./config.ts\";\nimport {\n\tclearFailure,\n\tgetFailureAgeSeconds,\n\tgetFailureMessage,\n\tmarkKeepAliveAfterConnect,\n\tnotifyToolMetadataUpdated,\n\trecordFailure,\n\tupdateMetadataCache,\n\tupdateStatusBar,\n} from \"./init.ts\";\nimport { getAuthStorageOptions, inspectAuthForUrl } from \"./mcp-auth.ts\";\nimport { authenticate, type McpOAuthRuntime, removeAuth, supportsOAuth } from \"./mcp-auth-flow.ts\";\nimport { inspectBearerTokenForUrl, removeBearerToken } from \"./mcp-bearer-store.ts\";\nimport { createMcpPanel } from \"./mcp-panel.ts\";\nimport { createMcpSetupPanel } from \"./mcp-setup-panel.ts\";\nimport { loadMetadataCache, reconstructPromptMetadata } from \"./metadata-cache.ts\";\nimport {\n\tloadOnboardingState,\n\tmarkSharedConfigHintShown,\n\tmarkSetupCompleted as persistSetupCompleted,\n} from \"./onboarding-state.ts\";\nimport { isAbortError } from \"./runtime-owner.ts\";\nimport type { McpExtensionState } from \"./state.ts\";\nimport { buildToolMetadata } from \"./tool-metadata.ts\";\nimport {\n\ttype ImportKind,\n\tisServerDisabled,\n\ttype McpAuthResult,\n\ttype McpConfig,\n\ttype McpPanelCallbacks,\n\ttype McpPanelResult,\n} from \"./types.ts\";\nimport { openPath, resolveServerUrl, sanitizeTerminalText } from \"./utils.ts\";\n\nfunction terminalHyperlink(label: string, url: string): string {\n\treturn `\\u001B]8;;${sanitizeTerminalText(url)}\\u001B\\\\${sanitizeTerminalText(label)}\\u001B]8;;\\u001B\\\\`;\n}\n\n/**\n * True when this run mode can display a `ctx.ui.custom()` overlay.\n *\n * `ctx.hasUI` only reports that *some* UI context is bound. In rpc/print mode\n * that context is a headless stub whose `custom()` returns immediately without\n * ever invoking the factory or the `done` callback, so a panel awaited through\n * `new Promise(resolve => ctx.ui.custom(...))` never settles and the command\n * hangs. Overlay panels therefore additionally require `ctx.mode === \"tui\"`.\n */\nfunction canRenderPanel(ctx: ExtensionContext): boolean {\n\treturn ctx.hasUI && ctx.mode === \"tui\";\n}\n\nexport async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst lines: string[] = [\"MCP Server Status:\", \"\"];\n\n\tfor (const name of Object.keys(state.config.mcpServers)) {\n\t\tconst definition = state.config.mcpServers[name];\n\t\tif (isServerDisabled(definition)) {\n\t\t\tlines.push(`⊘ ${name}: disabled (run /mcp enable ${name}, then /reload)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst connection = state.manager.getConnection(name);\n\t\tconst metadata = state.toolMetadata.get(name);\n\t\tconst toolCount = metadata?.length ?? 0;\n\t\tconst failedAgo = getFailureAgeSeconds(state, name);\n\t\tlet status = \"not connected\";\n\t\tlet statusIcon = \"○\";\n\t\tlet failed = false;\n\n\t\tif (connection?.status === \"connected\") {\n\t\t\tstatus = \"connected\";\n\t\t\tstatusIcon = \"✓\";\n\t\t} else if (connection?.status === \"needs-auth\") {\n\t\t\tstatus = \"needs auth\";\n\t\t\tstatusIcon = \"⚠\";\n\t\t} else if (failedAgo !== null) {\n\t\t\tconst reason = sanitizeTerminalText(getFailureMessage(state, name) ?? \"\");\n\t\t\tstatus = reason ? `failed ${failedAgo}s ago — ${reason}` : `failed ${failedAgo}s ago`;\n\t\t\tstatusIcon = \"✗\";\n\t\t\tfailed = true;\n\t\t} else if (metadata !== undefined) {\n\t\t\tstatus = \"cached\";\n\t\t}\n\n\t\tconst toolSuffix = failed ? \"\" : ` (${toolCount} tools${status === \"cached\" ? \", cached\" : \"\"})`;\n\t\tlines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);\n\t}\n\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\tlines.push(\"No MCP servers configured\");\n\t\tlines.push(\"Run /mcp setup to adopt imports or scaffold .nexus/mcp.json\");\n\t}\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\tconst allPrompts = [...(state.promptMetadata?.values() ?? [])].flat();\n\tconst failedPromptServers = [...(state.manager.getAllConnections?.() ?? [])]\n\t\t.filter(([, connection]) => connection.status === \"connected\" && connection.promptDiscoveryFailed)\n\t\t.map(([serverName]) => serverName)\n\t\t.sort();\n\tif (allPrompts.length === 0) {\n\t\tconst failureNote =\n\t\t\tfailedPromptServers.length > 0 ? ` Prompt discovery failed for: ${failedPromptServers.join(\", \")}.` : \"\";\n\t\tctx.ui.notify(\n\t\t\t`No MCP prompts available. Prompts are discovered when servers with the \\`prompts\\` capability connect.${failureNote}`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn;\n\t}\n\tconst lines = [\"MCP Prompts:\", \"\"];\n\tconst grouped = new Map<string, typeof allPrompts>();\n\tfor (const prompt of allPrompts) {\n\t\tconst list = grouped.get(prompt.serverName) ?? [];\n\t\tlist.push(prompt);\n\t\tgrouped.set(prompt.serverName, list);\n\t}\n\tfor (const [serverName, prompts] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n\t\tlines.push(`${serverName}:`);\n\t\tfor (const prompt of prompts.sort((a, b) => a.commandName.localeCompare(b.commandName))) {\n\t\t\tconst args = prompt.arguments\n\t\t\t\t.map((argument) => (argument.required ? `<${argument.name}>` : `[${argument.name}]`))\n\t\t\t\t.join(\" \");\n\t\t\tlines.push(` /${prompt.commandName}${args ? ` ${args}` : \"\"}`);\n\t\t\tif (prompt.description) lines.push(` ${prompt.description}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\tlines.push(`Total: ${allPrompts.length} prompt${allPrompts.length === 1 ? \"\" : \"s\"}`);\n\tif (failedPromptServers.length > 0) {\n\t\tlines.push(\n\t\t\t`Prompt discovery failed for: ${failedPromptServers.join(\", \")}. Cached prompt metadata may be stale.`,\n\t\t);\n\t}\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst allTools = [...state.toolMetadata.entries()]\n\t\t.filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))\n\t\t.flatMap(([, metadata]) => metadata.map((m) => m.name));\n\n\tif (allTools.length === 0) {\n\t\tctx.ui.notify(\"No MCP tools available\", \"info\");\n\t\treturn;\n\t}\n\n\tconst lines = [\"MCP Tools:\", \"\", ...allTools.map((t) => ` ${t}`), \"\", `Total: ${allTools.length} tools`];\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean> {\n\tconst definition = state.config.mcpServers[name];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst signal = state.owner?.signal;\n\tif (!definition) {\n\t\tif (ui) {\n\t\t\tui.notify(`Server \"${name}\" not found in config`, \"error\");\n\t\t}\n\t\treturn false;\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tif (ui) ui.notify(`MCP: ${name} is disabled. Run /mcp enable ${name}, then /reload.`, \"warning\");\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tawait state.manager.close(name);\n\t\tstate.owner?.throwIfInactive();\n\t\tconst connection = signal\n\t\t\t? await state.manager.connect(name, definition, signal)\n\t\t\t: await state.manager.connect(name, definition);\n\t\tstate.owner?.throwIfInactive();\n\t\tif (connection.status === \"needs-auth\") {\n\t\t\tif (ui) {\n\t\t\t\tui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, \"warning\");\n\t\t\t}\n\t\t\tupdateStatusBar(state);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst prefix = state.config.settings?.toolPrefix ?? \"server\";\n\t\tconst { metadata, failedTools } = buildToolMetadata(\n\t\t\tconnection.tools,\n\t\t\tconnection.resources,\n\t\t\tdefinition,\n\t\t\tname,\n\t\t\tprefix,\n\t\t\tstate.config.mcpServers,\n\t\t\tstate.toolMetadata,\n\t\t);\n\t\tstate.toolMetadata.set(name, metadata);\n\t\tif (!connection.promptDiscoveryFailed) {\n\t\t\tstate.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));\n\t\t\tstate.promptMetadataLive?.add(name);\n\t\t}\n\t\tif (connection.instructions) {\n\t\t\tstate.serverInstructions.set(name, connection.instructions);\n\t\t} else {\n\t\t\tstate.serverInstructions.delete(name);\n\t\t}\n\t\tupdateMetadataCache(state, name);\n\t\tnotifyToolMetadataUpdated(state, name, \"command-reconnect\");\n\t\tmarkKeepAliveAfterConnect(state, name);\n\t\tclearFailure(state, name);\n\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tif (failedTools.length > 0) {\n\t\t\t\tui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, \"warning\");\n\t\t\t}\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (isAbortError(error, signal)) throw error;\n\t\trecordFailure(state, name, message);\n\t\tif (ui) {\n\t\t\tui.notify(`MCP: Failed to reconnect to ${name}: ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn false;\n\t}\n}\n\nexport async function reconnectServers(\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n\ttargetServer?: string,\n): Promise<void> {\n\tif (targetServer && !state.config.mcpServers[targetServer]) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(`Server \"${targetServer}\" not found in config`, \"error\");\n\t\t}\n\t\treturn;\n\t}\n\n\tconst names = targetServer ? [targetServer] : Object.keys(state.config.mcpServers);\n\tfor (const name of names) {\n\t\tawait reconnectServer(state, ctx, name);\n\t}\n\n\tupdateStatusBar(state);\n}\n\nexport async function authenticateServer(\n\tserverName: string,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tsignal?: AbortSignal,\n\truntime?: McpOAuthRuntime,\n): Promise<McpAuthResult> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst cwd = ctx.cwd;\n\tsignal ??= ctx.signal;\n\tif (!ui) return { ok: false, message: \"OAuth authentication requires an interactive session.\" };\n\n\tconst definition = config.mcpServers[serverName];\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tconst message = `Server \"${serverName}\" is disabled. Run /mcp enable ${serverName}, then /reload.`;\n\t\tui.notify(message, \"warning\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (!supportsOAuth(definition)) {\n\t\tconst message = `Server \"${serverName}\" does not use OAuth authentication. Set \"auth\": \"oauth\" or omit auth for auto-detection.`;\n\t\tui.notify(\n\t\t\t`Server \"${serverName}\" does not use OAuth authentication.\\n` +\n\t\t\t\t`Set \"auth\": \"oauth\" or omit auth for auto-detection.`,\n\t\t\t\"error\",\n\t\t);\n\t\treturn { ok: false, message };\n\t}\n\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) {\n\t\t\tconst message = `Server \"${serverName}\" has no URL configured (OAuth requires HTTP transport)`;\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn { ok: false, message };\n\t\t}\n\n\t\tui.setStatus(\"mcp-auth\", `Authenticating ${serverName}...`);\n\t\tconst authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);\n\t\tconst status = await authenticate(serverName, serverUrl, definition, {\n\t\t\t...(authStorageOptions.baseDir ? { authStorageOptions } : {}),\n\t\t\tonAuthorizationUrl: () => {},\n\t\t\tonAuthorizationInput: async (authorizationUrl, inputSignal) => {\n\t\t\t\tif (inputSignal.aborted) return undefined;\n\t\t\t\treturn ui.input(\n\t\t\t\t\t`Complete ${serverName} OAuth\\n\\n` +\n\t\t\t\t\t\t`${terminalHyperlink(\"Open authorization page\", authorizationUrl)}\\n${authorizationUrl}\\n\\n` +\n\t\t\t\t\t\t\"Approve access, then paste the full localhost callback URL below.\",\n\t\t\t\t\tundefined,\n\t\t\t\t\t{ signal: inputSignal },\n\t\t\t\t);\n\t\t\t},\n\t\t\t...(signal ? { signal } : {}),\n\t\t\t...(runtime ? { runtime } : {}),\n\t\t});\n\t\tif (signal?.aborted) signal.throwIfAborted();\n\n\t\tif (status === \"authenticated\") {\n\t\t\tconst message = `OAuth authentication successful for \"${serverName}\".`;\n\t\t\tui.notify(message, \"info\");\n\t\t\treturn { ok: true, message };\n\t\t}\n\n\t\tconst message = `OAuth authentication failed for \"${serverName}\".`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t} catch (error) {\n\t\tif (signal?.aborted) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tui.notify(`Failed to authenticate \"${serverName}\": ${message}`, \"error\");\n\t\treturn { ok: false, message };\n\t} finally {\n\t\tif (!signal?.aborted) ui.setStatus(\"mcp-auth\", undefined);\n\t}\n}\n\nexport async function logoutServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst definition = state.config.mcpServers[serverName];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst signal = state.owner?.signal;\n\ttry {\n\t\tawait removeAuth(serverName, {\n\t\t\tauthStorageOptions: state.authStorageOptions,\n\t\t\tsignal,\n\t\t\truntime: state.oauthRuntime,\n\t\t});\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(`Failed to clear OAuth credentials for \"${serverName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\ttry {\n\t\tawait state.manager.close(serverName);\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`OAuth credentials were cleared for \"${serverName}\", but its connection could not be closed: ${sanitizeTerminalText(message)}`,\n\t\t\t\t\"error\",\n\t\t\t);\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\tupdateStatusBar(state);\n\n\tconst message = `OAuth credentials cleared for \"${serverName}\". Run /mcp-auth ${serverName} to authenticate again.`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nfunction validateBearerTokenStoreServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n): { ok: true; serverUrl: string } | { ok: false; message: string; type: \"error\" | \"warning\" } {\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst definition = state.config.mcpServers[serverName];\n\tif (!definition) return { ok: false, message: `Server \"${safeName}\" not found in config`, type: \"error\" };\n\tif (isServerDisabled(definition))\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Server \"${safeName}\" is disabled. Run /mcp enable ${safeName}, then /reload.`,\n\t\t\ttype: \"warning\",\n\t\t};\n\tif (definition.auth !== \"bearer\" || definition.bearerTokenStore !== true) {\n\t\treturn { ok: false, message: `Server \"${safeName}\" is not configured for bearerTokenStore.`, type: \"error\" };\n\t}\n\t// resolveServerUrl embeds the interpolated URL in its exceptions; redact it\n\t// because the URL can carry userinfo or interpolated secrets.\n\tlet serverUrl: string | undefined;\n\ttry {\n\t\tserverUrl = resolveServerUrl(definition);\n\t} catch {\n\t\treturn { ok: false, message: `Server \"${safeName}\" has an invalid or unresolvable URL.`, type: \"error\" };\n\t}\n\tif (!serverUrl) return { ok: false, message: `Server \"${safeName}\" has no URL configured.`, type: \"error\" };\n\treturn { ok: true, serverUrl };\n}\n\nexport async function manageBearerToken(\n\taction: \"set\" | \"remove\" | \"status\",\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst validation = validateBearerTokenStoreServer(serverName, state);\n\tif (!validation.ok) {\n\t\tif (ui) ui.notify(validation.message, validation.type);\n\t\treturn { ok: false, message: validation.message };\n\t}\n\n\tif (action === \"set\") {\n\t\tconst message = `Nexus will not collect bearer tokens through an unmasked prompt. Configure bearerTokenEnv for \"${safeName}\" in MCP config, then reload.`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (action === \"status\") {\n\t\tconst status = inspectBearerTokenForUrl(serverName, validation.serverUrl);\n\t\tconst message =\n\t\t\tstatus.status === \"present\"\n\t\t\t\t? `Bearer token is stored for \"${safeName}\".`\n\t\t\t\t: status.status === \"url-mismatch\"\n\t\t\t\t\t? `Bearer token is stored for \"${safeName}\", but its URL does not match the current server URL.`\n\t\t\t\t\t: status.status === \"unavailable\"\n\t\t\t\t\t\t? status.message\n\t\t\t\t\t\t: `No bearer token is stored for \"${safeName}\".`;\n\t\tif (ui) ui.notify(message, status.status === \"unavailable\" ? \"error\" : \"info\");\n\t\treturn { ok: status.status !== \"unavailable\", message };\n\t}\n\n\ttry {\n\t\tremoveBearerToken(serverName);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) ui.notify(`Failed to remove bearer token for \"${safeName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst message = `Bearer token removed for \"${safeName}\".`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nexport interface PanelFlowResult {\n\tconfigChanged: boolean;\n}\n\nfunction buildSharedConfigNoticeLines(\n\tconfigOverridePath: string | undefined,\n\tcwd: string,\n): { lines: string[]; fingerprint: string | null } {\n\tconst discovery = getMcpStandardConfigSummary(configOverridePath, cwd);\n\tconst onboardingState = loadOnboardingState();\n\tif (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {\n\t\treturn { lines: [], fingerprint: null };\n\t}\n\n\tconst sharedSources = discovery.sources.filter((source) => source.kind === \"shared\" && source.serverCount > 0);\n\tconst sourceList = sharedSources.map((source) => source.path).join(\", \");\n\treturn {\n\t\tlines: [\n\t\t\t`Using MCP configuration from ${sourceList}.`,\n\t\t\t\"Nexus writes compatibility imports and MCP-specific overrides only to Nexus configuration files.\",\n\t\t],\n\t\tfingerprint: discovery.fingerprint,\n\t};\n}\n\nexport async function openMcpSetup(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tmode: \"empty\" | \"setup\" = \"setup\",\n\toptions: { includeHostConfigs?: boolean } = {},\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP setup panel is only available in the terminal UI (current mode: ${ctx.mode}). Edit .nexus/mcp.json directly, or run /mcp status to review servers.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"MCP setup is unavailable when config is supplied by createMcpAdapter().\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);\n\tconst onboardingState = loadOnboardingState();\n\tlet configChanged = false;\n\n\tconst callbacks = {\n\t\tpreviewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),\n\t\tpreviewStarterProject: () => previewStarterProjectConfig(ctx.cwd),\n\t\tpreviewRepoPrompt: () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;\n\t\t\treturn previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t},\n\t\tpreviewKnownServer: (preset: KnownServerPreset) =>\n\t\t\tpreviewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),\n\t\tadoptImports: async (imports: ImportKind[]) => {\n\t\t\tconst result = ensureCompatibilityImports(imports, configOverridePath);\n\t\t\tif (result.added.length > 0) configChanged = true;\n\t\t\treturn result;\n\t\t},\n\t\tscaffoldProjectConfig: async () => {\n\t\t\tconst path = writeStarterProjectConfig(ctx.cwd);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path };\n\t\t},\n\t\taddRepoPrompt: async () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {\n\t\t\t\tthrow new Error(\"RepoPrompt is not available to add from this setup screen.\");\n\t\t\t}\n\t\t\tconst path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: repoPrompt.serverName };\n\t\t},\n\t\taddKnownServer: async (preset: KnownServerPreset) => {\n\t\t\tconst path = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: preset.name };\n\t\t},\n\t\topenPath: async (targetPath: string) => {\n\t\t\tawait openPath(nexus, targetPath);\n\t\t},\n\t\tmarkSetupCompleted: () => {\n\t\t\tpersistSetupCompleted(discovery.fingerprint);\n\t\t},\n\t};\n\n\treturn new Promise<PanelFlowResult>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {\n\t\t\t\t\tdone(undefined);\n\t\t\t\t\tresolve({ configChanged });\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ overlay: true, overlayOptions: { anchor: \"center\", width: 92 } },\n\t\t);\n\t});\n}\n\nfunction buildMcpPanelCallbacks(\n\tstate: McpExtensionState,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tgetOverlayHandle?: () => OverlayHandle | undefined,\n): McpPanelCallbacks {\n\t// Panel-only diagnostics keep status inspection from mutating connection\n\t// failure state while allowing the existing panel failure UI to show why the\n\t// credential store could not be inspected.\n\tconst authStatusFailures = new Map<string, string>();\n\n\treturn {\n\t\treconnect: (serverName: string) => reconnectServer(state, ctx, serverName),\n\t\tcanAuthenticate: (serverName: string) => {\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\treturn definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;\n\t\t},\n\t\tauthenticate: async (serverName: string) => {\n\t\t\tconst overlay = getOverlayHandle?.();\n\t\t\toverlay?.setHidden(true);\n\t\t\ttry {\n\t\t\t\treturn await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime);\n\t\t\t} finally {\n\t\t\t\toverlay?.setHidden(false);\n\t\t\t\toverlay?.focus();\n\t\t\t}\n\t\t},\n\t\tgetConnectionStatus: (serverName: string) => {\n\t\t\tauthStatusFailures.delete(serverName);\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\tif (isServerDisabled(definition)) return \"disabled\";\n\t\t\tconst connection = state.manager.getConnection(serverName);\n\t\t\tlet serverUrl: string | undefined;\n\t\t\ttry {\n\t\t\t\tserverUrl = definition ? resolveServerUrl(definition) : undefined;\n\t\t\t} catch {\n\t\t\t\treturn \"failed\";\n\t\t\t}\n\t\t\tif (\n\t\t\t\tdefinition?.auth === \"oauth\" &&\n\t\t\t\tserverUrl &&\n\t\t\t\tdefinition.oauth !== false &&\n\t\t\t\tdefinition.oauth?.grantType !== \"client_credentials\"\n\t\t\t) {\n\t\t\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\t\t\tif (authStatus.status === \"unavailable\") {\n\t\t\t\t\tauthStatusFailures.set(serverName, authStatus.message);\n\t\t\t\t\treturn \"failed\";\n\t\t\t\t}\n\t\t\t\tif (authStatus.status === \"absent\" || !authStatus.entry.tokens) {\n\t\t\t\t\treturn \"needs-auth\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (connection?.status === \"needs-auth\") return \"needs-auth\";\n\t\t\tif (connection?.status === \"connected\") return \"connected\";\n\t\t\tif (getFailureAgeSeconds(state, serverName) !== null) return \"failed\";\n\t\t\treturn \"idle\";\n\t\t},\n\t\tgetFailureMessage: (serverName: string) =>\n\t\t\tauthStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),\n\t\trefreshCacheAfterReconnect: (serverName: string) => {\n\t\t\tconst freshCache = loadMetadataCache();\n\t\t\treturn freshCache?.servers?.[serverName] ?? null;\n\t\t},\n\t};\n}\n\nexport async function openMcpPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tonDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>,\n): Promise<PanelFlowResult> {\n\tif (state.programmaticConfig) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(\n\t\t\t\t\"MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.\",\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tawait showStatus(state, ctx);\n\t\t}\n\t\treturn { configChanged: false };\n\t}\n\tif (!canRenderPanel(ctx)) {\n\t\t// No overlay here, but the same information is available as text.\n\t\tawait showStatus(state, ctx);\n\t\treturn { configChanged: false };\n\t}\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\treturn openMcpSetup(state, nexus, ctx, configOverridePath, \"empty\", { includeHostConfigs: false });\n\t}\n\n\tconst config = state.config;\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tconst { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);\n\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);\n\n\tlet configChanged = false;\n\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t(result: McpPanelResult) => {\n\t\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\t\tif (!result.cancelled && result.changes.size > 0) {\n\t\t\t\t\t\t\t\twriteDirectToolsConfig(result.changes, provenanceMap);\n\t\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\t\tawait onDirectToolsConfigChanged?.(result.changes);\n\t\t\t\t\t\t\t\tctx.ui.notify(\"Direct tools updated for this session.\", \"info\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t})().catch((error) => {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, \"error\");\n\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{ noticeLines, keybindings },\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\tif (noticeLines.length > 0 && fingerprint) {\n\t\tmarkSharedConfigHintShown(fingerprint);\n\t}\n\n\treturn { configChanged };\n}\n\nexport async function openMcpAuthPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP auth panel is only available in the terminal UI (current mode: ${ctx.mode}). Use /mcp-auth <server> to authenticate a specific server.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst config = state.config;\n\tconst oauthServers = Object.entries(config.mcpServers).filter(\n\t\t([, definition]) => !isServerDisabled(definition) && supportsOAuth(definition),\n\t);\n\tif (oauthServers.length === 0) {\n\t\tctx.ui.notify(\"No OAuth-capable MCP servers are configured.\", \"warning\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tauthOnly: true,\n\t\t\t\t\t\tkeybindings,\n\t\t\t\t\t\tnoticeLines: [\"Select an OAuth MCP server and press Enter or ctrl+a to authenticate.\"],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\treturn { configChanged: false };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/extensions/mcp/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OverlayHandle } from \"@earendil-works/pi-tui\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../core/extensions/types.ts\";\nimport {\n\tensureCompatibilityImports,\n\tgetMcpDiscoverySummary,\n\tgetMcpStandardConfigSummary,\n\tgetProjectConfigPath,\n\tgetServerProvenance,\n\ttype KnownServerPreset,\n\tpreviewCompatibilityImports,\n\tpreviewSharedServerEntry,\n\tpreviewStarterProjectConfig,\n\twriteDirectToolsConfig,\n\twriteProjectServerDisabledOverride,\n\twriteSharedServerEntry,\n\twriteStarterProjectConfig,\n} from \"./config.ts\";\nimport {\n\tclearFailure,\n\tgetFailureAgeSeconds,\n\tgetFailureMessage,\n\tmarkKeepAliveAfterConnect,\n\tnotifyToolMetadataUpdated,\n\trecordFailure,\n\tupdateMetadataCache,\n\tupdateStatusBar,\n} from \"./init.ts\";\nimport { getAuthStorageOptions, inspectAuthForUrl } from \"./mcp-auth.ts\";\nimport { authenticate, type McpOAuthRuntime, removeAuth, supportsOAuth } from \"./mcp-auth-flow.ts\";\nimport { inspectBearerTokenForUrl, removeBearerToken } from \"./mcp-bearer-store.ts\";\nimport { createMcpPanel } from \"./mcp-panel.ts\";\nimport { createMcpSetupPanel } from \"./mcp-setup-panel.ts\";\nimport { loadMetadataCache, reconstructPromptMetadata } from \"./metadata-cache.ts\";\nimport {\n\tloadOnboardingState,\n\tmarkSharedConfigHintShown,\n\tmarkSetupCompleted as persistSetupCompleted,\n} from \"./onboarding-state.ts\";\nimport { isAbortError } from \"./runtime-owner.ts\";\nimport type { McpExtensionState } from \"./state.ts\";\nimport { buildToolMetadata } from \"./tool-metadata.ts\";\nimport {\n\ttype ImportKind,\n\tisServerDisabled,\n\ttype McpAuthResult,\n\ttype McpConfig,\n\ttype McpPanelCallbacks,\n\ttype McpPanelResult,\n\ttype ServerEntry,\n} from \"./types.ts\";\nimport { openPath, resolveServerUrl, sanitizeTerminalText } from \"./utils.ts\";\n\nfunction terminalHyperlink(label: string, url: string): string {\n\treturn `\\u001B]8;;${sanitizeTerminalText(url)}\\u001B\\\\${sanitizeTerminalText(label)}\\u001B]8;;\\u001B\\\\`;\n}\n\n/**\n * True when this run mode can display a `ctx.ui.custom()` overlay.\n *\n * `ctx.hasUI` only reports that *some* UI context is bound. In rpc/print mode\n * that context is a headless stub whose `custom()` returns immediately without\n * ever invoking the factory or the `done` callback, so a panel awaited through\n * `new Promise(resolve => ctx.ui.custom(...))` never settles and the command\n * hangs. Overlay panels therefore additionally require `ctx.mode === \"tui\"`.\n */\nfunction canRenderPanel(ctx: ExtensionContext): boolean {\n\treturn ctx.hasUI && ctx.mode === \"tui\";\n}\n\nexport async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst lines: string[] = [\"MCP Server Status:\", \"\"];\n\n\tfor (const name of Object.keys(state.config.mcpServers)) {\n\t\tconst definition = state.config.mcpServers[name];\n\t\tif (isServerDisabled(definition)) {\n\t\t\tlines.push(`⊘ ${name}: disabled (run /mcp enable ${name}, then /reload)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst connection = state.manager.getConnection(name);\n\t\tconst metadata = state.toolMetadata.get(name);\n\t\tconst toolCount = metadata?.length ?? 0;\n\t\tconst failedAgo = getFailureAgeSeconds(state, name);\n\t\tlet status = \"not connected\";\n\t\tlet statusIcon = \"○\";\n\t\tlet failed = false;\n\n\t\tif (connection?.status === \"connected\") {\n\t\t\tstatus = \"connected\";\n\t\t\tstatusIcon = \"✓\";\n\t\t} else if (connection?.status === \"needs-auth\") {\n\t\t\tstatus = \"needs auth\";\n\t\t\tstatusIcon = \"⚠\";\n\t\t} else if (failedAgo !== null) {\n\t\t\tconst reason = sanitizeTerminalText(getFailureMessage(state, name) ?? \"\");\n\t\t\tstatus = reason ? `failed ${failedAgo}s ago — ${reason}` : `failed ${failedAgo}s ago`;\n\t\t\tstatusIcon = \"✗\";\n\t\t\tfailed = true;\n\t\t} else if (metadata !== undefined) {\n\t\t\tstatus = \"cached\";\n\t\t}\n\n\t\tconst toolSuffix = failed ? \"\" : ` (${toolCount} tools${status === \"cached\" ? \", cached\" : \"\"})`;\n\t\tlines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);\n\t}\n\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\tlines.push(\"No MCP servers configured\");\n\t\tlines.push(\"Run /mcp setup to adopt imports or scaffold .nexus/mcp.json\");\n\t}\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\tconst allPrompts = [...(state.promptMetadata?.values() ?? [])].flat();\n\tconst failedPromptServers = [...(state.manager.getAllConnections?.() ?? [])]\n\t\t.filter(([, connection]) => connection.status === \"connected\" && connection.promptDiscoveryFailed)\n\t\t.map(([serverName]) => serverName)\n\t\t.sort();\n\tif (allPrompts.length === 0) {\n\t\tconst failureNote =\n\t\t\tfailedPromptServers.length > 0 ? ` Prompt discovery failed for: ${failedPromptServers.join(\", \")}.` : \"\";\n\t\tctx.ui.notify(\n\t\t\t`No MCP prompts available. Prompts are discovered when servers with the \\`prompts\\` capability connect.${failureNote}`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn;\n\t}\n\tconst lines = [\"MCP Prompts:\", \"\"];\n\tconst grouped = new Map<string, typeof allPrompts>();\n\tfor (const prompt of allPrompts) {\n\t\tconst list = grouped.get(prompt.serverName) ?? [];\n\t\tlist.push(prompt);\n\t\tgrouped.set(prompt.serverName, list);\n\t}\n\tfor (const [serverName, prompts] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n\t\tlines.push(`${serverName}:`);\n\t\tfor (const prompt of prompts.sort((a, b) => a.commandName.localeCompare(b.commandName))) {\n\t\t\tconst args = prompt.arguments\n\t\t\t\t.map((argument) => (argument.required ? `<${argument.name}>` : `[${argument.name}]`))\n\t\t\t\t.join(\" \");\n\t\t\tlines.push(` /${prompt.commandName}${args ? ` ${args}` : \"\"}`);\n\t\t\tif (prompt.description) lines.push(` ${prompt.description}`);\n\t\t}\n\t\tlines.push(\"\");\n\t}\n\tlines.push(`Total: ${allPrompts.length} prompt${allPrompts.length === 1 ? \"\" : \"s\"}`);\n\tif (failedPromptServers.length > 0) {\n\t\tlines.push(\n\t\t\t`Prompt discovery failed for: ${failedPromptServers.join(\", \")}. Cached prompt metadata may be stale.`,\n\t\t);\n\t}\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {\n\tif (!ctx.hasUI) return;\n\n\tconst allTools = [...state.toolMetadata.entries()]\n\t\t.filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))\n\t\t.flatMap(([, metadata]) => metadata.map((m) => m.name));\n\n\tif (allTools.length === 0) {\n\t\tctx.ui.notify(\"No MCP tools available\", \"info\");\n\t\treturn;\n\t}\n\n\tconst lines = [\"MCP Tools:\", \"\", ...allTools.map((t) => ` ${t}`), \"\", `Total: ${allTools.length} tools`];\n\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\nexport async function reconnectServer(state: McpExtensionState, ctx: ExtensionContext, name: string): Promise<boolean> {\n\tconst definition = state.config.mcpServers[name];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst signal = state.owner?.signal;\n\tif (!definition) {\n\t\tif (ui) {\n\t\t\tui.notify(`Server \"${name}\" not found in config`, \"error\");\n\t\t}\n\t\treturn false;\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tif (ui) ui.notify(`MCP: ${name} is disabled. Run /mcp enable ${name}, then /reload.`, \"warning\");\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tawait state.manager.close(name);\n\t\tstate.owner?.throwIfInactive();\n\t\tconst connection = signal\n\t\t\t? await state.manager.connect(name, definition, signal)\n\t\t\t: await state.manager.connect(name, definition);\n\t\tstate.owner?.throwIfInactive();\n\t\tif (connection.status === \"needs-auth\") {\n\t\t\tif (ui) {\n\t\t\t\tui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, \"warning\");\n\t\t\t}\n\t\t\tupdateStatusBar(state);\n\t\t\treturn false;\n\t\t}\n\n\t\tconst prefix = state.config.settings?.toolPrefix ?? \"server\";\n\t\tconst { metadata, failedTools } = buildToolMetadata(\n\t\t\tconnection.tools,\n\t\t\tconnection.resources,\n\t\t\tdefinition,\n\t\t\tname,\n\t\t\tprefix,\n\t\t\tstate.config.mcpServers,\n\t\t\tstate.toolMetadata,\n\t\t);\n\t\tstate.toolMetadata.set(name, metadata);\n\t\tif (!connection.promptDiscoveryFailed) {\n\t\t\tstate.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));\n\t\t\tstate.promptMetadataLive?.add(name);\n\t\t}\n\t\tif (connection.instructions) {\n\t\t\tstate.serverInstructions.set(name, connection.instructions);\n\t\t} else {\n\t\t\tstate.serverInstructions.delete(name);\n\t\t}\n\t\tupdateMetadataCache(state, name);\n\t\tnotifyToolMetadataUpdated(state, name, \"command-reconnect\");\n\t\tmarkKeepAliveAfterConnect(state, name);\n\t\tclearFailure(state, name);\n\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tif (failedTools.length > 0) {\n\t\t\t\tui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, \"warning\");\n\t\t\t}\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (isAbortError(error, signal)) throw error;\n\t\trecordFailure(state, name, message);\n\t\tif (ui) {\n\t\t\tui.notify(`MCP: Failed to reconnect to ${name}: ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\tupdateStatusBar(state);\n\t\treturn false;\n\t}\n}\n\nexport async function reconnectServers(\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n\ttargetServer?: string,\n): Promise<void> {\n\tif (targetServer && !state.config.mcpServers[targetServer]) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(`Server \"${targetServer}\" not found in config`, \"error\");\n\t\t}\n\t\treturn;\n\t}\n\n\tconst names = targetServer ? [targetServer] : Object.keys(state.config.mcpServers);\n\tfor (const name of names) {\n\t\tawait reconnectServer(state, ctx, name);\n\t}\n\n\tupdateStatusBar(state);\n}\n\n/** Whether stored OAuth tokens actually exist for a server — independent of whether it's currently connected. */\nfunction isSignedInForOAuth(serverName: string, definition: ServerEntry, state: McpExtensionState): boolean {\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) return false;\n\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\treturn authStatus.status !== \"absent\" && authStatus.status !== \"unavailable\" && !!authStatus.entry.tokens;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function authenticateServer(\n\tserverName: string,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tsignal?: AbortSignal,\n\truntime?: McpOAuthRuntime,\n\tnexus?: ExtensionAPI,\n): Promise<McpAuthResult> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst cwd = ctx.cwd;\n\tsignal ??= ctx.signal;\n\tif (!ui) return { ok: false, message: \"OAuth authentication requires an interactive session.\" };\n\n\tconst definition = config.mcpServers[serverName];\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\tif (isServerDisabled(definition)) {\n\t\tconst message = `Server \"${serverName}\" is disabled. Run /mcp enable ${serverName}, then /reload.`;\n\t\tui.notify(message, \"warning\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (!supportsOAuth(definition)) {\n\t\tconst message = `Server \"${serverName}\" does not use OAuth authentication. Set \"auth\": \"oauth\" or omit auth for auto-detection.`;\n\t\tui.notify(\n\t\t\t`Server \"${serverName}\" does not use OAuth authentication.\\n` +\n\t\t\t\t`Set \"auth\": \"oauth\" or omit auth for auto-detection.`,\n\t\t\t\"error\",\n\t\t);\n\t\treturn { ok: false, message };\n\t}\n\n\ttry {\n\t\tconst serverUrl = resolveServerUrl(definition);\n\t\tif (!serverUrl) {\n\t\t\tconst message = `Server \"${serverName}\" has no URL configured (OAuth requires HTTP transport)`;\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn { ok: false, message };\n\t\t}\n\n\t\tui.setStatus(\"mcp-auth\", `Authenticating ${serverName}...`);\n\t\tconst authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);\n\t\tconst status = await authenticate(serverName, serverUrl, definition, {\n\t\t\t...(authStorageOptions.baseDir ? { authStorageOptions } : {}),\n\t\t\tonAuthorizationUrl: async (authorizationUrl) => {\n\t\t\t\t// Best-effort: launch the user's browser straight to the authorization page\n\t\t\t\t// instead of relying on them to click (or copy) the link printed below.\n\t\t\t\tif (!nexus) return;\n\t\t\t\ttry {\n\t\t\t\t\tawait openPath(nexus, authorizationUrl);\n\t\t\t\t} catch {\n\t\t\t\t\t// The link is still shown in the prompt text below; nothing else to do here.\n\t\t\t\t}\n\t\t\t},\n\t\t\tonAuthorizationInput: async (authorizationUrl, inputSignal) => {\n\t\t\t\tif (inputSignal.aborted) return undefined;\n\t\t\t\treturn ui.input(\n\t\t\t\t\t`Complete ${serverName} OAuth\\n\\n` +\n\t\t\t\t\t\t`${terminalHyperlink(\"Open authorization page\", authorizationUrl)}\\n${authorizationUrl}\\n\\n` +\n\t\t\t\t\t\t\"Approve access, then paste the full localhost callback URL below.\",\n\t\t\t\t\tundefined,\n\t\t\t\t\t{ signal: inputSignal },\n\t\t\t\t);\n\t\t\t},\n\t\t\t...(signal ? { signal } : {}),\n\t\t\t...(runtime ? { runtime } : {}),\n\t\t});\n\t\tif (signal?.aborted) signal.throwIfAborted();\n\n\t\tif (status === \"authenticated\") {\n\t\t\tconst message = `OAuth authentication successful for \"${serverName}\".`;\n\t\t\tui.notify(message, \"info\");\n\t\t\treturn { ok: true, message };\n\t\t}\n\n\t\tconst message = `OAuth authentication failed for \"${serverName}\".`;\n\t\tui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t} catch (error) {\n\t\tif (signal?.aborted) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tui.notify(`Failed to authenticate \"${serverName}\": ${message}`, \"error\");\n\t\treturn { ok: false, message };\n\t} finally {\n\t\tif (!signal?.aborted) ui.setStatus(\"mcp-auth\", undefined);\n\t}\n}\n\nexport async function logoutServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst definition = state.config.mcpServers[serverName];\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tif (!definition) {\n\t\tconst message = `Server \"${serverName}\" not found in config`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst signal = state.owner?.signal;\n\ttry {\n\t\tawait removeAuth(serverName, {\n\t\t\tauthStorageOptions: state.authStorageOptions,\n\t\t\tsignal,\n\t\t\truntime: state.oauthRuntime,\n\t\t});\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(`Failed to clear OAuth credentials for \"${serverName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\ttry {\n\t\tawait state.manager.close(serverName);\n\t} catch (error) {\n\t\tif (isAbortError(error, signal)) throw error;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) {\n\t\t\tui.notify(\n\t\t\t\t`OAuth credentials were cleared for \"${serverName}\", but its connection could not be closed: ${sanitizeTerminalText(message)}`,\n\t\t\t\t\"error\",\n\t\t\t);\n\t\t}\n\t\treturn { ok: false, message };\n\t}\n\n\tstate.owner?.throwIfInactive();\n\tupdateStatusBar(state);\n\n\tconst message = `OAuth credentials cleared for \"${serverName}\". Run /mcp-auth ${serverName} to authenticate again.`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\nfunction validateBearerTokenStoreServer(\n\tserverName: string,\n\tstate: McpExtensionState,\n): { ok: true; serverUrl: string } | { ok: false; message: string; type: \"error\" | \"warning\" } {\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst definition = state.config.mcpServers[serverName];\n\tif (!definition) return { ok: false, message: `Server \"${safeName}\" not found in config`, type: \"error\" };\n\tif (isServerDisabled(definition))\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Server \"${safeName}\" is disabled. Run /mcp enable ${safeName}, then /reload.`,\n\t\t\ttype: \"warning\",\n\t\t};\n\tif (definition.auth !== \"bearer\" || definition.bearerTokenStore !== true) {\n\t\treturn { ok: false, message: `Server \"${safeName}\" is not configured for bearerTokenStore.`, type: \"error\" };\n\t}\n\t// resolveServerUrl embeds the interpolated URL in its exceptions; redact it\n\t// because the URL can carry userinfo or interpolated secrets.\n\tlet serverUrl: string | undefined;\n\ttry {\n\t\tserverUrl = resolveServerUrl(definition);\n\t} catch {\n\t\treturn { ok: false, message: `Server \"${safeName}\" has an invalid or unresolvable URL.`, type: \"error\" };\n\t}\n\tif (!serverUrl) return { ok: false, message: `Server \"${safeName}\" has no URL configured.`, type: \"error\" };\n\treturn { ok: true, serverUrl };\n}\n\nexport async function manageBearerToken(\n\taction: \"set\" | \"remove\" | \"status\",\n\tserverName: string,\n\tstate: McpExtensionState,\n\tctx: ExtensionContext,\n): Promise<{ ok: boolean; message: string }> {\n\tconst ui = ctx.hasUI ? ctx.ui : undefined;\n\tconst safeName = sanitizeTerminalText(serverName);\n\tconst validation = validateBearerTokenStoreServer(serverName, state);\n\tif (!validation.ok) {\n\t\tif (ui) ui.notify(validation.message, validation.type);\n\t\treturn { ok: false, message: validation.message };\n\t}\n\n\tif (action === \"set\") {\n\t\tconst message = `Nexus will not collect bearer tokens through an unmasked prompt. Configure bearerTokenEnv for \"${safeName}\" in MCP config, then reload.`;\n\t\tif (ui) ui.notify(message, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tif (action === \"status\") {\n\t\tconst status = inspectBearerTokenForUrl(serverName, validation.serverUrl);\n\t\tconst message =\n\t\t\tstatus.status === \"present\"\n\t\t\t\t? `Bearer token is stored for \"${safeName}\".`\n\t\t\t\t: status.status === \"url-mismatch\"\n\t\t\t\t\t? `Bearer token is stored for \"${safeName}\", but its URL does not match the current server URL.`\n\t\t\t\t\t: status.status === \"unavailable\"\n\t\t\t\t\t\t? status.message\n\t\t\t\t\t\t: `No bearer token is stored for \"${safeName}\".`;\n\t\tif (ui) ui.notify(message, status.status === \"unavailable\" ? \"error\" : \"info\");\n\t\treturn { ok: status.status !== \"unavailable\", message };\n\t}\n\n\ttry {\n\t\tremoveBearerToken(serverName);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (ui) ui.notify(`Failed to remove bearer token for \"${safeName}\": ${sanitizeTerminalText(message)}`, \"error\");\n\t\treturn { ok: false, message };\n\t}\n\n\tconst message = `Bearer token removed for \"${safeName}\".`;\n\tif (ui) ui.notify(message, \"info\");\n\treturn { ok: true, message };\n}\n\ntype ParsedAddServer = { ok: true; name: string; entry: ServerEntry } | { ok: false; error: string };\n\nconst ADD_SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nfunction parseKeyValueFlag(\n\tflag: string,\n\traw: string | undefined,\n): { ok: true; key: string; value: string } | { ok: false; error: string } {\n\tconst eq = raw?.indexOf(\"=\") ?? -1;\n\tif (!raw || eq <= 0) return { ok: false, error: `${flag} expects KEY=VALUE, got \"${raw ?? \"\"}\".` };\n\treturn { ok: true, key: raw.slice(0, eq), value: raw.slice(eq + 1) };\n}\n\n/**\n * Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive\n * whitespace-split argument handling (no quoting support) rather than\n * inventing a shell-quote parser just for this subcommand.\n */\nexport function parseAddServerArgs(argv: string[]): ParsedAddServer {\n\tconst name = argv[0];\n\tif (!name) return { ok: false, error: \"Missing server name.\" };\n\tif (!ADD_SERVER_NAME_PATTERN.test(name)) {\n\t\treturn { ok: false, error: `Invalid server name \"${name}\" — use letters, digits, \".\", \"_\", \"-\".` };\n\t}\n\n\tconst rest = argv.slice(1);\n\tconst env: Record<string, string> = {};\n\tconst headers: Record<string, string> = {};\n\tlet cwd: string | undefined;\n\tlet url: string | undefined;\n\tlet auth: \"oauth\" | \"bearer\" | undefined;\n\tlet bearerTokenEnv: string | undefined;\n\tlet commandArgv: string[] | undefined;\n\n\tlet i = 0;\n\twhile (i < rest.length) {\n\t\tconst token = rest[i];\n\t\tif (token === \"--\") {\n\t\t\tcommandArgv = rest.slice(i + 1);\n\t\t\tbreak;\n\t\t}\n\t\tif (token === \"--url\") {\n\t\t\turl = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--env\" || token === \"--header\") {\n\t\t\tconst parsed = parseKeyValueFlag(token, rest[i + 1]);\n\t\t\tif (!parsed.ok) return parsed;\n\t\t\t(token === \"--env\" ? env : headers)[parsed.key] = parsed.value;\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--cwd\") {\n\t\t\tcwd = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--auth\") {\n\t\t\tconst value = rest[i + 1];\n\t\t\tif (value !== \"oauth\" && value !== \"bearer\") {\n\t\t\t\treturn { ok: false, error: `--auth expects \"oauth\" or \"bearer\", got \"${value ?? \"\"}\".` };\n\t\t\t}\n\t\t\tauth = value;\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (token === \"--bearer-env\") {\n\t\t\tbearerTokenEnv = rest[i + 1];\n\t\t\ti += 2;\n\t\t\tcontinue;\n\t\t}\n\t\treturn { ok: false, error: `Unrecognized argument \"${token}\".` };\n\t}\n\n\tif (url && commandArgv) {\n\t\treturn { ok: false, error: \"Use either --url (remote server) or -- <command> (local server), not both.\" };\n\t}\n\tif (!url && !commandArgv) {\n\t\treturn { ok: false, error: \"Specify a server type: --url <url> (remote), or -- <command> [args...] (local).\" };\n\t}\n\n\tif (commandArgv) {\n\t\tconst [command, ...args] = commandArgv;\n\t\tif (!command) return { ok: false, error: \"Missing command after --.\" };\n\t\tconst entry: ServerEntry = { command, ...(args.length ? { args } : {}) };\n\t\tif (Object.keys(env).length) entry.env = env;\n\t\tif (cwd) entry.cwd = cwd;\n\t\treturn { ok: true, name, entry };\n\t}\n\n\tconst entry: ServerEntry = { url: url as string };\n\tif (Object.keys(headers).length) entry.headers = headers;\n\tif (bearerTokenEnv) {\n\t\tentry.bearerTokenEnv = bearerTokenEnv;\n\t\tentry.auth = auth ?? \"bearer\";\n\t} else if (auth) {\n\t\tentry.auth = auth;\n\t}\n\treturn { ok: true, name, entry };\n}\n\nexport const ADD_SERVER_USAGE = [\n\t\"Usage:\",\n\t\" /mcp add <name> -- <command> [args...] local server, runs over stdio\",\n\t\" /mcp add <name> --url <url> remote server over HTTP\",\n\t\"\",\n\t\"Flags:\",\n\t\" --env KEY=VALUE extra environment variable (local, repeatable)\",\n\t\" --cwd <path> working directory (local)\",\n\t\" --header KEY=VALUE extra HTTP header (remote, repeatable)\",\n\t\" --auth oauth|bearer auth mode (remote; default is OAuth auto-detection)\",\n\t\" --bearer-env <VAR> read a bearer token from this env var (remote)\",\n\t\"\",\n\t\"For fields not covered here (lifecycle, includeTools, ...), edit the config\",\n\t'file directly — /mcp setup, then \"Open detected config paths\".',\n].join(\"\\n\");\n\n/** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */\nexport function addCustomServer(cwd: string, name: string, entry: ServerEntry): { path: string } {\n\tconst path = writeSharedServerEntry(getProjectConfigPath(cwd), name, entry);\n\treturn { path };\n}\n\nexport interface PanelFlowResult {\n\tconfigChanged: boolean;\n}\n\nfunction buildSharedConfigNoticeLines(\n\tconfigOverridePath: string | undefined,\n\tcwd: string,\n): { lines: string[]; fingerprint: string | null } {\n\tconst discovery = getMcpStandardConfigSummary(configOverridePath, cwd);\n\tconst onboardingState = loadOnboardingState();\n\tif (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {\n\t\treturn { lines: [], fingerprint: null };\n\t}\n\n\tconst sharedSources = discovery.sources.filter((source) => source.kind === \"shared\" && source.serverCount > 0);\n\tconst sourceList = sharedSources.map((source) => source.path).join(\", \");\n\treturn {\n\t\tlines: [\n\t\t\t`Using MCP configuration from ${sourceList}.`,\n\t\t\t\"Nexus writes compatibility imports and MCP-specific overrides only to Nexus configuration files.\",\n\t\t],\n\t\tfingerprint: discovery.fingerprint,\n\t};\n}\n\nexport async function openMcpSetup(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tmode: \"empty\" | \"setup\" = \"setup\",\n\toptions: { includeHostConfigs?: boolean } = {},\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP setup panel is only available in the terminal UI (current mode: ${ctx.mode}). Edit .nexus/mcp.json directly, or run /mcp status to review servers.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"MCP setup is unavailable when config is supplied by createMcpAdapter().\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);\n\tconst onboardingState = loadOnboardingState();\n\tconst installedServers = Object.entries(state.config.mcpServers).map(([name, definition]) => {\n\t\tconst authCapable = supportsOAuth(definition);\n\t\treturn {\n\t\t\tname,\n\t\t\tdisabled: isServerDisabled(definition),\n\t\t\tkind: (definition.command ? \"stdio\" : \"http\") as \"stdio\" | \"http\",\n\t\t\tauthCapable,\n\t\t\tsignedIn: authCapable ? isSignedInForOAuth(name, definition, state) : false,\n\t\t};\n\t});\n\tlet configChanged = false;\n\tlet overlayHandle: OverlayHandle | undefined;\n\n\tconst callbacks = {\n\t\tpreviewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),\n\t\tpreviewStarterProject: () => previewStarterProjectConfig(ctx.cwd),\n\t\tpreviewRepoPrompt: () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;\n\t\t\treturn previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t},\n\t\tpreviewKnownServer: (preset: KnownServerPreset) =>\n\t\t\tpreviewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),\n\t\tadoptImports: async (imports: ImportKind[]) => {\n\t\t\tconst result = ensureCompatibilityImports(imports, configOverridePath);\n\t\t\tif (result.added.length > 0) configChanged = true;\n\t\t\treturn result;\n\t\t},\n\t\tscaffoldProjectConfig: async () => {\n\t\t\tconst path = writeStarterProjectConfig(ctx.cwd);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path };\n\t\t},\n\t\taddRepoPrompt: async () => {\n\t\t\tconst repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;\n\t\t\tif (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {\n\t\t\t\tthrow new Error(\"RepoPrompt is not available to add from this setup screen.\");\n\t\t\t}\n\t\t\tconst path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: repoPrompt.serverName };\n\t\t},\n\t\taddKnownServer: async (preset: KnownServerPreset) => {\n\t\t\tconst path = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);\n\t\t\tconfigChanged = true;\n\t\t\treturn { path, serverName: preset.name };\n\t\t},\n\t\topenPath: async (targetPath: string) => {\n\t\t\tawait openPath(nexus, targetPath);\n\t\t},\n\t\tmarkSetupCompleted: () => {\n\t\t\tpersistSetupCompleted(discovery.fingerprint);\n\t\t},\n\t\tseedEditor: (text: string) => {\n\t\t\tif (ctx.hasUI) ctx.ui.setEditorText(text);\n\t\t},\n\t\ttoggleServerDisabled: async (serverName: string, disabled: boolean) => {\n\t\t\tconst result = writeProjectServerDisabledOverride(configOverridePath, ctx.cwd, serverName, disabled);\n\t\t\tif (result.changed) configChanged = true;\n\t\t\treturn { path: result.path, changed: result.changed };\n\t\t},\n\t\tauthenticateServer: async (serverName: string, fallbackEntry?: ServerEntry) => {\n\t\t\t// A server added this session isn't in the live config until /reload —\n\t\t\t// hand authenticateServer a synthetic config with just that one entry\n\t\t\t// merged in, rather than making the user reload first.\n\t\t\tconst effectiveConfig: McpConfig =\n\t\t\t\tstate.config.mcpServers[serverName] || !fallbackEntry\n\t\t\t\t\t? state.config\n\t\t\t\t\t: { ...state.config, mcpServers: { ...state.config.mcpServers, [serverName]: fallbackEntry } };\n\t\t\t// The auth prompt (the actual link, and the \"paste the callback URL\"\n\t\t\t// input) renders as a *separate* overlay. Without hiding this one first,\n\t\t\t// it stays drawn on top and the auth prompt is invisible underneath it.\n\t\t\toverlayHandle?.setHidden(true);\n\t\t\ttry {\n\t\t\t\tconst result = await authenticateServer(\n\t\t\t\t\tserverName,\n\t\t\t\t\teffectiveConfig,\n\t\t\t\t\tctx,\n\t\t\t\t\tctx.signal,\n\t\t\t\t\tstate.oauthRuntime,\n\t\t\t\t\tnexus,\n\t\t\t\t);\n\t\t\t\treturn {\n\t\t\t\t\tok: result.ok,\n\t\t\t\t\tmessage:\n\t\t\t\t\t\tresult.message ??\n\t\t\t\t\t\t(result.ok ? `Signed in to \"${serverName}\".` : `Sign-in failed for \"${serverName}\".`),\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\toverlayHandle?.setHidden(false);\n\t\t\t\toverlayHandle?.focus();\n\t\t\t}\n\t\t},\n\t};\n\n\treturn new Promise<PanelFlowResult>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpSetupPanel(\n\t\t\t\t\tdiscovery,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\t{ mode, onboardingState, keybindings, installedServers },\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve({ configChanged });\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 92, maxHeight: \"90%\" },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n}\n\nfunction buildMcpPanelCallbacks(\n\tstate: McpExtensionState,\n\tconfig: McpConfig,\n\tctx: ExtensionContext,\n\tnexus: ExtensionAPI,\n\tgetOverlayHandle?: () => OverlayHandle | undefined,\n): McpPanelCallbacks {\n\t// Panel-only diagnostics keep status inspection from mutating connection\n\t// failure state while allowing the existing panel failure UI to show why the\n\t// credential store could not be inspected.\n\tconst authStatusFailures = new Map<string, string>();\n\n\treturn {\n\t\treconnect: (serverName: string) => reconnectServer(state, ctx, serverName),\n\t\tcanAuthenticate: (serverName: string) => {\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\treturn definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;\n\t\t},\n\t\tauthenticate: async (serverName: string) => {\n\t\t\tconst overlay = getOverlayHandle?.();\n\t\t\toverlay?.setHidden(true);\n\t\t\ttry {\n\t\t\t\treturn await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime, nexus);\n\t\t\t} finally {\n\t\t\t\toverlay?.setHidden(false);\n\t\t\t\toverlay?.focus();\n\t\t\t}\n\t\t},\n\t\tgetConnectionStatus: (serverName: string) => {\n\t\t\tauthStatusFailures.delete(serverName);\n\t\t\tconst definition = config.mcpServers[serverName];\n\t\t\tif (isServerDisabled(definition)) return \"disabled\";\n\t\t\tconst connection = state.manager.getConnection(serverName);\n\t\t\tlet serverUrl: string | undefined;\n\t\t\ttry {\n\t\t\t\tserverUrl = definition ? resolveServerUrl(definition) : undefined;\n\t\t\t} catch {\n\t\t\t\treturn \"failed\";\n\t\t\t}\n\t\t\tif (\n\t\t\t\tdefinition?.auth === \"oauth\" &&\n\t\t\t\tserverUrl &&\n\t\t\t\tdefinition.oauth !== false &&\n\t\t\t\tdefinition.oauth?.grantType !== \"client_credentials\"\n\t\t\t) {\n\t\t\t\tconst authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);\n\t\t\t\tif (authStatus.status === \"unavailable\") {\n\t\t\t\t\tauthStatusFailures.set(serverName, authStatus.message);\n\t\t\t\t\treturn \"failed\";\n\t\t\t\t}\n\t\t\t\tif (authStatus.status === \"absent\" || !authStatus.entry.tokens) {\n\t\t\t\t\treturn \"needs-auth\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (connection?.status === \"needs-auth\") return \"needs-auth\";\n\t\t\tif (connection?.status === \"connected\") return \"connected\";\n\t\t\tif (getFailureAgeSeconds(state, serverName) !== null) return \"failed\";\n\t\t\treturn \"idle\";\n\t\t},\n\t\tgetFailureMessage: (serverName: string) =>\n\t\t\tauthStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),\n\t\trefreshCacheAfterReconnect: (serverName: string) => {\n\t\t\tconst freshCache = loadMetadataCache();\n\t\t\treturn freshCache?.servers?.[serverName] ?? null;\n\t\t},\n\t};\n}\n\nexport async function openMcpPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n\tonDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>,\n): Promise<PanelFlowResult> {\n\tif (state.programmaticConfig) {\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.notify(\n\t\t\t\t\"MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.\",\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t\tawait showStatus(state, ctx);\n\t\t}\n\t\treturn { configChanged: false };\n\t}\n\tif (!canRenderPanel(ctx)) {\n\t\t// No overlay here, but the same information is available as text.\n\t\tawait showStatus(state, ctx);\n\t\treturn { configChanged: false };\n\t}\n\tif (Object.keys(state.config.mcpServers).length === 0) {\n\t\treturn openMcpSetup(state, nexus, ctx, configOverridePath, \"empty\", { includeHostConfigs: false });\n\t}\n\n\tconst config = state.config;\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tconst { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);\n\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);\n\n\tlet configChanged = false;\n\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t(result: McpPanelResult) => {\n\t\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\t\tif (!result.cancelled && result.changes.size > 0) {\n\t\t\t\t\t\t\t\twriteDirectToolsConfig(result.changes, provenanceMap);\n\t\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\t\tawait onDirectToolsConfigChanged?.(result.changes);\n\t\t\t\t\t\t\t\tctx.ui.notify(\"Direct tools updated for this session.\", \"info\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t})().catch((error) => {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, \"error\");\n\t\t\t\t\t\t\tconfigChanged = true;\n\t\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{ noticeLines, keybindings },\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\tif (noticeLines.length > 0 && fingerprint) {\n\t\tmarkSharedConfigHintShown(fingerprint);\n\t}\n\n\treturn { configChanged };\n}\n\nexport async function openMcpAuthPanel(\n\tstate: McpExtensionState,\n\tnexus: ExtensionAPI,\n\tctx: ExtensionContext,\n\tconfigOverridePath?: string,\n): Promise<PanelFlowResult> {\n\tif (!ctx.hasUI) return { configChanged: false };\n\tif (!canRenderPanel(ctx)) {\n\t\tctx.ui.notify(\n\t\t\t`The interactive MCP auth panel is only available in the terminal UI (current mode: ${ctx.mode}). Use /mcp-auth <server> to authenticate a specific server.`,\n\t\t\t\"info\",\n\t\t);\n\t\treturn { configChanged: false };\n\t}\n\tif (state.programmaticConfig) {\n\t\tctx.ui.notify(\"Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.\", \"info\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst config = state.config;\n\tconst oauthServers = Object.entries(config.mcpServers).filter(\n\t\t([, definition]) => !isServerDisabled(definition) && supportsOAuth(definition),\n\t);\n\tif (oauthServers.length === 0) {\n\t\tctx.ui.notify(\"No OAuth-capable MCP servers are configured.\", \"warning\");\n\t\treturn { configChanged: false };\n\t}\n\n\tconst cache = loadMetadataCache();\n\tconst configPath = (nexus.getFlag(\"mcp-config\") as string | undefined) ?? configOverridePath;\n\tconst provenanceMap = getServerProvenance(configPath, ctx.cwd);\n\tlet overlayHandle: OverlayHandle | undefined;\n\tconst callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);\n\tawait new Promise<void>((resolve) => {\n\t\tctx.ui.custom(\n\t\t\t(tui, _theme, keybindings, done) => {\n\t\t\t\treturn createMcpPanel(\n\t\t\t\t\tconfig,\n\t\t\t\t\tcache,\n\t\t\t\t\tprovenanceMap,\n\t\t\t\t\tcallbacks,\n\t\t\t\t\ttui,\n\t\t\t\t\t() => {\n\t\t\t\t\t\tdone(undefined);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tauthOnly: true,\n\t\t\t\t\t\tkeybindings,\n\t\t\t\t\t\tnoticeLines: [\"Select an OAuth MCP server and press Enter or ctrl+a to authenticate.\"],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t\t{\n\t\t\t\toverlay: true,\n\t\t\t\toverlayOptions: { anchor: \"center\", width: 82 },\n\t\t\t\tonHandle: (handle) => {\n\t\t\t\t\toverlayHandle = handle;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t});\n\n\treturn { configChanged: false };\n}\n"]}
|