openmausbot 0.1.92 → 0.1.94

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 (163) hide show
  1. package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +45 -1
  63. package/dist-server/container-mcp.js +99 -77
  64. package/dist-server/drivers/agents-proxy.js +40 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +18361 -13794
  68. package/dist-server/local-computer-proxy.js +5 -1
  69. package/dist-server/local-computer.js +1 -0
  70. package/dist-server/mcp-gate.js +152 -20
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +8872 -7620
  73. package/dist-server/pair-cli.js +8872 -7620
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +1 -0
  76. package/dist-server/server/auto-approve.js +33 -11
  77. package/dist-server/server/boat.js +3 -3
  78. package/dist-server/server/browser-live.js +14 -5
  79. package/dist-server/server/browser-runtime.js +6 -0
  80. package/dist-server/server/channel-queue.js +4 -1
  81. package/dist-server/server/cli.js +32 -12
  82. package/dist-server/server/cloud-move-http.js +9 -3
  83. package/dist-server/server/cloud-move.js +18 -0
  84. package/dist-server/server/cloud-owner.js +1 -1
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/computer-wait.js +33 -13
  87. package/dist-server/server/config.js +57 -10
  88. package/dist-server/server/container-computer.js +43 -32
  89. package/dist-server/server/decision-log.js +9 -6
  90. package/dist-server/server/delegations.js +47 -22
  91. package/dist-server/server/digest.js +12 -2
  92. package/dist-server/server/drivers/acp/core.js +229 -20
  93. package/dist-server/server/drivers/acp/cursor.js +13 -1
  94. package/dist-server/server/drivers/acp/grok.js +208 -3
  95. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  96. package/dist-server/server/drivers/acp/qwen.js +18 -0
  97. package/dist-server/server/drivers/agents-call.js +28 -0
  98. package/dist-server/server/drivers/agents-catalog.js +10 -1
  99. package/dist-server/server/drivers/boatagent.js +2 -0
  100. package/dist-server/server/drivers/builtIn.js +2 -0
  101. package/dist-server/server/drivers/cerebras.js +108 -0
  102. package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
  103. package/dist-server/server/drivers/claude.js +35 -9
  104. package/dist-server/server/drivers/codex.js +84 -10
  105. package/dist-server/server/drivers/openai-chat.js +37 -5
  106. package/dist-server/server/drivers/pi.js +48 -4
  107. package/dist-server/server/drivers/prompt-split.js +2 -0
  108. package/dist-server/server/env-path.js +6 -2
  109. package/dist-server/server/environment.js +3 -0
  110. package/dist-server/server/guarded-requests.js +2 -1
  111. package/dist-server/server/index.js +1353 -311
  112. package/dist-server/server/live-call-controller.js +868 -0
  113. package/dist-server/server/live-call.js +216 -0
  114. package/dist-server/server/local-vm-stop-reason.js +26 -0
  115. package/dist-server/server/mcp-gate-config.js +43 -12
  116. package/dist-server/server/mcp-gate.js +127 -25
  117. package/dist-server/server/mcp-http.js +46 -8
  118. package/dist-server/server/mcp-remote-proxy.js +108 -0
  119. package/dist-server/server/new-bot-defaults.js +3 -0
  120. package/dist-server/server/peer-provenance.js +4 -2
  121. package/dist-server/server/permission-proxy.js +15 -1
  122. package/dist-server/server/plan-usage.js +797 -0
  123. package/dist-server/server/provider-key-check.js +2 -1
  124. package/dist-server/server/proxy-paths.js +1 -0
  125. package/dist-server/server/redact.js +2 -0
  126. package/dist-server/server/request-auth.js +3 -2
  127. package/dist-server/server/routes/desktop-viewer.js +50 -6
  128. package/dist-server/server/routes/live.js +98 -0
  129. package/dist-server/server/routines.js +6 -3
  130. package/dist-server/server/skill-library.js +207 -0
  131. package/dist-server/server/skills-library-migration.js +235 -0
  132. package/dist-server/server/skills.js +66 -3
  133. package/dist-server/server/steer-queue.js +13 -1
  134. package/dist-server/server/store.js +86 -4
  135. package/dist-server/server/surface.js +9 -3
  136. package/dist-server/server/team-backup.js +2 -1
  137. package/dist-server/server/thread-events.js +1 -1
  138. package/dist-server/server/tunnel.js +20 -1
  139. package/dist-server/server/turn-log.js +10 -0
  140. package/dist-server/server/turn-resources.js +124 -0
  141. package/dist-server/server/vps-computer.js +1 -1
  142. package/dist-server/server/workspace-backup.js +22 -0
  143. package/dist-server/server/workspace.js +1 -1
  144. package/dist-server/shared/approval-mode.js +1 -1
  145. package/dist-server/shared/ask-question.js +13 -0
  146. package/dist-server/shared/call-consent.js +28 -0
  147. package/dist-server/shared/live-approval.js +124 -0
  148. package/dist-server/shared/live-call.js +176 -0
  149. package/dist-server/shared/redact.js +1 -0
  150. package/dist-server/shared/skill-md.js +10 -0
  151. package/dist-server/shared/team-backup.js +3 -0
  152. package/dist-server/shared/tool-name.js +22 -0
  153. package/dist-server/shared/tool-scope-support.js +25 -0
  154. package/dist-server/shared/tool-scope.js +136 -0
  155. package/dist-server/src/lib/thread-refs.js +406 -0
  156. package/dist-server/vps-container-mcp.js +99 -77
  157. package/dist-server/workspace-backup.worker.js +103 -100
  158. package/enterprise/server/index.js +1 -0
  159. package/package.json +1 -1
  160. package/dist/assets/channel-Dtk5Mtic.js +0 -1
  161. package/dist/assets/index-Cdmjrl28.css +0 -1
  162. package/dist/assets/index-DR7YPMkW.js +0 -597
  163. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
@@ -1,137 +1,118 @@
1
- // pi-mcp-extension — the Pi-side half of "hands" for the pi engine.
2
- //
3
- // Pi core deliberately ships no MCP client (see pi's docs: "does not include
4
- // built-in MCP"). This extension is that client: loaded into the per-turn
5
- // `pi --mode rpc --no-session` process via `-e`, it reads a JSON file whose
6
- // path is handed in through OMB_MCP_CONFIG and mounts every server described
7
- // there as first-class pi tools (pi.registerTool). OpenMausBot ships this file
8
- // and the pi driver spawns it — the pi repo itself is never touched.
9
- //
10
- // Protocol: the OpenMausBot proxies speak raw JSON-RPC 2.0 over stdio, one
11
- // frame per line (no MCP SDK, no Content-Length framing) — see
12
- // server/mcp-bridge.ts and server/drivers/agents-proxy.ts. This client matches
13
- // that exactly.
14
- import { Type, type TObjectOptions, type TSchema, type TSchemaOptions } from "typebox";
15
- import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
16
- import { readFileSync } from "node:fs";
17
-
18
- interface McpServerDef {
19
- command: string;
20
- args?: string[];
21
- env?: Record<string, string>;
22
- /** "local-computer" marks the user's real host desktop: every tool on such
23
- * a server is gated behind a permission card before it executes. */
24
- scope?: string;
25
- }
26
-
27
- interface McpConfig {
28
- mcpServers?: Record<string, McpServerDef>;
1
+ // server/drivers/pi-mcp-extension.ts
2
+ import { Type } from "typebox";
3
+ import { spawn } from "node:child_process";
4
+ import { readFileSync, writeFileSync } from "node:fs";
5
+
6
+ // shared/tool-scope.ts
7
+ var SERVER_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
8
+ var CONTROL_CHARACTER = /[\x00-\x1f\x7f]/;
9
+ var MAX_SELECTORS = 256;
10
+ var MAX_SELECTOR_LENGTH = 1024;
11
+ function validName(name) {
12
+ return typeof name === "string" && name.trim().length > 0 && name.length <= MAX_SELECTOR_LENGTH && !CONTROL_CHARACTER.test(name) && !name.includes("*");
29
13
  }
30
-
31
- interface McpTool {
32
- name: string;
33
- description?: string;
34
- inputSchema?: unknown;
14
+ function selectorIdentity(selector) {
15
+ if (selector.length > MAX_SELECTOR_LENGTH || CONTROL_CHARACTER.test(selector)) return;
16
+ if (selector.startsWith("native:")) {
17
+ const name = selector.slice(7);
18
+ if (name === "*" || validName(name)) return { kind: "native", name };
19
+ } else if (selector.startsWith("mcp:")) {
20
+ const separator = selector.indexOf(":", 4);
21
+ if (separator < 0) return;
22
+ const server = selector.slice(4, separator);
23
+ const name = selector.slice(separator + 1);
24
+ if (SERVER_NAME.test(server) && (name === "*" || validName(name))) {
25
+ return { kind: "mcp", server, name };
26
+ }
27
+ }
35
28
  }
36
-
37
- interface McpTextContent {
38
- type: "text";
39
- text: unknown;
29
+ function parseToolScope(value) {
30
+ if (value === void 0) return { ok: true, scope: void 0 };
31
+ const invalid = { ok: false, error: "Tool selection must contain only valid allow and deny lists." };
32
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
33
+ const prototype = Object.getPrototypeOf(value);
34
+ if (prototype !== Object.prototype && prototype !== null) return invalid;
35
+ const scope = {};
36
+ for (const key of Reflect.ownKeys(value)) {
37
+ if (key !== "allow" && key !== "deny") return invalid;
38
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
39
+ if (!descriptor || !("value" in descriptor)) return invalid;
40
+ const list = descriptor.value;
41
+ if (!Array.isArray(list) || list.length > MAX_SELECTORS) return invalid;
42
+ if (!Array.from(list).every((entry) => typeof entry === "string" && selectorIdentity(entry))) return invalid;
43
+ scope[key] = [...new Set(list)];
44
+ }
45
+ return { ok: true, scope };
40
46
  }
41
-
42
- interface McpImageContent {
43
- type: "image";
44
- data: unknown;
45
- mimeType: unknown;
47
+ function matches(selector, tool) {
48
+ const selected = selectorIdentity(selector);
49
+ return selected?.kind === tool.kind && (selected.kind !== "mcp" || tool.kind === "mcp" && selected.server === tool.server) && (selected.name === "*" || selected.name === tool.name);
46
50
  }
47
-
48
- type PiToolContent = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
49
-
50
- interface PiToolContext {
51
- ui: {
52
- confirm(title: string, message: string): Promise<boolean>;
53
- };
51
+ function permits(scope, tool) {
52
+ if (!validName(tool.name) || tool.kind === "mcp" && !SERVER_NAME.test(tool.server)) return false;
53
+ return (scope?.allow === void 0 || scope.allow.some((selector) => matches(selector, tool))) && !scope?.deny?.some((selector) => matches(selector, tool));
54
54
  }
55
-
56
- interface PiToolDefinition {
57
- name: string;
58
- label: string;
59
- description: string;
60
- parameters: TSchema;
61
- execute(
62
- toolCallId: string,
63
- params: unknown,
64
- signal: AbortSignal | undefined,
65
- onUpdate: unknown,
66
- context: PiToolContext,
67
- ): Promise<{ content: PiToolContent[]; details: Record<string, never> }>;
55
+ function allowsTool(scope, tool) {
56
+ const parsed = parseToolScope(scope);
57
+ return parsed.ok && permits(parsed.scope, tool);
68
58
  }
69
-
70
- /** Structural slice of Pi 0.84's ExtensionAPI used by this standalone file.
71
- * Keeping it local avoids bundling Pi into OpenMausBot; the extension is
72
- * loaded by the user's installed Pi, which supplies the real implementation. */
73
- interface PiExtensionApi {
74
- registerTool(definition: PiToolDefinition): void;
75
- on(event: "session_shutdown", handler: () => void): void;
59
+ function canUseMcpServer(scope, server, names) {
60
+ const parsed = parseToolScope(scope);
61
+ if (!parsed.ok || !SERVER_NAME.test(server)) return false;
62
+ if (names !== void 0) {
63
+ return names.some((name) => permits(parsed.scope, { kind: "mcp", server, name }));
64
+ }
65
+ if (parsed.scope?.deny?.includes(`mcp:${server}:*`)) return false;
66
+ if (parsed.scope?.allow === void 0) return true;
67
+ return parsed.scope.allow.some((selector) => {
68
+ const selected = selectorIdentity(selector);
69
+ return selected?.kind === "mcp" && selected.server === server && (selected.name === "*" || permits(parsed.scope, selected));
70
+ });
76
71
  }
77
72
 
78
- const MCP_STARTUP_TIMEOUT_MS = 8_000;
79
- const MCP_TOOL_TIMEOUT_MS = 10 * 60_000;
80
- const MCP_MAX_LIST_PAGES = 100;
81
- const MCP_MAX_FRAME_BYTES = 32 * 1024 * 1024;
82
- const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
83
- const TOOL_OUTPUT_MAX_LINES = 2_000;
84
- const TOOL_NAME_MAX_LENGTH = 64;
85
-
86
- /** A minimal stdio JSON-RPC 2.0 MCP client, matched to OpenMausBot's
87
- * newline-delimited house protocol. */
88
- export class StdioMcp {
89
- private child: ChildProcessWithoutNullStreams;
90
- private buf = "";
91
- private nextId = 1;
92
- private disposed = false;
93
- private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
94
-
95
- constructor(def: McpServerDef) {
73
+ // server/drivers/pi-mcp-extension.ts
74
+ var MCP_STARTUP_TIMEOUT_MS = 8e3;
75
+ var MCP_TOOL_TIMEOUT_MS = 10 * 6e4;
76
+ var MCP_MAX_LIST_PAGES = 100;
77
+ var MCP_MAX_FRAME_BYTES = 32 * 1024 * 1024;
78
+ var TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
79
+ var TOOL_OUTPUT_MAX_LINES = 2e3;
80
+ var TOOL_NAME_MAX_LENGTH = 64;
81
+ var StdioMcp = class {
82
+ child;
83
+ buf = "";
84
+ nextId = 1;
85
+ disposed = false;
86
+ pending = /* @__PURE__ */ new Map();
87
+ constructor(def) {
96
88
  this.child = spawn(def.command, def.args ?? [], {
97
89
  stdio: ["pipe", "pipe", "pipe"],
98
- env: { ...process.env, ...def.env },
90
+ env: { ...process.env, ...def.env }
99
91
  });
100
92
  this.child.stderr.on("data", () => {
101
- /* best-effort drain so a chatty server never blocks */
102
93
  });
103
94
  this.child.stdout.setEncoding("utf8");
104
- this.child.stdout.on("data", (chunk: string) => this.onData(chunk));
95
+ this.child.stdout.on("data", (chunk) => this.onData(chunk));
105
96
  this.child.on("error", (err) => this.closeWithError(err));
106
- // A server that starts and then dies emits `exit`, never `error`; without
107
- // settling on it, an in-flight init/listTools would hang the extension
108
- // load for the whole turn.
109
97
  this.child.on("exit", (code) => this.closeWithError(new Error(`MCP server exited (code ${code ?? "?"})`)));
110
- // A write to a dead child errors asynchronously on the stream; an
111
- // unhandled stream error would kill the pi process (the same hazard
112
- // spawnCli documents for driver-spawned children in procs.ts).
113
98
  this.child.stdin.on("error", () => this.closeWithError(new Error("MCP server stdin closed")));
114
99
  }
115
-
116
- private failAll(err: Error): void {
100
+ failAll(err) {
117
101
  for (const [, p] of this.pending) p.reject(err);
118
102
  this.pending.clear();
119
103
  }
120
-
121
- private closeWithError(err: Error): void {
104
+ closeWithError(err) {
122
105
  if (this.disposed) return;
123
106
  this.disposed = true;
124
107
  this.failAll(err);
125
108
  try {
126
109
  this.child.kill();
127
110
  } catch {
128
- /* already gone */
129
111
  }
130
112
  }
131
-
132
- private onData(chunk: string): void {
113
+ onData(chunk) {
133
114
  this.buf += chunk;
134
- let nl: number;
115
+ let nl;
135
116
  while ((nl = this.buf.indexOf("\n")) !== -1) {
136
117
  const line = this.buf.slice(0, nl).trim();
137
118
  this.buf = this.buf.slice(nl + 1);
@@ -140,22 +121,20 @@ export class StdioMcp {
140
121
  this.closeWithError(new Error(`MCP frame exceeded ${MCP_MAX_FRAME_BYTES} bytes`));
141
122
  return;
142
123
  }
143
- let msg: { id?: unknown; method?: unknown; error?: { message?: string }; result?: unknown };
124
+ let msg;
144
125
  try {
145
126
  msg = JSON.parse(line);
146
127
  } catch {
147
128
  continue;
148
129
  }
149
130
  if (typeof msg.id === "number" && this.pending.has(msg.id)) {
150
- const p = this.pending.get(msg.id)!;
131
+ const p = this.pending.get(msg.id);
151
132
  this.pending.delete(msg.id);
152
133
  if (msg.error) p.reject(new Error(msg.error.message ?? "MCP error"));
153
134
  else p.resolve(msg.result);
154
135
  continue;
155
136
  }
156
- // This minimal client does not implement server→client requests. Reply
157
- // explicitly instead of leaving a conforming server waiting forever.
158
- if (msg.id !== undefined && typeof msg.method === "string") {
137
+ if (msg.id !== void 0 && typeof msg.method === "string") {
159
138
  try {
160
139
  this.write({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "Client method not supported" } });
161
140
  } catch (err) {
@@ -163,44 +142,38 @@ export class StdioMcp {
163
142
  return;
164
143
  }
165
144
  }
166
- // Notifications (e.g. notifications/tools/list_changed) are intentionally
167
- // ignored for this single-shot mount.
168
145
  }
169
146
  if (Buffer.byteLength(this.buf, "utf8") > MCP_MAX_FRAME_BYTES) {
170
147
  this.closeWithError(new Error(`MCP frame exceeded ${MCP_MAX_FRAME_BYTES} bytes without a newline`));
171
148
  }
172
149
  }
173
-
174
- private write(frame: unknown): void {
150
+ write(frame) {
175
151
  if (this.disposed || this.child.stdin.destroyed || !this.child.stdin.writable) {
176
152
  throw new Error("MCP server stdin is closed");
177
153
  }
178
154
  this.child.stdin.write(JSON.stringify(frame) + "\n");
179
155
  }
180
-
181
- private call(method: string, params: unknown, timeoutMs: number, signal?: AbortSignal): Promise<unknown> {
156
+ call(method, params, timeoutMs, signal) {
182
157
  if (this.disposed) return Promise.reject(new Error("MCP client is closed"));
183
158
  if (signal?.aborted) return Promise.reject(new Error(`MCP ${method} aborted`));
184
-
185
159
  const id = this.nextId++;
186
- return new Promise<unknown>((resolve, reject) => {
160
+ return new Promise((resolve, reject) => {
187
161
  let settled = false;
188
162
  const cleanup = () => {
189
163
  clearTimeout(timer);
190
164
  signal?.removeEventListener("abort", onAbort);
191
165
  };
192
- const settle = (fn: () => void) => {
166
+ const settle = (fn) => {
193
167
  if (settled) return;
194
168
  settled = true;
195
169
  cleanup();
196
170
  fn();
197
171
  };
198
- const cancel = (reason: string) => {
172
+ const cancel = (reason) => {
199
173
  this.pending.delete(id);
200
174
  try {
201
175
  this.notify("notifications/cancelled", { requestId: id, reason });
202
176
  } catch {
203
- /* the transport may already be gone */
204
177
  }
205
178
  settle(() => reject(new Error(reason)));
206
179
  };
@@ -210,7 +183,7 @@ export class StdioMcp {
210
183
  signal?.addEventListener("abort", onAbort, { once: true });
211
184
  this.pending.set(id, {
212
185
  resolve: (value) => settle(() => resolve(value)),
213
- reject: (err) => settle(() => reject(err)),
186
+ reject: (err) => settle(() => reject(err))
214
187
  });
215
188
  try {
216
189
  this.write({ jsonrpc: "2.0", id, method, params });
@@ -220,38 +193,35 @@ export class StdioMcp {
220
193
  }
221
194
  });
222
195
  }
223
-
224
- private notify(method: string, params?: unknown): void {
196
+ notify(method, params) {
225
197
  this.write({ jsonrpc: "2.0", method, params });
226
198
  }
227
-
228
- async init(): Promise<void> {
199
+ async init() {
229
200
  await this.call(
230
201
  "initialize",
231
202
  {
232
203
  protocolVersion: "2024-11-05",
233
204
  capabilities: {},
234
- clientInfo: { name: "openmausbot-pi", version: "1" },
205
+ clientInfo: { name: "openmausbot-pi", version: "1" }
235
206
  },
236
- MCP_STARTUP_TIMEOUT_MS,
207
+ MCP_STARTUP_TIMEOUT_MS
237
208
  );
238
209
  this.notify("notifications/initialized");
239
210
  }
240
-
241
- async listTools(): Promise<McpTool[]> {
242
- const tools: McpTool[] = [];
243
- const seenCursors = new Set<string>();
211
+ async listTools() {
212
+ const tools = [];
213
+ const seenCursors = /* @__PURE__ */ new Set();
244
214
  const deadline = Date.now() + MCP_STARTUP_TIMEOUT_MS;
245
- let cursor: string | undefined;
215
+ let cursor;
246
216
  for (let page = 0; page < MCP_MAX_LIST_PAGES; page += 1) {
247
217
  const remainingMs = Math.max(1, deadline - Date.now());
248
- const res = (await this.call(
218
+ const res = await this.call(
249
219
  "tools/list",
250
220
  cursor ? { cursor } : {},
251
- remainingMs,
252
- )) as { tools?: unknown; nextCursor?: unknown } | undefined;
221
+ remainingMs
222
+ );
253
223
  if (!Array.isArray(res?.tools)) throw new Error("MCP tools/list returned no tools array");
254
- tools.push(...(res.tools as McpTool[]));
224
+ tools.push(...res.tools);
255
225
  if (typeof res.nextCursor !== "string" || !res.nextCursor) return tools;
256
226
  if (seenCursors.has(res.nextCursor)) throw new Error("MCP tools/list repeated a pagination cursor");
257
227
  seenCursors.add(res.nextCursor);
@@ -259,84 +229,39 @@ export class StdioMcp {
259
229
  }
260
230
  throw new Error(`MCP tools/list exceeded ${MCP_MAX_LIST_PAGES} pages`);
261
231
  }
262
-
263
232
  /** tools/call → Pi content, preserving screenshots and bounding text so a
264
233
  * remote server cannot flood the model context. */
265
- async callTool(name: string, args: unknown, signal?: AbortSignal): Promise<{ content: PiToolContent[]; isError: boolean }> {
266
- const res = (await this.call(
234
+ async callTool(name, args, signal) {
235
+ const res = await this.call(
267
236
  "tools/call",
268
237
  { name, arguments: args ?? {} },
269
238
  MCP_TOOL_TIMEOUT_MS,
270
- signal,
271
- )) as { content?: unknown; isError?: boolean } | undefined;
239
+ signal
240
+ );
272
241
  const rawContent = Array.isArray(res?.content) ? res.content : [];
273
- const text = rawContent
274
- .filter((c): c is McpTextContent => Boolean(c) && (c as { type?: unknown }).type === "text")
275
- .map((c) => String(c.text ?? ""))
276
- .join("\n");
277
- const images = rawContent
278
- .filter(
279
- (c): c is McpImageContent =>
280
- Boolean(c) &&
281
- (c as { type?: unknown }).type === "image" &&
282
- typeof (c as McpImageContent).data === "string" &&
283
- typeof (c as McpImageContent).mimeType === "string",
284
- )
285
- .map((c) => ({ type: "image" as const, data: String(c.data), mimeType: String(c.mimeType) }));
286
- const unsupported = rawContent.length - rawContent.filter((c) => (c as { type?: unknown })?.type === "text").length - images.length;
242
+ const text = rawContent.filter((c) => Boolean(c) && c.type === "text").map((c) => String(c.text ?? "")).join("\n");
243
+ const images = rawContent.filter(
244
+ (c) => Boolean(c) && c.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string"
245
+ ).map((c) => ({ type: "image", data: String(c.data), mimeType: String(c.mimeType) }));
246
+ const unsupported = rawContent.length - rawContent.filter((c) => c?.type === "text").length - images.length;
287
247
  let boundedText = truncateToolText(text || (images.length ? "" : "(empty result)"));
288
248
  if (unsupported > 0) boundedText += `${boundedText ? "\n" : ""}[${unsupported} unsupported MCP content item(s) omitted]`;
289
249
  return {
290
- content: [...(boundedText ? [{ type: "text" as const, text: boundedText }] : []), ...images],
291
- isError: Boolean(res?.isError),
250
+ content: [...boundedText ? [{ type: "text", text: boundedText }] : [], ...images],
251
+ isError: Boolean(res?.isError)
292
252
  };
293
253
  }
294
-
295
- dispose(): void {
254
+ dispose() {
296
255
  if (this.disposed) return;
297
256
  this.disposed = true;
298
257
  this.failAll(new Error("MCP client disposed"));
299
258
  try {
300
259
  this.child.kill();
301
260
  } catch {
302
- /* already gone */
303
261
  }
304
262
  }
305
- }
306
-
307
- interface JsonSchema {
308
- type?: string | string[];
309
- title?: unknown;
310
- description?: unknown;
311
- default?: unknown;
312
- const?: unknown;
313
- enum?: unknown[];
314
- nullable?: unknown;
315
- anyOf?: unknown[];
316
- oneOf?: unknown[];
317
- allOf?: unknown[];
318
- items?: unknown | unknown[];
319
- prefixItems?: unknown[];
320
- properties?: Record<string, unknown>;
321
- required?: unknown;
322
- additionalProperties?: unknown;
323
- minimum?: unknown;
324
- maximum?: unknown;
325
- exclusiveMinimum?: unknown;
326
- exclusiveMaximum?: unknown;
327
- multipleOf?: unknown;
328
- minLength?: unknown;
329
- maxLength?: unknown;
330
- pattern?: unknown;
331
- format?: unknown;
332
- minItems?: unknown;
333
- maxItems?: unknown;
334
- uniqueItems?: unknown;
335
- minProperties?: unknown;
336
- maxProperties?: unknown;
337
- }
338
-
339
- const SCHEMA_OPTION_KEYS = [
263
+ };
264
+ var SCHEMA_OPTION_KEYS = [
340
265
  "title",
341
266
  "description",
342
267
  "default",
@@ -353,64 +278,54 @@ const SCHEMA_OPTION_KEYS = [
353
278
  "maxItems",
354
279
  "uniqueItems",
355
280
  "minProperties",
356
- "maxProperties",
357
- ] as const;
358
-
359
- function schemaOptions(schema: JsonSchema): TSchemaOptions {
360
- const options: TSchemaOptions = {};
281
+ "maxProperties"
282
+ ];
283
+ function schemaOptions(schema) {
284
+ const options = {};
361
285
  for (const key of SCHEMA_OPTION_KEYS) {
362
286
  const value = schema[key];
363
- if (value !== undefined) Reflect.set(options, key, value);
287
+ if (value !== void 0) Reflect.set(options, key, value);
364
288
  }
365
289
  return options;
366
290
  }
367
-
368
- function isSchemaObject(value: unknown): value is JsonSchema {
291
+ function isSchemaObject(value) {
369
292
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
370
293
  }
371
-
372
- function primitiveLiteral(value: unknown): TSchema | null {
294
+ function primitiveLiteral(value) {
373
295
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
374
296
  return Type.Literal(value);
375
297
  }
376
298
  if (value === null) return Type.Null();
377
299
  return null;
378
300
  }
379
-
380
- function withNullable(schema: TSchema, nullable: unknown): TSchema {
301
+ function withNullable(schema, nullable) {
381
302
  return nullable === true ? Type.Union([schema, Type.Null()]) : schema;
382
303
  }
383
-
384
- function collectObjectShape(schema: JsonSchema): { properties: Record<string, unknown>; required: Set<string> } {
385
- const properties: Record<string, unknown> = {};
386
- const required = new Set<string>();
387
-
388
- const visit = (candidate: unknown, mode: "root" | "all" | "choice") => {
304
+ function collectObjectShape(schema) {
305
+ const properties = {};
306
+ const required = /* @__PURE__ */ new Set();
307
+ const visit = (candidate, mode) => {
389
308
  if (!isSchemaObject(candidate)) return;
390
309
  for (const branch of candidate.allOf ?? []) visit(branch, mode === "choice" ? "choice" : "all");
391
- for (const branch of [...(candidate.anyOf ?? []), ...(candidate.oneOf ?? [])]) visit(branch, "choice");
310
+ for (const branch of [...candidate.anyOf ?? [], ...candidate.oneOf ?? []]) visit(branch, "choice");
392
311
  for (const [key, value] of Object.entries(candidate.properties ?? {})) {
393
- // The root declaration is authoritative; branch-only properties are
394
- // still retained so the model knows which arguments exist.
395
- if (mode === "root" || properties[key] === undefined) properties[key] = value;
312
+ if (mode === "root" || properties[key] === void 0) properties[key] = value;
396
313
  }
397
314
  if (mode !== "choice" && Array.isArray(candidate.required)) {
398
315
  for (const key of candidate.required) if (typeof key === "string") required.add(key);
399
316
  }
400
317
  };
401
-
402
318
  visit(schema, "root");
403
319
  return { properties, required };
404
320
  }
405
-
406
- function objectSchema(schema: JsonSchema, root = false): TSchema {
321
+ function objectSchema(schema, root = false) {
407
322
  const { properties, required } = collectObjectShape(schema);
408
- const converted: Record<string, TSchema> = {};
323
+ const converted = {};
409
324
  for (const [key, value] of Object.entries(properties)) {
410
325
  const nested = nestedTypebox(value);
411
326
  converted[key] = required.has(key) ? nested : Type.Optional(nested);
412
327
  }
413
- const options: TObjectOptions = schemaOptions(schema);
328
+ const options = schemaOptions(schema);
414
329
  if (typeof schema.additionalProperties === "boolean") {
415
330
  options.additionalProperties = schema.additionalProperties;
416
331
  } else if (isSchemaObject(schema.additionalProperties)) {
@@ -419,34 +334,26 @@ function objectSchema(schema: JsonSchema, root = false): TSchema {
419
334
  const object = Type.Object(converted, options);
420
335
  return root ? object : withNullable(object, schema.nullable);
421
336
  }
422
-
423
- function nestedTypebox(schema: unknown): TSchema {
337
+ function nestedTypebox(schema) {
424
338
  if (!isSchemaObject(schema)) return Type.Any();
425
339
  const options = schemaOptions(schema);
426
-
427
340
  const literal = primitiveLiteral(schema.const);
428
341
  if (literal) return withNullable(literal, schema.nullable);
429
-
430
342
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
431
343
  if (schema.enum.every((value) => typeof value === "string")) {
432
- // `{type:"string", enum:[...]}` works across Google/OpenAI/Anthropic;
433
- // Type.Union(Type.Literal(...)) does not work with Google's tool API.
434
344
  return withNullable(Type.String({ ...options, enum: schema.enum }), schema.nullable);
435
345
  }
436
- const literals = schema.enum.map(primitiveLiteral).filter((value): value is TSchema => value !== null);
346
+ const literals = schema.enum.map(primitiveLiteral).filter((value) => value !== null);
437
347
  if (literals.length === schema.enum.length) {
438
348
  const union = literals.length === 1 ? literals[0] : Type.Union(literals, options);
439
349
  return withNullable(union, schema.nullable);
440
350
  }
441
351
  }
442
-
443
352
  if (Array.isArray(schema.type)) {
444
353
  const variants = schema.type.map((type) => nestedTypebox({ ...schema, type, nullable: false }));
445
354
  return variants.length === 1 ? variants[0] : Type.Union(variants, options);
446
355
  }
447
-
448
356
  if (schema.type === "object" || schema.properties) return objectSchema(schema);
449
-
450
357
  const choice = schema.anyOf ?? schema.oneOf;
451
358
  if (Array.isArray(choice) && choice.length > 0) {
452
359
  const variants = choice.map(nestedTypebox);
@@ -456,7 +363,6 @@ function nestedTypebox(schema: unknown): TSchema {
456
363
  const variants = schema.allOf.map(nestedTypebox);
457
364
  return withNullable(variants.length === 1 ? variants[0] : Type.Intersect(variants, options), schema.nullable);
458
365
  }
459
-
460
366
  switch (schema.type) {
461
367
  case "string":
462
368
  return withNullable(Type.String(options), schema.nullable);
@@ -469,34 +375,22 @@ function nestedTypebox(schema: unknown): TSchema {
469
375
  case "null":
470
376
  return Type.Null();
471
377
  case "array": {
472
- const tupleItems = schema.prefixItems ?? (Array.isArray(schema.items) ? schema.items : undefined);
473
- const value = tupleItems
474
- ? Type.Tuple(tupleItems.map(nestedTypebox), options)
475
- : Type.Array(schema.items ? nestedTypebox(schema.items) : Type.Any(), options);
378
+ const tupleItems = schema.prefixItems ?? (Array.isArray(schema.items) ? schema.items : void 0);
379
+ const value = tupleItems ? Type.Tuple(tupleItems.map(nestedTypebox), options) : Type.Array(schema.items ? nestedTypebox(schema.items) : Type.Any(), options);
476
380
  return withNullable(value, schema.nullable);
477
381
  }
478
382
  default:
479
383
  return Type.Any();
480
384
  }
481
385
  }
482
-
483
- /** Best-effort JSON Schema → TypeBox. MCP tool parameters must always expose
484
- * an object at the root; Pi/provider tool serialization rejects a root schema
485
- * without `type: "object"`. Nested schemas retain unions, nullability and
486
- * constraints instead of being incorrectly rewritten as objects. */
487
- export function toTypebox(schema: unknown): TSchema {
386
+ function toTypebox(schema) {
488
387
  return objectSchema(isSchemaObject(schema) ? schema : {}, true);
489
388
  }
490
-
491
- /** pi tool names are lowercase snake identifiers; MCP tool names are not
492
- * (COMPOSIO_SEARCH_TOOLS, browser_navigate, mcp__x…). Normalize and prefix
493
- * with the server so two servers can never collide. */
494
- function sanitizeToolName(server: string, tool: string): string {
389
+ function sanitizeToolName(server, tool) {
495
390
  const raw = `${server}_${tool}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
496
391
  return raw.slice(0, TOOL_NAME_MAX_LENGTH) || "mcp_tool";
497
392
  }
498
-
499
- export function allocateToolName(server: string, tool: string, used: Set<string>): string {
393
+ function allocateToolName(server, tool, used) {
500
394
  const base = sanitizeToolName(server, tool);
501
395
  let candidate = base;
502
396
  for (let suffixNumber = 2; used.has(candidate); suffixNumber += 1) {
@@ -505,28 +399,25 @@ export function allocateToolName(server: string, tool: string, used: Set<string>
505
399
  }
506
400
  return candidate;
507
401
  }
508
-
509
- export function truncateToolText(text: string): string {
402
+ function truncateToolText(text) {
510
403
  const lines = text.split("\n");
511
404
  const lineLimited = lines.slice(0, TOOL_OUTPUT_MAX_LINES).join("\n");
512
405
  const bytes = Buffer.from(lineLimited, "utf8");
513
406
  let byteEnd = Math.min(bytes.length, TOOL_OUTPUT_MAX_BYTES);
514
- // Avoid returning a replacement character when the byte limit lands in the
515
- // middle of a UTF-8 code point.
516
- while (byteEnd > 0 && byteEnd < bytes.length && (bytes[byteEnd] & 0xc0) === 0x80) byteEnd -= 1;
407
+ while (byteEnd > 0 && byteEnd < bytes.length && (bytes[byteEnd] & 192) === 128) byteEnd -= 1;
517
408
  const content = bytes.subarray(0, byteEnd).toString("utf8");
518
409
  const truncatedByLines = lines.length > TOOL_OUTPUT_MAX_LINES;
519
410
  const truncatedByBytes = bytes.length > TOOL_OUTPUT_MAX_BYTES;
520
411
  if (!truncatedByLines && !truncatedByBytes) return content;
521
412
  const reasons = [
522
- ...(truncatedByLines ? [`${TOOL_OUTPUT_MAX_LINES}-line limit`] : []),
523
- ...(truncatedByBytes ? [`${TOOL_OUTPUT_MAX_BYTES / 1024}KB limit`] : []),
413
+ ...truncatedByLines ? [`${TOOL_OUTPUT_MAX_LINES}-line limit`] : [],
414
+ ...truncatedByBytes ? [`${TOOL_OUTPUT_MAX_BYTES / 1024}KB limit`] : []
524
415
  ];
525
- return `${content}\n\n[MCP output truncated: ${reasons.join(" and ")}. Refine the request for less output.]`;
526
- }
416
+ return `${content}
527
417
 
528
- /** A short human-readable line for the permission card's detail. */
529
- function summarizeParams(params: unknown): string {
418
+ [MCP output truncated: ${reasons.join(" and ")}. Refine the request for less output.]`;
419
+ }
420
+ function summarizeParams(params) {
530
421
  try {
531
422
  const s = JSON.stringify(params ?? {});
532
423
  return s === "{}" ? "" : s.slice(0, 300);
@@ -534,54 +425,111 @@ function summarizeParams(params: unknown): string {
534
425
  return "";
535
426
  }
536
427
  }
537
-
538
- export default async function (pi: PiExtensionApi): Promise<void> {
428
+ async function pi_mcp_extension_default(pi) {
539
429
  const configPath = process.env.OMB_MCP_CONFIG;
540
430
  if (!configPath) return;
541
-
542
- let config: McpConfig;
431
+ let config;
543
432
  try {
544
- config = JSON.parse(readFileSync(configPath, "utf8")) as McpConfig;
433
+ config = JSON.parse(readFileSync(configPath, "utf8"));
545
434
  } catch {
546
- return;
435
+ throw new Error("OpenMausBot Pi MCP configuration could not be read");
436
+ }
437
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("OpenMausBot Pi MCP configuration is invalid");
438
+ const parsed = parseToolScope(config.toolScope);
439
+ if (!parsed.ok) throw new Error(parsed.error);
440
+ const scope = parsed.scope;
441
+ if (scope !== void 0 && (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function")) throw new Error("Pi tool selection enforcement APIs are unavailable");
442
+ const used = /* @__PURE__ */ new Set();
443
+ const identities = /* @__PURE__ */ new Map();
444
+ let enforcementFailed = false;
445
+ const clients = [];
446
+ const serverEntries = Object.entries(config.mcpServers ?? {}).filter(([name]) => scope === void 0 || canUseMcpServer(scope, name));
447
+ if (scope !== void 0) {
448
+ const allowed = (name) => allowsTool(scope, identities.get(name) ?? { kind: "native", name });
449
+ const intersect = () => {
450
+ if (enforcementFailed) throw new Error("Pi tool selection enforcement is unavailable");
451
+ try {
452
+ const active = pi.getActiveTools();
453
+ if (!Array.isArray(active) || active.some((name) => typeof name !== "string")) throw new Error("Pi tool selection enforcement returned an invalid catalog");
454
+ const requested = new Set(active.filter(allowed));
455
+ pi.setActiveTools([...requested]);
456
+ const selected = pi.getActiveTools();
457
+ if (!Array.isArray(selected) || selected.some((name) => !requested.has(name))) throw new Error("Pi tool selection enforcement did not apply the active-tool restriction");
458
+ if (config.scopeReadyPath) writeFileSync(config.scopeReadyPath, JSON.stringify({ ok: true, toolScope: scope }), { mode: 384 });
459
+ return selected;
460
+ } catch (error) {
461
+ enforcementFailed = true;
462
+ if (config.scopeReadyPath) {
463
+ try {
464
+ writeFileSync(config.scopeReadyPath, JSON.stringify({ ok: false }), { mode: 384 });
465
+ } catch {
466
+ }
467
+ }
468
+ throw error;
469
+ }
470
+ };
471
+ for (const event of ["session_start", "session_switch", "model_select", "before_agent_start", "context"]) pi.on(event, () => {
472
+ intersect();
473
+ });
474
+ pi.on("tool_call", (event) => !enforcementFailed && typeof event?.toolName === "string" && allowed(event.toolName) ? void 0 : { block: true, reason: "Tool selection excludes this tool" });
475
+ pi.on("before_provider_request", (event, context) => {
476
+ let selected = /* @__PURE__ */ new Set();
477
+ try {
478
+ selected = new Set(intersect());
479
+ } catch {
480
+ try {
481
+ context?.abort?.();
482
+ } catch {
483
+ }
484
+ }
485
+ const declared = (name) => selected.has(name) && allowed(name);
486
+ const payload = event?.payload;
487
+ if (!payload || typeof payload !== "object") return payload;
488
+ const body = payload;
489
+ if (Array.isArray(body.tools)) body.tools = body.tools.flatMap((value) => {
490
+ if (!value || typeof value !== "object") return [];
491
+ const tool = value;
492
+ if (Array.isArray(tool.functionDeclarations)) {
493
+ const functionDeclarations = tool.functionDeclarations.filter((declaration) => typeof declaration.name === "string" && declared(declaration.name));
494
+ return functionDeclarations.length ? [{ ...tool, functionDeclarations }] : [];
495
+ }
496
+ const name = tool.function?.name ?? tool.name;
497
+ return typeof name === "string" && declared(name) ? [tool] : [];
498
+ });
499
+ return payload;
500
+ });
547
501
  }
548
-
549
- const used = new Set<string>();
550
- const clients: StdioMcp[] = [];
551
- const serverEntries = Object.entries(config.mcpServers ?? {});
552
-
553
- // Mount independent servers concurrently so one slow integration cannot
554
- // consume the startup timeout once per server. Registration remains in
555
- // config order below for deterministic tool names and collision suffixes.
556
502
  const mounts = await Promise.all(
557
503
  serverEntries.map(async ([serverName, def]) => {
558
504
  if (!def || typeof def.command !== "string" || !def.command.trim()) return { serverName };
559
- let client: StdioMcp | undefined;
505
+ let client;
560
506
  try {
561
507
  client = new StdioMcp(def);
562
508
  await client.init();
563
509
  return { serverName, def, client, tools: await client.listTools() };
564
510
  } catch (err) {
565
511
  const message = err instanceof Error ? err.message : String(err);
566
- process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: ${message}\n`);
512
+ process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: ${message}
513
+ `);
567
514
  client?.dispose();
568
515
  return { serverName };
569
516
  }
570
- }),
517
+ })
571
518
  );
572
-
573
519
  for (const mount of mounts) {
574
520
  if (!mount.client || !mount.def || !mount.tools) continue;
575
521
  const { serverName, def, client, tools } = mount;
576
- const gated = def.scope === "local-computer";
522
+ const gated = def.scope === "local-computer" || def.scope === "custom" && config.approvalMode !== "full";
577
523
  let registered = 0;
578
-
579
524
  for (const tool of tools) {
580
525
  if (!tool || typeof tool.name !== "string" || !tool.name.trim()) {
581
- process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: skipped a tool with no valid name\n`);
526
+ process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: skipped a tool with no valid name
527
+ `);
582
528
  continue;
583
529
  }
584
530
  const toolName = tool.name;
531
+ const identity = { kind: "mcp", server: serverName, name: toolName };
532
+ if (scope !== void 0 && !allowsTool(scope, identity)) continue;
585
533
  const name = allocateToolName(serverName, toolName, used);
586
534
  try {
587
535
  const parameters = toTypebox(tool.inputSchema);
@@ -591,14 +539,12 @@ export default async function (pi: PiExtensionApi): Promise<void> {
591
539
  description: typeof tool.description === "string" ? tool.description : `${toolName} (MCP tool from ${serverName})`,
592
540
  parameters,
593
541
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
594
- // Host tools ask first, using pi's native permission card
595
- // (ctx.ui.confirm → extension_ui_request → Allow/Deny card). This
596
- // mirrors ACP's session/request_permission and Codex's elicitation.
542
+ if (scope !== void 0 && (enforcementFailed || !allowsTool(scope, identity))) throw new Error("Tool selection excludes this tool");
597
543
  if (gated) {
598
544
  const detail = summarizeParams(params);
599
545
  const allowed = await ctx.ui.confirm(
600
- `Allow ${toolName} on your computer?`,
601
- detail || `Run ${serverName}:${toolName}`,
546
+ def.scope === "local-computer" ? `Allow ${toolName} on your computer?` : `Allow ${serverName}:${toolName}?`,
547
+ detail || `Run ${serverName}:${toolName}`
602
548
  );
603
549
  if (!allowed) {
604
550
  return { content: [{ type: "text", text: "Blocked by the user." }], details: {} };
@@ -606,33 +552,33 @@ export default async function (pi: PiExtensionApi): Promise<void> {
606
552
  }
607
553
  const res = await client.callTool(toolName, params, signal);
608
554
  if (res.isError) {
609
- const message = res.content
610
- .filter((item): item is { type: "text"; text: string } => item.type === "text")
611
- .map((item) => item.text)
612
- .join("\n");
613
- // Pi marks a custom tool as failed only when execute throws;
614
- // returning error-looking text incorrectly produces isError=false.
555
+ const message = res.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
615
556
  throw new Error(message || `MCP tool ${serverName}:${toolName} returned an error`);
616
557
  }
617
558
  return { content: res.content, details: {} };
618
- },
559
+ }
619
560
  });
620
561
  used.add(name);
562
+ identities.set(name, identity);
621
563
  registered += 1;
622
564
  } catch (err) {
623
- // One malformed tool must not dispose the client behind tools that
624
- // were already registered from the same server.
625
565
  const message = err instanceof Error ? err.message : String(err);
626
- process.stderr.write(`[openmausbot-pi-mcp] ${serverName}:${toolName}: ${message}\n`);
566
+ process.stderr.write(`[openmausbot-pi-mcp] ${serverName}:${toolName}: ${message}
567
+ `);
627
568
  }
628
569
  }
629
-
630
570
  if (registered > 0) clients.push(client);
631
571
  else client.dispose();
632
572
  }
633
-
634
573
  pi.on("session_shutdown", () => {
635
574
  for (const c of clients) c.dispose();
636
575
  clients.length = 0;
637
576
  });
638
577
  }
578
+ export {
579
+ StdioMcp,
580
+ allocateToolName,
581
+ pi_mcp_extension_default as default,
582
+ toTypebox,
583
+ truncateToolText
584
+ };