@tekmidian/pai 0.40.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +1 -0
  2. package/dist/aibroker-client-BP74gGwD.mjs +215 -0
  3. package/dist/aibroker-client-BP74gGwD.mjs.map +1 -0
  4. package/dist/aibroker-client-C6m71p6O.mjs +3 -0
  5. package/dist/{auto-route-BwzmlW3W.mjs → auto-route-_5zzJz5q.mjs} +2 -2
  6. package/dist/{auto-route-BwzmlW3W.mjs.map → auto-route-_5zzJz5q.mjs.map} +1 -1
  7. package/dist/browser-mcp/index.d.mts +1 -0
  8. package/dist/browser-mcp/index.mjs +352 -0
  9. package/dist/browser-mcp/index.mjs.map +1 -0
  10. package/dist/{chain-DhVHVnmT.mjs → chain-DGXNoTwc.mjs} +93 -320
  11. package/dist/chain-DGXNoTwc.mjs.map +1 -0
  12. package/dist/cli/index.mjs +12 -10
  13. package/dist/cli/index.mjs.map +1 -1
  14. package/dist/cli/program.mjs +12 -10
  15. package/dist/{clusters-DRHhfli9.mjs → clusters-j0ZuNYp9.mjs} +1 -1
  16. package/dist/{clusters-DRHhfli9.mjs.map → clusters-j0ZuNYp9.mjs.map} +1 -1
  17. package/dist/{config-D1G9IFpn.mjs → config-B64vFg14.mjs} +3 -50
  18. package/dist/config-B64vFg14.mjs.map +1 -0
  19. package/dist/{config-DD52BPB-.mjs → config-C_ErGddD.mjs} +1 -1
  20. package/dist/{config-Dl8lT4Lu.mjs → config-DgzR5sfb.mjs} +2 -2
  21. package/dist/{config-Dl8lT4Lu.mjs.map → config-DgzR5sfb.mjs.map} +1 -1
  22. package/dist/{context-handover-cache-C6_rs7JT.mjs → context-handover-cache-dUnSZwaA.mjs} +4 -4
  23. package/dist/{context-handover-cache-C6_rs7JT.mjs.map → context-handover-cache-dUnSZwaA.mjs.map} +1 -1
  24. package/dist/daemon/index.mjs +13 -12
  25. package/dist/daemon/index.mjs.map +1 -1
  26. package/dist/daemon-CKBkhOsZ.mjs +23 -0
  27. package/dist/{daemon-CpdVUnl_.mjs → daemon-DsvP8KMy.mjs} +357 -27
  28. package/dist/daemon-DsvP8KMy.mjs.map +1 -0
  29. package/dist/daemon-mcp/index.mjs +20 -6
  30. package/dist/daemon-mcp/index.mjs.map +1 -1
  31. package/dist/detector-C-7KRi42.mjs +5 -0
  32. package/dist/{detector-smpCm7ku.mjs → detector-C73Nxm94.mjs} +1 -1
  33. package/dist/{detector-smpCm7ku.mjs.map → detector-C73Nxm94.mjs.map} +1 -1
  34. package/dist/{factory-Bc5EyN54.mjs → factory-DEb9NmHJ.mjs} +7 -7
  35. package/dist/{factory-Bc5EyN54.mjs.map → factory-DEb9NmHJ.mjs.map} +1 -1
  36. package/dist/factory-Dbo8yNPi.mjs +3 -0
  37. package/dist/{fallback-CdXv-Np0.mjs → fallback-5UJO-RyX.mjs} +5 -4
  38. package/dist/{fallback-CdXv-Np0.mjs.map → fallback-5UJO-RyX.mjs.map} +1 -1
  39. package/dist/hooks/worker-status-line.mjs +9 -4
  40. package/dist/hooks/worker-status-line.mjs.map +3 -3
  41. package/dist/hooks/worker-supervision.mjs +593 -0
  42. package/dist/hooks/worker-supervision.mjs.map +7 -0
  43. package/dist/{indexer-backend-rohVVKrA.mjs → indexer-backend-BPTzKFZU.mjs} +1 -1
  44. package/dist/{indexer-backend-rohVVKrA.mjs.map → indexer-backend-BPTzKFZU.mjs.map} +1 -1
  45. package/dist/{ipc-client-C7AVpsyv.mjs → ipc-client-BmypMNYk.mjs} +2 -2
  46. package/dist/{ipc-client-C7AVpsyv.mjs.map → ipc-client-BmypMNYk.mjs.map} +1 -1
  47. package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
  48. package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-iNp5s55R.mjs → latent-ideas-CtSca4qG.mjs} +2 -2
  50. package/dist/{latent-ideas-iNp5s55R.mjs.map → latent-ideas-CtSca4qG.mjs.map} +1 -1
  51. package/dist/{link-boost-NNzADHX_.mjs → link-boost-Bucnz3n8.mjs} +1 -1
  52. package/dist/{link-boost-NNzADHX_.mjs.map → link-boost-Bucnz3n8.mjs.map} +1 -1
  53. package/dist/{main-resolver-CM1IHbuu.mjs → main-resolver-BgIrC-D9.mjs} +4 -214
  54. package/dist/main-resolver-BgIrC-D9.mjs.map +1 -0
  55. package/dist/main-resolver-DXGuygFG.mjs +7 -0
  56. package/dist/{neighborhood-AGL7oXQ2.mjs → neighborhood-CM7Jgb51.mjs} +1 -1
  57. package/dist/{neighborhood-AGL7oXQ2.mjs.map → neighborhood-CM7Jgb51.mjs.map} +1 -1
  58. package/dist/{note-context-BHwYpSFf.mjs → note-context-DCwE2A-M.mjs} +1 -1
  59. package/dist/{note-context-BHwYpSFf.mjs.map → note-context-DCwE2A-M.mjs.map} +1 -1
  60. package/dist/{planner-Cm3g6fWH.mjs → planner-BLDJt3ZS.mjs} +6 -5
  61. package/dist/{planner-Cm3g6fWH.mjs.map → planner-BLDJt3ZS.mjs.map} +1 -1
  62. package/dist/{postgres-Dwxe290H.mjs → postgres-CtOzfoQU.mjs} +1 -1
  63. package/dist/{postgres-Dwxe290H.mjs.map → postgres-CtOzfoQU.mjs.map} +1 -1
  64. package/dist/{program-AR0qlYRg.mjs → program-CruRnLtc.mjs} +99 -84
  65. package/dist/program-CruRnLtc.mjs.map +1 -0
  66. package/dist/query-feedback-DGoVRaWD.mjs +3 -0
  67. package/dist/{query-feedback-DZfjI34w.mjs → query-feedback-Dqz-HC__.mjs} +1 -1
  68. package/dist/{query-feedback-DZfjI34w.mjs.map → query-feedback-Dqz-HC__.mjs.map} +1 -1
  69. package/dist/router-BTIGZR2_.mjs +3 -0
  70. package/dist/{router-DHracTUC.mjs → router-DD6D4j31.mjs} +1 -1
  71. package/dist/{router-DHracTUC.mjs.map → router-DD6D4j31.mjs.map} +1 -1
  72. package/dist/runtime-paths-rni52zHX.mjs +50 -0
  73. package/dist/runtime-paths-rni52zHX.mjs.map +1 -0
  74. package/dist/scope-NIwCGxRI.mjs +351 -0
  75. package/dist/scope-NIwCGxRI.mjs.map +1 -0
  76. package/dist/{server-BOuAOj9b.mjs → server-Cp7tNzNC.mjs} +19 -2
  77. package/dist/server-Cp7tNzNC.mjs.map +1 -0
  78. package/dist/skills/Worker/SKILL.md +13 -0
  79. package/dist/{sources-ju6AA7lL.mjs → sources-CS43CpuS.mjs} +1 -1
  80. package/dist/{sources-ju6AA7lL.mjs.map → sources-CS43CpuS.mjs.map} +1 -1
  81. package/dist/{sqlite-kFZeWCJ2.mjs → sqlite-Cq3gJcq9.mjs} +1 -1
  82. package/dist/{sqlite-kFZeWCJ2.mjs.map → sqlite-Cq3gJcq9.mjs.map} +1 -1
  83. package/dist/{state-ahFAbD8b.mjs → state-BTa10SOM.mjs} +1 -1
  84. package/dist/{state-ahFAbD8b.mjs.map → state-BTa10SOM.mjs.map} +1 -1
  85. package/dist/{state-DH_mp5Xe.mjs → state-RXepvbnO.mjs} +1 -1
  86. package/dist/{themes-DqhNUUQk.mjs → themes-B7h9YDiy.mjs} +1 -1
  87. package/dist/{themes-DqhNUUQk.mjs.map → themes-B7h9YDiy.mjs.map} +1 -1
  88. package/dist/{tools-COna9vZR.mjs → tools-BW3NLMPk.mjs} +1 -1
  89. package/dist/{tools-CMlHa9lZ.mjs → tools-LlzcV_eK.mjs} +14 -14
  90. package/dist/{tools-CMlHa9lZ.mjs.map → tools-LlzcV_eK.mjs.map} +1 -1
  91. package/dist/{trace-DTP6k4B_.mjs → trace-u4hT-pLD.mjs} +1 -1
  92. package/dist/{trace-DTP6k4B_.mjs.map → trace-u4hT-pLD.mjs.map} +1 -1
  93. package/dist/{vault-indexer-YMH22mLU.mjs → vault-indexer-mata9S7b.mjs} +1 -1
  94. package/dist/{vault-indexer-YMH22mLU.mjs.map → vault-indexer-mata9S7b.mjs.map} +1 -1
  95. package/dist/{work-queue-worker-B3nl8yyn.mjs → work-queue-worker-7Vkli4Yh.mjs} +4 -4
  96. package/dist/{work-queue-worker-B3nl8yyn.mjs.map → work-queue-worker-7Vkli4Yh.mjs.map} +1 -1
  97. package/dist/work-queue-worker-D1p2V-ae.mjs +14 -0
  98. package/dist/{zettelkasten-DDvKRm2V.mjs → zettelkasten-F0D_XfQ4.mjs} +3 -3
  99. package/dist/{zettelkasten-DDvKRm2V.mjs.map → zettelkasten-F0D_XfQ4.mjs.map} +1 -1
  100. package/docs/commands/README.md +2 -2
  101. package/docs/commands/mcp.md +4 -4
  102. package/docs/commands/worker.md +3 -0
  103. package/docs/worker.md +40 -0
  104. package/package.json +1 -1
  105. package/plugins/productivity/hooks/hooks.json +5 -0
  106. package/src/hooks/ts/user-prompt/worker-supervision.ts +123 -0
  107. package/statusline-command.sh +65 -73
  108. package/dist/chain-DhVHVnmT.mjs.map +0 -1
  109. package/dist/config-D1G9IFpn.mjs.map +0 -1
  110. package/dist/daemon-BSL9sXxm.mjs +0 -22
  111. package/dist/daemon-CpdVUnl_.mjs.map +0 -1
  112. package/dist/detector-BLVOCiJ_.mjs +0 -5
  113. package/dist/factory-C7ua_3ik.mjs +0 -3
  114. package/dist/main-resolver-CM1IHbuu.mjs.map +0 -1
  115. package/dist/main-resolver-E5WlilhM.mjs +0 -6
  116. package/dist/program-AR0qlYRg.mjs.map +0 -1
  117. package/dist/query-feedback-CGcEiGtY.mjs +0 -3
  118. package/dist/router-CAtyVQud.mjs +0 -3
  119. package/dist/server-BOuAOj9b.mjs.map +0 -1
  120. package/dist/work-queue-worker-BM1GcAAq.mjs +0 -14
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["z.number","z.string","z.boolean"],"sources":["../../src/browser-mcp/bridge-client.ts","../../src/browser-mcp/tools.ts","../../src/browser-mcp/index.ts"],"sourcesContent":["/**\n * WebSocket client for the browser bridge.\n *\n * Talks to the native host's hand-rolled WS server (ws://127.0.0.1:8756) with\n * the global WebSocket — node >= 22, no external dependency. Requests carry an\n * id; replies are correlated by it. The socket is connected lazily per call\n * and reconnected on demand, so the server survives Chrome being closed.\n */\n\nexport const BRIDGE_PORT = Number(process.env.PAI_BROWSER_BRIDGE_PORT || 8756);\nexport const BRIDGE_URL = `ws://127.0.0.1:${BRIDGE_PORT}`;\n\nexport const NOT_CONNECTED_MESSAGE =\n \"Chrome bridge not connected — start Chrome (the PAI browser-bridge extension must be running) and try again.\";\n\n/** Minimal shape of the pieces of WebSocket this client uses. */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n onopen: (() => void) | null;\n onclose: (() => void) | null;\n onerror: (() => void) | null;\n onmessage: ((ev: { data: string }) => void) | null;\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport const globalWebSocketFactory: WebSocketFactory = (url) => {\n const WS = (globalThis as { WebSocket?: new (url: string) => WebSocketLike }).WebSocket;\n if (!WS) {\n throw new Error(\n \"this node runtime has no global WebSocket — pai-browser-mcp needs node >= 22\"\n );\n }\n return new WS(url);\n};\n\nexport class NotConnectedError extends Error {\n constructor() {\n super(NOT_CONNECTED_MESSAGE);\n this.name = \"NotConnectedError\";\n }\n}\n\nexport interface BridgeReply {\n id: number;\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst REQUEST_TIMEOUT_MS = 5_000;\nconst OPEN = 1;\n\nexport class BridgeClient {\n private socket: WebSocketLike | null = null;\n private connecting: Promise<WebSocketLike> | null = null;\n private nextId = 1;\n private readonly pending = new Map<\n number,\n { resolve: (r: BridgeReply) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }\n >();\n\n constructor(\n private readonly url: string = BRIDGE_URL,\n private readonly socketFactory: WebSocketFactory = globalWebSocketFactory\n ) {}\n\n /** True when the underlying socket is open. */\n get connected(): boolean {\n return this.socket !== null && this.socket.readyState === OPEN;\n }\n\n private ensureSocket(): Promise<WebSocketLike> {\n if (this.socket && this.socket.readyState === OPEN) return Promise.resolve(this.socket);\n if (this.connecting) return this.connecting; // concurrent sends share one socket\n this.dropSocket();\n const attempt = new Promise<WebSocketLike>((resolve, reject) => {\n let socket: WebSocketLike;\n try {\n socket = this.socketFactory(this.url);\n } catch (e) {\n reject(e);\n return;\n }\n const timer = setTimeout(() => {\n reject(new NotConnectedError());\n socket.onopen = null;\n socket.onclose = null;\n socket.onerror = null;\n socket.onmessage = null;\n try {\n socket.close();\n } catch {\n /* already gone */\n }\n }, CONNECT_TIMEOUT_MS);\n socket.onopen = () => {\n clearTimeout(timer);\n this.socket = socket;\n resolve(socket);\n };\n socket.onclose = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onerror = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onmessage = (ev) => this.onMessage(ev);\n });\n this.connecting = attempt;\n const clear = () => {\n if (this.connecting === attempt) this.connecting = null;\n };\n attempt.then(clear, clear);\n return attempt;\n }\n\n private dropSocket(): void {\n if (this.socket) {\n this.socket.onopen = null;\n this.socket.onclose = null;\n this.socket.onerror = null;\n this.socket.onmessage = null;\n try {\n this.socket.close();\n } catch {\n /* already gone */\n }\n this.socket = null;\n }\n // Fail everything still waiting — the peer is gone.\n for (const [, p] of this.pending) {\n clearTimeout(p.timer);\n p.reject(new NotConnectedError());\n }\n this.pending.clear();\n }\n\n private onMessage(ev: { data: string }): void {\n let reply: BridgeReply;\n try {\n reply = JSON.parse(ev.data) as BridgeReply;\n } catch {\n return; // bridge noise (keepalive pings echo nothing back); ignore\n }\n const p = reply?.id !== undefined ? this.pending.get(reply.id) : undefined;\n if (!p) return;\n this.pending.delete(reply.id);\n clearTimeout(p.timer);\n p.resolve(reply);\n }\n\n /**\n * Sends one command and awaits its correlated reply. Connection problems\n * and timeouts both surface as NotConnectedError with the user-facing text.\n */\n async send(command: string, params: Record<string, unknown> = {}): Promise<unknown> {\n await this.ensureSocket();\n const id = this.nextId++;\n const frame = JSON.stringify({ id, command, ...params });\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new NotConnectedError());\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, {\n resolve: (reply) => (reply.ok ? resolve(reply.result) : reject(new Error(reply.error ?? \"bridge error\"))),\n reject,\n timer,\n });\n try {\n this.socket!.send(frame);\n } catch (e) {\n this.pending.delete(id);\n clearTimeout(timer);\n reject(new NotConnectedError());\n }\n });\n }\n\n close(): void {\n this.dropSocket();\n }\n}\n","/**\n * pai-browser MCP tool table.\n *\n * One definition per tool: MCP-facing name/schema, the wire command it maps\n * to on the browser bridge, param translation, and result formatting. Kept as\n * a data table so tests can drive every mapping without a transport, and so\n * registerBrowserTools stays a five-line loop.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { NotConnectedError } from \"./bridge-client.js\";\n\nexport interface BridgeLike {\n send(command: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport interface BrowserToolDef {\n name: string;\n description: string;\n /** zod raw shape for server.tool() */\n shape: z.ZodRawShape;\n /** wire command the extension handles */\n command: string;\n /** MCP args → wire params */\n toParams: (args: Record<string, unknown>) => Record<string, unknown>;\n /** wire result → text shown to the model */\n format?: (result: unknown) => string;\n}\n\nconst tabSchema = z.number().int().describe(\"Tab id, as returned by tabs_list.\");\n\nfunction asRecord(v: unknown): Record<string, unknown> {\n return (v ?? {}) as Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Result formatters\n// ---------------------------------------------------------------------------\n\nfunction formatTabs(result: unknown): string {\n const tabs = (Array.isArray(result) ? result : asRecord(result).tabs) as Array<\n Record<string, unknown>\n >;\n if (!Array.isArray(tabs) || tabs.length === 0) return \"(no open tabs)\";\n return tabs\n .map(\n (t) =>\n `${t.id}${t.active ? \" *\" : \" \"} ${String(t.title ?? \"\")}\\n ${String(t.url ?? \"\")}`\n )\n .join(\"\\n\");\n}\n\nfunction formatSnapshot(result: unknown): string {\n return String(asRecord(result).yaml ?? \"\");\n}\n\nfunction formatLogs(result: unknown): string {\n const entries = asRecord(result).entries as Array<Record<string, unknown>>;\n if (!Array.isArray(entries) || entries.length === 0) return \"(no console output captured)\";\n return entries\n .map((e) => {\n const kind = e.source === \"console\" ? String(e.type ?? \"log\") : String(e.level ?? \"log\");\n return `[${kind}] ${String(e.text ?? \"\")}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatEval(result: unknown): string {\n const value = asRecord(result).value;\n return typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n}\n\nfunction formatScreenshot(result: unknown): string {\n // Raw base64 PNG — the tool contract is \"base64 png\", not prose.\n return String(asRecord(result).base64 ?? \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Tool table: MCP name → wire command\n// ---------------------------------------------------------------------------\n\nexport const BROWSER_TOOLS: BrowserToolDef[] = [\n {\n name: \"tabs_list\",\n description: \"List the tabs of the user's real running Chrome (id, title, url, active).\",\n shape: {},\n command: \"list_tabs\",\n toParams: () => ({}),\n format: formatTabs,\n },\n {\n name: \"tab_open\",\n description: \"Open a new tab in the user's Chrome and return its tab id.\",\n shape: {\n url: z.string().describe(\"URL to open.\"),\n active: z.boolean().optional().describe(\"Focus the new tab (default true).\"),\n },\n command: \"open_tab\",\n toParams: (a) => ({ url: a.url, ...(a.active !== undefined ? { active: a.active } : {}) }),\n },\n {\n name: \"tab_select\",\n description: \"Bring a tab to the front (activate it and focus its window).\",\n shape: { tab: tabSchema },\n command: \"select_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"tab_close\",\n description: \"Close a tab.\",\n shape: { tab: tabSchema },\n command: \"close_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"dom_snapshot\",\n description:\n \"Snapshot a tab's DOM as an accessibility-tree YAML with [ref=sN] ids for interactive elements. \" +\n \"Take a snapshot before dom_click/dom_type; refs stay valid until the next snapshot or navigation.\",\n shape: { tab: tabSchema },\n command: \"snapshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatSnapshot,\n },\n {\n name: \"dom_click\",\n description: \"Click an element by its [ref=sN] from the latest dom_snapshot (trusted CDP event).\",\n shape: { tab: tabSchema, ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s3.\") },\n command: \"click\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref }),\n },\n {\n name: \"dom_type\",\n description:\n \"Focus an element by its [ref=sN] and type text into it (inserts at the caret, works in SPAs).\",\n shape: {\n tab: tabSchema,\n ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s5.\"),\n text: z.string().describe(\"Text to type.\"),\n },\n command: \"type\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref, text: a.text }),\n },\n {\n name: \"page_text\",\n description: \"Read a tab's visible text (document.body.innerText).\",\n shape: { tab: tabSchema },\n command: \"eval\",\n toParams: () => ({ code: \"document.body.innerText\" }),\n format: formatEval,\n },\n {\n name: \"eval_js\",\n description: \"Evaluate JavaScript in a tab and return the JSON-safe result. Awaits promises.\",\n shape: {\n tab: tabSchema,\n code: z.string().describe(\"JavaScript expression to evaluate.\"),\n },\n command: \"eval\",\n toParams: (a) => ({ code: a.code }),\n format: formatEval,\n },\n {\n name: \"tab_screenshot\",\n description: \"Screenshot a tab (PNG, base64).\",\n shape: { tab: tabSchema },\n command: \"screenshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatScreenshot,\n },\n {\n name: \"console_logs\",\n description: \"Read the console output captured for a tab since its last snapshot.\",\n shape: { tab: tabSchema },\n command: \"console_logs\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatLogs,\n },\n];\n\n/** Runs one tool against the bridge; returns MCP content + isError. */\nexport async function runBrowserTool(\n def: BrowserToolDef,\n args: Record<string, unknown>,\n bridge: BridgeLike\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await bridge.send(def.command, def.toParams(args));\n return { content: [{ type: \"text\", text: def.format ? def.format(result) : JSON.stringify(result, null, 2) }] };\n } catch (e) {\n const msg =\n e instanceof NotConnectedError\n ? e.message\n : e instanceof Error\n ? e.message\n : String(e);\n return { content: [{ type: \"text\", text: msg }], isError: true };\n }\n}\n\n/** Registers every browser tool on an MCP server. */\nexport function registerBrowserTools(server: McpServer, bridge: BridgeLike): void {\n for (const def of BROWSER_TOOLS) {\n server.tool(def.name, def.description, def.shape, (args) => runBrowserTool(def, args, bridge));\n }\n}\n","#!/usr/bin/env node\n/**\n * pai-browser-mcp — stdio MCP server for the PAI browser bridge.\n *\n * The provider-independent claude-in-chrome replacement: drives the user's\n * real running Chrome (tabs + DOM) through the browser-bridge extension's\n * native messaging host, over ws://127.0.0.1:8756. Any MCP client works —\n * nothing here is Anthropic-specific.\n *\n * provider → MCP (this server) → WebSocket → native host → extension → Chrome\n *\n * If Chrome is not running (or the extension is not loaded), every tool\n * returns a clear \"start Chrome\" error; the server itself stays alive and\n * reconnects lazily per call.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { BridgeClient, globalWebSocketFactory } from \"./bridge-client.js\";\nimport { registerBrowserTools } from \"./tools.js\";\n\nasync function main(): Promise<void> {\n if (globalThis.WebSocket === undefined) {\n // Node < 22 has no global WebSocket client; say so instead of crashing\n // with an opaque TypeError when the first tool call connects.\n process.stderr.write(\n \"pai-browser-mcp: this node runtime has no global WebSocket — run with node >= 22\\n\"\n );\n process.exit(1);\n }\n\n const server = new McpServer({\n name: \"pai-browser\",\n version: \"0.1.0\",\n });\n\n const bridge = new BridgeClient(undefined, globalWebSocketFactory);\n registerBrowserTools(server, bridge);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((e) => {\n process.stderr.write(`pai-browser-mcp fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;;AASA,MAAa,cAAc,OAAO,QAAQ,IAAI,2BAA2B,KAAK;AAC9E,MAAa,aAAa,kBAAkB;AAE5C,MAAa,wBACX;AAeF,MAAa,0BAA4C,QAAQ;CAC/D,MAAM,KAAM,WAAkE;AAC9E,KAAI,CAAC,GACH,OAAM,IAAI,MACR,+EACD;AAEH,QAAO,IAAI,GAAG,IAAI;;AAGpB,IAAa,oBAAb,cAAuC,MAAM;CAC3C,cAAc;AACZ,QAAM,sBAAsB;AAC5B,OAAK,OAAO;;;AAWhB,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,OAAO;AAEb,IAAa,eAAb,MAA0B;CACxB,AAAQ,SAA+B;CACvC,AAAQ,aAA4C;CACpD,AAAQ,SAAS;CACjB,AAAiB,0BAAU,IAAI,KAG5B;CAEH,YACE,AAAiB,MAAc,YAC/B,AAAiB,gBAAkC,wBACnD;EAFiB;EACA;;;CAInB,IAAI,YAAqB;AACvB,SAAO,KAAK,WAAW,QAAQ,KAAK,OAAO,eAAe;;CAG5D,AAAQ,eAAuC;AAC7C,MAAI,KAAK,UAAU,KAAK,OAAO,eAAe,KAAM,QAAO,QAAQ,QAAQ,KAAK,OAAO;AACvF,MAAI,KAAK,WAAY,QAAO,KAAK;AACjC,OAAK,YAAY;EACjB,MAAM,UAAU,IAAI,SAAwB,SAAS,WAAW;GAC9D,IAAI;AACJ,OAAI;AACF,aAAS,KAAK,cAAc,KAAK,IAAI;YAC9B,GAAG;AACV,WAAO,EAAE;AACT;;GAEF,MAAM,QAAQ,iBAAiB;AAC7B,WAAO,IAAI,mBAAmB,CAAC;AAC/B,WAAO,SAAS;AAChB,WAAO,UAAU;AACjB,WAAO,UAAU;AACjB,WAAO,YAAY;AACnB,QAAI;AACF,YAAO,OAAO;YACR;MAGP,mBAAmB;AACtB,UAAO,eAAe;AACpB,iBAAa,MAAM;AACnB,SAAK,SAAS;AACd,YAAQ,OAAO;;AAEjB,UAAO,gBAAgB;AACrB,iBAAa,MAAM;AACnB,SAAK,YAAY;AACjB,WAAO,IAAI,mBAAmB,CAAC;;AAEjC,UAAO,gBAAgB;AACrB,iBAAa,MAAM;AACnB,SAAK,YAAY;AACjB,WAAO,IAAI,mBAAmB,CAAC;;AAEjC,UAAO,aAAa,OAAO,KAAK,UAAU,GAAG;IAC7C;AACF,OAAK,aAAa;EAClB,MAAM,cAAc;AAClB,OAAI,KAAK,eAAe,QAAS,MAAK,aAAa;;AAErD,UAAQ,KAAK,OAAO,MAAM;AAC1B,SAAO;;CAGT,AAAQ,aAAmB;AACzB,MAAI,KAAK,QAAQ;AACf,QAAK,OAAO,SAAS;AACrB,QAAK,OAAO,UAAU;AACtB,QAAK,OAAO,UAAU;AACtB,QAAK,OAAO,YAAY;AACxB,OAAI;AACF,SAAK,OAAO,OAAO;WACb;AAGR,QAAK,SAAS;;AAGhB,OAAK,MAAM,GAAG,MAAM,KAAK,SAAS;AAChC,gBAAa,EAAE,MAAM;AACrB,KAAE,OAAO,IAAI,mBAAmB,CAAC;;AAEnC,OAAK,QAAQ,OAAO;;CAGtB,AAAQ,UAAU,IAA4B;EAC5C,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,GAAG,KAAK;UACrB;AACN;;EAEF,MAAM,IAAI,OAAO,OAAO,SAAY,KAAK,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjE,MAAI,CAAC,EAAG;AACR,OAAK,QAAQ,OAAO,MAAM,GAAG;AAC7B,eAAa,EAAE,MAAM;AACrB,IAAE,QAAQ,MAAM;;;;;;CAOlB,MAAM,KAAK,SAAiB,SAAkC,EAAE,EAAoB;AAClF,QAAM,KAAK,cAAc;EACzB,MAAM,KAAK,KAAK;EAChB,MAAM,QAAQ,KAAK,UAAU;GAAE;GAAI;GAAS,GAAG;GAAQ,CAAC;AACxD,SAAO,IAAI,SAAkB,SAAS,WAAW;GAC/C,MAAM,QAAQ,iBAAiB;AAC7B,SAAK,QAAQ,OAAO,GAAG;AACvB,WAAO,IAAI,mBAAmB,CAAC;MAC9B,mBAAmB;AACtB,QAAK,QAAQ,IAAI,IAAI;IACnB,UAAU,UAAW,MAAM,KAAK,QAAQ,MAAM,OAAO,GAAG,OAAO,IAAI,MAAM,MAAM,SAAS,eAAe,CAAC;IACxG;IACA;IACD,CAAC;AACF,OAAI;AACF,SAAK,OAAQ,KAAK,MAAM;YACjB,GAAG;AACV,SAAK,QAAQ,OAAO,GAAG;AACvB,iBAAa,MAAM;AACnB,WAAO,IAAI,mBAAmB,CAAC;;IAEjC;;CAGJ,QAAc;AACZ,OAAK,YAAY;;;;;;AC9JrB,MAAM,YAAYA,QAAU,CAAC,KAAK,CAAC,SAAS,oCAAoC;AAEhF,SAAS,SAAS,GAAqC;AACrD,QAAQ,KAAK,EAAE;;AAOjB,SAAS,WAAW,QAAyB;CAC3C,MAAM,OAAQ,MAAM,QAAQ,OAAO,GAAG,SAAS,SAAS,OAAO,CAAC;AAGhE,KAAI,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,WAAW,EAAG,QAAO;AACtD,QAAO,KACJ,KACE,MACC,GAAG,EAAE,KAAK,EAAE,SAAS,OAAO,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,CAAC,SAAS,OAAO,EAAE,OAAO,GAAG,GACzF,CACA,KAAK,KAAK;;AAGf,SAAS,eAAe,QAAyB;AAC/C,QAAO,OAAO,SAAS,OAAO,CAAC,QAAQ,GAAG;;AAG5C,SAAS,WAAW,QAAyB;CAC3C,MAAM,UAAU,SAAS,OAAO,CAAC;AACjC,KAAI,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,WAAW,EAAG,QAAO;AAC5D,QAAO,QACJ,KAAK,MAAM;AAEV,SAAO,IADM,EAAE,WAAW,YAAY,OAAO,EAAE,QAAQ,MAAM,GAAG,OAAO,EAAE,SAAS,MAAM,CACxE,IAAI,OAAO,EAAE,QAAQ,GAAG;GACxC,CACD,KAAK,KAAK;;AAGf,SAAS,WAAW,QAAyB;CAC3C,MAAM,QAAQ,SAAS,OAAO,CAAC;AAC/B,QAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,OAAO,MAAM,EAAE;;AAG3E,SAAS,iBAAiB,QAAyB;AAEjD,QAAO,OAAO,SAAS,OAAO,CAAC,UAAU,GAAG;;AAO9C,MAAa,gBAAkC;CAC7C;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE;EACT,SAAS;EACT,iBAAiB,EAAE;EACnB,QAAQ;EACT;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO;GACL,KAAKC,QAAU,CAAC,SAAS,eAAe;GACxC,QAAQC,SAAW,CAAC,UAAU,CAAC,SAAS,oCAAoC;GAC7E;EACD,SAAS;EACT,WAAW,OAAO;GAAE,KAAK,EAAE;GAAK,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE;GAAG;EAC1F;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,WAAW,OAAO,EAAE,OAAO,EAAE,KAAK;EACnC;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,WAAW,OAAO,EAAE,OAAO,EAAE,KAAK;EACnC;CACD;EACE,MAAM;EACN,aACE;EAEF,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,WAAW,OAAO,EAAE,OAAO,EAAE,KAAK;EAClC,QAAQ;EACT;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO;GAAE,KAAK;GAAW,KAAKD,QAAU,CAAC,SAAS,qCAAqC;GAAE;EACzF,SAAS;EACT,WAAW,OAAO;GAAE,OAAO,EAAE;GAAK,KAAK,EAAE;GAAK;EAC/C;CACD;EACE,MAAM;EACN,aACE;EACF,OAAO;GACL,KAAK;GACL,KAAKA,QAAU,CAAC,SAAS,qCAAqC;GAC9D,MAAMA,QAAU,CAAC,SAAS,gBAAgB;GAC3C;EACD,SAAS;EACT,WAAW,OAAO;GAAE,OAAO,EAAE;GAAK,KAAK,EAAE;GAAK,MAAM,EAAE;GAAM;EAC7D;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,iBAAiB,EAAE,MAAM,2BAA2B;EACpD,QAAQ;EACT;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO;GACL,KAAK;GACL,MAAMA,QAAU,CAAC,SAAS,qCAAqC;GAChE;EACD,SAAS;EACT,WAAW,OAAO,EAAE,MAAM,EAAE,MAAM;EAClC,QAAQ;EACT;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,WAAW,OAAO,EAAE,OAAO,EAAE,KAAK;EAClC,QAAQ;EACT;CACD;EACE,MAAM;EACN,aAAa;EACb,OAAO,EAAE,KAAK,WAAW;EACzB,SAAS;EACT,WAAW,OAAO,EAAE,OAAO,EAAE,KAAK;EAClC,QAAQ;EACT;CACF;;AAGD,eAAsB,eACpB,KACA,MACA,QACgF;AAChF,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,KAAK,IAAI,SAAS,IAAI,SAAS,KAAK,CAAC;AACjE,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,IAAI,SAAS,IAAI,OAAO,OAAO,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EAAE;UACxG,GAAG;AAOV,SAAO;GAAE,SAAS,CAAC;IAAE,MAAM;IAAQ,MALjC,aAAa,oBACT,EAAE,UACF,aAAa,QACX,EAAE,UACF,OAAO,EAAE;IAC6B,CAAC;GAAE,SAAS;GAAM;;;;AAKpE,SAAgB,qBAAqB,QAAmB,QAA0B;AAChF,MAAK,MAAM,OAAO,cAChB,QAAO,KAAK,IAAI,MAAM,IAAI,aAAa,IAAI,QAAQ,SAAS,eAAe,KAAK,MAAM,OAAO,CAAC;;;;;;;;;;;;;;;;;;;ACvLlG,eAAe,OAAsB;AACnC,KAAI,WAAW,cAAc,QAAW;AAGtC,UAAQ,OAAO,MACb,qFACD;AACD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,SAAS,IAAI,UAAU;EAC3B,MAAM;EACN,SAAS;EACV,CAAC;AAGF,sBAAqB,QADN,IAAI,aAAa,QAAW,uBAAuB,CAC9B;CAEpC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;;AAGjC,MAAM,CAAC,OAAO,MAAM;AAClB,SAAQ,OAAO,MAAM,gCAAgC,OAAO,EAAE,CAAC,IAAI;AACnE,SAAQ,KAAK,EAAE;EACf"}
@@ -1,93 +1,13 @@
1
- import { a as WorkersConfigError, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, o as assertProviderRunnable, p as providerKeyPath, s as classModelCapability } from "./config-Dl8lT4Lu.mjs";
2
- import { _ as workersLogDir, d as setCooldown, f as eventsPath, g as statusPath, h as panesDir, i as buildRunEnv, m as noMcpConfigPath, n as ensureProxyRunning, o as isQuotaFailure, p as ledgerPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-BOuAOj9b.mjs";
3
- import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { a as WorkersConfigError, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, o as assertProviderRunnable, p as providerKeyPath, s as classModelCapability } from "./config-DgzR5sfb.mjs";
2
+ import { _ as workersLogDir, d as setCooldown, f as eventsPath, g as statusPath, h as panesDir, i as buildRunEnv, m as noMcpConfigPath, n as ensureProxyRunning, o as isQuotaFailure, p as ledgerPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-Cp7tNzNC.mjs";
3
+ import { S as stripPromptValues, _ as newWorkerId, a as resolveSpawnerSession, b as parseRunnerArgs, d as alive, g as loadStatuses, h as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as nowStamp, x as shortText, y as saveStatus } from "./scope-NIwCGxRI.mjs";
4
+ import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
5
  import { homedir, tmpdir } from "node:os";
5
- import { dirname, join, relative } from "node:path";
6
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
7
  import { connect, createServer } from "node:net";
7
8
  import { execFileSync, spawn } from "node:child_process";
8
9
  import { createInterface } from "node:readline";
9
10
 
10
- //#region src/workers/args.ts
11
- function parseRunnerArgs(argv) {
12
- let prompt = null;
13
- let outputFormat = "text";
14
- const rest = [];
15
- let headless = false;
16
- let callerModel = false;
17
- let callerMcpConfig = false;
18
- let callerSystemPrompt = false;
19
- const mcp = [];
20
- let i = 0;
21
- while (i < argv.length) {
22
- const a = argv[i];
23
- if (a === "-p" || a === "--print") {
24
- headless = true;
25
- rest.push(a);
26
- if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
27
- prompt = argv[i + 1];
28
- rest.push(prompt);
29
- i += 1;
30
- }
31
- } else if (a === "--output-format") {
32
- const v = argv[i + 1];
33
- if (v === "json" || v === "stream-json") outputFormat = v;
34
- i += 1;
35
- } else if (a.startsWith("--output-format=")) {
36
- const v = a.slice(16);
37
- if (v === "json" || v === "stream-json") outputFormat = v;
38
- } else if (a === "--verbose") {} else if (a === "--mcp") {
39
- const v = argv[i + 1];
40
- if (v !== void 0 && !v.startsWith("-")) {
41
- mcp.push(v);
42
- i += 1;
43
- }
44
- } else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
45
- else {
46
- if (a === "--model") callerModel = true;
47
- if (a.startsWith("--model=")) callerModel = true;
48
- if (a === "--mcp-config") callerMcpConfig = true;
49
- if (a.startsWith("--mcp-config=")) callerMcpConfig = true;
50
- if (a === "--append-system-prompt") callerSystemPrompt = true;
51
- if (a.startsWith("--append-system-prompt=")) callerSystemPrompt = true;
52
- if (prompt === null && !a.startsWith("-") && rest.length > 0 && (rest[rest.length - 1] === "-p" || rest[rest.length - 1] === "--print")) prompt = a;
53
- rest.push(a);
54
- }
55
- i += 1;
56
- }
57
- return {
58
- prompt,
59
- outputFormat,
60
- rest,
61
- headless,
62
- callerModel,
63
- callerMcpConfig,
64
- callerSystemPrompt,
65
- mcp
66
- };
67
- }
68
- /**
69
- * Turn `-p "<prompt>"` into bare `-p` (same for --print): for stream-json
70
- * stdin runs the prompt moves to the first user message on stdin, so the
71
- * value must not stay on the command line. Only prompt values directly
72
- * following the flag are touched; everything else passes through.
73
- */
74
- function stripPromptValues(argv) {
75
- const out = [];
76
- for (let i = 0; i < argv.length; i++) {
77
- const a = argv[i];
78
- const isPrint = a === "-p" || a === "--print";
79
- out.push(a);
80
- if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith("-")) i += 1;
81
- }
82
- return out;
83
- }
84
- /** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */
85
- function shortText(s, n) {
86
- const t = String(s ?? "").split(/\s+/).filter(Boolean).join(" ");
87
- return t.length <= n ? t : t.slice(0, n - 1) + "…";
88
- }
89
-
90
- //#endregion
91
11
  //#region src/workers/ledger.ts
92
12
  /**
93
13
  * ledger.ts — the routing ledger, one append-only log for every worker event.
@@ -157,231 +77,6 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
157
77
  };
158
78
  }
159
79
 
160
- //#endregion
161
- //#region src/workers/status.ts
162
- /**
163
- * status.ts — the live per-worker status file.
164
- *
165
- * One JSON file per run, rewritten atomically on every turn, read by `ps`,
166
- * `follow`, the status line and the pane logic. Same field set the Python
167
- * runner wrote, plus `provider` (routing is multi-provider now) and `session`
168
- * (the AIBroker identity of the launching session, see scope.ts).
169
- */
170
- /** Label a worker gets when launched with neither --label nor a prompt. */
171
- const UNLABELED = "unlabeled";
172
- /** Context-meter percentage 0–100, null when the numbers are missing. */
173
- function contextPercent(s) {
174
- if (!s.contextTokens || !s.contextWindow) return null;
175
- return Math.round(s.contextTokens / s.contextWindow * 100);
176
- }
177
- /** Compact token count: 84k, 200k, 900. */
178
- function fmtContextK(n) {
179
- return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
180
- }
181
- /**
182
- * The one context-meter label, shared by every renderer so identical
183
- * numbers always render identically: `ctx 84k/200k (42%)` — used-style
184
- * percent. Empty when the numbers are missing (callers drop the part).
185
- */
186
- function contextLabel(s) {
187
- const pct = contextPercent(s);
188
- if (pct === null) return "";
189
- return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;
190
- }
191
- /** Timestamp format shared by status files and the ledger. */
192
- function nowStamp(d = /* @__PURE__ */ new Date()) {
193
- const p = (n) => String(n).padStart(2, "0");
194
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
195
- }
196
- let lastId = "";
197
- let idSeq = 0;
198
- function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
199
- const p = (n) => String(n).padStart(2, "0");
200
- const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;
201
- if (base === lastId) {
202
- idSeq += 1;
203
- return `${base}-${String(idSeq).padStart(2, "0")}`;
204
- }
205
- lastId = base;
206
- idSeq = 0;
207
- return base;
208
- }
209
- /** Write status atomically (temp + rename) and stamp `updated`. */
210
- function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
211
- status.updated = nowStamp(d);
212
- const path = statusPath(logDir, status.id);
213
- const tmp = `${path}.tmp`;
214
- writeFileSync(tmp, JSON.stringify(status), "utf8");
215
- renameSync(tmp, path);
216
- }
217
- /** Load every status file in the logDir, oldest id first, skipping damage. */
218
- function loadStatuses(logDir) {
219
- if (!existsSync(logDir)) return [];
220
- const out = [];
221
- for (const name of readdirSync(logDir).sort()) {
222
- if (!name.endsWith(".status")) continue;
223
- try {
224
- out.push(JSON.parse(readFileSync(join(logDir, name), "utf8")));
225
- } catch {}
226
- }
227
- return out;
228
- }
229
- function loadStatus(logDir, id) {
230
- const path = statusPath(logDir, id);
231
- if (!existsSync(path)) return null;
232
- try {
233
- return JSON.parse(readFileSync(path, "utf8"));
234
- } catch {
235
- return null;
236
- }
237
- }
238
- function alive(pid) {
239
- if (!pid || pid <= 0) return false;
240
- try {
241
- process.kill(pid, 0);
242
- return true;
243
- } catch {
244
- return false;
245
- }
246
- }
247
- /** "42s" under 90s, "7m" above — the coarse age the table and bar show. */
248
- function ageOf(ts, now = /* @__PURE__ */ new Date()) {
249
- const t = Date.parse(ts.replace(" ", "T"));
250
- if (Number.isNaN(t)) return "?";
251
- const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
252
- return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;
253
- }
254
- /** One-line description of a tool call, e.g. "Bash: npm test". */
255
- function describeTool(name, inp) {
256
- if (typeof inp !== "object" || inp === null) return name;
257
- const i = inp;
258
- const get = (k) => typeof i[k] === "string" ? i[k] : "";
259
- if (name === "Bash") return `Bash: ${shortText(get("command"), 70)}`;
260
- if (name === "Read" || name === "Edit" || name === "Write" || name === "MultiEdit") return `${name}: ${get("file_path").split("/").pop() ?? ""}`;
261
- if (name === "Grep" || name === "Glob") return `${name}: ${shortText(get("pattern"), 50)}`;
262
- if (name === "WebSearch") return `${name}: ${shortText(get("query"), 50)}`;
263
- if (name === "WebFetch") return `${name}: ${shortText(get("url"), 60)}`;
264
- return name;
265
- }
266
-
267
- //#endregion
268
- //#region src/workers/scope.ts
269
- /**
270
- * scope.ts — which workers "belong" to the terminal asking about them.
271
- *
272
- * Two identities, in priority order:
273
- *
274
- * 1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm
275
- * session UUID → session name. The launching session's UUID is stored in
276
- * each worker's status file as `session.id`, so every pane of a named
277
- * session sees its workers — regardless of tab layout.
278
- * 2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the
279
- * original heuristic, kept as the fallback when no registry entry
280
- * matches (AIBroker absent, unnamed session, non-iTerm terminal).
281
- *
282
- * If neither is available, viewers fall back to "all workers".
283
- *
284
- * A third case: workers spawned from a Claude Code Bash tool (the
285
- * orchestrator pattern) have neither — Claude Code exports no terminal or
286
- * session identity to its Bash children. Those record `spawnerSession`, the
287
- * orchestrator's claude session id bridged through the status line's
288
- * session map (see below), so their tab still claims them.
289
- */
290
- const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
291
- /**
292
- * `w<window>t<tab>` prefix of an iTerm session id, "" if empty or malformed.
293
- * Panes split from the same tab share this prefix and differ only in `p<n>`.
294
- */
295
- function tabKey(term) {
296
- if (!term) return "";
297
- const head = term.split("p", 1)[0];
298
- const parts = head.slice(1).split("t");
299
- if (head.startsWith("w") && parts.length === 2 && parts.every((p) => /^\d+$/.test(p))) return head;
300
- return "";
301
- }
302
- /** Tab key of the iTerm tab this process runs in, "" if not inside iTerm2. */
303
- function currentTabKey(env = process.env) {
304
- return tabKey(env.ITERM_SESSION_ID ?? "");
305
- }
306
- /** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */
307
- function itermUuid(term) {
308
- if (!term) return "";
309
- return term.split(":").pop() ?? "";
310
- }
311
- /**
312
- * Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent
313
- * name registry (the same store `aibroker_rename` writes); returns null when
314
- * AIBroker is absent or the session is not in it — callers then use the tab
315
- * key, which is the pre-AIBroker behaviour.
316
- */
317
- function resolveSession(term, registryPath = AIBROKER_REGISTRY) {
318
- const uuid = itermUuid(term);
319
- if (!uuid) return null;
320
- let names;
321
- try {
322
- if (!existsSync(registryPath)) return null;
323
- names = JSON.parse(readFileSync(registryPath, "utf8"));
324
- } catch {
325
- return null;
326
- }
327
- const name = names[uuid];
328
- if (typeof name !== "string" || !name) return null;
329
- return {
330
- id: uuid,
331
- name
332
- };
333
- }
334
- /**
335
- * True when `worker` was launched from `term` (the terminal asking about it).
336
- * Session-id match first; the tab key only when no registry entry matched —
337
- * a session without a name keeps the tab-scoped behaviour it always had.
338
- */
339
- function workerInScope(worker, term) {
340
- if (!term) return false;
341
- const uuid = itermUuid(term);
342
- if (worker.session?.id && uuid) return worker.session.id === uuid;
343
- return tabKey(worker.term) === tabKey(term) && tabKey(term) !== "";
344
- }
345
- /**
346
- * Registry key for pane files: the session id when AIBroker knows this
347
- * terminal, else the tab key, else the raw iTerm UUID.
348
- */
349
- function scopeKey(term) {
350
- const session = resolveSession(term);
351
- if (session) return session.id;
352
- return currentTabKey() || itermUuid(term);
353
- }
354
- /** `[Name]` when the worker has an AIBroker session name, else "". */
355
- function sessionTag(worker) {
356
- return worker.session?.name ? `[${worker.session.name}]` : "";
357
- }
358
- /** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */
359
- const SPAWNER_SESSION_TTL_MS = 10 * 6e4;
360
- /** Map entries not refreshed within this window are pruned on write. */
361
- const SESSION_MAP_PRUNE_MS = 60 * 6e4;
362
- function sessionMapPath(logDir) {
363
- return join(logDir, "claude-session-map.json");
364
- }
365
- /**
366
- * The claude session a new run was spawned by, when it can be known: a run
367
- * inside another worker inherits its spawner (chain stages keep the
368
- * orchestrator's tab that way); otherwise a fresh map entry for `cwd`.
369
- */
370
- function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now()) {
371
- const parentId = env.PAI_WORKER_ID;
372
- if (parentId) {
373
- const inherited = loadStatus(logDir, parentId)?.spawnerSession;
374
- if (inherited) return inherited;
375
- }
376
- const path = sessionMapPath(logDir);
377
- if (!existsSync(path) || !cwd) return null;
378
- try {
379
- const entry = JSON.parse(readFileSync(path, "utf8"))[cwd];
380
- if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;
381
- } catch {}
382
- return null;
383
- }
384
-
385
80
  //#endregion
386
81
  //#region src/workers/pane.ts
387
82
  /**
@@ -1219,6 +914,11 @@ function worktreeWanted(flag, opts) {
1219
914
  * degrade to an in-place run.
1220
915
  */
1221
916
  function addWorktree(logDir, id, cwd) {
917
+ const swept = sweepOrphanWorktrees(logDir);
918
+ if (swept.length) appendLedger(ledgerPath(logDir), "WORKER-NOTE", {
919
+ id,
920
+ note: `swept orphan worktree(s): ${swept.join(", ")}`
921
+ });
1222
922
  const dir = worktreePath(logDir, id);
1223
923
  const branch = worktreeBranch(id);
1224
924
  const base = git(cwd, ["rev-parse", "HEAD"]);
@@ -1235,6 +935,46 @@ function addWorktree(logDir, id, cwd) {
1235
935
  base
1236
936
  };
1237
937
  }
938
+ /**
939
+ * Remove worktrees (and their `worker/<id>` branches) whose worker no longer
940
+ * exists — no status file left in the log dir. Killed and failed runs clean
941
+ * up after themselves, but a `kill -9` or a crash strands a directory and a
942
+ * branch; every worktree creation sweeps first so they cannot accumulate.
943
+ * Directories younger than `minAgeMin` minutes are left alone: a run that is
944
+ * just starting owns its worktree a moment before its status file exists.
945
+ */
946
+ function sweepOrphanWorktrees(logDir, minAgeMin = 10) {
947
+ const root = worktreesDir(logDir);
948
+ if (!existsSync(root)) return [];
949
+ const swept = [];
950
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
951
+ if (!ent.isDirectory()) continue;
952
+ const id = ent.name;
953
+ const dir = join(root, id);
954
+ if (existsSync(statusPath(logDir, id))) continue;
955
+ try {
956
+ if ((Date.now() - statSync(dir).mtimeMs) / 6e4 < minAgeMin) continue;
957
+ } catch {}
958
+ let gitDir = null;
959
+ try {
960
+ const raw = git(dir, ["rev-parse", "--git-common-dir"]);
961
+ gitDir = isAbsolute(raw) ? raw : resolve(dir, raw);
962
+ } catch {
963
+ gitDir = null;
964
+ }
965
+ removeWorktree(gitDir ?? dir, dir, true);
966
+ if (gitDir) try {
967
+ git(gitDir, ["worktree", "prune"]);
968
+ git(gitDir, [
969
+ "branch",
970
+ "-D",
971
+ worktreeBranch(id)
972
+ ]);
973
+ } catch {}
974
+ swept.push(id);
975
+ }
976
+ return swept;
977
+ }
1238
978
  /** Commits the branch collected on top of its base. */
1239
979
  function commitsSince(dir, base) {
1240
980
  try {
@@ -1520,6 +1260,9 @@ const WORKER_CONTRACT_PROMPT = [
1520
1260
  "notes: one line, the headline a reviewer reads first.",
1521
1261
  "Never write shell heredocs — write the script to a file with the Write tool, then run it",
1522
1262
  "(heredocs fail silently or get swallowed).",
1263
+ "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
1264
+ "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
1265
+ "Write tool and run it (python3 script.py), or use jq for JSON inspection.",
1523
1266
  "",
1524
1267
  `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
1525
1268
  "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
@@ -1607,9 +1350,11 @@ function relShort(p, cwd) {
1607
1350
  * Headless workers start with NO MCP servers: every server definition is a
1608
1351
  * prompt-time tool inventory the model pays to know, and a worker that only
1609
1352
  * reads and edits files needs none of it. A run may opt in with `--mcp
1610
- * name[,name…]` (or a role carrying `"mcp": [...]`); names may be single
1611
- * servers from ~/.claude.json's `mcpServers` or `workers.mcpSets` set names,
1612
- * which expand to their member list. The filtered config lands in
1353
+ * name[,name…]`, a role carrying `"mcp": [...]`, or implicitly by naming
1354
+ * `mcp__server__tool` in --allowedTools (a grant without its server loaded is
1355
+ * a dead letter). Names may be single servers from ~/.claude.json's
1356
+ * `mcpServers` or `workers.mcpSets` set names, which expand to their member
1357
+ * list. The filtered config lands in
1613
1358
  * `<logDir>/<id>.mcp.json` and is passed with `--strict-mcp-config
1614
1359
  * --mcp-config` so exactly those servers load. MCP servers are chosen at
1615
1360
  * launch only — a mid-run `say` cannot add any.
@@ -1646,6 +1391,22 @@ function expandMcpNames(names, config, claudeJson = CLAUDE_JSON) {
1646
1391
  }
1647
1392
  return out;
1648
1393
  }
1394
+ /**
1395
+ * Derive server names from tool grants: every `mcp__<server>__<tool>` (or bare
1396
+ * `mcp__<server>`, or `mcp__<server>__*`) in an --allowedTools list names a
1397
+ * server the run expects to be loaded. A grant is a dead letter unless its
1398
+ * server is in the filtered config, so the runner treats these as implicit
1399
+ * --mcp names — and only these; non-mcp grants load nothing.
1400
+ */
1401
+ function mcpServersFromToolGrants(tools) {
1402
+ const out = [];
1403
+ for (const entry of tools) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
1404
+ if (!name.startsWith("mcp__")) continue;
1405
+ const server = name.slice(5).split("__")[0];
1406
+ if (server && !out.includes(server)) out.push(server);
1407
+ }
1408
+ return out;
1409
+ }
1649
1410
  /** Path of a run's filtered MCP config. */
1650
1411
  function runMcpConfigPath(logDir, id) {
1651
1412
  return join(logDir, `${id}.mcp.json`);
@@ -1859,9 +1620,12 @@ function codexInstalled() {
1859
1620
  * - env: ANTHROPIC_BASE_URL/AUTH_TOKEN from the provider (token from its
1860
1621
  * key file, never from the environment), the three DEFAULT_*_MODEL vars,
1861
1622
  * the provider's extra env, nonessential traffic off, and ANTHROPIC_API_KEY
1862
- * stripped so nothing can fall back to Anthropic billing. OpenAI-protocol
1863
- * providers point at the PAI proxy instead (started on demand, the
1864
- * provider name in the URL path); codex-engine providers run the Codex CLI.
1623
+ * stripped so nothing can fall back to Anthropic billing. Headless runs
1624
+ * also drop the spawner's session identity (messaging socket, session id,
1625
+ * nesting markers) inherited, a worktree child starts tool-blind.
1626
+ * OpenAI-protocol providers point at the PAI proxy instead (started on
1627
+ * demand, the provider name in the URL path); codex-engine providers run
1628
+ * the Codex CLI.
1865
1629
  * - headless (-p): strict empty MCP config unless the caller brings one or
1866
1630
  * names servers via --mcp / a role (then a filtered <id>.mcp.json),
1867
1631
  * PAI_WORKER=1 so PAI's per-session hooks leave it alone, the worker
@@ -1961,7 +1725,7 @@ async function runWorker(opts) {
1961
1725
  const logDir = workersLogDir(config);
1962
1726
  mkdirSync(logDir, { recursive: true });
1963
1727
  if (opts.className === "plan" && !opts._planner) {
1964
- const { runPlanner } = await import("./planner-Cm3g6fWH.mjs");
1728
+ const { runPlanner } = await import("./planner-BLDJt3ZS.mjs");
1965
1729
  return runPlanner(opts);
1966
1730
  }
1967
1731
  const parent = launchParent(opts.parent);
@@ -2093,7 +1857,8 @@ async function executeRun(a) {
2093
1857
  const wanted = [
2094
1858
  ...a.mcpFlag ? [a.mcpFlag] : [],
2095
1859
  ...parsed.mcp,
2096
- ...target.classMcp ?? []
1860
+ ...target.classMcp ?? [],
1861
+ ...mcpServersFromToolGrants(parsed.allowedTools)
2097
1862
  ];
2098
1863
  if (wanted.length) mcpArgs = [
2099
1864
  "--strict-mcp-config",
@@ -2182,6 +1947,9 @@ async function executeRun(a) {
2182
1947
  killed: 1,
2183
1948
  label
2184
1949
  });
1950
+ if (worktree) try {
1951
+ recordWorktree(logDir, status, worktree, false);
1952
+ } catch {}
2185
1953
  cleanup();
2186
1954
  try {
2187
1955
  proc.kill();
@@ -2190,6 +1958,7 @@ async function executeRun(a) {
2190
1958
  };
2191
1959
  process.once("SIGTERM", () => onSignal("SIGTERM"));
2192
1960
  process.once("SIGINT", () => onSignal("SIGINT"));
1961
+ process.once("SIGHUP", () => onSignal("SIGHUP"));
2193
1962
  const ctx = {
2194
1963
  resultEvent: null,
2195
1964
  resultReport: null
@@ -2425,6 +2194,7 @@ async function executeCodexRun(a) {
2425
2194
  let killed = false;
2426
2195
  process.once("SIGTERM", onCodexSignal("SIGTERM"));
2427
2196
  process.once("SIGINT", onCodexSignal("SIGINT"));
2197
+ process.once("SIGHUP", onCodexSignal("SIGHUP"));
2428
2198
  function onCodexSignal(sig) {
2429
2199
  return () => {
2430
2200
  killed = true;
@@ -2433,6 +2203,9 @@ async function executeCodexRun(a) {
2433
2203
  status.secs = Math.floor((Date.now() - t0) / 1e3);
2434
2204
  status.last = `killed by signal ${sig}`;
2435
2205
  saveStatus(logDir, status);
2206
+ if (worktree) try {
2207
+ recordWorktree(logDir, status, worktree, false);
2208
+ } catch {}
2436
2209
  try {
2437
2210
  proc.kill();
2438
2211
  } catch {}
@@ -2793,5 +2566,5 @@ async function runChain(opts, deps = {}) {
2793
2566
  }
2794
2567
 
2795
2568
  //#endregion
2796
- export { appendLedger as A, ageOf as C, loadStatus as D, contextPercent as E, parseRunnerArgs as M, shortText as N, loadStatuses as O, UNLABELED as S, contextLabel as T, openPaneForWorker as _, testProvider as a, sessionTag as b, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, ledgerSummary as j, newWorkerId as k, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, currentTabKey as v, alive as w, workerInScope as x, resolveSession as y };
2797
- //# sourceMappingURL=chain-DhVHVnmT.mjs.map
2569
+ export { openPaneForWorker as _, testProvider as a, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, appendLedger as v, ledgerSummary as y };
2570
+ //# sourceMappingURL=chain-DGXNoTwc.mjs.map