@agimon-ai/doompi-mcp 0.0.1-alpha.13 → 0.0.1-alpha.14

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.
@@ -1,2 +1,2 @@
1
- const e=require("../../types/constants.cjs"),t=require("../../commands/mcpCommand.cjs"),n=require("./leader.cjs"),r=require("../../services/sessionConfig.cjs"),i=require("../../services/mcpSession.cjs");let a=require("@agimon-ai/doompi-extension-contracts/mcp-status"),o=require("@agimon-ai/doompi-extension-contracts/protocol");function s(s,c={}){let l,u,d=new i.McpSession({pi:s,onAuthorizationUrl:(e,t)=>{l?.hasUI||l?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),f=r.readSessionConfig();d.install(c.configurationSource===`doom`?f:{repoRoot:f.repoRoot,stagingDirectory:f.stagingDirectory}),t.registerCommand(s,d,{openOverlay:async e=>{let{openMcpOverlay:t}=await Promise.resolve().then(()=>require("../../tui/mcpOverlay.cjs"));return t(e,d)}}),n.registerLeaderContribution(s),s.on(`session_start`,(t,n)=>{l=n,c.configurationSource!==`doom`&&d.install(r.readSessionConfig({},n.cwd));let i=(0,o.createProtocolRuntime)({bus:s.events,source:e.PACKAGE_SOURCE,sessionId:n.sessionManager.getSessionId()});u?.(),u=i.provide(a.McpStatusQueryProtocol,()=>d.getSnapshot()),d.activate();for(let e of d.getDiagnostics())n.ui?.notify(e,`warning`);d.start()}),s.on(`session_shutdown`,()=>{u?.(),u=void 0,l=void 0,d.dispose()})}exports.registerMcpExtension=s;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../types/constants.cjs"),n=require("../../commands/mcpCommand.cjs"),r=require("../../services/sessionConfig.cjs"),i=require("./leader.cjs"),a=require("./mcpSession.cjs");let o=require("@agimon-ai/doompi-extension-contracts/mcp-status"),s=require("@agimon-ai/doompi-extension-contracts/protocol"),c=require("open");c=e.__toESM(c,1);const l=`warning`,u=`doom`;function d(e,d={}){let f,p,m=new a.McpSession({pi:e,onAuthorizationUrl:async(e,t,{openBrowser:n})=>{if(f?.mode===`tui`){if(!n)return;try{await(0,c.default)(e.toString())}catch(e){throw f.ui.notify(`Could not open the authorization page for ${t}: ${e instanceof Error?e.message:String(e)}. Press a in the MCP overlay to retry.`,l),e}return}f?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),h=r.readSessionConfig();m.install(d.configurationSource===u?h:{repoRoot:h.repoRoot,stagingDirectory:h.stagingDirectory}),n.registerCommand(e,m,{openOverlay:async e=>{let{openMcpOverlay:t}=await Promise.resolve().then(()=>require("../../tui/mcpOverlay.cjs"));return t(e,m)}}),i.registerLeaderContribution(e),e.on(`session_start`,(n,i)=>{f=i,d.configurationSource!==u&&m.install(r.readSessionConfig({},i.cwd));let a=(0,s.createProtocolRuntime)({bus:e.events,source:t.PACKAGE_SOURCE,sessionId:i.sessionManager.getSessionId()});p?.(),p=a.provide(o.McpStatusQueryProtocol,()=>m.getSnapshot()),m.activate();for(let e of m.getDiagnostics())i.ui?.notify(e,l);m.start()}),e.on(`session_shutdown`,()=>{p?.(),p=void 0,f=void 0,m.dispose()})}exports.registerMcpExtension=d;
2
2
  //# sourceMappingURL=extension.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.cjs","names":["McpSession","readSessionConfig","createProtocolRuntime","PACKAGE_SOURCE","McpStatusQueryProtocol"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from '../../commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { McpSession } from '../../services/mcpSession.ts';\nimport { readSessionConfig } from '../../services/sessionConfig.ts';\nimport { PACKAGE_SOURCE } from '../../types/constants.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('../../tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"yUA2BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAIA,EAAAA,WAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuBC,EAAAA,kBAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAA,gBAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,0BAAA,CAAA,EACjC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAAA,2BAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQA,EAAAA,kBAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,GAAA,EAAWC,EAAAA,sBAAAA,CAAsB,CACrC,IAAK,EAAG,OACR,OAAQC,EAAAA,eACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQC,EAAAA,2BAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
1
+ {"version":3,"file":"extension.cjs","names":["McpSession","readSessionConfig","createProtocolRuntime","PACKAGE_SOURCE","McpStatusQueryProtocol"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport open from 'open';\nimport { registerCommand } from '../../commands/mcpCommand.ts';\nimport { readSessionConfig } from '../../services/sessionConfig.ts';\nimport { PACKAGE_SOURCE } from '../../types/constants.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { McpSession } from './mcpSession.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\nconst DOOM_CONFIGURATION_SOURCE = 'doom';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | typeof DOOM_CONFIGURATION_SOURCE;\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: async (url, serverName, { openBrowser }) => {\n // Explicit auth from a terminal owns a desktop browser. Background startup\n // still only records the URL, so opening Pi never creates surprise tabs.\n if (activeContext?.mode === 'tui') {\n if (!openBrowser) return;\n try {\n await open(url.toString());\n } catch (error) {\n activeContext.ui.notify(\n `Could not open the authorization page for ${serverName}: ${error instanceof Error ? error.message : String(error)}. Press a in the MCP overlay to retry.`,\n WARNING,\n );\n throw error;\n }\n return;\n }\n // Headless and RPC sessions cannot launch a local desktop browser, so their\n // usable fallback remains the complete URL in the host notification stream.\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === DOOM_CONFIGURATION_SOURCE\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('../../tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== DOOM_CONFIGURATION_SOURCE) session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"iZAUA,MAGM,EAAU,UACV,EAA4B,OAelC,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAIA,EAAAA,WAAW,CAC7B,KACA,mBAAoB,MAAO,EAAK,EAAY,CAAE,iBAAkB,CAG9D,GAAI,GAAe,OAAS,MAAO,CACjC,GAAI,CAAC,EAAa,OAClB,GAAI,CACF,MAAA,EAAM,EAAA,QAAA,CAAK,EAAI,SAAS,CAAC,CAC3B,OAAS,EAAO,CAKd,MAJA,EAAc,GAAG,OACf,6CAA6C,EAAW,IAAI,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAAE,wCACnH,CACF,EACM,CACR,CACA,MACF,CAGA,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuBC,EAAAA,kBAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,EAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAA,gBAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,0BAAA,CAAA,EACjC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAAA,2BAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,GAA2B,EAAQ,QAAQA,EAAAA,kBAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC7G,IAAM,GAAA,EAAWC,EAAAA,sBAAAA,CAAsB,CACrC,IAAK,EAAG,OACR,OAAQC,EAAAA,eACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQC,EAAAA,2BAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,CAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
@@ -1,8 +1,9 @@
1
1
  import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  //#region src/adapters/pi/extension.d.ts
3
+ declare const DOOM_CONFIGURATION_SOURCE = "doom";
3
4
  interface McpExtensionOptions {
4
5
  /** Doom loading retains the environment-provided plugin overlays and allowlists. */
5
- configurationSource?: 'standard' | 'doom';
6
+ configurationSource?: 'standard' | typeof DOOM_CONFIGURATION_SOURCE;
6
7
  }
7
8
  /**
8
9
  * MCP support for a Pi session.
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;UAciB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;cAcM;UAEW;;EAEf,0CAA0C;;;;;;;;;;iBAW5B,qBAAqB,IAAI,cAAc,UAAS"}
@@ -1,8 +1,9 @@
1
1
  import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  //#region src/adapters/pi/extension.d.ts
3
+ declare const DOOM_CONFIGURATION_SOURCE = "doom";
3
4
  interface McpExtensionOptions {
4
5
  /** Doom loading retains the environment-provided plugin overlays and allowlists. */
5
- configurationSource?: 'standard' | 'doom';
6
+ configurationSource?: 'standard' | typeof DOOM_CONFIGURATION_SOURCE;
6
7
  }
7
8
  /**
8
9
  * MCP support for a Pi session.
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;UAciB;;EAEf;;;;;;;;;;iBAWc,qBAAqB,IAAI,cAAc,UAAS"}
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;cAcM;UAEW;;EAEf,0CAA0C;;;;;;;;;;iBAW5B,qBAAqB,IAAI,cAAc,UAAS"}
@@ -1,2 +1,2 @@
1
- import{PACKAGE_SOURCE as e}from"../../types/constants.mjs";import{registerCommand as t}from"../../commands/mcpCommand.mjs";import{registerLeaderContribution as n}from"./leader.mjs";import{readSessionConfig as r}from"../../services/sessionConfig.mjs";import{McpSession as i}from"../../services/mcpSession.mjs";import{McpStatusQueryProtocol as a}from"@agimon-ai/doompi-extension-contracts/mcp-status";import{createProtocolRuntime as o}from"@agimon-ai/doompi-extension-contracts/protocol";function s(s,c={}){let l,u,d=new i({pi:s,onAuthorizationUrl:(e,t)=>{l?.hasUI||l?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),f=r();d.install(c.configurationSource===`doom`?f:{repoRoot:f.repoRoot,stagingDirectory:f.stagingDirectory}),t(s,d,{openOverlay:async e=>{let{openMcpOverlay:t}=await import(`../../tui/mcpOverlay.mjs`);return t(e,d)}}),n(s),s.on(`session_start`,(t,n)=>{l=n,c.configurationSource!==`doom`&&d.install(r({},n.cwd));let i=o({bus:s.events,source:e,sessionId:n.sessionManager.getSessionId()});u?.(),u=i.provide(a,()=>d.getSnapshot()),d.activate();for(let e of d.getDiagnostics())n.ui?.notify(e,`warning`);d.start()}),s.on(`session_shutdown`,()=>{u?.(),u=void 0,l=void 0,d.dispose()})}export{s as registerMcpExtension};
1
+ import{PACKAGE_SOURCE as e}from"../../types/constants.mjs";import{registerCommand as t}from"../../commands/mcpCommand.mjs";import{readSessionConfig as n}from"../../services/sessionConfig.mjs";import{registerLeaderContribution as r}from"./leader.mjs";import{McpSession as i}from"./mcpSession.mjs";import{McpStatusQueryProtocol as a}from"@agimon-ai/doompi-extension-contracts/mcp-status";import{createProtocolRuntime as o}from"@agimon-ai/doompi-extension-contracts/protocol";import s from"open";const c=`warning`,l=`doom`;function u(u,d={}){let f,p,m=new i({pi:u,onAuthorizationUrl:async(e,t,{openBrowser:n})=>{if(f?.mode===`tui`){if(!n)return;try{await s(e.toString())}catch(e){throw f.ui.notify(`Could not open the authorization page for ${t}: ${e instanceof Error?e.message:String(e)}. Press a in the MCP overlay to retry.`,c),e}return}f?.ui?.notify(`Authorize ${t} by opening:\n${e.toString()}`,`info`)}}),h=n();m.install(d.configurationSource===l?h:{repoRoot:h.repoRoot,stagingDirectory:h.stagingDirectory}),t(u,m,{openOverlay:async e=>{let{openMcpOverlay:t}=await import(`../../tui/mcpOverlay.mjs`);return t(e,m)}}),r(u),u.on(`session_start`,(t,r)=>{f=r,d.configurationSource!==l&&m.install(n({},r.cwd));let i=o({bus:u.events,source:e,sessionId:r.sessionManager.getSessionId()});p?.(),p=i.provide(a,()=>m.getSnapshot()),m.activate();for(let e of m.getDiagnostics())r.ui?.notify(e,c);m.start()}),u.on(`session_shutdown`,()=>{p?.(),p=void 0,f=void 0,m.dispose()})}export{u as registerMcpExtension};
2
2
  //# sourceMappingURL=extension.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport { registerCommand } from '../../commands/mcpCommand.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { McpSession } from '../../services/mcpSession.ts';\nimport { readSessionConfig } from '../../services/sessionConfig.ts';\nimport { PACKAGE_SOURCE } from '../../types/constants.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | 'doom';\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: (url, serverName) => {\n // The overlay carries the URL for anyone who can see one, and it is where the\n // flow was started from. Announcing it here as well would only put it in the\n // transcript underneath a fullscreen panel. A session with no UI has no\n // overlay to read, so that is the one case still worth notifying.\n if (activeContext?.hasUI) return;\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === 'doom'\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('../../tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== 'doom') session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"seA2BA,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAI,EAAW,CAC7B,KACA,oBAAqB,EAAK,IAAe,CAKnC,GAAe,OACnB,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuB,EAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,OAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAM,OAAO,4BACxC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,QAAQ,EAAQ,QAAQ,EAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC1F,IAAM,EAAW,EAAsB,CACrC,IAAK,EAAG,OACR,OAAQ,EACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQ,MAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,SAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
1
+ {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { McpStatusQueryProtocol } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport { createProtocolRuntime } from '@agimon-ai/doompi-extension-contracts/protocol';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport open from 'open';\nimport { registerCommand } from '../../commands/mcpCommand.ts';\nimport { readSessionConfig } from '../../services/sessionConfig.ts';\nimport { PACKAGE_SOURCE } from '../../types/constants.ts';\nimport { registerLeaderContribution } from './leader.ts';\nimport { McpSession } from './mcpSession.ts';\n\nconst SESSION_START = 'session_start';\nconst SESSION_SHUTDOWN = 'session_shutdown';\nconst INFO = 'info';\nconst WARNING = 'warning';\nconst DOOM_CONFIGURATION_SOURCE = 'doom';\n\nexport interface McpExtensionOptions {\n /** Doom loading retains the environment-provided plugin overlays and allowlists. */\n configurationSource?: 'standard' | typeof DOOM_CONFIGURATION_SOURCE;\n}\n\n/**\n * MCP support for a Pi session.\n *\n * Tools are registered here, synchronously, from the catalog the previous run left\n * behind. Connecting starts on `session_start` and runs detached, so the first\n * prompt is never held up by a slow or unreachable server; each server folds into\n * the active tool set as it lands.\n */\nexport function registerMcpExtension(pi: ExtensionAPI, options: McpExtensionOptions = {}): void {\n let activeContext: ExtensionContext | undefined;\n let disposeStatusProvider: (() => void) | undefined;\n\n const session = new McpSession({\n pi,\n onAuthorizationUrl: async (url, serverName, { openBrowser }) => {\n // Explicit auth from a terminal owns a desktop browser. Background startup\n // still only records the URL, so opening Pi never creates surprise tabs.\n if (activeContext?.mode === 'tui') {\n if (!openBrowser) return;\n try {\n await open(url.toString());\n } catch (error) {\n activeContext.ui.notify(\n `Could not open the authorization page for ${serverName}: ${error instanceof Error ? error.message : String(error)}. Press a in the MCP overlay to retry.`,\n WARNING,\n );\n throw error;\n }\n return;\n }\n // Headless and RPC sessions cannot launch a local desktop browser, so their\n // usable fallback remains the complete URL in the host notification stream.\n activeContext?.ui?.notify(`Authorize ${serverName} by opening:\\n${url.toString()}`, INFO);\n },\n });\n\n const initialConfiguration = readSessionConfig();\n session.install(\n options.configurationSource === DOOM_CONFIGURATION_SOURCE\n ? initialConfiguration\n : {\n repoRoot: initialConfiguration.repoRoot,\n stagingDirectory: initialConfiguration.stagingDirectory,\n },\n );\n // The opener is injected rather than imported by the command: `src/commands/**`\n // may not reach into `src/tui/**`, and handing it in is also what lets the\n // command be tested without a terminal.\n registerCommand(pi, session, {\n openOverlay: async (ctx) => {\n const { openMcpOverlay } = await import('../../tui/mcpOverlay.ts');\n return openMcpOverlay(ctx, session);\n },\n });\n registerLeaderContribution(pi);\n\n pi.on(SESSION_START, (_event, ctx) => {\n activeContext = ctx;\n if (options.configurationSource !== DOOM_CONFIGURATION_SOURCE) session.install(readSessionConfig({}, ctx.cwd));\n const protocol = createProtocolRuntime({\n bus: pi.events,\n source: PACKAGE_SOURCE,\n sessionId: ctx.sessionManager.getSessionId(),\n });\n disposeStatusProvider?.();\n disposeStatusProvider = protocol.provide(McpStatusQueryProtocol, () => session.getSnapshot());\n\n // First point Pi accepts an active-list call: the tools registered while the\n // extension loaded are held back until a server actually answers.\n session.activate();\n for (const diagnostic of session.getDiagnostics()) ctx.ui?.notify(diagnostic, WARNING);\n // Detached on purpose: awaiting here would put every downstream connection on\n // the path between launching Pi and its first prompt.\n void session.start();\n });\n\n pi.on(SESSION_SHUTDOWN, () => {\n disposeStatusProvider?.();\n disposeStatusProvider = undefined;\n activeContext = undefined;\n void session.dispose();\n });\n}\n"],"mappings":"6eAUA,MAGM,EAAU,UACV,EAA4B,OAelC,SAAgB,EAAqB,EAAkB,EAA+B,CAAC,EAAS,CAC9F,IAAI,EACA,EAEE,EAAU,IAAI,EAAW,CAC7B,KACA,mBAAoB,MAAO,EAAK,EAAY,CAAE,iBAAkB,CAG9D,GAAI,GAAe,OAAS,MAAO,CACjC,GAAI,CAAC,EAAa,OAClB,GAAI,CACF,MAAM,EAAK,EAAI,SAAS,CAAC,CAC3B,OAAS,EAAO,CAKd,MAJA,EAAc,GAAG,OACf,6CAA6C,EAAW,IAAI,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAAE,wCACnH,CACF,EACM,CACR,CACA,MACF,CAGA,GAAe,IAAI,OAAO,aAAa,EAAW,gBAAgB,EAAI,SAAS,IAAK,MAAI,CAC1F,CACF,CAAC,EAEK,EAAuB,EAAkB,EAC/C,EAAQ,QACN,EAAQ,sBAAwB,EAC5B,EACA,CACE,SAAU,EAAqB,SAC/B,iBAAkB,EAAqB,gBACzC,CACN,EAIA,EAAgB,EAAI,EAAS,CAC3B,YAAa,KAAO,IAAQ,CAC1B,GAAM,CAAE,kBAAmB,MAAM,OAAO,4BACxC,OAAO,EAAe,EAAK,CAAO,CACpC,CACF,CAAC,EACD,EAA2B,CAAE,EAE7B,EAAG,GAAG,iBAAgB,EAAQ,IAAQ,CACpC,EAAgB,EACZ,EAAQ,sBAAwB,GAA2B,EAAQ,QAAQ,EAAkB,CAAC,EAAG,EAAI,GAAG,CAAC,EAC7G,IAAM,EAAW,EAAsB,CACrC,IAAK,EAAG,OACR,OAAQ,EACR,UAAW,EAAI,eAAe,aAAa,CAC7C,CAAC,EACD,IAAwB,EACxB,EAAwB,EAAS,QAAQ,MAA8B,EAAQ,YAAY,CAAC,EAI5F,EAAQ,SAAS,EACjB,IAAK,IAAM,KAAc,EAAQ,eAAe,EAAG,EAAI,IAAI,OAAO,EAAY,CAAO,EAGrF,EAAa,MAAM,CACrB,CAAC,EAED,EAAG,GAAG,uBAAwB,CAC5B,IAAwB,EACxB,EAAwB,IAAA,GACxB,EAAgB,IAAA,GAChB,EAAa,QAAQ,CACvB,CAAC,CACH"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../services/sessionConfig.cjs"),t=require("../../commands/tools/mcpTools.cjs"),n=require("../../services/configSources.cjs"),r=require("../../services/directTools.cjs"),i=require("../../services/mcpCatalog.cjs"),a=require("../../services/mcpRuntime.cjs"),o=`The MCP runtime has not started yet.`;var s=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){t.applyActiveTools(this.pi,this.catalog)}createCatalog(){return new i.McpCatalog(r.readDirectToolFilter(this.options.environment))}install(r=e.readSessionConfig(this.options.environment)){this.groups=n.buildMcpConfigGroups(r),this.catalog=this.createCatalog();let i=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(a.readCachedCatalog(this.configPaths()),i),t.registerNewTools(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([Promise.resolve().then(()=>require("../../services/keyringTokenStore.cjs")),Promise.resolve().then(()=>require("../../services/mcpRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange();let n=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:n})).catch(e=>{let n=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${n}`),this.emitChange()})}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(o);this.resourceCache.delete(e),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,n){this.catalog.setDisabled(e,!n),t.applyActiveTools(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(o);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,this.browserAuthorizationRequests.clear(),await this.runtime?.dispose()}onServerStateChange(e){e.state!==`connecting`&&this.authorizationUrls.delete(e.serverName);let n=this.runtime,r=n?.getServices();(e.state===`connected`&&r?r.clientManager.ensureConnected(e.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(i=>{if(!n?.isCurrent(r))return;let a=this.catalog.applyStateChange(e,i);t.registerNewTools(this.pi,this.clientManager,a),t.applyActiveTools(this.pi,this.catalog),this.emitChange()})}};exports.McpSession=s;
2
+ //# sourceMappingURL=mcpSession.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpSession.cjs","names":["McpCatalog","readDirectToolFilter","readSessionConfig","buildMcpConfigGroups","readCachedCatalog"],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../../commands/tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from '../../services/configSources.ts';\nimport { readDirectToolFilter } from '../../services/directTools.ts';\nimport { McpCatalog } from '../../services/mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../../services/mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from '../../services/sessionConfig.ts';\nimport type { McpResourceView, McpServerView } from '../../types/index.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<void>;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../../services/keyringTokenStore.ts'),\n import('../../services/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n this.browserAuthorizationRequests.clear();\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{readSessionConfig as e}from"../../services/sessionConfig.mjs";import{applyActiveTools as t,registerNewTools as n}from"../../commands/tools/mcpTools.mjs";import{buildMcpConfigGroups as r}from"../../services/configSources.mjs";import{readDirectToolFilter as i}from"../../services/directTools.mjs";import{McpCatalog as a}from"../../services/mcpCatalog.mjs";import{readCachedCatalog as o}from"../../services/mcpRuntime.mjs";const s=`The MCP runtime has not started yet.`;var c=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;browserAuthorizationRequests=new Set;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){t(this.pi,this.catalog)}createCatalog(){return new a(i(this.options.environment))}install(t=e(this.options.environment)){this.groups=r(t),this.catalog=this.createCatalog();let i=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(o(this.configPaths()),i),n(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([import(`../../services/keyringTokenStore.mjs`),import(`../../services/mcpRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange();let n=this.browserAuthorizationRequests.has(t);Promise.resolve().then(()=>this.options.onAuthorizationUrl?.(e,t,{openBrowser:n})).catch(e=>{let n=e instanceof Error?e.message:String(e);this.catalog.addDiagnostic(`Could not open authorization page for "${t}": ${n}`),this.emitChange()})}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(s);this.resourceCache.delete(e),this.browserAuthorizationRequests.add(e);try{await t.disconnectServer(e),await t.ensureConnected(e)}finally{this.browserAuthorizationRequests.delete(e)}}async openAuthorizationPage(e){let t=this.authorizationUrls.get(e);if(!t)throw Error(`No authorization page is waiting for this MCP server.`);let n=this.options.onAuthorizationUrl;if(!n)throw Error(`This host cannot open authorization pages.`);await n(new URL(t),e,{openBrowser:!0})}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(e,n){this.catalog.setDisabled(e,!n),t(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(s);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,this.browserAuthorizationRequests.clear(),await this.runtime?.dispose()}onServerStateChange(e){e.state!==`connecting`&&this.authorizationUrls.delete(e.serverName);let r=this.runtime,i=r?.getServices();(e.state===`connected`&&i?i.clientManager.ensureConnected(e.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(a=>{if(!r?.isCurrent(i))return;let o=this.catalog.applyStateChange(e,a);n(this.pi,this.clientManager,o),t(this.pi,this.catalog),this.emitChange()})}};export{c as McpSession};
2
+ //# sourceMappingURL=mcpSession.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../../src/adapters/pi/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../../commands/tools/mcpTools.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from '../../services/configSources.ts';\nimport { readDirectToolFilter } from '../../services/directTools.ts';\nimport { McpCatalog } from '../../services/mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from '../../services/mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from '../../services/sessionConfig.ts';\nimport type { McpResourceView, McpServerView } from '../../types/index.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\nconst AUTHORIZATION_URL_NOT_READY = 'No authorization page is waiting for this MCP server.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Surfaces an OAuth URL, opening it when an explicit authorization requested that. */\n onAuthorizationUrl?: (url: URL, serverName: string, options: { openBrowser: boolean }) => void | Promise<void>;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Servers whose explicit auth action should launch the browser when its URL arrives. */\n private readonly browserAuthorizationRequests = new Set<string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('../../services/keyringTokenStore.ts'),\n import('../../services/mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it. Host work is\n * detached so a failed desktop browser launch never cancels a still-usable OAuth\n * callback; the overlay retains a clickable fallback and a retry action.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n const openBrowser = this.browserAuthorizationRequests.has(serverName);\n void Promise.resolve()\n .then(() => this.options.onAuthorizationUrl?.(url, serverName, { openBrowser }))\n .catch((error: unknown) => {\n const detail = error instanceof Error ? error.message : String(error);\n this.catalog.addDiagnostic(`Could not open authorization page for \"${serverName}\": ${detail}`);\n this.emitChange();\n });\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n this.browserAuthorizationRequests.add(serverName);\n try {\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n } finally {\n this.browserAuthorizationRequests.delete(serverName);\n }\n }\n\n /** Reopens the URL already reserved for a flow without restarting that flow. */\n async openAuthorizationPage(serverName: string): Promise<void> {\n const authorizationUrl = this.authorizationUrls.get(serverName);\n if (!authorizationUrl) throw new Error(AUTHORIZATION_URL_NOT_READY);\n const onAuthorizationUrl = this.options.onAuthorizationUrl;\n if (!onAuthorizationUrl) throw new Error('This host cannot open authorization pages.');\n await onAuthorizationUrl(new URL(authorizationUrl), serverName, { openBrowser: true });\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n this.browserAuthorizationRequests.clear();\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/configSources.cjs"),t=require("./services/directTools.cjs"),n=require("./services/mcpCatalog.cjs"),r=require("./services/mcpRuntime.cjs"),i=require("./services/sessionConfig.cjs"),a=require("./adapters/pi/extension.cjs");exports.DIRECT_TOOLS_ENV=t.DIRECT_TOOLS_ENV,exports.NO_DIRECT_TOOLS=t.NO_DIRECT_TOOLS,exports.PROXY_SERVER_NAME=e.PROXY_SERVER_NAME,exports.SESSION_ENV_VAR=i.SESSION_ENV_VAR,exports.buildMcpConfigGroups=e.buildMcpConfigGroups,exports.readCachedCatalog=r.readCachedCatalog,exports.readSessionConfig=i.readSessionConfig,exports.registerMcpExtension=a.registerMcpExtension,exports.sessionConfigEnvironment=i.sessionConfigEnvironment,exports.toPiToolName=n.toPiToolName;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/sessionConfig.cjs"),t=require("./services/configSources.cjs"),n=require("./services/directTools.cjs"),r=require("./services/mcpCatalog.cjs"),i=require("./services/mcpRuntime.cjs"),a=require("./adapters/pi/extension.cjs");exports.DIRECT_TOOLS_ENV=n.DIRECT_TOOLS_ENV,exports.NO_DIRECT_TOOLS=n.NO_DIRECT_TOOLS,exports.PROXY_SERVER_NAME=t.PROXY_SERVER_NAME,exports.SESSION_ENV_VAR=e.SESSION_ENV_VAR,exports.buildMcpConfigGroups=t.buildMcpConfigGroups,exports.readCachedCatalog=i.readCachedCatalog,exports.readSessionConfig=e.readSessionConfig,exports.registerMcpExtension=a.registerMcpExtension,exports.sessionConfigEnvironment=e.sessionConfigEnvironment,exports.toPiToolName=r.toPiToolName;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{PROXY_SERVER_NAME as e,buildMcpConfigGroups as t}from"./services/configSources.mjs";import{DIRECT_TOOLS_ENV as n,NO_DIRECT_TOOLS as r}from"./services/directTools.mjs";import{toPiToolName as i}from"./services/mcpCatalog.mjs";import{readCachedCatalog as a}from"./services/mcpRuntime.mjs";import{SESSION_ENV_VAR as o,readSessionConfig as s,sessionConfigEnvironment as c}from"./services/sessionConfig.mjs";import{registerMcpExtension as l}from"./adapters/pi/extension.mjs";export{n as DIRECT_TOOLS_ENV,r as NO_DIRECT_TOOLS,e as PROXY_SERVER_NAME,o as SESSION_ENV_VAR,t as buildMcpConfigGroups,a as readCachedCatalog,s as readSessionConfig,l as registerMcpExtension,c as sessionConfigEnvironment,i as toPiToolName};
1
+ import{SESSION_ENV_VAR as e,readSessionConfig as t,sessionConfigEnvironment as n}from"./services/sessionConfig.mjs";import{PROXY_SERVER_NAME as r,buildMcpConfigGroups as i}from"./services/configSources.mjs";import{DIRECT_TOOLS_ENV as a,NO_DIRECT_TOOLS as o}from"./services/directTools.mjs";import{toPiToolName as s}from"./services/mcpCatalog.mjs";import{readCachedCatalog as c}from"./services/mcpRuntime.mjs";import{registerMcpExtension as l}from"./adapters/pi/extension.mjs";export{a as DIRECT_TOOLS_ENV,o as NO_DIRECT_TOOLS,r as PROXY_SERVER_NAME,e as SESSION_ENV_VAR,i as buildMcpConfigGroups,c as readCachedCatalog,t as readSessionConfig,l as registerMcpExtension,n as sessionConfigEnvironment,s as toPiToolName};
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto"),n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("yaml");const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){let t=r.default.extname(e).toLowerCase();return t===`.yaml`||t===`.yml`}function l(e){try{let t=n.default.readFileSync(e,`utf8`),r=c(e)?(0,i.parse)(t):JSON.parse(t);return s(r)?r:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(e){let t=u(l(r.default.join(e,`.mcp.json`))),i=[...o].map(e=>t[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?r.default.resolve(e,d):r.default.join(e,`mcp-config.yaml`);return n.default.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(e,n,i){let a=(0,t.createHash)(`sha256`).update(e).update(`\0`).update(n).digest(`hex`).slice(0,16),o=c(e)?`.yaml`:`.json`;return r.default.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?(0,i.stringify)(t):`${JSON.stringify(t,null,2)}\n`}function _(e,t,r,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,n]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,t)?c.push(e):s[e]=n;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};n.default.mkdirSync(r,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,r);return n.default.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(e){let t=[r.default.join(e.repoRoot,`.mcp.json`),...(e.pluginConfigPaths??[]).map(t=>r.default.resolve(e.repoRoot,t))].filter(e=>n.default.existsSync(e)),i=[],a=[],o=[];for(let n of t){let t=_(n,e.allowlist?.servers,e.stagingDirectory,{stripProxyWrappers:!0});t&&(i.push(t.path),a.push(...t.kept),o.push(...t.dropped))}let s=p(e.repoRoot),c=s?_(s,e.allowlist?.proxy,e.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}exports.PROXY_SERVER_NAME=a,exports.buildMcpConfigGroups=v,exports.resolveSharedConfigPath=p;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:crypto"),r=require("node:fs");r=e.__toESM(r,1);let i=require("yaml");const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){let n=t.default.extname(e).toLowerCase();return n===`.yaml`||n===`.yml`}function l(e){try{let t=r.default.readFileSync(e,`utf8`),n=c(e)?(0,i.parse)(t):JSON.parse(t);return s(n)?n:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(e){let n=u(l(t.default.join(e,`.mcp.json`))),i=[...o].map(e=>n[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?t.default.resolve(e,d):t.default.join(e,`mcp-config.yaml`);return r.default.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(e,r,i){let a=(0,n.createHash)(`sha256`).update(e).update(`\0`).update(r).digest(`hex`).slice(0,16),o=c(e)?`.yaml`:`.json`;return t.default.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?(0,i.stringify)(t):`${JSON.stringify(t,null,2)}\n`}function _(e,t,n,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,n]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,t)?c.push(e):s[e]=n;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};r.default.mkdirSync(n,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,n);return r.default.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(e){let n=[t.default.join(e.repoRoot,`.mcp.json`),...(e.pluginConfigPaths??[]).map(n=>t.default.resolve(e.repoRoot,n))].filter(e=>r.default.existsSync(e)),i=[],a=[],o=[];for(let t of n){let n=_(t,e.allowlist?.servers,e.stagingDirectory,{stripProxyWrappers:!0});n&&(i.push(n.path),a.push(...n.kept),o.push(...n.dropped))}let s=p(e.repoRoot),c=s?_(s,e.allowlist?.proxy,e.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}exports.PROXY_SERVER_NAME=a,exports.buildMcpConfigGroups=v,exports.resolveSharedConfigPath=p;
2
2
  //# sourceMappingURL=configSources.cjs.map
@@ -1,2 +1,2 @@
1
- import{createHash as e}from"node:crypto";import t from"node:fs";import n from"node:path";import{parse as r,stringify as i}from"yaml";const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){let t=n.extname(e).toLowerCase();return t===`.yaml`||t===`.yml`}function l(e){try{let n=t.readFileSync(e,`utf8`),i=c(e)?r(n):JSON.parse(n);return s(i)?i:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(e){let r=u(l(n.join(e,`.mcp.json`))),i=[...o].map(e=>r[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?n.resolve(e,d):n.join(e,`mcp-config.yaml`);return t.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(t,r,i){let a=e(`sha256`).update(t).update(`\0`).update(r).digest(`hex`).slice(0,16),o=c(t)?`.yaml`:`.json`;return n.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?i(t):`${JSON.stringify(t,null,2)}\n`}function _(e,n,r,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,t]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,n)?c.push(e):s[e]=t;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};t.mkdirSync(r,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,r);return t.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(e){let r=[n.join(e.repoRoot,`.mcp.json`),...(e.pluginConfigPaths??[]).map(t=>n.resolve(e.repoRoot,t))].filter(e=>t.existsSync(e)),i=[],a=[],o=[];for(let t of r){let n=_(t,e.allowlist?.servers,e.stagingDirectory,{stripProxyWrappers:!0});n&&(i.push(n.path),a.push(...n.kept),o.push(...n.dropped))}let s=p(e.repoRoot),c=s?_(s,e.allowlist?.proxy,e.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}export{a as PROXY_SERVER_NAME,v as buildMcpConfigGroups,p as resolveSharedConfigPath};
1
+ import e from"node:path";import{createHash as t}from"node:crypto";import n from"node:fs";import{parse as r,stringify as i}from"yaml";const a=`mcp-proxy`,o=new Set([a,`agiflow-proxy`]);function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(t){let n=e.extname(t).toLowerCase();return n===`.yaml`||n===`.yml`}function l(e){try{let t=n.readFileSync(e,`utf8`),i=c(e)?r(t):JSON.parse(t);return s(i)?i:void 0}catch{return}}function u(e){return s(e?.mcpServers)?e.mcpServers:{}}function d(e){return!s(e)||e.disabled!==!0}function f(e){return o.has(e)}function p(t){let r=u(l(e.join(t,`.mcp.json`))),i=[...o].map(e=>r[e]).find(e=>s(e))?.args,a=Array.isArray(i)?i:[],c=a.indexOf(`--config`),d=a[c+1],f=c>=0&&typeof d==`string`?e.resolve(t,d):e.join(t,`mcp-config.yaml`);return n.existsSync(f)?f:void 0}function m(e,t){return!t||t.length===0||t.includes(e)}function h(n,r,i){let a=t(`sha256`).update(n).update(`\0`).update(r).digest(`hex`).slice(0,16),o=c(n)?`.yaml`:`.json`;return e.join(i,`mcp-${a}${o}`)}function g(e,t){return c(e)?i(t):`${JSON.stringify(t,null,2)}\n`}function _(e,t,r,i){let a=l(e);if(!a)return;let o=u(a),s={},c=[];for(let[e,n]of Object.entries(o))i.stripProxyWrappers&&f(e)||!m(e,t)?c.push(e):s[e]=n;let p=Object.entries(s).filter(([,e])=>d(e)).map(([e])=>e);if(c.length===0)return{path:e,kept:p,dropped:c};n.mkdirSync(r,{mode:448,recursive:!0});let _=g(e,{...a,mcpServers:s}),v=h(e,_,r);return n.writeFileSync(v,_,{mode:384}),{path:v,kept:p,dropped:c}}function v(t){let r=[e.join(t.repoRoot,`.mcp.json`),...(t.pluginConfigPaths??[]).map(n=>e.resolve(t.repoRoot,n))].filter(e=>n.existsSync(e)),i=[],a=[],o=[];for(let e of r){let n=_(e,t.allowlist?.servers,t.stagingDirectory,{stripProxyWrappers:!0});n&&(i.push(n.path),a.push(...n.kept),o.push(...n.dropped))}let s=p(t.repoRoot),c=s?_(s,t.allowlist?.proxy,t.stagingDirectory,{stripProxyWrappers:!0}):void 0;return c&&o.push(...c.dropped),{shared:{configPaths:c?[c.path]:[],serverNames:c?c.kept:[]},sessionLocal:{configPaths:i,serverNames:[...new Set(a)]},droppedServers:[...new Set(o)]}}export{a as PROXY_SERVER_NAME,v as buildMcpConfigGroups,p as resolveSharedConfigPath};
2
2
  //# sourceMappingURL=configSources.mjs.map
@@ -1,2 +1,2 @@
1
- let e=require("node:crypto"),t=require("node:fs"),n=require("node:path"),r=require("node:os");function i(e){return(0,n.resolve)(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function a(t){if(t.length===0)throw Error(`definitionsCachePath requires at least one config path`);let a=i(t[0]),o=t.length===1?``:`.${(0,e.createHash)(`sha256`).update(t.map(e=>(0,n.resolve)(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return(0,n.join)((0,r.homedir)(),`.mcp-proxy`,`${a}${o}.definitions-cache.json`)}function o(e,n=a){if(e.length===0)return{servers:[]};try{let r=n(e),i=JSON.parse((0,t.readFileSync)(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var s=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};exports.McpRuntimeOwner=s,exports.definitionsCachePath=a,exports.readCachedCatalog=o;
1
+ let e=require("node:os"),t=require("node:path"),n=require("node:crypto"),r=require("node:fs");function i(e){return(0,t.resolve)(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function a(r){if(r.length===0)throw Error(`definitionsCachePath requires at least one config path`);let a=i(r[0]),o=r.length===1?``:`.${(0,n.createHash)(`sha256`).update(r.map(e=>(0,t.resolve)(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return(0,t.join)((0,e.homedir)(),`.mcp-proxy`,`${a}${o}.definitions-cache.json`)}function o(e,t=a){if(e.length===0)return{servers:[]};try{let n=t(e),i=JSON.parse((0,r.readFileSync)(n,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var s=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};exports.McpRuntimeOwner=s,exports.definitionsCachePath=a,exports.readCachedCatalog=o;
2
2
  //# sourceMappingURL=mcpRuntime.cjs.map
@@ -1,2 +1,2 @@
1
- import{createHash as e}from"node:crypto";import{readFileSync as t}from"node:fs";import{join as n,resolve as r}from"node:path";import{homedir as i}from"node:os";function a(e){return r(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function o(t){if(t.length===0)throw Error(`definitionsCachePath requires at least one config path`);let o=a(t[0]),s=t.length===1?``:`.${e(`sha256`).update(t.map(e=>r(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return n(i(),`.mcp-proxy`,`${o}${s}.definitions-cache.json`)}function s(e,n=o){if(e.length===0)return{servers:[]};try{let r=n(e),i=JSON.parse(t(r,`utf8`));return{servers:Object.values(i.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var c=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};export{c as McpRuntimeOwner,o as definitionsCachePath,s as readCachedCatalog};
1
+ import{homedir as e}from"node:os";import{join as t,resolve as n}from"node:path";import{createHash as r}from"node:crypto";import{readFileSync as i}from"node:fs";function a(e){return n(e).replace(/^([A-Z]):/,(e,t)=>`${t.toLowerCase()}:`).replace(/[^a-zA-Z0-9._-]+/g,`_`).replace(/^_+|_+$/g,``)}function o(i){if(i.length===0)throw Error(`definitionsCachePath requires at least one config path`);let o=a(i[0]),s=i.length===1?``:`.${r(`sha256`).update(i.map(e=>n(e)).join(`\0`)).digest(`hex`).slice(0,12)}`;return t(e(),`.mcp-proxy`,`${o}${s}.definitions-cache.json`)}function s(e,t=o){if(e.length===0)return{servers:[]};try{let n=t(e),r=JSON.parse(i(n,`utf8`));return{servers:Object.values(r.servers??{}).map(e=>({name:e.serverName,tools:e.tools??[]}))}}catch{return{servers:[]}}}var c=class{generation=0;active;unsubscribe;get currentGeneration(){return this.generation}getServices(){return this.active}isCurrent(e){return e!==void 0&&e===this.active}async start(e){let t=++this.generation;if(await this.retire(),e.configPaths.length===0)return;let{createProxyContainer:n}=await import(`@agimon-ai/mcp-proxy`),r=await n({configFilePath:e.configPaths,startupMode:`background`,auth:{tokenStore:e.tokenStore,onAuthorizationUrl:e.onAuthorizationUrl}});if(t!==this.generation){await r.dispose().catch(()=>void 0);return}this.active=r;let i=e.onServerStateChange;return i&&(this.unsubscribe=r.clientManager.onServerStateChange(e=>{t===this.generation&&i(e)})),{generation:t,services:r,connectionsSettled:r.connectionsSettled}}async dispose(){++this.generation,await this.retire()}async retire(){this.unsubscribe?.(),this.unsubscribe=void 0;let e=this.active;this.active=void 0,e&&await e.dispose().catch(()=>void 0)}};export{c as McpRuntimeOwner,o as definitionsCachePath,s as readCachedCatalog};
2
2
  //# sourceMappingURL=mcpRuntime.mjs.map
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:os");n=e.__toESM(n,1);const r=`DOOM_MCP_SESSION`;function i(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function a(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,r={},a=i(t),o=i(n);return a&&(r.servers=a),o&&(r.proxy=o),r.servers||r.proxy?r:void 0}function o(e=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:t.default.join(n.default.tmpdir(),`doom-mcp`)},c=e[r];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=i(d),g=a(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function s(e){return{[r]:JSON.stringify(e)}}exports.SESSION_ENV_VAR=r,exports.readSessionConfig=o,exports.sessionConfigEnvironment=s;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:os");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);const r=`DOOM_MCP_SESSION`;function i(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function a(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,r={},a=i(t),o=i(n);return a&&(r.servers=a),o&&(r.proxy=o),r.servers||r.proxy?r:void 0}function o(e=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:n.default.join(t.default.tmpdir(),`doom-mcp`)},c=e[r];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=i(d),g=a(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function s(e){return{[r]:JSON.stringify(e)}}exports.SESSION_ENV_VAR=r,exports.readSessionConfig=o,exports.sessionConfigEnvironment=s;
2
2
  //# sourceMappingURL=sessionConfig.cjs.map
@@ -1,2 +1,2 @@
1
- import e from"node:path";import t from"node:os";const n=`DOOM_MCP_SESSION`;function r(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function i(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,i={},a=r(t),o=r(n);return a&&(i.servers=a),o&&(i.proxy=o),i.servers||i.proxy?i:void 0}function a(a=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:e.join(t.tmpdir(),`doom-mcp`)},c=a[n];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=r(d),g=i(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function o(e){return{[n]:JSON.stringify(e)}}export{n as SESSION_ENV_VAR,a as readSessionConfig,o as sessionConfigEnvironment};
1
+ import e from"node:os";import t from"node:path";const n=`DOOM_MCP_SESSION`;function r(e){if(!Array.isArray(e))return;let t=e.filter(e=>typeof e==`string`&&e.length>0);return t.length>0?t:void 0}function i(e){if(typeof e!=`object`||!e)return;let{servers:t,proxy:n}=e,i={},a=r(t),o=r(n);return a&&(i.servers=a),o&&(i.proxy=o),i.servers||i.proxy?i:void 0}function a(a=process.env,o=process.cwd()){let s={repoRoot:o,stagingDirectory:t.join(e.tmpdir(),`doom-mcp`)},c=a[n];if(!c)return s;let l;try{l=JSON.parse(c)}catch{return s}if(typeof l!=`object`||!l)return s;let{repoRoot:u,pluginConfigPaths:d,allowlist:f,stagingDirectory:p}=l,m=typeof u==`string`&&u?u:s.repoRoot,h=r(d),g=i(f);return{repoRoot:m,stagingDirectory:typeof p==`string`&&p?p:s.stagingDirectory,...h?{pluginConfigPaths:h}:{},...g?{allowlist:g}:{}}}function o(e){return{[n]:JSON.stringify(e)}}export{n as SESSION_ENV_VAR,a as readSessionConfig,o as sessionConfigEnvironment};
2
2
  //# sourceMappingURL=sessionConfig.mjs.map
@@ -1,2 +1,2 @@
1
- let e=require("@earendil-works/pi-tui"),t=require("@agimon-ai/doompi-ui/components/doomOverlay");const n=[[`↑↓`,`server`],[`⇧JK`,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],r=n.map(([e,t])=>`${e} ${t}`).join(` · `),i=[`tools`,`resources`],a=[`auth`,`disable`,`enable`],o={auth:`a`,disable:`d`,enable:`e`},s={auth:`reconnect`,disable:`session`,enable:void 0},c=new Map(a.map(e=>[o[e],e]));function l(e){return e?e.enabled?{auth:e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function u(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function d(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function f(e){return e.enabled?e.state:`disabled`}function p(t,n){let r=(0,e.truncateToWidth)(t,Math.max(0,n),`…`);return r+` `.repeat(Math.max(0,n-(0,e.visibleWidth)(r)))}function m(t,n,r){let i=(0,e.visibleWidth)(n),a=Math.max(0,r-i-1);return p(t,a)+` `.repeat(Math.max(1,r-a-i))+p(n,i)}function h(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}var g=class extends t.DoomOverlay{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · waiting for you to open the URL`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(t){if((0,e.matchesKey)(t,`escape`)||(0,e.matchesKey)(t,`ctrl+c`)||(0,e.matchesKey)(t,`q`)){this.done(void 0);return}if((0,e.matchesKey)(t,e.Key.shift(`k`)))return this.scrollDetail(-1);if((0,e.matchesKey)(t,e.Key.shift(`j`)))return this.scrollDetail(1);if((0,e.matchesKey)(t,`up`)||(0,e.matchesKey)(t,`k`))return this.moveSelection(-1);if((0,e.matchesKey)(t,`down`)||(0,e.matchesKey)(t,`j`))return this.moveSelection(1);if((0,e.matchesKey)(t,`pageUp`))return this.scrollDetail(-this.detailViewportHeight);if((0,e.matchesKey)(t,`pageDown`))return this.scrollDetail(this.detailViewportHeight);if((0,e.matchesKey)(t,`tab`))return this.toggleTab();if((0,e.matchesKey)(t,`ctrl+r`))return this.loadResources({refresh:!0});if((0,e.matchesKey)(t,`r`))return this.reload();let n=c.get(t.toLowerCase());n&&this.triggerControl(n)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${_(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:_(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!l(t)[e]){this.setNotice(u(e,t));return}if(!t)return;if(e===`auth`){this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${_(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${_(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return m(`${a} ${d(n,this.theme)} ${o}`,this.theme.fg(`dim`,f(n)),e)})}authorizationLines(e,t){let n=Math.max(1,t-2),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return[this.theme.fg(`accent`,p(` Open this to authorize:`,t)),...r.map(e=>this.theme.fg(`text`,p(` ${e}`,t)))]}field(e,t,n){let r=Math.max(1,n-10);return h(t,r).map((t,n)=>`${this.theme.fg(`dim`,p(n===0?e:``,10))}${p(t,r)}`)}controlLine(t,n){let r=l(t),i=a.map(e=>{let t=s[e],n=`${e}${t?` (${t})`:``}`;return this.keyCap(o[e],n,r[e])});return(0,e.truncateToWidth)(` ${i.join(` `)}`,n)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return m(` ${i.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,p(` ${r.toolName}`,t))),r.description)for(let e of h(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${f(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return h(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[m(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,p(` ${e.uri}`,t))),e.description)for(let r of h(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[f(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:t.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:r,footerHints:n}}noticeRow(e){return this.theme.fg(`warning`,p(` ${this.actionNotice??``}`,e))}renderBody(t,n){let r=this.actionNotice?Math.max(1,n-1):n,i=this.actionNotice?[this.noticeRow(t)]:[];if(this.bodyHeight=r,t<48)return[...this.renderStacked(t,r),...i];let a=Math.max(18,Math.floor((t-1)*.3333333333333333)),o=Math.max(1,t-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(n,r)=>{let i=`${p(l[r]??``,s)} ${d} ${p(u[r]??``,c)}`;return(0,e.truncateToWidth)(i,t,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function _(e){return e instanceof Error?e.message:String(e)}async function v(e,n){await e.ui.custom((e,t,r,i)=>new g(e,t,n,i),t.DOOM_FULLSCREEN_UI_OPTIONS)}exports.McpOverlayComponent=g,exports.openMcpOverlay=v;
1
+ let e=require("@earendil-works/pi-tui"),t=require("@agimon-ai/doompi-ui/components/doomOverlay");const n=[[t.DOOM_NAVIGATION_KEYS.list,`server`],[t.DOOM_NAVIGATION_KEYS.detail,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],r=n.map(([e,t])=>`${e} ${t}`).join(` · `),i=`\x1B]8;;`,a=`${i}`,o=[`tools`,`resources`],s=[`auth`,`disable`,`enable`],c={auth:`a`,disable:`d`,enable:`e`},l={auth:`reconnect`,disable:`session`,enable:void 0},u=new Map(s.map(e=>[c[e],e]));function d(e){return e?e.enabled?{auth:e.authorizationUrl!==void 0||e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function f(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function p(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function m(e){return e.enabled?e.state:`disabled`}function h(t,n){let r=(0,e.truncateToWidth)(t,Math.max(0,n),`…`);return r+` `.repeat(Math.max(0,n-(0,e.visibleWidth)(r)))}function g(t,n,r){let i=(0,e.visibleWidth)(n),a=Math.max(0,r-i-1);return h(t,a)+` `.repeat(Math.max(1,r-a-i))+h(n,i)}function _(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}function v(e,t){return`${i}${t}${e}${a}`}var y=class extends t.DoomOverlay{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · page ready · a reopens browser`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(t){if((0,e.matchesKey)(t,`escape`)||(0,e.matchesKey)(t,`ctrl+c`)||(0,e.matchesKey)(t,`q`)){this.done(void 0);return}if((0,e.matchesKey)(t,e.Key.shift(`k`))){this.scrollDetail(-1);return}if((0,e.matchesKey)(t,e.Key.shift(`j`))){this.scrollDetail(1);return}if((0,e.matchesKey)(t,`up`)||(0,e.matchesKey)(t,`k`)){this.moveSelection(-1);return}if((0,e.matchesKey)(t,`down`)||(0,e.matchesKey)(t,`j`)){this.moveSelection(1);return}if((0,e.matchesKey)(t,`pageUp`)){this.scrollDetail(-this.detailViewportHeight);return}if((0,e.matchesKey)(t,`pageDown`)){this.scrollDetail(this.detailViewportHeight);return}if((0,e.matchesKey)(t,`tab`)){this.toggleTab();return}if((0,e.matchesKey)(t,`ctrl+r`)){this.loadResources({refresh:!0});return}if((0,e.matchesKey)(t,`r`)){this.reload();return}let n=u.get(t.toLowerCase());n&&this.triggerControl(n)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${b(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:b(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!d(t)[e]){this.setNotice(f(e,t));return}if(!t)return;if(e===`auth`){t.authorizationUrl?this.openAuthorizationPage(t.name):this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${b(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${b(t)}`)}}async openAuthorizationPage(e){this.setNotice(`auth ${e} · opening browser…`);try{await this.target.openAuthorizationPage(e),this.disposed||this.setNotice(`auth ${e} · browser opened · waiting for approval`)}catch(t){this.disposed||this.setNotice(`auth ${e} · browser failed · ${b(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return g(`${a} ${p(n,this.theme)} ${o}`,this.theme.fg(`dim`,m(n)),e)})}authorizationLines(e,t){return[this.theme.fg(`accent`,h(` Authorization page ready`,t)),this.theme.fg(`text`,h(` ${v(`Open authorization page`,e)} · a reopens`,t))]}field(e,t,n){let r=Math.max(1,n-10);return _(t,r).map((t,n)=>`${this.theme.fg(`dim`,h(n===0?e:``,10))}${h(t,r)}`)}controlLine(t,n){let r=d(t),i=s.map(e=>{let n=e===`auth`&&t?.authorizationUrl?`reopen`:l[e],i=`${e}${n?` (${n})`:``}`;return this.keyCap(c[e],i,r[e])});return(0,e.truncateToWidth)(` ${i.join(` `)}`,n)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return g(` ${o.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,h(` ${r.toolName}`,t))),r.description)for(let e of _(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${m(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return _(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[g(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,h(` ${e.uri}`,t))),e.description)for(let r of _(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[m(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:t.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:r,footerHints:n}}noticeRow(e){return this.theme.fg(`warning`,h(` ${this.actionNotice??``}`,e))}renderBody(t,n){let r=this.actionNotice?Math.max(1,n-1):n,i=this.actionNotice?[this.noticeRow(t)]:[];if(this.bodyHeight=r,t<48)return[...this.renderStacked(t,r),...i];let a=Math.max(18,Math.floor((t-1)*.3333333333333333)),o=Math.max(1,t-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(n,r)=>{let i=`${h(l[r]??``,s)} ${d} ${h(u[r]??``,c)}`;return(0,e.truncateToWidth)(i,t,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function b(e){return e instanceof Error?e.message:String(e)}async function x(e,n){await e.ui.custom((e,t,r,i)=>new y(e,t,n,i),t.DOOM_FULLSCREEN_UI_OPTIONS)}exports.McpOverlayComponent=y,exports.openMcpOverlay=x;
2
2
  //# sourceMappingURL=mcpOverlay.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpOverlay.cjs","names":["truncateToWidth","visibleWidth","DoomOverlay","matchesKey","Key","DOOM_OVERLAY_ACCENT","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types/index.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'server'],\n ['⇧JK', 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects, so it is offered wherever a reconnect is meaningful, including\n * a server that has not been dialled yet. It is withheld only mid-connect, because\n * reauthorizing disconnects first and would tear down the attempt in flight.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching, it is waiting on\n // the person reading this, and the notice has to stop saying otherwise.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · waiting for you to open the URL`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.moveSelection(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.moveSelection(1);\n if (matchesKey(data, 'pageUp')) return this.scrollDetail(-this.detailViewportHeight);\n if (matchesKey(data, 'pageDown')) return this.scrollDetail(this.detailViewportHeight);\n if (matchesKey(data, 'tab')) return this.toggleTab();\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) return this.loadResources({ refresh: true });\n if (matchesKey(data, 'r')) return this.reload();\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * The authorization URL, wrapped across as many rows as it needs.\n *\n * Broken on width rather than truncated, and given the accent the controls use,\n * because it is the only thing in this overlay that cannot be acted on with a\n * keystroke: the user has to read it and open it somewhere else.\n */\n private authorizationLines(url: string, width: number): string[] {\n const indent = TOOL_INDENT;\n const body = Math.max(1, width - indent.length);\n const chunks: string[] = [];\n for (let start = 0; start < url.length; start += body) chunks.push(url.slice(start, start + body));\n return [\n this.theme.fg('accent', fit(`${indent}Open this to authorize:`, width)),\n ...chunks.map((chunk) => this.theme.fg('text', fit(`${indent}${chunk}`, width))),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The one thing on this pane the user has to act on outside the terminal, so\n // it is never abbreviated: a truncated URL cannot be opened.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"iGAoCA,MAUM,EAAgD,CACpD,CAAC,KAAM,QAAQ,EACf,CAAC,MAAO,QAAQ,EAChB,CAAC,MAAO,MAAM,EACd,CAAC,IAAK,MAAM,EACZ,CAAC,IAAK,SAAS,EACf,CAAC,IAAK,QAAQ,EACd,CAAC,IAAK,QAAQ,EACd,CAAC,MAAO,OAAO,CACjB,EAEM,EAAkB,EAAM,KAAK,CAAC,EAAK,KAAW,GAAG,EAAI,GAAG,GAAO,CAAC,CAAC,KAAK,KAAK,EAgB3E,EAAuC,CAAC,QAAS,WAAW,EAI5D,EAA0C,CAAC,OAAQ,UAAW,QAAQ,EACtE,EAA6C,CAAE,KAAM,IAAK,QAAS,IAAK,OAAQ,GAAI,EAEpF,EAA0D,CAC9D,KAAM,YACN,QAAS,UACT,OAAQ,IAAA,EACV,EACM,EAAiB,IAAI,IAA2B,EAAc,IAAK,GAAW,CAAC,EAAY,GAAS,CAAM,CAAC,CAAC,EASlH,SAAS,EAAoB,EAAmE,CAG9F,OAFK,EACA,EAAO,QACL,CAAE,KAAM,EAAO,QAAU,aAAc,QAAS,GAAM,OAAQ,EAAM,EAD/C,CAAE,KAAM,GAAO,QAAS,GAAO,OAAQ,EAAK,EADpD,CAAE,KAAM,GAAO,QAAS,GAAO,OAAQ,EAAM,CAGnE,CAEA,SAAS,EAAkB,EAAuB,EAA2C,CAI3F,OAHK,EACA,EAAO,QACR,IAAW,SAAiB,wBAAwB,EAAO,KAAK,qBAC7D,GAAG,EAAO,iBAAiB,EAAO,KAAK,MAAM,EAAO,QAF/B,GAAG,EAAO,iBAAiB,EAAO,KAAK,+BAD/C,GAAG,EAAO,qCAIhC,CAEA,SAAS,EAAY,EAAuB,EAAsB,CAOhE,OANK,EAAO,QACR,EAAO,QAAU,YAAoB,EAAM,GAAG,UAAW,GAAG,EAC5D,EAAO,QAAU,aAAqB,EAAM,GAAG,SAAU,GAAG,EAC5D,EAAO,QAAU,WAAmB,EAAM,GAAG,UAAW,GAAG,EAC3D,EAAO,QAAU,aAAqB,EAAM,GAAG,UAAW,GAAG,EAC7D,EAAO,QAAU,SAAiB,EAAM,GAAG,QAAS,GAAG,EACpD,EAAM,GAAG,QAAS,GAAG,EANA,EAAM,GAAG,MAAO,GAAG,CAOjD,CAGA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAO,QAAU,EAAO,MAAQ,UACzC,CAEA,SAAS,EAAI,EAAc,EAAuB,CAChD,IAAM,GAAA,EAAUA,EAAAA,gBAAAA,CAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,GAAQ,EAClE,OAAO,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,GAAA,EAAQC,EAAAA,aAAAA,CAAa,CAAO,CAAC,CAAC,CACxE,CAEA,SAAS,EAAa,EAAc,EAAe,EAAuB,CACxE,IAAM,GAAA,EAAaA,EAAAA,aAAAA,CAAa,CAAK,EAC/B,EAAY,KAAK,IAAI,EAAG,EAAQ,EAAa,CAAC,EACpD,OAAO,EAAI,EAAM,CAAS,EAAI,IAAI,OAAO,KAAK,IAAI,EAAG,EAAQ,EAAY,CAAU,CAAC,EAAI,EAAI,EAAO,CAAU,CAC/G,CAEA,SAAS,EAAK,EAAc,EAAyB,CACnD,GAAI,GAAS,EAAG,MAAO,CAAC,EACxB,IAAM,EAAkB,CAAC,EACrB,EAAU,GACd,IAAK,IAAM,KAAQ,EAAK,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAC7C,EAAQ,SAAW,EAAG,EAAU,EAC3B,EAAQ,OAAS,EAAI,EAAK,QAAU,EAAO,GAAW,IAAI,KAEjE,EAAM,KAAK,CAAO,EAClB,EAAU,GAId,OADI,EAAQ,OAAS,GAAG,EAAM,KAAK,CAAO,EACnC,CACT,CASA,IAAa,EAAb,cAAyCC,EAAAA,WAAY,CAiBhC,OACA,KAjBnB,QAA4C,CAAC,EAC7C,SAAmB,EACnB,aACA,UAAkC,QAClC,aAAuB,EACvB,gBAA0B,EAC1B,qBAA+B,EAC/B,WAAqB,EACrB,aACA,SAAmB,GACnB,cAAiC,IAAI,IACrC,YAEA,YACE,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAHC,KAAA,OAAA,EACA,KAAA,KAAA,EAGjB,KAAK,QAAQ,EACb,KAAK,YAAc,KAAK,OAAO,aAAe,CAC5C,GAAI,KAAK,SAAU,OACnB,KAAK,QAAQ,EAGb,IAAM,EAAS,KAAK,QAAQ,EACxB,GAAQ,mBAAkB,KAAK,aAAe,QAAQ,EAAO,KAAK,qCACtE,KAAK,IAAI,cAAc,CACzB,CAAC,CACH,CAEA,SAAgB,CACd,KAAK,SAAW,GAChB,KAAK,YAAY,CACnB,CAGA,SAAwB,CACtB,IAAM,EAAe,KAAK,QAAQ,KAAK,SAAS,EAAE,MAAQ,KAAK,aAC/D,KAAK,QAAU,KAAK,OAAO,WAAW,EACtC,IAAM,EAAY,EAAe,KAAK,QAAQ,UAAW,GAAW,EAAO,OAAS,CAAY,EAAI,GACpG,KAAK,SAAW,GAAa,EAAI,EAAY,KAAK,IAAI,KAAK,SAAU,KAAK,IAAI,EAAG,KAAK,QAAQ,OAAS,CAAC,CAAC,EACzG,KAAK,aAAe,KAAK,QAAQ,KAAK,SAAS,EAAE,IACnD,CAEA,SAA6C,CAC3C,OAAO,KAAK,QAAQ,KAAK,SAC3B,CAEA,YAAY,EAAoB,CAC9B,IAAA,EAAIC,EAAAA,WAAAA,CAAW,EAAM,QAAQ,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,GAAG,EAAG,CACrF,KAAK,KAAK,IAAA,EAAS,EACnB,MACF,CACA,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAMC,EAAAA,IAAI,MAAM,GAAG,CAAC,EAAG,OAAO,KAAK,aAAa,EAAE,EACjE,IAAA,EAAID,EAAAA,WAAAA,CAAW,EAAMC,EAAAA,IAAI,MAAM,GAAG,CAAC,EAAG,OAAO,KAAK,aAAa,CAAC,EAChE,IAAA,EAAID,EAAAA,WAAAA,CAAW,EAAM,IAAI,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,GAAG,EAAG,OAAO,KAAK,cAAc,EAAE,EACjF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,MAAM,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,GAAG,EAAG,OAAO,KAAK,cAAc,CAAC,EAClF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,OAAO,KAAK,aAAa,CAAC,KAAK,oBAAoB,EACnF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,UAAU,EAAG,OAAO,KAAK,aAAa,KAAK,oBAAoB,EACpF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,KAAK,EAAG,OAAO,KAAK,UAAU,EAGnD,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,OAAO,KAAK,cAAc,CAAE,QAAS,EAAK,CAAC,EAC3E,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,GAAG,EAAG,OAAO,KAAK,OAAO,EAC9C,IAAM,EAAS,EAAe,IAAI,EAAK,YAAY,CAAC,EAChD,GAAQ,KAAK,eAAe,CAAM,CACxC,CAEA,cAAsB,EAAqB,CACrC,KAAK,QAAQ,SAAW,IAC5B,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,QAAQ,OAAS,EAAG,KAAK,SAAW,CAAK,CAAC,EACpF,KAAK,aAAe,KAAK,QAAQ,KAAK,SAAS,EAAE,KACjD,KAAK,aAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,IAAI,cAAc,EACzB,CAEA,aAAqB,EAAqB,CACxC,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,gBAAkB,KAAK,oBAAoB,EAC9E,KAAK,aAAe,KAAK,IAAI,EAAG,KAAK,IAAI,EAAW,KAAK,aAAe,CAAK,CAAC,EAC9E,KAAK,IAAI,cAAc,CACzB,CAEA,WAA0B,CACxB,KAAK,UAAY,KAAK,YAAc,QAAU,YAAc,QAC5D,KAAK,aAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,IAAI,cAAc,CACzB,CAEA,QAAuB,CACrB,KAAK,UAAU,uBAAuB,EAGtC,KAAU,OACP,MAAM,CAAC,CACP,SAAW,CACL,KAAK,UAAU,KAAK,UAAU,uBAAuB,CAC5D,CAAC,CAAC,CACD,MAAO,GAAmB,CACpB,KAAK,UAAU,KAAK,UAAU,qBAAqB,EAAQ,CAAK,GAAG,CAC1E,CAAC,CACL,CAQA,oBAAmC,CACjC,GAAI,KAAK,YAAc,YAAa,OACpC,IAAM,EAAS,KAAK,QAAQ,EACxB,CAAC,GAAU,CAAC,EAAO,SAAW,EAAO,QAAU,aAC/C,KAAK,cAAc,IAAI,EAAO,IAAI,GACtC,KAAK,cAAc,CACrB,CAEA,cAAsB,EAAiC,CAAC,EAAS,CAC/D,IAAM,EAAS,KAAK,QAAQ,EACvB,IACL,KAAK,cAAc,IAAI,EAAO,KAAM,CAAE,OAAQ,SAAU,CAAC,EACzD,KAAK,IAAI,cAAc,EACvB,KAAU,OACP,cAAc,EAAO,KAAM,CAAO,CAAC,CACnC,KAAM,GAAU,CACX,KAAK,WACT,KAAK,cAAc,IAAI,EAAO,KAAM,CAAE,OAAQ,QAAS,OAAM,CAAC,EAC9D,KAAK,IAAI,cAAc,EACzB,CAAC,CAAC,CACD,MAAO,GAAmB,CACrB,KAAK,WACT,KAAK,cAAc,IAAI,EAAO,KAAM,CAAE,OAAQ,QAAS,MAAO,EAAQ,CAAK,CAAE,CAAC,EAC9E,KAAK,IAAI,cAAc,EACzB,CAAC,EACL,CAEA,eAAuB,EAA6B,CAClD,IAAM,EAAS,KAAK,QAAQ,EAC5B,GAAI,CAAC,EAAoB,CAAM,CAAC,CAAC,GAAS,CACxC,KAAK,UAAU,EAAkB,EAAQ,CAAM,CAAC,EAChD,MACF,CACA,GAAI,CAAC,EAAQ,OACb,GAAI,IAAW,OAAQ,CACrB,KAAU,UAAU,EAAO,IAAI,EAC/B,MACF,CACA,IAAM,EAAU,IAAW,SAC3B,GAAI,CACF,KAAK,OAAO,WAAW,EAAO,KAAM,CAAO,EAC3C,KAAK,QAAQ,EACb,KAAK,UAAU,GAAG,EAAO,GAAG,EAAO,KAAK,KAAK,EAAU,iBAAmB,mCAAmC,CAC/G,OAAS,EAAO,CACd,KAAK,UAAU,GAAG,EAAO,GAAG,EAAO,KAAK,cAAc,EAAQ,CAAK,GAAG,CACxE,CACF,CAEA,MAAc,UAAU,EAAmC,CACzD,KAAK,UAAU,QAAQ,EAAW,gBAAgB,EAClD,GAAI,CAEF,GADA,MAAM,KAAK,OAAO,YAAY,CAAU,EACpC,KAAK,SAAU,OAEnB,KAAK,cAAc,OAAO,CAAU,EACpC,KAAK,QAAQ,EACb,KAAK,UAAU,QAAQ,EAAW,cAAc,CAClD,OAAS,EAAO,CACd,GAAI,KAAK,SAAU,OACnB,KAAK,UAAU,QAAQ,EAAW,cAAc,EAAQ,CAAK,GAAG,CAClE,CACF,CAEA,UAAkB,EAAsB,CACtC,KAAK,aAAe,EACpB,KAAK,IAAI,cAAc,CACzB,CAEA,YAAoB,EAAyB,CAC3C,GAAI,KAAK,QAAQ,SAAW,EAAG,MAAO,CAAC,KAAK,MAAM,GAAG,MAAO,YAAY,CAAC,EACzE,IAAM,EAAQ,KAAK,IACjB,EACA,KAAK,IAAI,KAAK,SAAW,KAAK,WAAa,EAAG,KAAK,IAAI,EAAG,KAAK,QAAQ,OAAS,KAAK,UAAU,CAAC,CAClG,EACA,OAAO,KAAK,QAAQ,MAAM,EAAO,EAAQ,KAAK,UAAU,CAAC,CAAC,KAAK,EAAQ,IAAW,CAChF,IAAM,EAAQ,EAAQ,EAChB,EAAS,IAAU,KAAK,SAAW,KAAK,MAAM,GAAG,SAAU,GAAG,EAAI,IAGlE,EAAO,EAAO,QAChB,IAAU,KAAK,SACb,KAAK,MAAM,KAAK,EAAO,IAAI,EAC3B,EAAO,KACT,KAAK,MAAM,GAAG,MAAO,EAAO,IAAI,EAEpC,OAAO,EAAa,GADJ,EAAO,GAAG,EAAY,EAAQ,KAAK,KAAK,EAAE,GAAG,IACnC,KAAK,MAAM,GAAG,MAAO,EAAW,CAAM,CAAC,EAAG,CAAK,CAC3E,CAAC,CACH,CASA,mBAA2B,EAAa,EAAyB,CAC/D,IACM,EAAO,KAAK,IAAI,EAAG,EAAQ,CAAa,EACxC,EAAmB,CAAC,EAC1B,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAI,OAAQ,GAAS,EAAM,EAAO,KAAK,EAAI,MAAM,EAAO,EAAQ,CAAI,CAAC,EACjG,MAAO,CACL,KAAK,MAAM,GAAG,SAAU,EAAI,4BAAoC,CAAK,CAAC,EACtE,GAAG,EAAO,IAAK,GAAU,KAAK,MAAM,GAAG,OAAQ,EAAI,KAAY,IAAS,CAAK,CAAC,CAAC,CACjF,CACF,CAEA,MAAc,EAAe,EAAe,EAAyB,CACnE,IAAM,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAY,EACnD,OAAO,EAAK,EAAO,CAAU,CAAC,CAAC,KAC5B,EAAM,IAAU,GAAG,KAAK,MAAM,GAAG,MAAO,EAAI,IAAU,EAAI,EAAQ,GAAI,EAAY,CAAC,IAAI,EAAI,EAAM,CAAU,GAC9G,CACF,CASA,YAAoB,EAAmC,EAAuB,CAC5E,IAAM,EAAe,EAAoB,CAAM,EACzC,EAAQ,EAAc,IAAK,GAAW,CAC1C,IAAM,EAAO,EAAa,GACpB,EAAQ,GAAG,IAAS,EAAO,KAAK,EAAK,GAAK,KAChD,OAAO,KAAK,OAAO,EAAY,GAAS,EAAO,EAAa,EAAO,CACrE,CAAC,EACD,OAAA,EAAOH,EAAAA,gBAAAA,CAAgB,KAAK,EAAM,KAAK,KAAa,IAAK,CAAK,CAChE,CAGA,OAAe,EAAa,EAAe,EAAU,GAAc,CAIjE,MAAO,GAHK,EACR,KAAK,MAAM,GAAG,aAAc,KAAK,MAAM,GAAG,OAAQ,IAAI,EAAI,EAAE,CAAC,EAC7D,KAAK,MAAM,GAAG,MAAO,IAAI,EAAI,EAAE,IACnB,KAAK,MAAM,GAAG,MAAO,IAAI,GAAO,GAClD,CAEA,SAAiB,EAAmC,EAAuB,CACzE,IAAM,EAAS,GACb,IAAS,KAAK,UACV,KAAK,MAAM,GAAG,SAAU,KAAK,MAAM,KAAK,KAAK,EAAK,GAAG,CAAC,EACtD,KAAK,MAAM,GAAG,MAAO,KAAK,EAAK,GAAG,EAClC,EACJ,GAAU,KAAK,YAAc,QACzB,GAAG,EAAO,MAAM,OAAQ,GAAS,EAAK,MAAM,CAAC,CAAC,OAAO,GAAG,EAAO,MAAM,SACrE,iBACN,OAAO,EAAa,KAAK,EAAY,IAAI,CAAK,CAAC,CAAC,KAAK,EAAE,IAAK,KAAK,MAAM,GAAG,MAAO,CAAM,EAAG,CAAK,CACjG,CAEA,UAAkB,EAAuB,EAAyB,CAChE,GAAI,EAAO,MAAM,SAAW,EAAG,MAAO,CAAC,KAAK,MAAM,GAAG,MAAO,YAAwB,CAAC,EACrF,IAAM,EAAkB,CAAC,EACzB,IAAK,IAAM,KAAQ,EAAO,MAAO,CAG/B,IAAM,EAAS,EAAK,QAAU,EAAO,QAErC,GADA,EAAM,KAAK,KAAK,MAAM,GAAG,EAAS,OAAS,MAAO,EAAI,KAAiB,EAAK,WAAY,CAAK,CAAC,CAAC,EAC3F,EAAK,YACP,IAAK,IAAM,KAAQ,EAAK,EAAK,YAAa,KAAK,IAAI,EAAG,EAAQ,CAAsB,CAAC,EACnF,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,OAA+B,GAAM,CAAC,CAG5E,CACA,OAAO,CACT,CAEA,cAAsB,EAAuB,EAAyB,CACpE,IAAM,EAAO,KAAK,cAAc,IAAI,EAAO,IAAI,EAC/C,GAAI,CAAC,EACH,MAAO,CACL,KAAK,MAAM,GACT,MACA,KAAiB,EAAO,QAAU,aAAe,EAAO,QAAU,iCAAmC,GAAG,EAAO,KAAK,MAAM,EAAW,CAAM,EAAE,gCAC/I,CACF,EAEF,GAAI,EAAK,SAAW,UAAW,MAAO,CAAC,KAAK,MAAM,GAAG,MAAO,sBAAkC,CAAC,EAC/F,GAAI,EAAK,SAAW,QAClB,OAAO,EACL,6BAA6B,EAAK,OAAS,kBAC3C,KAAK,IAAI,EAAG,EAAQ,CAAkB,CACxC,CAAC,CAAC,IAAK,GAAS,KAAK,MAAM,GAAG,UAAW,KAAiB,GAAM,CAAC,EAEnE,IAAM,EAAQ,EAAK,OAAS,CAAC,EAE7B,OADI,EAAM,SAAW,EAAU,CAAC,KAAK,MAAM,GAAG,MAAO,gBAA4B,CAAC,EAC3E,EAAM,QAAS,GAAa,CACjC,IAAM,EAAQ,CACZ,EACE,KAAiB,EAAS,MAAQ,EAAS,MAC3C,KAAK,MAAM,GAAG,MAAO,EAAS,UAAY,EAAE,EAC5C,CACF,CACF,EAEA,GADI,EAAS,MAAM,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,EAAI,OAA+B,EAAS,MAAO,CAAK,CAAC,CAAC,EACzG,EAAS,YACX,IAAK,IAAM,KAAQ,EAAK,EAAS,YAAa,KAAK,IAAI,EAAG,EAAQ,CAAsB,CAAC,EACvF,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,OAA+B,GAAM,CAAC,EAG1E,OAAO,CACT,CAAC,CACH,CAEA,YAAoB,EAAe,EAA0B,CAC3D,IAAM,EAAS,KAAK,QAAQ,EAC5B,GAAI,CAAC,EAAQ,MAAO,CAAC,KAAK,MAAM,GAAG,MAAO,iDAAa,CAAC,EAExD,IAAM,EAAU,CACd,EAAW,CAAM,EACjB,GAAG,EAAO,MAAM,OAAO,GAAG,EAAO,MAAM,SAAW,EAAI,OAAS,UAC/D,GAAG,EAAO,cAAc,KAC1B,CAAC,CAAC,KAAK,KAAK,EACN,EAAS,CACb,KAAK,MAAM,KAAK,EAAO,IAAI,EAC3B,GACA,GAAG,KAAK,MAAM,QAAS,EAAS,CAAK,EACrC,GAAG,KAAK,MAAM,QAAS,EAAO,OAAS,IAAgB,CAAK,EAG5D,GAAI,EAAO,iBAAmB,CAAC,GAAI,GAAG,KAAK,mBAAmB,EAAO,iBAAkB,CAAK,CAAC,EAAI,CAAC,EAClG,GACA,KAAK,YAAY,EAAQ,CAAK,EAC9B,GACA,KAAK,SAAS,EAAQ,CAAK,EAC3B,EACF,EAEM,EAAO,KAAK,YAAc,QAAU,KAAK,UAAU,EAAQ,CAAK,EAAI,KAAK,cAAc,EAAQ,CAAK,EAC1G,KAAK,gBAAkB,EAAK,OAC5B,KAAK,qBAAuB,KAAK,IAAI,EAAG,EAAS,EAAO,MAAM,EAC9D,IAAM,EAAQ,KAAK,IAAI,KAAK,aAAc,KAAK,IAAI,EAAG,EAAK,OAAS,KAAK,oBAAoB,CAAC,EAC9F,MAAO,CAAC,GAAG,EAAQ,GAAG,EAAK,MAAM,EAAO,EAAQ,KAAK,oBAAoB,CAAC,CAC5E,CAEA,WAAyC,CACvC,IAAM,EAAY,KAAK,QAAQ,OAAQ,GAAW,EAAO,SAAW,EAAO,QAAU,WAAW,CAAC,CAAC,OAClG,MAAO,CACL,MAAO,MACP,OAAQK,EAAAA,oBACR,WAAY,oBACZ,YAAa,GAAG,EAAU,GAAG,KAAK,QAAQ,OAAO,wBAGjD,OAAQ,EACR,YAAa,CACf,CACF,CASA,UAAkB,EAAuB,CACvC,OAAO,KAAK,MAAM,GAAG,UAAW,EAAI,KAAK,KAAK,cAAgB,KAAM,CAAK,CAAC,CAC5E,CAEA,WAAqB,EAAe,EAA0B,CAC5D,IAAM,EAAa,KAAK,aAAe,KAAK,IAAI,EAAG,EAAS,CAAC,EAAI,EAC3D,EAAS,KAAK,aAAe,CAAC,KAAK,UAAU,CAAK,CAAC,EAAI,CAAC,EAE9D,GADA,KAAK,WAAa,EACd,EAAQ,GAAW,MAAO,CAAC,GAAG,KAAK,cAAc,EAAO,CAAU,EAAG,GAAG,CAAM,EAElF,IAAM,EAAY,KAAK,IAAI,GAAgB,KAAK,OAAO,EAAQ,GAAK,iBAAiB,CAAC,EAChF,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAY,CAAC,EAC9C,EAAc,KAAK,IAAI,EAAG,EAAY,CAAW,EACjD,EAAe,KAAK,IAAI,EAAG,EAAa,CAAW,EACnD,EAAO,KAAK,YAAY,CAAW,EACnC,EAAQ,KAAK,YAAY,EAAc,CAAU,EAEjD,EAAU,KAAK,MAAM,GAAG,cAAe,GAAG,EAKhD,MAAO,CAAC,GAJK,MAAM,KAAK,CAAE,OAAQ,CAAW,GAAI,EAAG,IAAU,CAC5D,IAAM,EAAM,GAAG,EAAI,EAAK,IAAU,GAAI,CAAW,EAAE,GAAG,EAAQ,GAAG,EAAI,EAAM,IAAU,GAAI,CAAY,IACrG,OAAA,EAAOL,EAAAA,gBAAAA,CAAgB,EAAK,EAAO,GAAQ,CAC7C,CACc,EAAG,GAAG,CAAM,CAC5B,CAEA,cAAsB,EAAe,EAA0B,CAC7D,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,EAAS,GAAK,CAAC,CAAC,EACpD,EAAe,KAAK,IAAI,EAAG,EAAS,EAAY,CAAC,EACvD,KAAK,WAAa,EAClB,IAAM,EAAM,KAAK,YAAY,CAAK,EAC5B,EAAS,KAAK,YAAY,EAAO,CAAY,EACnD,MAAO,CACL,GAAG,EAAI,MAAM,EAAG,CAAS,EACzB,GAAG,MAAM,KAAK,CAAE,OAAQ,KAAK,IAAI,EAAG,EAAY,EAAI,MAAM,CAAE,MAAS,EAAE,EACvE,KAAK,MAAM,GAAG,cAAe,IAAI,OAAO,CAAK,CAAC,EAC9C,GAAG,EAAO,MAAM,EAAG,CAAY,CACjC,CACF,CACF,EAEA,SAAS,EAAQ,EAAwB,CACvC,OAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAEA,eAAsB,EAAe,EAAuB,EAAyC,CACnG,MAAM,EAAI,GAAG,QACV,EAAK,EAAO,EAAc,IAAS,IAAI,EAAoB,EAAK,EAAO,EAAQ,CAAI,EACpFM,EAAAA,0BACF,CACF"}
1
+ {"version":3,"file":"mcpOverlay.cjs","names":["DOOM_NAVIGATION_KEYS","truncateToWidth","visibleWidth","DoomOverlay","matchesKey","Key","DOOM_OVERLAY_ACCENT","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_NAVIGATION_KEYS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types/index.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n [DOOM_NAVIGATION_KEYS.list, 'server'],\n [DOOM_NAVIGATION_KEYS.detail, 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\nconst OSC_LINK_OPEN = '\\u001B]8;;';\nconst OSC_LINK_SEPARATOR = '\\u0007';\nconst OSC_LINK_CLOSE = `${OSC_LINK_OPEN}${OSC_LINK_SEPARATOR}`;\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects wherever that is meaningful, including a server that has not\n * been dialled yet. Mid-connect it is withheld until the flow has a reserved URL;\n * from then on the same key safely reopens that page without reconnecting.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.authorizationUrl !== undefined || server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** A compact clickable fallback whose visible width does not include its target URL. */\nfunction terminalLink(label: string, url: string): string {\n return `${OSC_LINK_OPEN}${url}${OSC_LINK_SEPARATOR}${label}${OSC_LINK_CLOSE}`;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching. Explicit auth\n // opens it automatically, while the same key now reopens the reserved page.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · page ready · a reopens browser`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) {\n this.scrollDetail(-1);\n return;\n }\n if (matchesKey(data, Key.shift('j'))) {\n this.scrollDetail(1);\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) {\n this.moveSelection(-1);\n return;\n }\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) {\n this.moveSelection(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.scrollDetail(-this.detailViewportHeight);\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.scrollDetail(this.detailViewportHeight);\n return;\n }\n if (matchesKey(data, 'tab')) {\n this.toggleTab();\n return;\n }\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) {\n this.loadResources({ refresh: true });\n return;\n }\n if (matchesKey(data, 'r')) {\n this.reload();\n return;\n }\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n if (server.authorizationUrl) void this.openAuthorizationPage(server.name);\n else void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private async openAuthorizationPage(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · opening browser…`);\n try {\n await this.target.openAuthorizationPage(serverName);\n if (!this.disposed) this.setNotice(`auth ${serverName} · browser opened · waiting for approval`);\n } catch (cause) {\n if (!this.disposed) this.setNotice(`auth ${serverName} · browser failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * A single-line fallback for the page that explicit auth already opened.\n *\n * The full URL lives in an OSC 8 target rather than printable layout cells, so a\n * long OAuth state token cannot wrap through pane borders and become corrupted.\n * Terminals without clickable links still have the same `a` key to reopen it.\n */\n private authorizationLines(url: string, width: number): string[] {\n return [\n this.theme.fg('accent', fit(`${TOOL_INDENT}Authorization page ready`, width)),\n this.theme.fg('text', fit(`${TOOL_INDENT}${terminalLink('Open authorization page', url)} · a reopens`, width)),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = action === 'auth' && server?.authorizationUrl ? 'reopen' : CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The compact label is clickable, while `a` reopens the same reserved URL.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{Key as e,matchesKey as t,truncateToWidth as n,visibleWidth as r}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as i,DOOM_OVERLAY_ACCENT as a,DoomOverlay as o}from"@agimon-ai/doompi-ui/components/doomOverlay";const s=[[`↑↓`,`server`],[`⇧JK`,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],c=s.map(([e,t])=>`${e} ${t}`).join(` · `),l=[`tools`,`resources`],u=[`auth`,`disable`,`enable`],d={auth:`a`,disable:`d`,enable:`e`},f={auth:`reconnect`,disable:`session`,enable:void 0},p=new Map(u.map(e=>[d[e],e]));function m(e){return e?e.enabled?{auth:e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function h(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function g(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function _(e){return e.enabled?e.state:`disabled`}function v(e,t){let i=n(e,Math.max(0,t),`…`);return i+` `.repeat(Math.max(0,t-r(i)))}function y(e,t,n){let i=r(t),a=Math.max(0,n-i-1);return v(e,a)+` `.repeat(Math.max(1,n-a-i))+v(t,i)}function b(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}var x=class extends o{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · waiting for you to open the URL`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(n){if(t(n,`escape`)||t(n,`ctrl+c`)||t(n,`q`)){this.done(void 0);return}if(t(n,e.shift(`k`)))return this.scrollDetail(-1);if(t(n,e.shift(`j`)))return this.scrollDetail(1);if(t(n,`up`)||t(n,`k`))return this.moveSelection(-1);if(t(n,`down`)||t(n,`j`))return this.moveSelection(1);if(t(n,`pageUp`))return this.scrollDetail(-this.detailViewportHeight);if(t(n,`pageDown`))return this.scrollDetail(this.detailViewportHeight);if(t(n,`tab`))return this.toggleTab();if(t(n,`ctrl+r`))return this.loadResources({refresh:!0});if(t(n,`r`))return this.reload();let r=p.get(n.toLowerCase());r&&this.triggerControl(r)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${S(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:S(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!m(t)[e]){this.setNotice(h(e,t));return}if(!t)return;if(e===`auth`){this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${S(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${S(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return y(`${a} ${g(n,this.theme)} ${o}`,this.theme.fg(`dim`,_(n)),e)})}authorizationLines(e,t){let n=Math.max(1,t-2),r=[];for(let t=0;t<e.length;t+=n)r.push(e.slice(t,t+n));return[this.theme.fg(`accent`,v(` Open this to authorize:`,t)),...r.map(e=>this.theme.fg(`text`,v(` ${e}`,t)))]}field(e,t,n){let r=Math.max(1,n-10);return b(t,r).map((t,n)=>`${this.theme.fg(`dim`,v(n===0?e:``,10))}${v(t,r)}`)}controlLine(e,t){let r=m(e),i=u.map(e=>{let t=f[e],n=`${e}${t?` (${t})`:``}`;return this.keyCap(d[e],n,r[e])});return n(` ${i.join(` `)}`,t)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return y(` ${l.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,v(` ${r.toolName}`,t))),r.description)for(let e of b(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${_(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return b(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[y(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,v(` ${e.uri}`,t))),e.description)for(let r of b(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[_(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:a,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:c,footerHints:s}}noticeRow(e){return this.theme.fg(`warning`,v(` ${this.actionNotice??``}`,e))}renderBody(e,t){let r=this.actionNotice?Math.max(1,t-1):t,i=this.actionNotice?[this.noticeRow(e)]:[];if(this.bodyHeight=r,e<48)return[...this.renderStacked(e,r),...i];let a=Math.max(18,Math.floor((e-1)*.3333333333333333)),o=Math.max(1,e-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(t,r)=>{let i=`${v(l[r]??``,s)} ${d} ${v(u[r]??``,c)}`;return n(i,e,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function S(e){return e instanceof Error?e.message:String(e)}async function C(e,t){await e.ui.custom((e,n,r,i)=>new x(e,n,t,i),i)}export{x as McpOverlayComponent,C as openMcpOverlay};
1
+ import{Key as e,matchesKey as t,truncateToWidth as n,visibleWidth as r}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as i,DOOM_NAVIGATION_KEYS as a,DOOM_OVERLAY_ACCENT as o,DoomOverlay as s}from"@agimon-ai/doompi-ui/components/doomOverlay";const c=[[a.list,`server`],[a.detail,`scroll`],[`tab`,`pane`],[`a`,`auth`],[`d`,`disable`],[`e`,`enable`],[`r`,`reload`],[`esc`,`close`]],l=c.map(([e,t])=>`${e} ${t}`).join(` · `),u=`\x1B]8;;`,d=`${u}`,f=[`tools`,`resources`],p=[`auth`,`disable`,`enable`],m={auth:`a`,disable:`d`,enable:`e`},h={auth:`reconnect`,disable:`session`,enable:void 0},g=new Map(p.map(e=>[m[e],e]));function _(e){return e?e.enabled?{auth:e.authorizationUrl!==void 0||e.state!==`connecting`,disable:!0,enable:!1}:{auth:!1,disable:!1,enable:!0}:{auth:!1,disable:!1,enable:!1}}function v(e,t){return t?t.enabled?e===`enable`?`enable unavailable · ${t.name} is already enabled`:`${e} unavailable · ${t.name} is ${t.state}`:`${e} unavailable · ${t.name} is disabled for this session`:`${e} unavailable · no server is selected`}function y(e,t){return e.enabled?e.state===`connected`?t.fg(`success`,`●`):e.state===`connecting`?t.fg(`accent`,`◐`):e.state===`degraded`?t.fg(`warning`,`▲`):e.state===`needs-auth`?t.fg(`warning`,`✗`):e.state===`failed`?t.fg(`error`,`✗`):t.fg(`muted`,`◦`):t.fg(`dim`,`■`)}function b(e){return e.enabled?e.state:`disabled`}function x(e,t){let i=n(e,Math.max(0,t),`…`);return i+` `.repeat(Math.max(0,t-r(i)))}function S(e,t,n){let i=r(t),a=Math.max(0,n-i-1);return x(e,a)+` `.repeat(Math.max(1,n-a-i))+x(t,i)}function C(e,t){if(t<=0)return[];let n=[],r=``;for(let i of e.split(/\s+/).filter(Boolean))r.length===0?r=i:r.length+1+i.length<=t?r+=` ${i}`:(n.push(r),r=i);return r.length>0&&n.push(r),n}function w(e,t){return`${u}${t}${e}${d}`}var T=class extends s{target;done;servers=[];selected=0;selectedName;detailTab=`tools`;detailScroll=0;detailLineCount=0;detailViewportHeight=8;bodyHeight=8;actionNotice;disposed=!1;resourcePanes=new Map;unsubscribe;constructor(e,t,n,r){super(e,t),this.target=n,this.done=r,this.refresh(),this.unsubscribe=this.target.onChange(()=>{if(this.disposed)return;this.refresh();let e=this.current();e?.authorizationUrl&&(this.actionNotice=`auth ${e.name} · page ready · a reopens browser`),this.tui.requestRender()})}dispose(){this.disposed=!0,this.unsubscribe()}refresh(){let e=this.servers[this.selected]?.name??this.selectedName;this.servers=this.target.getServers();let t=e?this.servers.findIndex(t=>t.name===e):-1;this.selected=t>=0?t:Math.min(this.selected,Math.max(0,this.servers.length-1)),this.selectedName=this.servers[this.selected]?.name}current(){return this.servers[this.selected]}handleInput(n){if(t(n,`escape`)||t(n,`ctrl+c`)||t(n,`q`)){this.done(void 0);return}if(t(n,e.shift(`k`))){this.scrollDetail(-1);return}if(t(n,e.shift(`j`))){this.scrollDetail(1);return}if(t(n,`up`)||t(n,`k`)){this.moveSelection(-1);return}if(t(n,`down`)||t(n,`j`)){this.moveSelection(1);return}if(t(n,`pageUp`)){this.scrollDetail(-this.detailViewportHeight);return}if(t(n,`pageDown`)){this.scrollDetail(this.detailViewportHeight);return}if(t(n,`tab`)){this.toggleTab();return}if(t(n,`ctrl+r`)){this.loadResources({refresh:!0});return}if(t(n,`r`)){this.reload();return}let r=g.get(n.toLowerCase());r&&this.triggerControl(r)}moveSelection(e){this.servers.length!==0&&(this.selected=Math.max(0,Math.min(this.servers.length-1,this.selected+e)),this.selectedName=this.servers[this.selected]?.name,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender())}scrollDetail(e){let t=Math.max(0,this.detailLineCount-this.detailViewportHeight);this.detailScroll=Math.max(0,Math.min(t,this.detailScroll+e)),this.tui.requestRender()}toggleTab(){this.detailTab=this.detailTab===`tools`?`resources`:`tools`,this.detailScroll=0,this.maybeLoadResources(),this.tui.requestRender()}reload(){this.setNotice(`reload · dispatching…`),this.target.start().then(()=>{this.disposed||this.setNotice(`reload · reconnecting`)}).catch(e=>{this.disposed||this.setNotice(`reload · failed · ${E(e)}`)})}maybeLoadResources(){if(this.detailTab!==`resources`)return;let e=this.current();!e||!e.enabled||e.state!==`connected`||this.resourcePanes.has(e.name)||this.loadResources()}loadResources(e={}){let t=this.current();t&&(this.resourcePanes.set(t.name,{status:`loading`}),this.tui.requestRender(),this.target.listResources(t.name,e).then(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`ready`,items:e}),this.tui.requestRender())}).catch(e=>{this.disposed||(this.resourcePanes.set(t.name,{status:`error`,error:E(e)}),this.tui.requestRender())}))}triggerControl(e){let t=this.current();if(!_(t)[e]){this.setNotice(v(e,t));return}if(!t)return;if(e===`auth`){t.authorizationUrl?this.openAuthorizationPage(t.name):this.authorize(t.name);return}let n=e===`enable`;try{this.target.setEnabled(t.name,n),this.refresh(),this.setNotice(`${e} ${t.name} · ${n?`tools restored`:`tools withheld for this session`}`)}catch(n){this.setNotice(`${e} ${t.name} · failed · ${E(n)}`)}}async authorize(e){this.setNotice(`auth ${e} · dispatching…`);try{if(await this.target.reauthorize(e),this.disposed)return;this.resourcePanes.delete(e),this.refresh(),this.setNotice(`auth ${e} · authorized`)}catch(t){if(this.disposed)return;this.setNotice(`auth ${e} · failed · ${E(t)}`)}}async openAuthorizationPage(e){this.setNotice(`auth ${e} · opening browser…`);try{await this.target.openAuthorizationPage(e),this.disposed||this.setNotice(`auth ${e} · browser opened · waiting for approval`)}catch(t){this.disposed||this.setNotice(`auth ${e} · browser failed · ${E(t)}`)}}setNotice(e){this.actionNotice=e,this.tui.requestRender()}rosterLines(e){if(this.servers.length===0)return[this.theme.fg(`dim`,`No servers`)];let t=Math.max(0,Math.min(this.selected-this.bodyHeight+1,Math.max(0,this.servers.length-this.bodyHeight)));return this.servers.slice(t,t+this.bodyHeight).map((n,r)=>{let i=t+r,a=i===this.selected?this.theme.fg(`accent`,`›`):` `,o=n.enabled?i===this.selected?this.theme.bold(n.name):n.name:this.theme.fg(`dim`,n.name);return S(`${a} ${y(n,this.theme)} ${o}`,this.theme.fg(`dim`,b(n)),e)})}authorizationLines(e,t){return[this.theme.fg(`accent`,x(` Authorization page ready`,t)),this.theme.fg(`text`,x(` ${w(`Open authorization page`,e)} · a reopens`,t))]}field(e,t,n){let r=Math.max(1,n-10);return C(t,r).map((t,n)=>`${this.theme.fg(`dim`,x(n===0?e:``,10))}${x(t,r)}`)}controlLine(e,t){let r=_(e),i=p.map(t=>{let n=t===`auth`&&e?.authorizationUrl?`reopen`:h[t],i=`${t}${n?` (${n})`:``}`;return this.keyCap(m[t],i,r[t])});return n(` ${i.join(` `)}`,t)}keyCap(e,t,n=!0){return`${n?this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${e} `)):this.theme.fg(`dim`,` ${e} `)}${this.theme.fg(`dim`,` ${t}`)}`}tabStrip(e,t){let n=e=>e===this.detailTab?this.theme.fg(`accent`,this.theme.bold(`[ ${e} ]`)):this.theme.fg(`dim`,` ${e} `),r=e&&this.detailTab===`tools`?`${e.tools.filter(e=>e.active).length}/${e.tools.length}`:`ctrl+r refresh`;return S(` ${f.map(n).join(``)}`,this.theme.fg(`dim`,r),t)}toolLines(e,t){if(e.tools.length===0)return[this.theme.fg(`dim`,` No tools`)];let n=[];for(let r of e.tools){let i=r.active&&e.enabled;if(n.push(this.theme.fg(i?`text`:`dim`,x(` ${r.toolName}`,t))),r.description)for(let e of C(r.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${e}`))}return n}resourceLines(e,t){let n=this.resourcePanes.get(e.name);if(!n)return[this.theme.fg(`dim`,` ${e.state===`connected`&&e.enabled?`Press ctrl+r to list resources`:`${e.name} is ${b(e)} · press ctrl+r to try anyway`}`)];if(n.status===`loading`)return[this.theme.fg(`dim`,` Listing resources…`)];if(n.status===`error`)return C(`Could not list resources: ${n.error??`unknown error`}`,Math.max(1,t-2)).map(e=>this.theme.fg(`warning`,` ${e}`));let r=n.items??[];return r.length===0?[this.theme.fg(`dim`,` No resources`)]:r.flatMap(e=>{let n=[S(` ${e.name??e.uri}`,this.theme.fg(`dim`,e.mimeType??``),t)];if(e.name&&n.push(this.theme.fg(`dim`,x(` ${e.uri}`,t))),e.description)for(let r of C(e.description,Math.max(1,t-4)))n.push(this.theme.fg(`dim`,` ${r}`));return n})}detailLines(e,t){let n=this.current();if(!n)return[this.theme.fg(`dim`,`No MCP servers are configured for this session.`)];let r=[b(n),`${n.tools.length} ${n.tools.length===1?`tool`:`tools`}`,`${n.resourceCount} res`].join(` · `),i=[this.theme.bold(n.name),``,...this.field(`State`,r,e),...this.field(`Error`,n.error??`—`,e),...n.authorizationUrl?[``,...this.authorizationLines(n.authorizationUrl,e)]:[],``,this.controlLine(n,e),``,this.tabStrip(n,e),``],a=this.detailTab===`tools`?this.toolLines(n,e):this.resourceLines(n,e);this.detailLineCount=a.length,this.detailViewportHeight=Math.max(1,t-i.length);let o=Math.min(this.detailScroll,Math.max(0,a.length-this.detailViewportHeight));return[...i,...a.slice(o,o+this.detailViewportHeight)]}getChrome(){let e=this.servers.filter(e=>e.enabled&&e.state===`connected`).length;return{title:`MCP`,accent:o,breadcrumb:`SPC › e / m · mcp`,headerRight:`${e}/${this.servers.length} connected · esc close`,footer:l,footerHints:c}}noticeRow(e){return this.theme.fg(`warning`,x(` ${this.actionNotice??``}`,e))}renderBody(e,t){let r=this.actionNotice?Math.max(1,t-1):t,i=this.actionNotice?[this.noticeRow(e)]:[];if(this.bodyHeight=r,e<48)return[...this.renderStacked(e,r),...i];let a=Math.max(18,Math.floor((e-1)*.3333333333333333)),o=Math.max(1,e-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),u=this.detailLines(c,r),d=this.theme.fg(`borderMuted`,`│`);return[...Array.from({length:r},(t,r)=>{let i=`${x(l[r]??``,s)} ${d} ${x(u[r]??``,c)}`;return n(i,e,`…`)}),...i]}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1);this.bodyHeight=n;let i=this.rosterLines(e),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}};function E(e){return e instanceof Error?e.message:String(e)}async function D(e,t){await e.ui.custom((e,n,r,i)=>new T(e,n,t,i),i)}export{T as McpOverlayComponent,D as openMcpOverlay};
2
2
  //# sourceMappingURL=mcpOverlay.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcpOverlay.mjs","names":[],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types/index.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'server'],\n ['⇧JK', 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects, so it is offered wherever a reconnect is meaningful, including\n * a server that has not been dialled yet. It is withheld only mid-connect, because\n * reauthorizing disconnects first and would tear down the attempt in flight.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching, it is waiting on\n // the person reading this, and the notice has to stop saying otherwise.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · waiting for you to open the URL`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.moveSelection(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.moveSelection(1);\n if (matchesKey(data, 'pageUp')) return this.scrollDetail(-this.detailViewportHeight);\n if (matchesKey(data, 'pageDown')) return this.scrollDetail(this.detailViewportHeight);\n if (matchesKey(data, 'tab')) return this.toggleTab();\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) return this.loadResources({ refresh: true });\n if (matchesKey(data, 'r')) return this.reload();\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * The authorization URL, wrapped across as many rows as it needs.\n *\n * Broken on width rather than truncated, and given the accent the controls use,\n * because it is the only thing in this overlay that cannot be acted on with a\n * keystroke: the user has to read it and open it somewhere else.\n */\n private authorizationLines(url: string, width: number): string[] {\n const indent = TOOL_INDENT;\n const body = Math.max(1, width - indent.length);\n const chunks: string[] = [];\n for (let start = 0; start < url.length; start += body) chunks.push(url.slice(start, start + body));\n return [\n this.theme.fg('accent', fit(`${indent}Open this to authorize:`, width)),\n ...chunks.map((chunk) => this.theme.fg('text', fit(`${indent}${chunk}`, width))),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The one thing on this pane the user has to act on outside the terminal, so\n // it is never abbreviated: a truncated URL cannot be opened.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcpOverlay.mjs","names":[],"sources":["../../src/tui/mcpOverlay.ts"],"sourcesContent":["/**\n * MCP browser: the `SPC e m` overlay.\n *\n * A roster of this session's MCP servers on the left, and on the right what the\n * selected one offers plus the controls that act on it. It replaces a `/mcp status`\n * text blob that could be read but not acted on.\n *\n * DESIGN PATTERNS:\n * - Controls always list every action, unavailable ones dimmed rather than hidden,\n * so the key map never shifts under the user\n * - Every collaborator arrives through `McpOverlayTarget`, so the component renders\n * and dispatches in a test with no terminal and no session behind it\n *\n * The roster stays a flat list and the detail pane a tab strip rather than a second\n * navigation level: selection is one index, and every row is assertable as a string.\n *\n * Repainting is driven by `target.onChange`, not a timer. Servers connect on their\n * own schedule and the session already announces each one as it folds in, so a poll\n * here would only be sampling an event that had already fired.\n *\n * Disabling is a visibility control, not a permission boundary: Pi 0.84 cannot\n * unregister a tool, so a disabled server's tools leave the active list but stay\n * registered and callable. `DirectToolFilter` has the same property.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_NAVIGATION_KEYS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { McpOverlayTarget, McpResourceView, McpServerView } from '../types/index.ts';\n\nconst TITLE = 'MCP';\nconst BREADCRUMB = 'SPC › e / m · mcp';\n/**\n * Ordered by what a user reaches for first, because the chrome drops trailing\n * hints rather than shrinking them when the terminal is narrow.\n *\n * Scrolling earns a slot: a long tool list is the common case (the proxy alone\n * carries twelve), and without a legend entry the keys that reach the rest of it\n * are undiscoverable.\n */\nconst HINTS: readonly (readonly [string, string])[] = [\n [DOOM_NAVIGATION_KEYS.list, 'server'],\n [DOOM_NAVIGATION_KEYS.detail, 'scroll'],\n ['tab', 'pane'],\n ['a', 'auth'],\n ['d', 'disable'],\n ['e', 'enable'],\n ['r', 'reload'],\n ['esc', 'close'],\n];\n/** Derived so the plain-text fallback cannot drift from the caps above it. */\nconst FOOTER_FALLBACK = HINTS.map(([key, label]) => `${key} ${label}`).join(' · ');\n/** Matches the chrome's spacing between footer caps, so both legends read alike. */\nconst CAP_SEPARATOR = ' ';\n\nconst MIN_WIDTH = 48;\n/** A third for the roster, two thirds for what the server offers. */\nconst ROSTER_PANE_RATIO = 1 / 3;\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\nconst ELLIPSIS = '…';\nconst LABEL_COLUMN = 10;\nconst FIELD_FALLBACK = '—';\nconst EMPTY_MESSAGE = 'No MCP servers are configured for this session.';\nconst TOOL_INDENT = ' ';\nconst OSC_LINK_OPEN = '\\u001B]8;;';\nconst OSC_LINK_SEPARATOR = '\\u0007';\nconst OSC_LINK_CLOSE = `${OSC_LINK_OPEN}${OSC_LINK_SEPARATOR}`;\n\nexport type McpDetailTab = 'tools' | 'resources';\nconst DETAIL_TABS: readonly McpDetailTab[] = ['tools', 'resources'];\n\ntype McpActionName = 'auth' | 'disable' | 'enable';\n\nconst CONTROL_ORDER: readonly McpActionName[] = ['auth', 'disable', 'enable'];\nconst CONTROL_KEY: Record<McpActionName, string> = { auth: 'a', disable: 'd', enable: 'e' };\n/** Short qualifier where the verb alone would understate what the key does. */\nconst CONTROL_HINT: Record<McpActionName, string | undefined> = {\n auth: 'reconnect',\n disable: 'session',\n enable: undefined,\n};\nconst CONTROL_BY_KEY = new Map<string, McpActionName>(CONTROL_ORDER.map((action) => [CONTROL_KEY[action], action]));\n\n/**\n * What each control can do to a server in its current condition.\n *\n * `auth` reconnects wherever that is meaningful, including a server that has not\n * been dialled yet. Mid-connect it is withheld until the flow has a reserved URL;\n * from then on the same key safely reopens that page without reconnecting.\n */\nfunction controlAvailability(server: McpServerView | undefined): Record<McpActionName, boolean> {\n if (!server) return { auth: false, disable: false, enable: false };\n if (!server.enabled) return { auth: false, disable: false, enable: true };\n return { auth: server.authorizationUrl !== undefined || server.state !== 'connecting', disable: true, enable: false };\n}\n\nfunction unavailableReason(action: McpActionName, server: McpServerView | undefined): string {\n if (!server) return `${action} unavailable · no server is selected`;\n if (!server.enabled) return `${action} unavailable · ${server.name} is disabled for this session`;\n if (action === 'enable') return `enable unavailable · ${server.name} is already enabled`;\n return `${action} unavailable · ${server.name} is ${server.state}`;\n}\n\nfunction statusGlyph(server: McpServerView, theme: Theme): string {\n if (!server.enabled) return theme.fg('dim', '■');\n if (server.state === 'connected') return theme.fg('success', '●');\n if (server.state === 'connecting') return theme.fg('accent', '◐');\n if (server.state === 'degraded') return theme.fg('warning', '▲');\n if (server.state === 'needs-auth') return theme.fg('warning', '✗');\n if (server.state === 'failed') return theme.fg('error', '✗');\n return theme.fg('muted', '◦');\n}\n\n/** What the roster row and the detail header both call the server's condition. */\nfunction stateLabel(server: McpServerView): string {\n return server.enabled ? server.state : 'disabled';\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (current.length + 1 + word.length <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** A compact clickable fallback whose visible width does not include its target URL. */\nfunction terminalLink(label: string, url: string): string {\n return `${OSC_LINK_OPEN}${url}${OSC_LINK_SEPARATOR}${label}${OSC_LINK_CLOSE}`;\n}\n\n/** What the resources pane knows about one server. Absent until the pane asks. */\ninterface ResourcePane {\n status: 'loading' | 'ready' | 'error';\n items?: readonly McpResourceView[];\n error?: string;\n}\n\nexport class McpOverlayComponent extends DoomOverlay {\n private servers: readonly McpServerView[] = [];\n private selected = 0;\n private selectedName: string | undefined;\n private detailTab: McpDetailTab = 'tools';\n private detailScroll = 0;\n private detailLineCount = 0;\n private detailViewportHeight = 8;\n private bodyHeight = 8;\n private actionNotice: string | undefined;\n private disposed = false;\n private readonly resourcePanes = new Map<string, ResourcePane>();\n private readonly unsubscribe: () => void;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly target: McpOverlayTarget,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n this.refresh();\n this.unsubscribe = this.target.onChange(() => {\n if (this.disposed) return;\n this.refresh();\n // A flow that has surfaced its URL is no longer dispatching. Explicit auth\n // opens it automatically, while the same key now reopens the reserved page.\n const server = this.current();\n if (server?.authorizationUrl) this.actionNotice = `auth ${server.name} · page ready · a reopens browser`;\n this.tui.requestRender();\n });\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribe();\n }\n\n /** Re-reads the roster, keeping the cursor on the server it was already on. */\n private refresh(): void {\n const previousName = this.servers[this.selected]?.name ?? this.selectedName;\n this.servers = this.target.getServers();\n const preserved = previousName ? this.servers.findIndex((server) => server.name === previousName) : -1;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.servers.length - 1));\n this.selectedName = this.servers[this.selected]?.name;\n }\n\n private current(): McpServerView | undefined {\n return this.servers[this.selected];\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) {\n this.scrollDetail(-1);\n return;\n }\n if (matchesKey(data, Key.shift('j'))) {\n this.scrollDetail(1);\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) {\n this.moveSelection(-1);\n return;\n }\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) {\n this.moveSelection(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.scrollDetail(-this.detailViewportHeight);\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.scrollDetail(this.detailViewportHeight);\n return;\n }\n if (matchesKey(data, 'tab')) {\n this.toggleTab();\n return;\n }\n // Checked before the single-letter controls: ctrl+r is the deliberate path that\n // discards what a server said about its resources and asks again.\n if (matchesKey(data, 'ctrl+r')) {\n this.loadResources({ refresh: true });\n return;\n }\n if (matchesKey(data, 'r')) {\n this.reload();\n return;\n }\n const action = CONTROL_BY_KEY.get(data.toLowerCase());\n if (action) this.triggerControl(action);\n }\n\n private moveSelection(delta: number): void {\n if (this.servers.length === 0) return;\n this.selected = Math.max(0, Math.min(this.servers.length - 1, this.selected + delta));\n this.selectedName = this.servers[this.selected]?.name;\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private scrollDetail(delta: number): void {\n const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);\n this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));\n this.tui.requestRender();\n }\n\n private toggleTab(): void {\n this.detailTab = this.detailTab === 'tools' ? 'resources' : 'tools';\n this.detailScroll = 0;\n this.maybeLoadResources();\n this.tui.requestRender();\n }\n\n private reload(): void {\n this.setNotice('reload · dispatching…');\n // Not awaited: a server that never answers must not wedge the overlay. The\n // roster repaints through onChange as each one lands.\n void this.target\n .start()\n .then(() => {\n if (!this.disposed) this.setNotice('reload · reconnecting');\n })\n .catch((cause: unknown) => {\n if (!this.disposed) this.setNotice(`reload · failed · ${message(cause)}`);\n });\n }\n\n /**\n * Lists the selected server's resources, but only where a listing is meaningful.\n *\n * A server that has not connected is left alone: turning a `tab` keypress into a\n * silent dial-out is not what switching panes should mean.\n */\n private maybeLoadResources(): void {\n if (this.detailTab !== 'resources') return;\n const server = this.current();\n if (!server || !server.enabled || server.state !== 'connected') return;\n if (this.resourcePanes.has(server.name)) return;\n this.loadResources();\n }\n\n private loadResources(options: { refresh?: boolean } = {}): void {\n const server = this.current();\n if (!server) return;\n this.resourcePanes.set(server.name, { status: 'loading' });\n this.tui.requestRender();\n void this.target\n .listResources(server.name, options)\n .then((items) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'ready', items });\n this.tui.requestRender();\n })\n .catch((cause: unknown) => {\n if (this.disposed) return;\n this.resourcePanes.set(server.name, { status: 'error', error: message(cause) });\n this.tui.requestRender();\n });\n }\n\n private triggerControl(action: McpActionName): void {\n const server = this.current();\n if (!controlAvailability(server)[action]) {\n this.setNotice(unavailableReason(action, server));\n return;\n }\n if (!server) return;\n if (action === 'auth') {\n if (server.authorizationUrl) void this.openAuthorizationPage(server.name);\n else void this.authorize(server.name);\n return;\n }\n const enabled = action === 'enable';\n try {\n this.target.setEnabled(server.name, enabled);\n this.refresh();\n this.setNotice(`${action} ${server.name} · ${enabled ? 'tools restored' : 'tools withheld for this session'}`);\n } catch (cause) {\n this.setNotice(`${action} ${server.name} · failed · ${message(cause)}`);\n }\n }\n\n private async authorize(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · dispatching…`);\n try {\n await this.target.reauthorize(serverName);\n if (this.disposed) return;\n // The listing is about whatever the reconnect just settled on.\n this.resourcePanes.delete(serverName);\n this.refresh();\n this.setNotice(`auth ${serverName} · authorized`);\n } catch (cause) {\n if (this.disposed) return;\n this.setNotice(`auth ${serverName} · failed · ${message(cause)}`);\n }\n }\n\n private async openAuthorizationPage(serverName: string): Promise<void> {\n this.setNotice(`auth ${serverName} · opening browser…`);\n try {\n await this.target.openAuthorizationPage(serverName);\n if (!this.disposed) this.setNotice(`auth ${serverName} · browser opened · waiting for approval`);\n } catch (cause) {\n if (!this.disposed) this.setNotice(`auth ${serverName} · browser failed · ${message(cause)}`);\n }\n }\n\n private setNotice(notice: string): void {\n this.actionNotice = notice;\n this.tui.requestRender();\n }\n\n private rosterLines(width: number): string[] {\n if (this.servers.length === 0) return [this.theme.fg('dim', 'No servers')];\n const start = Math.max(\n 0,\n Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.servers.length - this.bodyHeight)),\n );\n return this.servers.slice(start, start + this.bodyHeight).map((server, offset) => {\n const index = start + offset;\n const marker = index === this.selected ? this.theme.fg('accent', '›') : ' ';\n // Dimming a disabled server states in the roster what the detail pane would\n // otherwise have to be visited to learn.\n const name = server.enabled\n ? index === this.selected\n ? this.theme.bold(server.name)\n : server.name\n : this.theme.fg('dim', server.name);\n const left = `${marker} ${statusGlyph(server, this.theme)} ${name}`;\n return rightAligned(left, this.theme.fg('dim', stateLabel(server)), width);\n });\n }\n\n /**\n * A single-line fallback for the page that explicit auth already opened.\n *\n * The full URL lives in an OSC 8 target rather than printable layout cells, so a\n * long OAuth state token cannot wrap through pane borders and become corrupted.\n * Terminals without clickable links still have the same `a` key to reopen it.\n */\n private authorizationLines(url: string, width: number): string[] {\n return [\n this.theme.fg('accent', fit(`${TOOL_INDENT}Authorization page ready`, width)),\n this.theme.fg('text', fit(`${TOOL_INDENT}${terminalLink('Open authorization page', url)} · a reopens`, width)),\n ];\n }\n\n private field(label: string, value: string, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) => `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${fit(line, valueWidth)}`,\n );\n }\n\n /**\n * Runtime controls as filled key caps, matching the footer legend.\n *\n * Every action is always listed; an unavailable one loses its cap fill rather\n * than its place, so the key map never shifts under the user and a control that\n * cannot act still reads as a key rather than as prose.\n */\n private controlLine(server: McpServerView | undefined, width: number): string {\n const availability = controlAvailability(server);\n const parts = CONTROL_ORDER.map((action) => {\n const hint = action === 'auth' && server?.authorizationUrl ? 'reopen' : CONTROL_HINT[action];\n const label = `${action}${hint ? ` (${hint})` : ''}`;\n return this.keyCap(CONTROL_KEY[action], label, availability[action]);\n });\n return truncateToWidth(` ${parts.join(CAP_SEPARATOR)}`, width);\n }\n\n /** One filled key cap and its label. Unfilled when the key would do nothing. */\n private keyCap(key: string, label: string, enabled = true): string {\n const cap = enabled\n ? this.theme.bg('selectedBg', this.theme.fg('text', ` ${key} `))\n : this.theme.fg('dim', ` ${key} `);\n return `${cap}${this.theme.fg('dim', ` ${label}`)}`;\n }\n\n private tabStrip(server: McpServerView | undefined, width: number): string {\n const label = (name: McpDetailTab): string =>\n name === this.detailTab\n ? this.theme.fg('accent', this.theme.bold(`[ ${name} ]`))\n : this.theme.fg('dim', ` ${name} `);\n const counts =\n server && this.detailTab === 'tools'\n ? `${server.tools.filter((tool) => tool.active).length}/${server.tools.length}`\n : 'ctrl+r refresh';\n return rightAligned(` ${DETAIL_TABS.map(label).join('')}`, this.theme.fg('dim', counts), width);\n }\n\n private toolLines(server: McpServerView, width: number): string[] {\n if (server.tools.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No tools`)];\n const lines: string[] = [];\n for (const tool of server.tools) {\n // Colour separates what the model can call from what is merely registered,\n // so a withheld tool never has to be selected to be recognised.\n const usable = tool.active && server.enabled;\n lines.push(this.theme.fg(usable ? 'text' : 'dim', fit(`${TOOL_INDENT}${tool.toolName}`, width)));\n if (tool.description) {\n for (const line of wrap(tool.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n }\n return lines;\n }\n\n private resourceLines(server: McpServerView, width: number): string[] {\n const pane = this.resourcePanes.get(server.name);\n if (!pane) {\n return [\n this.theme.fg(\n 'dim',\n `${TOOL_INDENT}${server.state === 'connected' && server.enabled ? 'Press ctrl+r to list resources' : `${server.name} is ${stateLabel(server)} · press ctrl+r to try anyway`}`,\n ),\n ];\n }\n if (pane.status === 'loading') return [this.theme.fg('dim', `${TOOL_INDENT}Listing resources…`)];\n if (pane.status === 'error') {\n return wrap(\n `Could not list resources: ${pane.error ?? 'unknown error'}`,\n Math.max(1, width - TOOL_INDENT.length),\n ).map((line) => this.theme.fg('warning', `${TOOL_INDENT}${line}`));\n }\n const items = pane.items ?? [];\n if (items.length === 0) return [this.theme.fg('dim', `${TOOL_INDENT}No resources`)];\n return items.flatMap((resource) => {\n const lines = [\n rightAligned(\n `${TOOL_INDENT}${resource.name ?? resource.uri}`,\n this.theme.fg('dim', resource.mimeType ?? ''),\n width,\n ),\n ];\n if (resource.name) lines.push(this.theme.fg('dim', fit(`${TOOL_INDENT}${TOOL_INDENT}${resource.uri}`, width)));\n if (resource.description) {\n for (const line of wrap(resource.description, Math.max(1, width - TOOL_INDENT.length * 2))) {\n lines.push(this.theme.fg('dim', `${TOOL_INDENT}${TOOL_INDENT}${line}`));\n }\n }\n return lines;\n });\n }\n\n private detailLines(width: number, height: number): string[] {\n const server = this.current();\n if (!server) return [this.theme.fg('dim', EMPTY_MESSAGE)];\n\n const summary = [\n stateLabel(server),\n `${server.tools.length} ${server.tools.length === 1 ? 'tool' : 'tools'}`,\n `${server.resourceCount} res`,\n ].join(' · ');\n const header = [\n this.theme.bold(server.name),\n '',\n ...this.field('State', summary, width),\n ...this.field('Error', server.error ?? FIELD_FALLBACK, width),\n // The compact label is clickable, while `a` reopens the same reserved URL.\n ...(server.authorizationUrl ? ['', ...this.authorizationLines(server.authorizationUrl, width)] : []),\n '',\n this.controlLine(server, width),\n '',\n this.tabStrip(server, width),\n '',\n ];\n\n const body = this.detailTab === 'tools' ? this.toolLines(server, width) : this.resourceLines(server, width);\n this.detailLineCount = body.length;\n this.detailViewportHeight = Math.max(1, height - header.length);\n const start = Math.min(this.detailScroll, Math.max(0, body.length - this.detailViewportHeight));\n return [...header, ...body.slice(start, start + this.detailViewportHeight)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const connected = this.servers.filter((server) => server.enabled && server.state === 'connected').length;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${connected}/${this.servers.length} connected · esc close`,\n // The chrome renders the caps and decides what fits; `footer` is only the\n // plain-sentence fallback it uses when hints are absent.\n footer: FOOTER_FALLBACK,\n footerHints: HINTS,\n };\n }\n\n /**\n * The notice line, on its own full-width row at the foot of the body.\n *\n * Not `footerRight`: the chrome drops a right-hand status whole when the row\n * cannot spare the columns, and the hint legend always claims them, so an action's\n * only feedback would be silently discarded on every terminal.\n */\n private noticeRow(width: number): string {\n return this.theme.fg('warning', fit(` ${this.actionNotice ?? ''}`, width));\n }\n\n protected renderBody(width: number, height: number): string[] {\n const paneHeight = this.actionNotice ? Math.max(1, height - 1) : height;\n const notice = this.actionNotice ? [this.noticeRow(width)] : [];\n this.bodyHeight = paneHeight;\n if (width < MIN_WIDTH) return [...this.renderStacked(width, paneHeight), ...notice];\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * ROSTER_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n const left = this.rosterLines(leftContent);\n const right = this.detailLines(rightContent, paneHeight);\n\n const divider = this.theme.fg('borderMuted', '│');\n const rows = Array.from({ length: paneHeight }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n return [...rows, ...notice];\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n this.bodyHeight = topHeight;\n const top = this.rosterLines(width);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', '─'.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n}\n\nfunction message(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nexport async function openMcpOverlay(ctx: ExtensionContext, target: McpOverlayTarget): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new McpOverlayComponent(tui, theme, target, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-mcp",
3
- "version": "0.0.1-alpha.13",
3
+ "version": "0.0.1-alpha.14",
4
4
  "description": "MCP server tools for Doompi",
5
5
  "homepage": "https://agimon.ai",
6
6
  "license": "MIT",
@@ -37,11 +37,12 @@
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "1.29.0",
39
39
  "@napi-rs/keyring": "1.3.0",
40
+ "open": "8.4.2",
40
41
  "typebox": "1.1.38",
41
42
  "yaml": "2.9.0",
42
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.13",
43
- "@agimon-ai/doompi-ui": "0.0.1-alpha.13",
44
- "@agimon-ai/mcp-proxy": "0.31.3"
43
+ "@agimon-ai/mcp-proxy": "0.31.4",
44
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.14",
45
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.14"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@earendil-works/pi-coding-agent": "0.84.1",
@@ -1,2 +0,0 @@
1
- const e=require("../commands/tools/mcpTools.cjs"),t=require("./configSources.cjs"),n=require("./directTools.cjs"),r=require("./mcpCatalog.cjs"),i=require("./mcpRuntime.cjs"),a=require("./sessionConfig.cjs"),o=`The MCP runtime has not started yet.`;var s=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){e.applyActiveTools(this.pi,this.catalog)}createCatalog(){return new r.McpCatalog(n.readDirectToolFilter(this.options.environment))}install(n=a.readSessionConfig(this.options.environment)){this.groups=t.buildMcpConfigGroups(n),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(i.readCachedCatalog(this.configPaths()),r),e.registerNewTools(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([Promise.resolve().then(()=>require("./keyringTokenStore.cjs")),Promise.resolve().then(()=>require("./mcpRuntime.cjs"))]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(o);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(t,n){this.catalog.setDisabled(t,!n),e.applyActiveTools(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(o);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(t){t.state!==`connecting`&&this.authorizationUrls.delete(t.serverName);let n=this.runtime,r=n?.getServices();(t.state===`connected`&&r?r.clientManager.ensureConnected(t.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(i=>{if(!n?.isCurrent(r))return;let a=this.catalog.applyStateChange(t,i);e.registerNewTools(this.pi,this.clientManager,a),e.applyActiveTools(this.pi,this.catalog),this.emitChange()})}};exports.McpSession=s;
2
- //# sourceMappingURL=mcpSession.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcpSession.cjs","names":["McpCatalog","readDirectToolFilter","readSessionConfig","buildMcpConfigGroups","readCachedCatalog"],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../commands/tools/mcpTools.ts';\nimport type { McpResourceView, McpServerView } from '../types/index.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from './sessionConfig.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- import{applyActiveTools as e,registerNewTools as t}from"../commands/tools/mcpTools.mjs";import{buildMcpConfigGroups as n}from"./configSources.mjs";import{readDirectToolFilter as r}from"./directTools.mjs";import{McpCatalog as i}from"./mcpCatalog.mjs";import{readCachedCatalog as a}from"./mcpRuntime.mjs";import{readSessionConfig as o}from"./sessionConfig.mjs";const s=`The MCP runtime has not started yet.`;var c=class{pi;runtime;catalog;groups={shared:{configPaths:[],serverNames:[]},sessionLocal:{configPaths:[],serverNames:[]},droppedServers:[]};options;resourceCache=new Map;changeListeners=new Set;authorizationUrls=new Map;lifecycleGeneration=0;clientManager=()=>this.runtime?.getServices()?.clientManager;constructor(e){this.pi=e.pi,this.options=e,this.catalog=this.createCatalog()}activate(){e(this.pi,this.catalog)}createCatalog(){return new i(r(this.options.environment))}install(e=o(this.options.environment)){this.groups=n(e),this.catalog=this.createCatalog();let r=[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])];return this.catalog.seed(a(this.configPaths()),r),t(this.pi,this.clientManager,this.catalog.allTools()),this.catalog.toSnapshot()}async start(){let e=++this.lifecycleGeneration,[{createTokenStore:t},{McpRuntimeOwner:n}]=await Promise.all([import(`./keyringTokenStore.mjs`),import(`./mcpRuntime.mjs`)]);if(e!==this.lifecycleGeneration)return;this.runtime??=new n;let r=this.runtime;this.resourceCache.clear();let i=this.configPaths();if(this.configuredServerNames().length===0)return;let a=this.options.tokenStore??await t();await r.start({configPaths:i,tokenStore:a,onAuthorizationUrl:(e,t)=>this.onAuthorizationUrl(e,t),onServerStateChange:e=>this.onServerStateChange(e)}),e!==this.lifecycleGeneration&&await r.dispose()}onAuthorizationUrl(e,t){this.authorizationUrls.set(t,e.toString()),this.emitChange(),this.options.onAuthorizationUrl?.(e,t)}configPaths(){return[...this.groups.sessionLocal.configPaths,...this.groups.shared.configPaths]}configuredServerNames(){return[...new Set([...this.groups.sessionLocal.serverNames,...this.groups.shared.serverNames])]}async reauthorize(e){let t=this.clientManager();if(!t)throw Error(s);this.resourceCache.delete(e),await t.disconnectServer(e),await t.ensureConnected(e)}getSnapshot(){return this.catalog.toSnapshot()}getServers(){return this.catalog.toView().map(e=>{let t=this.authorizationUrls.get(e.name);return t?{...e,authorizationUrl:t}:e})}setEnabled(t,n){this.catalog.setDisabled(t,!n),e(this.pi,this.catalog),this.emitChange()}async listResources(e,t={}){if(!t.refresh){let t=this.resourceCache.get(e);if(t)return t}let n=this.runtime,r=n?.getServices();if(!n||!r)throw Error(s);let i=await(await r.clientManager.ensureConnected(e)).listResources();return n.isCurrent(r)?(this.resourceCache.set(e,i),this.catalog.setResourceCount(e,i.length),this.emitChange(),i):i}onChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}emitChange(){for(let e of this.changeListeners)try{e()}catch(e){this.catalog.addDiagnostic(`An MCP change listener failed: ${e instanceof Error?e.message:String(e)}`)}}getDiagnostics(){return this.catalog.getDiagnostics()}async dispose(){++this.lifecycleGeneration,await this.runtime?.dispose()}onServerStateChange(n){n.state!==`connecting`&&this.authorizationUrls.delete(n.serverName);let r=this.runtime,i=r?.getServices();(n.state===`connected`&&i?i.clientManager.ensureConnected(n.serverName).then(e=>e.listTools()):Promise.resolve([])).catch(()=>[]).then(a=>{if(!r?.isCurrent(i))return;let o=this.catalog.applyStateChange(n,a);t(this.pi,this.clientManager,o),e(this.pi,this.catalog),this.emitChange()})}};export{c as McpSession};
2
- //# sourceMappingURL=mcpSession.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcpSession.mjs","names":[],"sources":["../../src/services/mcpSession.ts"],"sourcesContent":["import type { McpStatusSnapshot } from '@agimon-ai/doompi-extension-contracts/mcp-status';\nimport type { McpClientManagerService, McpServerStateChange, TokenStore } from '@agimon-ai/mcp-proxy';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { applyActiveTools, registerNewTools } from '../commands/tools/mcpTools.ts';\nimport type { McpResourceView, McpServerView } from '../types/index.ts';\nimport { buildMcpConfigGroups, type McpConfigGroups } from './configSources.ts';\nimport { readDirectToolFilter } from './directTools.ts';\nimport { McpCatalog } from './mcpCatalog.ts';\nimport { type McpRuntimeOwner, readCachedCatalog } from './mcpRuntime.ts';\nimport { type McpSessionConfig, readSessionConfig } from './sessionConfig.ts';\n\n/** Raised by anything needing a live container before one has been built. */\nconst RUNTIME_NOT_STARTED = 'The MCP runtime has not started yet.';\n\nexport interface McpSessionOptions {\n pi: ExtensionAPI;\n environment?: NodeJS.ProcessEnv;\n /** Receives an OAuth authorization URL the user has to open. */\n onAuthorizationUrl?: (url: URL, serverName: string) => void;\n /** Overrides the OS keyring store, for tests. */\n tokenStore?: TokenStore;\n}\n\n/**\n * The MCP picture for one Pi session.\n *\n * Install is synchronous and cheap: the previous run's catalog is read off disk and\n * its tools registered, so Pi's first turn already has them. Connecting happens on\n * a detached promise afterwards, and each server folds into the active tool set as\n * it lands. Nothing on this path waits for a socket.\n */\nexport class McpSession {\n private readonly pi: ExtensionAPI;\n /** Created after session_start, never while Pi is importing extensions. */\n private runtime: McpRuntimeOwner | undefined;\n private catalog: McpCatalog;\n private groups: McpConfigGroups = {\n shared: { configPaths: [], serverNames: [] },\n sessionLocal: { configPaths: [], serverNames: [] },\n droppedServers: [],\n };\n private readonly options: McpSessionOptions;\n /** Resources per server, listed on demand. Errors are never cached, so a retry re-dials. */\n private readonly resourceCache = new Map<string, readonly McpResourceView[]>();\n private readonly changeListeners = new Set<() => void>();\n /** Authorization URLs waiting to be opened, by server. */\n private readonly authorizationUrls = new Map<string, string>();\n /** Invalidates deferred startup work when a session reloads or shuts down. */\n private lifecycleGeneration = 0;\n\n /** Resolved per tool call, so a registration outlives the container it was made under. */\n private readonly clientManager = (): McpClientManagerService | undefined =>\n this.runtime?.getServices()?.clientManager;\n\n constructor(options: McpSessionOptions) {\n this.pi = options.pi;\n this.options = options;\n this.catalog = this.createCatalog();\n }\n\n /**\n * Applies the tool set to Pi. Runs on session start, never during install.\n *\n * Pi's active-list calls throw until the runtime is bound, so the tools\n * registered at install stay at Pi's default visibility until this runs, which\n * is the first point the session can express that nothing has connected yet.\n */\n activate(): void {\n applyActiveTools(this.pi, this.catalog);\n }\n\n private createCatalog(): McpCatalog {\n return new McpCatalog(readDirectToolFilter(this.options.environment));\n }\n\n /**\n * Registers the cached catalog with Pi. Runs at extension install.\n *\n * Returns the servers it seeded, so a caller can report what the session started\n * with before anything connected.\n */\n install(configuration: McpSessionConfig = readSessionConfig(this.options.environment)): McpStatusSnapshot {\n this.groups = buildMcpConfigGroups(configuration);\n this.catalog = this.createCatalog();\n // Doom embeds the client runtime, so upstreams referenced by a proxy wrapper\n // remain individual servers in the catalog rather than collapsing into it.\n const configuredServers = [\n ...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames]),\n ];\n this.catalog.seed(readCachedCatalog(this.configPaths()), configuredServers);\n // Registration only. Pi allows registerTool while extensions load but rejects\n // the active-list calls until the runtime is bound, so activation waits for\n // `activate()` on session start.\n registerNewTools(this.pi, this.clientManager, this.catalog.allTools());\n return this.catalog.toSnapshot();\n }\n\n /**\n * Starts connecting. Never awaited on Pi's critical path.\n *\n * Resolves once the container exists, not once servers are reachable. A start\n * that a newer generation superseded resolves to nothing and registers nothing.\n */\n async start(): Promise<void> {\n const generation = ++this.lifecycleGeneration;\n const [{ createTokenStore }, { McpRuntimeOwner }] = await Promise.all([\n import('./keyringTokenStore.ts'),\n import('./mcpRuntime.ts'),\n ]);\n if (generation !== this.lifecycleGeneration) return;\n this.runtime ??= new McpRuntimeOwner();\n const runtime = this.runtime;\n // The container is about to be replaced, so every resource list read from the\n // old one is now hearsay.\n this.resourceCache.clear();\n const configPaths = this.configPaths();\n // A repository whose layers declare nothing still yields a config path, and\n // building a container for it would spend a keyring probe on a session that\n // has nothing to connect to.\n if (this.configuredServerNames().length === 0) return;\n const tokenStore = this.options.tokenStore ?? (await createTokenStore());\n await runtime.start({\n configPaths,\n tokenStore,\n onAuthorizationUrl: (url, serverName) => this.onAuthorizationUrl(url, serverName),\n onServerStateChange: (change) => this.onServerStateChange(change),\n });\n // Shutdown can land while the proxy package or token store is loading. Make\n // sure that late container cannot survive and mutate a closed Pi session.\n if (generation !== this.lifecycleGeneration) await runtime.dispose();\n }\n\n /**\n * Records the URL a flow is waiting on, then passes it to the host.\n *\n * Kept here as well as announced, because the announcement lands in the\n * transcript and the panel a user starts the flow from covers it.\n */\n private onAuthorizationUrl(url: URL, serverName: string): void {\n this.authorizationUrls.set(serverName, url.toString());\n this.emitChange();\n this.options.onAuthorizationUrl?.(url, serverName);\n }\n\n /** Ordered layers for the one in-process client container. */\n private configPaths(): string[] {\n return [...this.groups.sessionLocal.configPaths, ...this.groups.shared.configPaths];\n }\n\n private configuredServerNames(): string[] {\n return [...new Set([...this.groups.sessionLocal.serverNames, ...this.groups.shared.serverNames])];\n }\n\n /**\n * Reconnects one server, running its OAuth flow when it demands one.\n *\n * The existing connection is dropped first: a server holding a stale token would\n * otherwise keep answering from cache and never reach the authorization step.\n */\n async reauthorize(serverName: string): Promise<void> {\n const clientManager = this.clientManager();\n if (!clientManager) throw new Error(RUNTIME_NOT_STARTED);\n // What this server offers is exactly what the reconnect is about to settle.\n this.resourceCache.delete(serverName);\n await clientManager.disconnectServer(serverName);\n await clientManager.ensureConnected(serverName);\n }\n\n getSnapshot(): McpStatusSnapshot {\n return this.catalog.toSnapshot();\n }\n\n /** The fuller picture the overlay renders, with tool names and descriptions. */\n getServers(): readonly McpServerView[] {\n return this.catalog.toView().map((server) => {\n const authorizationUrl = this.authorizationUrls.get(server.name);\n return authorizationUrl ? { ...server, authorizationUrl } : server;\n });\n }\n\n /**\n * Hides or restores a server's tools for the rest of this session.\n *\n * Deliberately does not disconnect: this is a session visibility control, so a\n * later enable is instant and does not discard an authenticated live connection.\n * Because each catalog row is now an actual downstream server, the control acts\n * on only that server's tools rather than on an aggregate proxy group.\n *\n * Nothing is re-registered on the way back: Pi 0.84 cannot unregister a tool, so\n * every tool is still registered and only its place in the active list moved.\n */\n setEnabled(serverName: string, enabled: boolean): void {\n this.catalog.setDisabled(serverName, !enabled);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n }\n\n /**\n * Lists one server's resources, from cache unless asked for a fresh call.\n *\n * The cache lives here rather than in the overlay so closing and reopening the\n * panel does not re-dial. A rejection is never cached, so a server that has no\n * `resources/list` capability, or one that was briefly unreachable, is retried by\n * the next request rather than remembered as empty.\n */\n async listResources(serverName: string, options: { refresh?: boolean } = {}): Promise<readonly McpResourceView[]> {\n if (!options.refresh) {\n const cached = this.resourceCache.get(serverName);\n if (cached) return cached;\n }\n const runtime = this.runtime;\n const services = runtime?.getServices();\n if (!runtime || !services) throw new Error(RUNTIME_NOT_STARTED);\n const connection = await services.clientManager.ensureConnected(serverName);\n const resources = await connection.listResources();\n // A reload that landed while this was in flight retired the container it was\n // read from, so the answer is returned to the caller but never written down.\n if (!runtime.isCurrent(services)) return resources;\n this.resourceCache.set(serverName, resources);\n this.catalog.setResourceCount(serverName, resources.length);\n this.emitChange();\n return resources;\n }\n\n /** Fires whenever the server picture changes. Returns its own disposer. */\n onChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => {\n this.changeListeners.delete(listener);\n };\n }\n\n /**\n * Announces a change to whoever is watching.\n *\n * Listener failures are contained rather than propagated: this runs inside the\n * continuation that folds a server into Pi's tool set, and a throw escaping here\n * would abandon that fold, leaving the session silently short of tools because\n * something failed to repaint.\n */\n private emitChange(): void {\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch (error) {\n // Recorded rather than rethrown: a watcher that cannot render is a defect\n // worth seeing, but not one worth abandoning the tool set over.\n this.catalog.addDiagnostic(\n `An MCP change listener failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n getDiagnostics(): readonly string[] {\n return this.catalog.getDiagnostics();\n }\n\n async dispose(): Promise<void> {\n ++this.lifecycleGeneration;\n await this.runtime?.dispose();\n }\n\n /**\n * Folds one server transition into Pi's tool set.\n *\n * The tool list is read from the live connection rather than the state event,\n * which carries no tools; a server that is not readable contributes none and its\n * cached tools simply stay inactive.\n *\n * `ensureConnected` rather than `getClient`: the manager announces `connected`\n * before it registers the client, so a listener that looks the client up\n * synchronously finds nothing and the server lands with an empty tool list.\n * This handler runs detached from the connect, so awaiting it cannot deadlock.\n */\n private onServerStateChange(change: McpServerStateChange): void {\n // The flow this URL belonged to is over, one way or the other: a server that\n // reached a terminal state is not still waiting on a redirect.\n if (change.state !== 'connecting') this.authorizationUrls.delete(change.serverName);\n const runtime = this.runtime;\n const services = runtime?.getServices();\n const tools =\n change.state === 'connected' && services\n ? services.clientManager.ensureConnected(change.serverName).then((connection) => connection.listTools())\n : Promise.resolve([]);\n void tools\n .catch(() => [])\n .then((tools) => {\n if (!runtime?.isCurrent(services)) return;\n const added = this.catalog.applyStateChange(change, tools);\n registerNewTools(this.pi, this.clientManager, added);\n applyActiveTools(this.pi, this.catalog);\n this.emitChange();\n });\n }\n}\n"],"mappings":"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"}