@akon-labs/nexus-cli 0.1.10 → 0.1.11

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.
Files changed (127) hide show
  1. package/dist/cli/require-login.d.ts.map +1 -1
  2. package/dist/cli/require-login.js +2 -2
  3. package/dist/cli/require-login.js.map +1 -1
  4. package/dist/cli.js +2492 -1088
  5. package/dist/core/agent-session.d.ts +3 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +4 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/extensions/index.d.ts +1 -1
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js.map +1 -1
  12. package/dist/core/extensions/loader.d.ts.map +1 -1
  13. package/dist/core/extensions/loader.js +6 -0
  14. package/dist/core/extensions/loader.js.map +1 -1
  15. package/dist/core/extensions/runner.d.ts +7 -1
  16. package/dist/core/extensions/runner.d.ts.map +1 -1
  17. package/dist/core/extensions/runner.js +14 -0
  18. package/dist/core/extensions/runner.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +18 -0
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/graph-rail-store.d.ts +10 -0
  23. package/dist/core/graph-rail-store.d.ts.map +1 -1
  24. package/dist/core/graph-rail-store.js +9 -0
  25. package/dist/core/graph-rail-store.js.map +1 -1
  26. package/dist/core/keybindings.d.ts +2 -1
  27. package/dist/core/keybindings.d.ts.map +1 -1
  28. package/dist/core/keybindings.js +4 -1
  29. package/dist/core/keybindings.js.map +1 -1
  30. package/dist/core/tools/find.d.ts.map +1 -1
  31. package/dist/core/tools/find.js +1 -1
  32. package/dist/core/tools/find.js.map +1 -1
  33. package/dist/core/tools/grep.d.ts.map +1 -1
  34. package/dist/core/tools/grep.js +1 -1
  35. package/dist/core/tools/grep.js.map +1 -1
  36. package/dist/extensions/gitnexus/client.d.ts.map +1 -1
  37. package/dist/extensions/gitnexus/client.js +45 -14
  38. package/dist/extensions/gitnexus/client.js.map +1 -1
  39. package/dist/extensions/gitnexus/config.d.ts +15 -1
  40. package/dist/extensions/gitnexus/config.d.ts.map +1 -1
  41. package/dist/extensions/gitnexus/config.js +86 -2
  42. package/dist/extensions/gitnexus/config.js.map +1 -1
  43. package/dist/extensions/gitnexus/hub-api.d.ts +56 -0
  44. package/dist/extensions/gitnexus/hub-api.d.ts.map +1 -0
  45. package/dist/extensions/gitnexus/hub-api.js +105 -0
  46. package/dist/extensions/gitnexus/hub-api.js.map +1 -0
  47. package/dist/extensions/gitnexus/index-repo.d.ts +50 -0
  48. package/dist/extensions/gitnexus/index-repo.d.ts.map +1 -0
  49. package/dist/extensions/gitnexus/index-repo.js +162 -0
  50. package/dist/extensions/gitnexus/index-repo.js.map +1 -0
  51. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  52. package/dist/extensions/gitnexus/index.js +137 -17
  53. package/dist/extensions/gitnexus/index.js.map +1 -1
  54. package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
  55. package/dist/extensions/gitnexus/observatory.js +2 -2
  56. package/dist/extensions/gitnexus/observatory.js.map +1 -1
  57. package/dist/extensions/gitnexus/resolve.d.ts +9 -0
  58. package/dist/extensions/gitnexus/resolve.d.ts.map +1 -1
  59. package/dist/extensions/gitnexus/resolve.js +52 -2
  60. package/dist/extensions/gitnexus/resolve.js.map +1 -1
  61. package/dist/extensions/index.d.ts.map +1 -1
  62. package/dist/extensions/index.js +3 -0
  63. package/dist/extensions/index.js.map +1 -1
  64. package/dist/extensions/mcp/commands.d.ts +22 -2
  65. package/dist/extensions/mcp/commands.d.ts.map +1 -1
  66. package/dist/extensions/mcp/commands.js +201 -9
  67. package/dist/extensions/mcp/commands.js.map +1 -1
  68. package/dist/extensions/mcp/config.d.ts +2 -0
  69. package/dist/extensions/mcp/config.d.ts.map +1 -1
  70. package/dist/extensions/mcp/config.js +6 -1
  71. package/dist/extensions/mcp/config.js.map +1 -1
  72. package/dist/extensions/mcp/index.d.ts.map +1 -1
  73. package/dist/extensions/mcp/index.js +54 -5
  74. package/dist/extensions/mcp/index.js.map +1 -1
  75. package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
  76. package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
  77. package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
  78. package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
  79. package/dist/extensions/subagent/index.d.ts +2 -1
  80. package/dist/extensions/subagent/index.d.ts.map +1 -1
  81. package/dist/extensions/subagent/index.js +19 -6
  82. package/dist/extensions/subagent/index.js.map +1 -1
  83. package/dist/extensions/subagent-render.d.ts +3 -0
  84. package/dist/extensions/subagent-render.d.ts.map +1 -0
  85. package/dist/extensions/subagent-render.js +39 -0
  86. package/dist/extensions/subagent-render.js.map +1 -0
  87. package/dist/index.js +2524 -1120
  88. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/assistant-message.js +2 -0
  90. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  91. package/dist/modes/interactive/components/command-palette.d.ts +2 -1
  92. package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/command-palette.js +11 -4
  94. package/dist/modes/interactive/components/command-palette.js.map +1 -1
  95. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/footer.js +9 -3
  97. package/dist/modes/interactive/components/footer.js.map +1 -1
  98. package/dist/modes/interactive/components/graph-rail.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/graph-rail.js +18 -3
  100. package/dist/modes/interactive/components/graph-rail.js.map +1 -1
  101. package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
  102. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/keybinding-hints.js +7 -1
  104. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  105. package/dist/modes/interactive/components/landing-panel.d.ts.map +1 -1
  106. package/dist/modes/interactive/components/landing-panel.js +15 -2
  107. package/dist/modes/interactive/components/landing-panel.js.map +1 -1
  108. package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
  109. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/tool-execution.js +12 -2
  111. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  112. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  113. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +42 -8
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/rpc-entry.js +2492 -1088
  117. package/dist/utils/hidden-spawn.d.ts +29 -0
  118. package/dist/utils/hidden-spawn.d.ts.map +1 -0
  119. package/dist/utils/hidden-spawn.js +101 -0
  120. package/dist/utils/hidden-spawn.js.map +1 -0
  121. package/dist/vendor/ai.js +233 -233
  122. package/dist/vendor/tui.js +1 -1
  123. package/native/win32/build.mjs +95 -0
  124. package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
  125. package/native/win32/src/hidden-spawn.c +157 -0
  126. package/npm-shrinkwrap.json +2 -37
  127. package/package.json +7 -3
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** One indexed repo as reported by the hub's list_repos tool. */\nexport interface IndexedRepo {\n\tname: string;\n\tsymbols: string;\n\trelationships: string;\n\tindexedAt: string;\n}\n\nexport interface IndexedGroup {\n\tname: string;\n\trepoCount: string;\n}\n\nexport interface RepoInventory {\n\trepos: IndexedRepo[];\n\tgroups: IndexedGroup[];\n}\n\n/** Parse the markdown returned by the hub list_repos tool. */\nexport function parseRepoInventory(markdown: string): RepoInventory {\n\tconst repos: IndexedRepo[] = [];\n\tconst groups: IndexedGroup[] = [];\n\tconst repoRe = /^- \\*\\*([^*]+)\\*\\* \\(([\\d,]+) symbols, ([\\d,]+) relationships\\) — indexed (.+)$/;\n\tconst groupRe = /^- \\*\\*(@[^*]+)\\*\\* \\((\\d+) repos?/;\n\tfor (const line of markdown.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst repoMatch = repoRe.exec(trimmed);\n\t\tif (repoMatch?.[1] && repoMatch[2] && repoMatch[3] && repoMatch[4]) {\n\t\t\trepos.push({\n\t\t\t\tname: repoMatch[1],\n\t\t\t\tsymbols: repoMatch[2],\n\t\t\t\trelationships: repoMatch[3],\n\t\t\t\tindexedAt: repoMatch[4],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupMatch = groupRe.exec(trimmed);\n\t\tif (groupMatch?.[1] && groupMatch[2]) {\n\t\t\tgroups.push({ name: groupMatch[1], repoCount: groupMatch[2] });\n\t\t}\n\t}\n\treturn { repos, groups };\n}\n\n/** Read the git remote of cwd and normalize to \"owner/name\" (undefined outside a repo). */\nexport function gitRemoteSlug(cwd: string): string | undefined {\n\tconst result = spawnSync(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n\t\tcwd,\n\t\tencoding: \"utf8\",\n\t\ttimeout: 3000,\n\t});\n\tif (result.status !== 0) return undefined;\n\tconst url = (result.stdout ?? \"\").trim();\n\tif (!url) return undefined;\n\t// https://github.com/owner/name(.git) | git@github.com:owner/name(.git)\n\tconst match = /[/:]([^/:]+\\/[^/:]+?)(?:\\.git)?$/.exec(url);\n\treturn match?.[1];\n}\n\n/** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */\nexport function matchCurrentRepo(inventory: RepoInventory, slug: string | undefined): IndexedRepo | undefined {\n\tif (!slug) return undefined;\n\tconst lower = slug.toLowerCase();\n\tconst exact = inventory.repos.find((r) => r.name.toLowerCase() === lower);\n\tif (exact) return exact;\n\tconst name = lower.split(\"/\")[1] ?? lower;\n\treturn inventory.repos.find((r) => r.name.toLowerCase().endsWith(`/${name}`));\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** One indexed repo as reported by the hub's list_repos tool. */\nexport interface IndexedRepo {\n\tname: string;\n\tsymbols: string;\n\trelationships: string;\n\tindexedAt: string;\n}\n\nexport interface IndexedGroup {\n\tname: string;\n\trepoCount: string;\n}\n\nexport interface RepoInventory {\n\trepos: IndexedRepo[];\n\tgroups: IndexedGroup[];\n}\n\nexport interface GitHubRemote {\n\tname: string;\n\tfullName: string;\n\tcloneUrl: string;\n}\n\n/** Parse the markdown returned by the hub list_repos tool. */\nexport function parseRepoInventory(markdown: string): RepoInventory {\n\tconst repos: IndexedRepo[] = [];\n\tconst groups: IndexedGroup[] = [];\n\tconst repoRe = /^- \\*\\*([^*]+)\\*\\* \\(([\\d,]+) symbols, ([\\d,]+) relationships\\) — indexed (.+)$/;\n\tconst groupRe = /^- \\*\\*(@[^*]+)\\*\\* \\((\\d+) repos?/;\n\tfor (const line of markdown.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst repoMatch = repoRe.exec(trimmed);\n\t\tif (repoMatch?.[1] && repoMatch[2] && repoMatch[3] && repoMatch[4]) {\n\t\t\trepos.push({\n\t\t\t\tname: repoMatch[1],\n\t\t\t\tsymbols: repoMatch[2],\n\t\t\t\trelationships: repoMatch[3],\n\t\t\t\tindexedAt: repoMatch[4],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupMatch = groupRe.exec(trimmed);\n\t\tif (groupMatch?.[1] && groupMatch[2]) {\n\t\t\tgroups.push({ name: groupMatch[1], repoCount: groupMatch[2] });\n\t\t}\n\t}\n\treturn { repos, groups };\n}\n\nfunction gitOriginUrl(cwd: string): string | undefined {\n\tconst result = spawnSync(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n\t\tcwd,\n\t\tencoding: \"utf8\",\n\t\ttimeout: 3000,\n\t});\n\tif (result.status !== 0) return undefined;\n\tconst url = (result.stdout ?? \"\").trim();\n\treturn url || undefined;\n}\n\n/** Parse a supported GitHub origin without preserving credentials or URL parameters. */\nexport function parseGitHubRemote(remoteUrl: string): GitHubRemote | undefined {\n\tif (!remoteUrl || /\\s/.test(remoteUrl)) return undefined;\n\n\tconst scpMatch = /^git@github\\.com:([^/]+)\\/([^/]+)$/.exec(remoteUrl);\n\tif (scpMatch?.[1] && scpMatch[2]) return normalizeGitHubRemote(scpMatch[1], scpMatch[2]);\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(remoteUrl);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed.hostname !== \"github.com\" || parsed.port || parsed.search || parsed.hash) return undefined;\n\tif (parsed.protocol === \"https:\") {\n\t\tif (parsed.username || parsed.password) return undefined;\n\t} else if (parsed.protocol === \"ssh:\") {\n\t\tif (parsed.username !== \"git\" || parsed.password) return undefined;\n\t} else {\n\t\treturn undefined;\n\t}\n\n\tconst pathMatch = /^\\/([^/]+)\\/([^/]+)$/.exec(parsed.pathname);\n\tif (!pathMatch?.[1] || !pathMatch[2]) return undefined;\n\treturn normalizeGitHubRemote(pathMatch[1], pathMatch[2]);\n}\n\nfunction normalizeGitHubRemote(owner: string, rawName: string): GitHubRemote | undefined {\n\tconst name = rawName.endsWith(\".git\") ? rawName.slice(0, -4) : rawName;\n\tif (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner)) return undefined;\n\tif (name.length > 100 || !/^[A-Za-z0-9._-]+$/.test(name) || name === \".\" || name === \"..\") return undefined;\n\tconst fullName = `${owner}/${name}`;\n\treturn { name, fullName, cloneUrl: `https://github.com/${fullName}.git` };\n}\n\n/** Resolve the current repository's origin as a canonical GitHub remote. */\nexport function currentGitHubRemote(cwd: string): GitHubRemote | undefined {\n\tconst remoteUrl = gitOriginUrl(cwd);\n\treturn remoteUrl ? parseGitHubRemote(remoteUrl) : undefined;\n}\n\n/** Read the git remote of cwd and normalize to \"owner/name\" (undefined outside a repo). */\nexport function gitRemoteSlug(cwd: string): string | undefined {\n\tconst url = gitOriginUrl(cwd);\n\tif (!url) return undefined;\n\t// https://github.com/owner/name(.git) | git@github.com:owner/name(.git)\n\tconst match = /[/:]([^/:]+\\/[^/:]+?)(?:\\.git)?$/.exec(url);\n\treturn match?.[1];\n}\n\n/** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */\nexport function matchCurrentRepo(inventory: RepoInventory, slug: string | undefined): IndexedRepo | undefined {\n\tif (!slug) return undefined;\n\tconst lower = slug.toLowerCase();\n\tconst exact = inventory.repos.find((r) => r.name.toLowerCase() === lower);\n\tif (exact) return exact;\n\tconst name = lower.split(\"/\")[1] ?? lower;\n\treturn inventory.repos.find((r) => r.name.toLowerCase().endsWith(`/${name}`));\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;AAQnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAM9C,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.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"]}
@@ -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"]}
@@ -1,4 +1,4 @@
1
- import { ensureCompatibilityImports, getMcpDiscoverySummary, getMcpStandardConfigSummary, getProjectConfigPath, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
1
+ import { ensureCompatibilityImports, getMcpDiscoverySummary, getMcpStandardConfigSummary, getProjectConfigPath, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeProjectServerDisabledOverride, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
2
2
  import { clearFailure, getFailureAgeSeconds, getFailureMessage, markKeepAliveAfterConnect, notifyToolMetadataUpdated, recordFailure, updateMetadataCache, updateStatusBar, } from "./init.js";
3
3
  import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.js";
4
4
  import { authenticate, removeAuth, supportsOAuth } from "./mcp-auth-flow.js";
@@ -200,7 +200,20 @@ export async function reconnectServers(state, ctx, targetServer) {
200
200
  }
201
201
  updateStatusBar(state);
202
202
  }
203
- export async function authenticateServer(serverName, config, ctx, signal, runtime) {
203
+ /** Whether stored OAuth tokens actually exist for a server — independent of whether it's currently connected. */
204
+ function isSignedInForOAuth(serverName, definition, state) {
205
+ try {
206
+ const serverUrl = resolveServerUrl(definition);
207
+ if (!serverUrl)
208
+ return false;
209
+ const authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);
210
+ return authStatus.status !== "absent" && authStatus.status !== "unavailable" && !!authStatus.entry.tokens;
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ }
216
+ export async function authenticateServer(serverName, config, ctx, signal, runtime, nexus) {
204
217
  const ui = ctx.hasUI ? ctx.ui : undefined;
205
218
  const cwd = ctx.cwd;
206
219
  signal ??= ctx.signal;
@@ -234,7 +247,18 @@ export async function authenticateServer(serverName, config, ctx, signal, runtim
234
247
  const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);
235
248
  const status = await authenticate(serverName, serverUrl, definition, {
236
249
  ...(authStorageOptions.baseDir ? { authStorageOptions } : {}),
237
- onAuthorizationUrl: () => { },
250
+ onAuthorizationUrl: async (authorizationUrl) => {
251
+ // Best-effort: launch the user's browser straight to the authorization page
252
+ // instead of relying on them to click (or copy) the link printed below.
253
+ if (!nexus)
254
+ return;
255
+ try {
256
+ await openPath(nexus, authorizationUrl);
257
+ }
258
+ catch {
259
+ // The link is still shown in the prompt text below; nothing else to do here.
260
+ }
261
+ },
238
262
  onAuthorizationInput: async (authorizationUrl, inputSignal) => {
239
263
  if (inputSignal.aborted)
240
264
  return undefined;
@@ -383,6 +407,123 @@ export async function manageBearerToken(action, serverName, state, ctx) {
383
407
  ui.notify(message, "info");
384
408
  return { ok: true, message };
385
409
  }
410
+ const ADD_SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
411
+ function parseKeyValueFlag(flag, raw) {
412
+ const eq = raw?.indexOf("=") ?? -1;
413
+ if (!raw || eq <= 0)
414
+ return { ok: false, error: `${flag} expects KEY=VALUE, got "${raw ?? ""}".` };
415
+ return { ok: true, key: raw.slice(0, eq), value: raw.slice(eq + 1) };
416
+ }
417
+ /**
418
+ * Parses `/mcp add <name> ...` arguments. Mirrors the rest of /mcp's naive
419
+ * whitespace-split argument handling (no quoting support) rather than
420
+ * inventing a shell-quote parser just for this subcommand.
421
+ */
422
+ export function parseAddServerArgs(argv) {
423
+ const name = argv[0];
424
+ if (!name)
425
+ return { ok: false, error: "Missing server name." };
426
+ if (!ADD_SERVER_NAME_PATTERN.test(name)) {
427
+ return { ok: false, error: `Invalid server name "${name}" — use letters, digits, ".", "_", "-".` };
428
+ }
429
+ const rest = argv.slice(1);
430
+ const env = {};
431
+ const headers = {};
432
+ let cwd;
433
+ let url;
434
+ let auth;
435
+ let bearerTokenEnv;
436
+ let commandArgv;
437
+ let i = 0;
438
+ while (i < rest.length) {
439
+ const token = rest[i];
440
+ if (token === "--") {
441
+ commandArgv = rest.slice(i + 1);
442
+ break;
443
+ }
444
+ if (token === "--url") {
445
+ url = rest[i + 1];
446
+ i += 2;
447
+ continue;
448
+ }
449
+ if (token === "--env" || token === "--header") {
450
+ const parsed = parseKeyValueFlag(token, rest[i + 1]);
451
+ if (!parsed.ok)
452
+ return parsed;
453
+ (token === "--env" ? env : headers)[parsed.key] = parsed.value;
454
+ i += 2;
455
+ continue;
456
+ }
457
+ if (token === "--cwd") {
458
+ cwd = rest[i + 1];
459
+ i += 2;
460
+ continue;
461
+ }
462
+ if (token === "--auth") {
463
+ const value = rest[i + 1];
464
+ if (value !== "oauth" && value !== "bearer") {
465
+ return { ok: false, error: `--auth expects "oauth" or "bearer", got "${value ?? ""}".` };
466
+ }
467
+ auth = value;
468
+ i += 2;
469
+ continue;
470
+ }
471
+ if (token === "--bearer-env") {
472
+ bearerTokenEnv = rest[i + 1];
473
+ i += 2;
474
+ continue;
475
+ }
476
+ return { ok: false, error: `Unrecognized argument "${token}".` };
477
+ }
478
+ if (url && commandArgv) {
479
+ return { ok: false, error: "Use either --url (remote server) or -- <command> (local server), not both." };
480
+ }
481
+ if (!url && !commandArgv) {
482
+ return { ok: false, error: "Specify a server type: --url <url> (remote), or -- <command> [args...] (local)." };
483
+ }
484
+ if (commandArgv) {
485
+ const [command, ...args] = commandArgv;
486
+ if (!command)
487
+ return { ok: false, error: "Missing command after --." };
488
+ const entry = { command, ...(args.length ? { args } : {}) };
489
+ if (Object.keys(env).length)
490
+ entry.env = env;
491
+ if (cwd)
492
+ entry.cwd = cwd;
493
+ return { ok: true, name, entry };
494
+ }
495
+ const entry = { url: url };
496
+ if (Object.keys(headers).length)
497
+ entry.headers = headers;
498
+ if (bearerTokenEnv) {
499
+ entry.bearerTokenEnv = bearerTokenEnv;
500
+ entry.auth = auth ?? "bearer";
501
+ }
502
+ else if (auth) {
503
+ entry.auth = auth;
504
+ }
505
+ return { ok: true, name, entry };
506
+ }
507
+ export const ADD_SERVER_USAGE = [
508
+ "Usage:",
509
+ " /mcp add <name> -- <command> [args...] local server, runs over stdio",
510
+ " /mcp add <name> --url <url> remote server over HTTP",
511
+ "",
512
+ "Flags:",
513
+ " --env KEY=VALUE extra environment variable (local, repeatable)",
514
+ " --cwd <path> working directory (local)",
515
+ " --header KEY=VALUE extra HTTP header (remote, repeatable)",
516
+ " --auth oauth|bearer auth mode (remote; default is OAuth auto-detection)",
517
+ " --bearer-env <VAR> read a bearer token from this env var (remote)",
518
+ "",
519
+ "For fields not covered here (lifecycle, includeTools, ...), edit the config",
520
+ 'file directly — /mcp setup, then "Open detected config paths".',
521
+ ].join("\n");
522
+ /** Adds a server to the project's shared MCP config. Reused by both `/mcp add` and any UI entry point. */
523
+ export function addCustomServer(cwd, name, entry) {
524
+ const path = writeSharedServerEntry(getProjectConfigPath(cwd), name, entry);
525
+ return { path };
526
+ }
386
527
  function buildSharedConfigNoticeLines(configOverridePath, cwd) {
387
528
  const discovery = getMcpStandardConfigSummary(configOverridePath, cwd);
388
529
  const onboardingState = loadOnboardingState();
@@ -412,7 +553,18 @@ export async function openMcpSetup(state, nexus, ctx, configOverridePath, mode =
412
553
  }
413
554
  const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options);
414
555
  const onboardingState = loadOnboardingState();
556
+ const installedServers = Object.entries(state.config.mcpServers).map(([name, definition]) => {
557
+ const authCapable = supportsOAuth(definition);
558
+ return {
559
+ name,
560
+ disabled: isServerDisabled(definition),
561
+ kind: (definition.command ? "stdio" : "http"),
562
+ authCapable,
563
+ signedIn: authCapable ? isSignedInForOAuth(name, definition, state) : false,
564
+ };
565
+ });
415
566
  let configChanged = false;
567
+ let overlayHandle;
416
568
  const callbacks = {
417
569
  previewImports: (imports) => previewCompatibilityImports(imports, configOverridePath),
418
570
  previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
@@ -454,17 +606,57 @@ export async function openMcpSetup(state, nexus, ctx, configOverridePath, mode =
454
606
  markSetupCompleted: () => {
455
607
  persistSetupCompleted(discovery.fingerprint);
456
608
  },
609
+ seedEditor: (text) => {
610
+ if (ctx.hasUI)
611
+ ctx.ui.setEditorText(text);
612
+ },
613
+ toggleServerDisabled: async (serverName, disabled) => {
614
+ const result = writeProjectServerDisabledOverride(configOverridePath, ctx.cwd, serverName, disabled);
615
+ if (result.changed)
616
+ configChanged = true;
617
+ return { path: result.path, changed: result.changed };
618
+ },
619
+ authenticateServer: async (serverName, fallbackEntry) => {
620
+ // A server added this session isn't in the live config until /reload —
621
+ // hand authenticateServer a synthetic config with just that one entry
622
+ // merged in, rather than making the user reload first.
623
+ const effectiveConfig = state.config.mcpServers[serverName] || !fallbackEntry
624
+ ? state.config
625
+ : { ...state.config, mcpServers: { ...state.config.mcpServers, [serverName]: fallbackEntry } };
626
+ // The auth prompt (the actual link, and the "paste the callback URL"
627
+ // input) renders as a *separate* overlay. Without hiding this one first,
628
+ // it stays drawn on top and the auth prompt is invisible underneath it.
629
+ overlayHandle?.setHidden(true);
630
+ try {
631
+ const result = await authenticateServer(serverName, effectiveConfig, ctx, ctx.signal, state.oauthRuntime, nexus);
632
+ return {
633
+ ok: result.ok,
634
+ message: result.message ??
635
+ (result.ok ? `Signed in to "${serverName}".` : `Sign-in failed for "${serverName}".`),
636
+ };
637
+ }
638
+ finally {
639
+ overlayHandle?.setHidden(false);
640
+ overlayHandle?.focus();
641
+ }
642
+ },
457
643
  };
458
644
  return new Promise((resolve) => {
459
645
  ctx.ui.custom((tui, _theme, keybindings, done) => {
460
- return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
646
+ return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings, installedServers }, tui, () => {
461
647
  done(undefined);
462
648
  resolve({ configChanged });
463
649
  });
464
- }, { overlay: true, overlayOptions: { anchor: "center", width: 92 } });
650
+ }, {
651
+ overlay: true,
652
+ overlayOptions: { anchor: "center", width: 92, maxHeight: "90%" },
653
+ onHandle: (handle) => {
654
+ overlayHandle = handle;
655
+ },
656
+ });
465
657
  });
466
658
  }
467
- function buildMcpPanelCallbacks(state, config, ctx, getOverlayHandle) {
659
+ function buildMcpPanelCallbacks(state, config, ctx, nexus, getOverlayHandle) {
468
660
  // Panel-only diagnostics keep status inspection from mutating connection
469
661
  // failure state while allowing the existing panel failure UI to show why the
470
662
  // credential store could not be inspected.
@@ -479,7 +671,7 @@ function buildMcpPanelCallbacks(state, config, ctx, getOverlayHandle) {
479
671
  const overlay = getOverlayHandle?.();
480
672
  overlay?.setHidden(true);
481
673
  try {
482
- return await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime);
674
+ return await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime, nexus);
483
675
  }
484
676
  finally {
485
677
  overlay?.setHidden(false);
@@ -549,7 +741,7 @@ export async function openMcpPanel(state, nexus, ctx, configOverridePath, onDire
549
741
  const provenanceMap = getServerProvenance(configPath, ctx.cwd);
550
742
  const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
551
743
  let overlayHandle;
552
- const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
744
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);
553
745
  let configChanged = false;
554
746
  await new Promise((resolve) => {
555
747
  ctx.ui.custom((tui, _theme, keybindings, done) => {
@@ -605,7 +797,7 @@ export async function openMcpAuthPanel(state, nexus, ctx, configOverridePath) {
605
797
  const configPath = nexus.getFlag("mcp-config") ?? configOverridePath;
606
798
  const provenanceMap = getServerProvenance(configPath, ctx.cwd);
607
799
  let overlayHandle;
608
- const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
800
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx, nexus, () => overlayHandle);
609
801
  await new Promise((resolve) => {
610
802
  ctx.ui.custom((tui, _theme, keybindings, done) => {
611
803
  return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {