@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,376 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { getAgentDir } from "../../shared/utils.ts";
6
+
7
+ const CACHE_VERSION = 1;
8
+ const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
10
+ const GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".config", "mcp", "mcp.json");
11
+ const IMPORT_PATHS = {
12
+ cursor: [path.join(os.homedir(), ".cursor", "mcp.json")],
13
+ "claude-code": [
14
+ path.join(os.homedir(), ".claude", "mcp.json"),
15
+ path.join(os.homedir(), ".claude.json"),
16
+ path.join(os.homedir(), ".claude", "claude_desktop_config.json"),
17
+ ],
18
+ "claude-desktop": [path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
19
+ codex: [path.join(os.homedir(), ".codex", "config.json")],
20
+ windsurf: [path.join(os.homedir(), ".windsurf", "mcp.json")],
21
+ vscode: [".vscode/mcp.json"],
22
+ } as const;
23
+
24
+ type ToolPrefix = "server" | "none" | "short";
25
+ type ImportKind = keyof typeof IMPORT_PATHS;
26
+
27
+ interface ServerEntry {
28
+ command?: string;
29
+ args?: string[];
30
+ socket?: string;
31
+ env?: Record<string, string>;
32
+ cwd?: string;
33
+ url?: string;
34
+ headers?: Record<string, string>;
35
+ auth?: "oauth" | "bearer" | false;
36
+ bearerToken?: string;
37
+ bearerTokenEnv?: string;
38
+ exposeResources?: boolean;
39
+ excludeTools?: string[];
40
+ includeTools?: string[];
41
+ toolPrefix?: ToolPrefix;
42
+ disabled?: boolean;
43
+ }
44
+
45
+ interface McpConfig {
46
+ mcpServers: Record<string, ServerEntry>;
47
+ imports?: ImportKind[];
48
+ settings?: {
49
+ toolPrefix?: ToolPrefix;
50
+ };
51
+ }
52
+
53
+ interface CachedTool {
54
+ name?: string;
55
+ }
56
+
57
+ interface CachedResource {
58
+ uri?: string;
59
+ name?: string;
60
+ }
61
+
62
+ interface ServerCacheEntry {
63
+ configHash?: string;
64
+ tools?: CachedTool[];
65
+ resources?: CachedResource[];
66
+ cachedAt?: number;
67
+ }
68
+
69
+ interface MetadataCache {
70
+ version: number;
71
+ servers: Record<string, ServerCacheEntry>;
72
+ }
73
+
74
+ export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
75
+ if (!mcpDirectTools?.length) return [];
76
+
77
+ const names = new Set<string>();
78
+ // Keep pre-v5 and independent Fitch configs paired with their own caches.
79
+ for (const directory of ["", "fitch-mcp-adapter"]) {
80
+ try {
81
+ const config = loadMcpConfig(cwd, directory);
82
+ const cache = loadMetadataCache(directory);
83
+ if (!cache) continue;
84
+ for (const name of resolveDirectToolNames(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools)) {
85
+ names.add(name);
86
+ }
87
+ } catch { /* An unavailable adapter does not hide the other adapter's tools. */ }
88
+ }
89
+ return [...names];
90
+ }
91
+
92
+ function loadMetadataCache(directory: string): MetadataCache | null {
93
+ const cachePath = path.join(getAgentDir(), directory, "mcp-cache.json");
94
+ let parsed: unknown;
95
+ try {
96
+ parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
97
+ } catch {
98
+ return null;
99
+ }
100
+
101
+ if (!parsed || typeof parsed !== "object") return null;
102
+ const raw = parsed as Record<string, unknown>;
103
+ if (raw.version !== CACHE_VERSION || !raw.servers || typeof raw.servers !== "object" || Array.isArray(raw.servers)) {
104
+ return null;
105
+ }
106
+ return raw as unknown as MetadataCache;
107
+ }
108
+
109
+ function loadMcpConfig(cwd: string, directory: string): McpConfig {
110
+ let config: McpConfig = { mcpServers: {} };
111
+ for (const sourcePath of getConfigPaths(cwd, directory)) {
112
+ const loaded = readConfig(sourcePath);
113
+ if (!loaded) continue;
114
+ config = mergeConfigs(config, expandImports(loaded, cwd));
115
+ }
116
+ return config;
117
+ }
118
+
119
+ function getConfigPaths(cwd: string, directory: string): string[] {
120
+ const piGlobalPath = path.join(getAgentDir(), directory, "mcp.json");
121
+ const projectPath = path.resolve(cwd, ".mcp.json");
122
+ const projectPiPath = path.resolve(cwd, ".pi", directory, "mcp.json");
123
+ const sources: string[] = [];
124
+ if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);
125
+ sources.push(piGlobalPath);
126
+ if (projectPath !== piGlobalPath) sources.push(projectPath);
127
+ if (projectPiPath !== piGlobalPath && projectPiPath !== projectPath) sources.push(projectPiPath);
128
+ return sources;
129
+ }
130
+
131
+ function readConfig(configPath: string): McpConfig | null {
132
+ let parsed: unknown;
133
+ try {
134
+ parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
135
+ } catch {
136
+ return null;
137
+ }
138
+ return validateConfig(parsed);
139
+ }
140
+
141
+ function validateConfig(raw: unknown): McpConfig {
142
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { mcpServers: {} };
143
+ const obj = raw as Record<string, unknown>;
144
+ const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
145
+ return {
146
+ mcpServers: servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {},
147
+ imports: Array.isArray(obj.imports) ? obj.imports.filter((value): value is ImportKind => isImportKind(value)) : undefined,
148
+ settings: obj.settings && typeof obj.settings === "object" && !Array.isArray(obj.settings)
149
+ ? obj.settings as McpConfig["settings"]
150
+ : undefined,
151
+ };
152
+ }
153
+
154
+ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
155
+ const imports = [...(base.imports ?? []), ...(next.imports ?? [])];
156
+ return {
157
+ mcpServers: { ...base.mcpServers, ...next.mcpServers },
158
+ imports: imports.length ? [...new Set(imports)] : undefined,
159
+ settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
160
+ };
161
+ }
162
+
163
+ function expandImports(config: McpConfig, cwd: string): McpConfig {
164
+ if (!config.imports?.length) return config;
165
+
166
+ const importedServers: Record<string, ServerEntry> = {};
167
+ for (const importKind of config.imports) {
168
+ const importPath = resolveImportPath(importKind, cwd);
169
+ if (!importPath) continue;
170
+ let imported: unknown;
171
+ try {
172
+ imported = JSON.parse(fs.readFileSync(importPath, "utf-8"));
173
+ } catch {
174
+ continue;
175
+ }
176
+ for (const [name, definition] of Object.entries(extractServers(imported, importKind))) {
177
+ if (!importedServers[name]) importedServers[name] = definition;
178
+ }
179
+ }
180
+
181
+ return {
182
+ imports: config.imports,
183
+ settings: config.settings,
184
+ mcpServers: { ...importedServers, ...config.mcpServers },
185
+ };
186
+ }
187
+
188
+ function resolveImportPath(importKind: ImportKind, cwd: string): string | null {
189
+ for (const candidate of IMPORT_PATHS[importKind]) {
190
+ const fullPath = candidate.startsWith(".") ? path.resolve(cwd, candidate) : candidate;
191
+ if (fs.existsSync(fullPath)) return fullPath;
192
+ }
193
+ return null;
194
+ }
195
+
196
+ function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
197
+ if (!config || typeof config !== "object" || Array.isArray(config)) return {};
198
+ const obj = config as Record<string, unknown>;
199
+ const servers = kind === "cursor" || kind === "windsurf" || kind === "vscode"
200
+ ? obj.mcpServers ?? obj["mcp-servers"]
201
+ : obj.mcpServers;
202
+ return servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {};
203
+ }
204
+
205
+ function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, defaultPrefix: ToolPrefix, envOverride: string[]): string[] {
206
+ const names: string[] = [];
207
+ const seenNames = new Set<string>();
208
+ const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
209
+
210
+ for (const [serverName, definition] of Object.entries(config.mcpServers)) {
211
+ if (definition.disabled) continue;
212
+ const serverCache = cache.servers[serverName];
213
+ if (!isServerCacheValid(serverCache, definition)) continue;
214
+ const prefix = definition.toolPrefix ?? defaultPrefix;
215
+
216
+ const toolFilter = selectedServers.has(serverName)
217
+ ? true
218
+ : selectedTools.get(serverName);
219
+ if (!toolFilter) continue;
220
+
221
+ for (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {
222
+ if (typeof tool?.name !== "string" || !tool.name) continue;
223
+ if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
224
+ if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
225
+ const prefixedName = formatToolName(tool.name, serverName, prefix);
226
+ if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
227
+ seenNames.add(prefixedName);
228
+ names.push(prefixedName);
229
+ }
230
+
231
+ if (definition.exposeResources === false) continue;
232
+ for (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {
233
+ if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
234
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
235
+ if (toolFilter !== true && !toolFilter.has(baseName)) continue;
236
+ if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
237
+ const prefixedName = formatToolName(baseName, serverName, prefix);
238
+ if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
239
+ seenNames.add(prefixedName);
240
+ names.push(prefixedName);
241
+ }
242
+ }
243
+
244
+ return names;
245
+ }
246
+
247
+ function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
248
+ const servers = new Set<string>();
249
+ const tools = new Map<string, Set<string>>();
250
+ for (let item of selections) {
251
+ item = item.replace(/\/+$/, "");
252
+ if (item.includes("/")) {
253
+ const [server, tool] = item.split("/", 2);
254
+ if (server && tool) {
255
+ if (!tools.has(server)) tools.set(server, new Set());
256
+ tools.get(server)!.add(tool);
257
+ } else if (server) {
258
+ servers.add(server);
259
+ }
260
+ } else if (item) {
261
+ servers.add(item);
262
+ }
263
+ }
264
+ return { servers, tools };
265
+ }
266
+
267
+ function isServerCacheValid(entry: ServerCacheEntry | undefined, definition: ServerEntry): entry is ServerCacheEntry {
268
+ if (!entry || entry.configHash !== computeMcpServerHash(definition)) return false;
269
+ if (!entry.cachedAt || typeof entry.cachedAt !== "number") return false;
270
+ return Date.now() - entry.cachedAt <= CACHE_MAX_AGE_MS;
271
+ }
272
+
273
+ function computeMcpServerHash(definition: ServerEntry): string {
274
+ const identity: Record<string, unknown> = {
275
+ command: definition.command,
276
+ args: definition.args,
277
+ socket: resolveConfigPath(definition.socket),
278
+ env: interpolateEnvRecord(definition.env),
279
+ cwd: resolveConfigPath(definition.cwd),
280
+ url: definition.url ? interpolateEnvVars(definition.url) : definition.url,
281
+ headers: interpolateEnvRecord(definition.headers),
282
+ auth: definition.auth,
283
+ bearerToken: resolveBearerToken(definition),
284
+ bearerTokenEnv: definition.bearerTokenEnv,
285
+ exposeResources: definition.exposeResources,
286
+ includeTools: definition.includeTools,
287
+ excludeTools: definition.excludeTools,
288
+ };
289
+ return createHash("sha256").update(stableStringify(identity)).digest("hex");
290
+ }
291
+
292
+ function getToolPrefix(value: unknown): ToolPrefix {
293
+ return value === "none" || value === "short" || value === "server" ? value : "server";
294
+ }
295
+
296
+ function isImportKind(value: unknown): value is ImportKind {
297
+ return typeof value === "string" && Object.hasOwn(IMPORT_PATHS, value);
298
+ }
299
+
300
+ function getServerPrefix(serverName: string, mode: ToolPrefix): string {
301
+ if (mode === "none") return "";
302
+ if (mode === "short") {
303
+ const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
304
+ return short || "mcp";
305
+ }
306
+ return serverName.replace(/-/g, "_");
307
+ }
308
+
309
+ function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {
310
+ const serverPrefix = getServerPrefix(serverName, prefix);
311
+ return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
312
+ }
313
+
314
+ function isToolExcluded(toolName: string, serverName: string, prefix: ToolPrefix, excludeTools: unknown): boolean {
315
+ if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
316
+ const candidates = new Set([
317
+ normalizeToolName(toolName),
318
+ normalizeToolName(formatToolName(toolName, serverName, prefix)),
319
+ normalizeToolName(formatToolName(toolName, serverName, "server")),
320
+ normalizeToolName(formatToolName(toolName, serverName, "short")),
321
+ ]);
322
+ return excludeTools.some((excluded) => typeof excluded === "string" && candidates.has(normalizeToolName(excluded)));
323
+ }
324
+
325
+ function normalizeToolName(value: string): string {
326
+ return value.replace(/-/g, "_");
327
+ }
328
+
329
+ function resourceNameToToolName(name: string): string {
330
+ let result = name
331
+ .replace(/[^a-zA-Z0-9]/g, "_")
332
+ .replace(/_+/g, "_")
333
+ .replace(/^_+/, "")
334
+ .replace(/_+$/, "")
335
+ .toLowerCase();
336
+ if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
337
+ return result;
338
+ }
339
+
340
+ function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
341
+ if (!values || typeof values !== "object" || Array.isArray(values)) return undefined;
342
+ const resolved: Record<string, string> = {};
343
+ for (const [key, value] of Object.entries(values)) {
344
+ if (typeof value === "string") resolved[key] = interpolateEnvVars(value);
345
+ }
346
+ return resolved;
347
+ }
348
+
349
+ function interpolateEnvVars(value: string): string {
350
+ return value
351
+ .replace(/\$\{(\w+)\}/g, (_, name: string) => process.env[name] ?? "")
352
+ .replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "");
353
+ }
354
+
355
+ function resolveConfigPath(value: string | undefined): string | undefined {
356
+ if (typeof value !== "string") return undefined;
357
+ const resolved = interpolateEnvVars(value);
358
+ if (resolved === "~") return os.homedir();
359
+ if (resolved.startsWith("~/") || resolved.startsWith("~\\")) return path.join(os.homedir(), resolved.slice(2));
360
+ return resolved;
361
+ }
362
+
363
+ function resolveBearerToken(definition: Pick<ServerEntry, "bearerToken" | "bearerTokenEnv">): string | undefined {
364
+ if (typeof definition.bearerToken === "string") return interpolateEnvVars(definition.bearerToken);
365
+ return typeof definition.bearerTokenEnv === "string" ? process.env[definition.bearerTokenEnv] : undefined;
366
+ }
367
+
368
+ function stableStringify(value: unknown): string {
369
+ if (value === null || value === undefined || typeof value !== "object") {
370
+ const serialized = JSON.stringify(value);
371
+ return serialized === undefined ? "undefined" : serialized;
372
+ }
373
+ if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
374
+ const obj = value as Record<string, unknown>;
375
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
376
+ }
@@ -0,0 +1,219 @@
1
+ import type { ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
2
+ import type { ModelAttempt, ResourceLimitExceeded, Usage } from "../../shared/types.ts";
3
+
4
+ export type { AvailableModelInfo };
5
+
6
+ interface ModelAttemptSummary {
7
+ model: string;
8
+ success: boolean;
9
+ exitCode?: number | null;
10
+ error?: string;
11
+ usage?: Usage;
12
+ }
13
+
14
+ export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
15
+ const colonIdx = model.lastIndexOf(":");
16
+ if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
17
+ return {
18
+ baseModel: model.substring(0, colonIdx),
19
+ thinkingSuffix: model.substring(colonIdx),
20
+ };
21
+ }
22
+
23
+ export function resolveModelCandidate(
24
+ model: string | undefined,
25
+ availableModels: AvailableModelInfo[] | undefined,
26
+ preferredProvider?: string,
27
+ ): string | undefined {
28
+ if (!model) return undefined;
29
+ if (model.includes("/")) return model;
30
+ if (!availableModels || availableModels.length === 0) return model;
31
+
32
+ const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
33
+ const matches = availableModels.filter((entry) => entry.id === baseModel);
34
+ if (preferredProvider) {
35
+ const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
36
+ if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
37
+ }
38
+ if (matches.length !== 1) return model;
39
+ return `${matches[0]!.fullId}${thinkingSuffix}`;
40
+ }
41
+
42
+ export function buildModelCandidates(
43
+ primaryModel: string | undefined,
44
+ fallbackModels: string[] | undefined,
45
+ availableModels: AvailableModelInfo[] | undefined,
46
+ preferredProvider?: string,
47
+ ): string[] {
48
+ const seen = new Set<string>();
49
+ const candidates: string[] = [];
50
+ for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
51
+ if (!raw) continue;
52
+ const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
53
+ if (!normalized || seen.has(normalized)) continue;
54
+ seen.add(normalized);
55
+ candidates.push(normalized);
56
+ }
57
+ return candidates;
58
+ }
59
+
60
+ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
61
+ /rate\s*limit/i,
62
+ /usage\s*limit/i,
63
+ /too many requests/i,
64
+ /\b429\b/,
65
+ /quota/i,
66
+ /billing/i,
67
+ /credit/i,
68
+ /auth(?:entication)?/i,
69
+ /unauthori[sz]ed/i,
70
+ /forbidden/i,
71
+ /api key/i,
72
+ /token expired/i,
73
+ /invalid key/i,
74
+ /provider.*unavailable/i,
75
+ /model.*unavailable/i,
76
+ /model.*disabled/i,
77
+ /model.*not found/i,
78
+ /unknown model/i,
79
+ /overloaded/i,
80
+ /service unavailable/i,
81
+ /temporar(?:ily)? unavailable/i,
82
+ /connection refused/i,
83
+ /database is locked/i,
84
+ /\bsqlite_busy\b/i,
85
+ /fetch failed/i,
86
+ /network error/i,
87
+ /socket hang up/i,
88
+ /upstream/i,
89
+ /timed? out/i,
90
+ /timeout/i,
91
+ /\b502\b/,
92
+ /\b503\b/,
93
+ /\b504\b/,
94
+ ];
95
+
96
+ export function isRetryableModelFailure(error: string | undefined): boolean {
97
+ if (!error) return false;
98
+ return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
99
+ }
100
+
101
+ const NON_RECOVERABLE_SAME_MODEL_PATTERNS = [
102
+ /usage\s*limit/i,
103
+ /quota/i,
104
+ /billing/i,
105
+ /credit/i,
106
+ /auth(?:entication)?/i,
107
+ /unauthori[sz]ed/i,
108
+ /forbidden/i,
109
+ /api key/i,
110
+ /token expired/i,
111
+ /invalid key/i,
112
+ /model.*disabled/i,
113
+ /model.*not found/i,
114
+ /unknown model/i,
115
+ ];
116
+
117
+ const SAME_MODEL_RECOVERY_PATTERNS = [
118
+ /web\s*socket/i,
119
+ /websocket/i,
120
+ /transport/i,
121
+ /stream/i,
122
+ /socket/i,
123
+ /connection/i,
124
+ /fetch failed/i,
125
+ /network/i,
126
+ /http idle/i,
127
+ /timed? out/i,
128
+ /timeout/i,
129
+ /overloaded/i,
130
+ /service unavailable/i,
131
+ /temporar(?:ily)? unavailable/i,
132
+ /connection refused/i,
133
+ /database is locked/i,
134
+ /\bsqlite_busy\b/i,
135
+ /econnreset/i,
136
+ /etimedout/i,
137
+ /\b502\b/,
138
+ /\b503\b/,
139
+ /\b504\b/,
140
+ ];
141
+
142
+ export function isRecoverableSameModelFailure(error: string | undefined, exitCode?: number | null): boolean {
143
+ if (exitCode === 143) return true;
144
+ if (!error) return false;
145
+ if (NON_RECOVERABLE_SAME_MODEL_PATTERNS.some((pattern) => pattern.test(error))) return false;
146
+ return SAME_MODEL_RECOVERY_PATTERNS.some((pattern) => pattern.test(error));
147
+ }
148
+
149
+ export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
150
+ const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
151
+ return nextModel
152
+ ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
153
+ : `[fallback] ${attempt.model} failed: ${failure}.`;
154
+ }
155
+
156
+ export function formatModelRecoveryAttemptNote(attempt: ModelAttemptSummary, retryNumber: number, maxRetries: number): string {
157
+ const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
158
+ return `[retry] ${attempt.model} failed: ${failure}. Retrying same model (${retryNumber}/${maxRetries}).`;
159
+ }
160
+
161
+ export interface AttemptOutcome {
162
+ accounting?: ModelAttempt["accounting"];
163
+ exitCode: number | null;
164
+ error?: string;
165
+ model?: string;
166
+ usage: Usage;
167
+ interrupted?: boolean;
168
+ timedOut?: boolean;
169
+ resourceLimitExceeded?: ResourceLimitExceeded;
170
+ terminalFailure?: boolean;
171
+ }
172
+
173
+ export function sumAttemptUsage(attempts: readonly ModelAttempt[]): Usage {
174
+ const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
175
+ for (const attempt of attempts) {
176
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "cost", "turns"] as const) usage[key] += attempt.usage?.[key] ?? 0;
177
+ if (attempt.usage?.contributions?.length) (usage.contributions ??= []).push(...attempt.usage.contributions);
178
+ }
179
+ return usage;
180
+ }
181
+
182
+ export async function runModelAttempts<T extends AttemptOutcome>(input: {
183
+ candidates: readonly (string | undefined)[];
184
+ signal?: AbortSignal;
185
+ runAttempt: (model: string | undefined, notes: string[]) => Promise<T>;
186
+ }): Promise<{ result: T; modelAttempts: ModelAttempt[]; attemptedModels: string[]; notes: string[]; usage: Usage }> {
187
+ const candidates = input.candidates.length ? input.candidates : [undefined];
188
+ const modelAttempts: ModelAttempt[] = [];
189
+ const attemptedModels: string[] = [];
190
+ const notes: string[] = [];
191
+ let result!: T;
192
+ modelLoop:
193
+ for (let index = 0; index < candidates.length; index++) {
194
+ const model = candidates[index];
195
+ for (let recovery = 0; ; recovery++) {
196
+ result = await input.runAttempt(model, notes);
197
+ if (model) attemptedModels.push(model);
198
+ const attempt: ModelAttempt = {
199
+ model: model ?? result.model ?? "default",
200
+ success: result.exitCode === 0 && !result.error && !result.interrupted,
201
+ exitCode: result.exitCode,
202
+ error: result.error,
203
+ accounting: result.accounting,
204
+ usage: { ...result.usage },
205
+ };
206
+ modelAttempts.push(attempt);
207
+ if (attempt.success || input.signal?.aborted || result.interrupted || result.timedOut || result.resourceLimitExceeded || result.terminalFailure) break modelLoop;
208
+ const recoverable = isRecoverableSameModelFailure(result.error, result.exitCode);
209
+ if (recoverable && recovery === 0) {
210
+ notes.push(formatModelRecoveryAttemptNote(attempt, 1, 1));
211
+ continue;
212
+ }
213
+ if ((!recoverable && !isRetryableModelFailure(result.error)) || index === candidates.length - 1) break modelLoop;
214
+ notes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
215
+ break;
216
+ }
217
+ }
218
+ return { result, modelAttempts, attemptedModels, notes, usage: sumAttemptUsage(modelAttempts) };
219
+ }