@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4
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.
- package/CHANGELOG.md +1645 -0
- package/LICENSE +21 -0
- package/README.md +1529 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +194 -0
- package/dist/history/ingest.js +447 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +461 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +77 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +139 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +103 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +320 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom.md +579 -0
- package/docs/performance.md +121 -0
- package/package.json +101 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +152 -0
- package/src/history/ingest.ts +332 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +317 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +70 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +121 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +93 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +372 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1259 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { getSingleResultOutput } from "../../shared/utils.js";
|
|
2
|
+
import { validateStructuredOutputValue } from "./structured-output.js";
|
|
3
|
+
export class DynamicFanoutError extends Error {
|
|
4
|
+
}
|
|
5
|
+
const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
|
+
const ITEM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7
|
+
const ITEM_REF_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)(?:\.([^{}]+))?\}/g;
|
|
8
|
+
const RESERVED_TEMPLATE_NAMES = new Set(["task", "previous", "chain_dir", "outputs"]);
|
|
9
|
+
const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance"]);
|
|
10
|
+
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "sessionFiles"]);
|
|
11
|
+
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
12
|
+
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
13
|
+
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "acceptance"]);
|
|
14
|
+
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
15
|
+
...DYNAMIC_PARALLEL_KEYS,
|
|
16
|
+
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "outputPathFromAgentDefault", "allowSubagents", "maxSubagentDepth",
|
|
17
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
18
|
+
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "maxExecutionTimeMs", "maxTokens", "launch",
|
|
19
|
+
]);
|
|
20
|
+
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
21
|
+
export function isSafeOutputName(name) {
|
|
22
|
+
return SAFE_OUTPUT_NAME_PATTERN.test(name);
|
|
23
|
+
}
|
|
24
|
+
export function assertJsonPointer(pointer, label) {
|
|
25
|
+
if (pointer === "")
|
|
26
|
+
return;
|
|
27
|
+
if (!pointer.startsWith("/")) {
|
|
28
|
+
throw new DynamicFanoutError(`${label} must be a JSON Pointer starting with '/'.`);
|
|
29
|
+
}
|
|
30
|
+
for (const segment of pointer.slice(1).split("/")) {
|
|
31
|
+
if (/~(?![01])/.test(segment)) {
|
|
32
|
+
throw new DynamicFanoutError(`${label} contains invalid JSON Pointer escape.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function decodePointerSegment(segment) {
|
|
37
|
+
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
38
|
+
}
|
|
39
|
+
export function resolveJsonPointer(value, pointer, label) {
|
|
40
|
+
assertJsonPointer(pointer, label);
|
|
41
|
+
if (pointer === "")
|
|
42
|
+
return value;
|
|
43
|
+
let current = value;
|
|
44
|
+
for (const rawSegment of pointer.slice(1).split("/")) {
|
|
45
|
+
const segment = decodePointerSegment(rawSegment);
|
|
46
|
+
if (Array.isArray(current)) {
|
|
47
|
+
if (!/^(0|[1-9][0-9]*)$/.test(segment)) {
|
|
48
|
+
throw new DynamicFanoutError(`${label} segment '${segment}' does not address an array index.`);
|
|
49
|
+
}
|
|
50
|
+
const index = Number(segment);
|
|
51
|
+
if (index >= current.length)
|
|
52
|
+
throw new DynamicFanoutError(`${label} does not exist.`);
|
|
53
|
+
current = current[index];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!current || typeof current !== "object") {
|
|
57
|
+
throw new DynamicFanoutError(`${label} does not exist.`);
|
|
58
|
+
}
|
|
59
|
+
const record = current;
|
|
60
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
61
|
+
throw new DynamicFanoutError(`${label} does not exist.`);
|
|
62
|
+
}
|
|
63
|
+
current = record[segment];
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function scalarToKey(value, label) {
|
|
68
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
69
|
+
const key = String(value);
|
|
70
|
+
if (!key.trim())
|
|
71
|
+
throw new DynamicFanoutError(`${label} resolved to an empty key.`);
|
|
72
|
+
if (/[\u0000-\u001F\u007F]/.test(key))
|
|
73
|
+
throw new DynamicFanoutError(`${label} resolved to an unsafe key.`);
|
|
74
|
+
if (key.length > 200)
|
|
75
|
+
throw new DynamicFanoutError(`${label} resolved to a key longer than 200 characters.`);
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
throw new DynamicFanoutError(`${label} must resolve to a string, number, or boolean.`);
|
|
79
|
+
}
|
|
80
|
+
export function normalizeItemKeyForId(key) {
|
|
81
|
+
const normalized = key
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
84
|
+
.replace(/^-+|-+$/g, "")
|
|
85
|
+
.slice(0, 80);
|
|
86
|
+
return normalized || "item";
|
|
87
|
+
}
|
|
88
|
+
function valueToTemplateText(value, reference) {
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
throw new DynamicFanoutError(`Unresolved item reference '${reference}'.`);
|
|
91
|
+
if (typeof value === "string")
|
|
92
|
+
return value;
|
|
93
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null)
|
|
94
|
+
return String(value);
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
function resolveItemPath(item, pathText, reference) {
|
|
98
|
+
if (!pathText)
|
|
99
|
+
return item;
|
|
100
|
+
const pointer = `/${pathText.split(".").map((segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
|
|
101
|
+
return resolveJsonPointer(item, pointer, reference);
|
|
102
|
+
}
|
|
103
|
+
export function resolveItemTemplate(template, itemName, item) {
|
|
104
|
+
return template.replace(ITEM_REF_PATTERN, (raw, name, pathText) => {
|
|
105
|
+
if (name !== itemName)
|
|
106
|
+
return raw;
|
|
107
|
+
if (pathText !== undefined && (!pathText.trim() || pathText.includes(".."))) {
|
|
108
|
+
throw new DynamicFanoutError(`Invalid item reference '${raw}'.`);
|
|
109
|
+
}
|
|
110
|
+
return valueToTemplateText(resolveItemPath(item, pathText, raw), raw);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
114
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
115
|
+
throw new DynamicFanoutError(`${label} must be an object.`);
|
|
116
|
+
for (const key of Object.keys(value)) {
|
|
117
|
+
if (!allowed.has(key))
|
|
118
|
+
throw new DynamicFanoutError(`${label} does not support field '${key}'.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function assertNoUnresolvedItemReferences(template, itemName, label) {
|
|
122
|
+
for (const match of template.matchAll(/\{([^{}]*)\}/g)) {
|
|
123
|
+
const raw = match[0];
|
|
124
|
+
const reference = match[1];
|
|
125
|
+
if (reference === itemName || reference.startsWith(`${itemName}.`)) {
|
|
126
|
+
if (!ITEM_REF_PATTERN.test(raw) || reference === `${itemName}.` || reference.includes("..")) {
|
|
127
|
+
throw new DynamicFanoutError(`Invalid item reference '${raw}' in ${label}.`);
|
|
128
|
+
}
|
|
129
|
+
ITEM_REF_PATTERN.lastIndex = 0;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
ITEM_REF_PATTERN.lastIndex = 0;
|
|
133
|
+
const name = reference.match(/^[A-Za-z_][A-Za-z0-9_]*/)?.[0];
|
|
134
|
+
if (name === itemName)
|
|
135
|
+
throw new DynamicFanoutError(`Invalid item reference '${raw}' in ${label}.`);
|
|
136
|
+
if (name && RESERVED_TEMPLATE_NAMES.has(name))
|
|
137
|
+
continue;
|
|
138
|
+
if (name)
|
|
139
|
+
throw new DynamicFanoutError(`Unsupported template reference '${raw}' in ${label}.`);
|
|
140
|
+
}
|
|
141
|
+
ITEM_REF_PATTERN.lastIndex = 0;
|
|
142
|
+
if (template.includes(`{${itemName}.}`) || new RegExp(`\\{${itemName}(?:\\.|$)[^}]*$`).test(template)) {
|
|
143
|
+
throw new DynamicFanoutError(`Invalid item reference in ${label}.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function hasDynamicFanoutFields(step) {
|
|
147
|
+
return !!step && typeof step === "object" && !Array.isArray(step)
|
|
148
|
+
&& (Object.prototype.hasOwnProperty.call(step, "expand") || Object.prototype.hasOwnProperty.call(step, "collect"));
|
|
149
|
+
}
|
|
150
|
+
export function validateDynamicStepShape(step, stepIndex, config = {}) {
|
|
151
|
+
const prefix = `Dynamic chain step ${stepIndex + 1}`;
|
|
152
|
+
if (Object.hasOwn(step, "acceptance") || Object.hasOwn(step, "effectiveAcceptance")) {
|
|
153
|
+
throw new DynamicFanoutError(`Dynamic fanout step ${stepIndex + 1} does not support group-level acceptance; set acceptance on the child template instead.`);
|
|
154
|
+
}
|
|
155
|
+
assertOnlyKeys(step, config.allowRunnerFields ? RUNNER_DYNAMIC_STEP_KEYS : DYNAMIC_STEP_KEYS, prefix);
|
|
156
|
+
if (!step.expand || !step.expand.from)
|
|
157
|
+
throw new DynamicFanoutError(`${prefix} requires expand.from.`);
|
|
158
|
+
assertOnlyKeys(step.expand, DYNAMIC_EXPAND_KEYS, `${prefix} expand`);
|
|
159
|
+
assertOnlyKeys(step.expand.from, DYNAMIC_EXPAND_FROM_KEYS, `${prefix} expand.from`);
|
|
160
|
+
if (!isSafeOutputName(step.expand.from.output))
|
|
161
|
+
throw new DynamicFanoutError(`${prefix} has invalid expand.from.output '${step.expand.from.output}'.`);
|
|
162
|
+
assertJsonPointer(step.expand.from.path, `${prefix} expand.from.path`);
|
|
163
|
+
if (step.expand.key !== undefined)
|
|
164
|
+
assertJsonPointer(step.expand.key, `${prefix} expand.key`);
|
|
165
|
+
const itemName = step.expand.item ?? "item";
|
|
166
|
+
if (!ITEM_NAME_PATTERN.test(itemName))
|
|
167
|
+
throw new DynamicFanoutError(`${prefix} has invalid expand.item '${itemName}'.`);
|
|
168
|
+
if (step.expand.maxItems === undefined && config.maxItems === undefined) {
|
|
169
|
+
throw new DynamicFanoutError(`${prefix} requires expand.maxItems or config.chain.dynamicFanout.maxItems.`);
|
|
170
|
+
}
|
|
171
|
+
if (step.expand.maxItems !== undefined && (!Number.isInteger(step.expand.maxItems) || step.expand.maxItems < 0)) {
|
|
172
|
+
throw new DynamicFanoutError(`${prefix} expand.maxItems must be an integer >= 0.`);
|
|
173
|
+
}
|
|
174
|
+
if (config.maxItems !== undefined && (!Number.isInteger(config.maxItems) || config.maxItems < 0)) {
|
|
175
|
+
throw new DynamicFanoutError("config.chain.dynamicFanout.maxItems must be an integer >= 0.");
|
|
176
|
+
}
|
|
177
|
+
if (!step.parallel || Array.isArray(step.parallel))
|
|
178
|
+
throw new DynamicFanoutError(`${prefix} requires a single parallel template object and cannot mix dynamic expand/collect with static parallel arrays.`);
|
|
179
|
+
assertOnlyKeys(step.parallel, config.allowRunnerFields ? RUNNER_DYNAMIC_PARALLEL_KEYS : DYNAMIC_PARALLEL_KEYS, `${prefix} parallel`);
|
|
180
|
+
if ("expand" in step.parallel)
|
|
181
|
+
throw new DynamicFanoutError(`${prefix} does not support nested dynamic fanout.`);
|
|
182
|
+
if (!step.parallel.agent)
|
|
183
|
+
throw new DynamicFanoutError(`${prefix} parallel.agent is required.`);
|
|
184
|
+
if (!step.collect?.as || !isSafeOutputName(step.collect.as))
|
|
185
|
+
throw new DynamicFanoutError(`${prefix} requires collect.as with a safe output name.`);
|
|
186
|
+
assertOnlyKeys(step.collect, DYNAMIC_COLLECT_KEYS, `${prefix} collect`);
|
|
187
|
+
for (const [label, template] of [
|
|
188
|
+
["parallel.task", step.parallel.task],
|
|
189
|
+
["parallel.label", step.parallel.label],
|
|
190
|
+
]) {
|
|
191
|
+
if (template)
|
|
192
|
+
assertNoUnresolvedItemReferences(template, itemName, `${prefix} ${label}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function resolveDynamicFanoutItems(step, outputs, stepIndex, config = {}) {
|
|
196
|
+
validateDynamicStepShape(step, stepIndex, config);
|
|
197
|
+
const sourceName = step.expand.from.output;
|
|
198
|
+
const source = outputs[sourceName];
|
|
199
|
+
if (!source)
|
|
200
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} references unknown output '${sourceName}'.`);
|
|
201
|
+
if (source.structured === undefined)
|
|
202
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} requires structured output '${sourceName}'.`);
|
|
203
|
+
const value = resolveJsonPointer(source.structured, step.expand.from.path, `Dynamic chain step ${stepIndex + 1} expand.from.path`);
|
|
204
|
+
if (!Array.isArray(value))
|
|
205
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} expand.from.path must resolve to an array.`);
|
|
206
|
+
const maxItems = step.expand.maxItems ?? config.maxItems;
|
|
207
|
+
if (maxItems === undefined)
|
|
208
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} requires an effective maxItems.`);
|
|
209
|
+
if (value.length > maxItems)
|
|
210
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} resolved ${value.length} items, exceeding maxItems ${maxItems}.`);
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const seenIds = new Set();
|
|
213
|
+
return value.map((item, index) => {
|
|
214
|
+
const key = step.expand.key === undefined
|
|
215
|
+
? String(index)
|
|
216
|
+
: scalarToKey(resolveJsonPointer(item, step.expand.key, `Dynamic chain step ${stepIndex + 1} expand.key`), `Dynamic chain step ${stepIndex + 1} expand.key`);
|
|
217
|
+
if (seen.has(key))
|
|
218
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} produced duplicate item key '${key}'.`);
|
|
219
|
+
seen.add(key);
|
|
220
|
+
const idKey = normalizeItemKeyForId(key);
|
|
221
|
+
if (seenIds.has(idKey))
|
|
222
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} produced colliding item id '${idKey}'.`);
|
|
223
|
+
seenIds.add(idKey);
|
|
224
|
+
return { index, key, idKey, item };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export function materializeDynamicParallelStep(step, outputs, stepIndex, config = {}) {
|
|
228
|
+
const items = resolveDynamicFanoutItems(step, outputs, stepIndex, config);
|
|
229
|
+
if (items.length === 0) {
|
|
230
|
+
if ((step.expand.onEmpty ?? "skip") === "fail") {
|
|
231
|
+
throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} source array is empty.`);
|
|
232
|
+
}
|
|
233
|
+
return { items, parallel: [] };
|
|
234
|
+
}
|
|
235
|
+
const itemName = step.expand.item ?? "item";
|
|
236
|
+
const parallel = items.map((entry) => {
|
|
237
|
+
const task = resolveItemTemplate(step.parallel.task ?? "{previous}", itemName, entry.item);
|
|
238
|
+
const label = step.parallel.label ? resolveItemTemplate(step.parallel.label, itemName, entry.item) : undefined;
|
|
239
|
+
return {
|
|
240
|
+
...step.parallel,
|
|
241
|
+
task,
|
|
242
|
+
...(label !== undefined ? { label } : {}),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return { items, parallel };
|
|
246
|
+
}
|
|
247
|
+
export function collectDynamicResults(step, items, results) {
|
|
248
|
+
return items.map((entry, index) => {
|
|
249
|
+
const result = results[index];
|
|
250
|
+
const text = result
|
|
251
|
+
? ("output" in result && typeof result.output === "string" ? result.output : getSingleResultOutput(result))
|
|
252
|
+
: "";
|
|
253
|
+
return {
|
|
254
|
+
key: entry.key,
|
|
255
|
+
index: entry.index,
|
|
256
|
+
item: entry.item,
|
|
257
|
+
agent: result?.agent ?? step.parallel.agent,
|
|
258
|
+
exitCode: result?.exitCode ?? null,
|
|
259
|
+
text,
|
|
260
|
+
...(result?.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
261
|
+
...(result?.error ? { error: result.error } : {}),
|
|
262
|
+
...(result?.savedOutputPath ? { outputPath: result.savedOutputPath } : {}),
|
|
263
|
+
...(result?.artifactPaths ? { artifactPaths: result.artifactPaths } : {}),
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
export function validateDynamicCollection(schema, value) {
|
|
268
|
+
if (!schema)
|
|
269
|
+
return;
|
|
270
|
+
const validation = validateStructuredOutputValue(schema, value);
|
|
271
|
+
if (validation.status === "invalid") {
|
|
272
|
+
throw new DynamicFanoutError(`Collected output validation failed: ${validation.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SubagentHistoryIndex } from "../../history/index.js";
|
|
2
|
+
import { getAgentDir } from "../../shared/utils.js";
|
|
3
|
+
export function closeRunHistory(state) {
|
|
4
|
+
const index = state.historyIndex;
|
|
5
|
+
state.historyIndex = undefined;
|
|
6
|
+
state.historyReady = undefined;
|
|
7
|
+
state.historyClosing = Promise.all([state.historyClosing, index?.close()]).then(() => { });
|
|
8
|
+
return state.historyClosing;
|
|
9
|
+
}
|
|
10
|
+
function admitOwner(state, ownerSessionId, ownerSessionFile) {
|
|
11
|
+
const index = state.historyIndex = new SubagentHistoryIndex(getAgentDir());
|
|
12
|
+
const snapshot = { ownerSessionId, ownerSessionFile,
|
|
13
|
+
runs: [...(state.ownedRuns?.values() ?? [])], foregroundRuns: [...(state.foregroundRuns?.values() ?? [])] };
|
|
14
|
+
state.historyReady = (state.historyClosing ?? Promise.resolve()).then(() => index.setOwner(snapshot));
|
|
15
|
+
index.onChanged(() => { if (state.historyIndex === index)
|
|
16
|
+
state.onRunsChanged?.(); });
|
|
17
|
+
// Retain failed readiness until an explicit retry; background updates must not start a crash loop.
|
|
18
|
+
void state.historyReady.catch(() => {
|
|
19
|
+
if (state.historyIndex === index)
|
|
20
|
+
void index.close();
|
|
21
|
+
});
|
|
22
|
+
return index;
|
|
23
|
+
}
|
|
24
|
+
export function startRunHistory(state, ctx) {
|
|
25
|
+
void closeRunHistory(state).catch((error) => console.error("Could not close subagent history:", error));
|
|
26
|
+
// Fresh sessions pay no process/database startup cost until they actually browse or own work.
|
|
27
|
+
if (state.ownedRuns?.size)
|
|
28
|
+
admitOwner(state, ctx.sessionManager.getSessionId(), ctx.sessionManager.getSessionFile());
|
|
29
|
+
}
|
|
30
|
+
export function updateRunHistory(state, run) {
|
|
31
|
+
const index = state.historyIndex;
|
|
32
|
+
if (!index)
|
|
33
|
+
return;
|
|
34
|
+
state.historyReady = (state.historyReady ?? Promise.resolve()).then(() => index.updateRun(run, state.foregroundRuns?.get(run.runId)));
|
|
35
|
+
void state.historyReady.catch(() => {
|
|
36
|
+
if (state.historyIndex === index)
|
|
37
|
+
void index.close();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function historyOwner(state) {
|
|
41
|
+
const current = state.lastUiContext?.sessionManager.getSessionId();
|
|
42
|
+
if (current)
|
|
43
|
+
return current;
|
|
44
|
+
// Non-UI callers can hold restored owner handles without an ExtensionContext.
|
|
45
|
+
// Never infer ownership from a directory or adopt records found by the index.
|
|
46
|
+
const owners = new Set([...(state.ownedRuns?.values() ?? [])].map((run) => run.ownerSessionId));
|
|
47
|
+
if (owners.size > 1)
|
|
48
|
+
throw new Error("History requires one verified owning session.");
|
|
49
|
+
const owner = owners.values().next().value ?? state.currentSessionId;
|
|
50
|
+
if (!owner)
|
|
51
|
+
throw new Error("History requires an active owning session.");
|
|
52
|
+
return owner;
|
|
53
|
+
}
|
|
54
|
+
export async function runHistoryIndex(state, retry = false) {
|
|
55
|
+
const ownerSessionId = historyOwner(state);
|
|
56
|
+
if (retry && state.historyIndex?.failure) {
|
|
57
|
+
const closing = closeRunHistory(state);
|
|
58
|
+
await closing;
|
|
59
|
+
if (state.historyClosing !== closing || historyOwner(state) !== ownerSessionId)
|
|
60
|
+
throw new Error("Owning session changed while history was loading.");
|
|
61
|
+
}
|
|
62
|
+
if (!state.historyIndex) {
|
|
63
|
+
admitOwner(state, ownerSessionId, state.lastUiContext?.sessionManager.getSessionFile());
|
|
64
|
+
}
|
|
65
|
+
const index = state.historyIndex;
|
|
66
|
+
await state.historyReady;
|
|
67
|
+
if (state.historyIndex !== index || historyOwner(state) !== ownerSessionId)
|
|
68
|
+
throw new Error("Owning session changed while history was loading.");
|
|
69
|
+
if (index.failure)
|
|
70
|
+
throw index.failure;
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
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.js";
|
|
6
|
+
const CACHE_VERSION = 1;
|
|
7
|
+
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
9
|
+
const GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".config", "mcp", "mcp.json");
|
|
10
|
+
const IMPORT_PATHS = {
|
|
11
|
+
cursor: [path.join(os.homedir(), ".cursor", "mcp.json")],
|
|
12
|
+
"claude-code": [
|
|
13
|
+
path.join(os.homedir(), ".claude", "mcp.json"),
|
|
14
|
+
path.join(os.homedir(), ".claude.json"),
|
|
15
|
+
path.join(os.homedir(), ".claude", "claude_desktop_config.json"),
|
|
16
|
+
],
|
|
17
|
+
"claude-desktop": [path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
|
|
18
|
+
codex: [path.join(os.homedir(), ".codex", "config.json")],
|
|
19
|
+
windsurf: [path.join(os.homedir(), ".windsurf", "mcp.json")],
|
|
20
|
+
vscode: [".vscode/mcp.json"],
|
|
21
|
+
};
|
|
22
|
+
export function resolveMcpDirectToolNames(mcpDirectTools, cwd = process.cwd()) {
|
|
23
|
+
if (!mcpDirectTools?.length)
|
|
24
|
+
return [];
|
|
25
|
+
const names = new Set();
|
|
26
|
+
// Keep pre-v5 and independent Fitch configs paired with their own caches.
|
|
27
|
+
for (const directory of ["", "fitch-mcp-adapter"]) {
|
|
28
|
+
try {
|
|
29
|
+
const config = loadMcpConfig(cwd, directory);
|
|
30
|
+
const cache = loadMetadataCache(directory);
|
|
31
|
+
if (!cache)
|
|
32
|
+
continue;
|
|
33
|
+
for (const name of resolveDirectToolNames(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools)) {
|
|
34
|
+
names.add(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { /* An unavailable adapter does not hide the other adapter's tools. */ }
|
|
38
|
+
}
|
|
39
|
+
return [...names];
|
|
40
|
+
}
|
|
41
|
+
function loadMetadataCache(directory) {
|
|
42
|
+
const cachePath = path.join(getAgentDir(), directory, "mcp-cache.json");
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!parsed || typeof parsed !== "object")
|
|
51
|
+
return null;
|
|
52
|
+
const raw = parsed;
|
|
53
|
+
if (raw.version !== CACHE_VERSION || !raw.servers || typeof raw.servers !== "object" || Array.isArray(raw.servers)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
function loadMcpConfig(cwd, directory) {
|
|
59
|
+
let config = { mcpServers: {} };
|
|
60
|
+
for (const sourcePath of getConfigPaths(cwd, directory)) {
|
|
61
|
+
const loaded = readConfig(sourcePath);
|
|
62
|
+
if (!loaded)
|
|
63
|
+
continue;
|
|
64
|
+
config = mergeConfigs(config, expandImports(loaded, cwd));
|
|
65
|
+
}
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
function getConfigPaths(cwd, directory) {
|
|
69
|
+
const piGlobalPath = path.join(getAgentDir(), directory, "mcp.json");
|
|
70
|
+
const projectPath = path.resolve(cwd, ".mcp.json");
|
|
71
|
+
const projectPiPath = path.resolve(cwd, ".pi", directory, "mcp.json");
|
|
72
|
+
const sources = [];
|
|
73
|
+
if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath)
|
|
74
|
+
sources.push(GENERIC_GLOBAL_CONFIG_PATH);
|
|
75
|
+
sources.push(piGlobalPath);
|
|
76
|
+
if (projectPath !== piGlobalPath)
|
|
77
|
+
sources.push(projectPath);
|
|
78
|
+
if (projectPiPath !== piGlobalPath && projectPiPath !== projectPath)
|
|
79
|
+
sources.push(projectPiPath);
|
|
80
|
+
return sources;
|
|
81
|
+
}
|
|
82
|
+
function readConfig(configPath) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return validateConfig(parsed);
|
|
91
|
+
}
|
|
92
|
+
function validateConfig(raw) {
|
|
93
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
94
|
+
return { mcpServers: {} };
|
|
95
|
+
const obj = raw;
|
|
96
|
+
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
97
|
+
return {
|
|
98
|
+
mcpServers: servers && typeof servers === "object" && !Array.isArray(servers) ? servers : {},
|
|
99
|
+
imports: Array.isArray(obj.imports) ? obj.imports.filter((value) => isImportKind(value)) : undefined,
|
|
100
|
+
settings: obj.settings && typeof obj.settings === "object" && !Array.isArray(obj.settings)
|
|
101
|
+
? obj.settings
|
|
102
|
+
: undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function mergeConfigs(base, next) {
|
|
106
|
+
const imports = [...(base.imports ?? []), ...(next.imports ?? [])];
|
|
107
|
+
return {
|
|
108
|
+
mcpServers: { ...base.mcpServers, ...next.mcpServers },
|
|
109
|
+
imports: imports.length ? [...new Set(imports)] : undefined,
|
|
110
|
+
settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function expandImports(config, cwd) {
|
|
114
|
+
if (!config.imports?.length)
|
|
115
|
+
return config;
|
|
116
|
+
const importedServers = {};
|
|
117
|
+
for (const importKind of config.imports) {
|
|
118
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
119
|
+
if (!importPath)
|
|
120
|
+
continue;
|
|
121
|
+
let imported;
|
|
122
|
+
try {
|
|
123
|
+
imported = JSON.parse(fs.readFileSync(importPath, "utf-8"));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const [name, definition] of Object.entries(extractServers(imported, importKind))) {
|
|
129
|
+
if (!importedServers[name])
|
|
130
|
+
importedServers[name] = definition;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
imports: config.imports,
|
|
135
|
+
settings: config.settings,
|
|
136
|
+
mcpServers: { ...importedServers, ...config.mcpServers },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function resolveImportPath(importKind, cwd) {
|
|
140
|
+
for (const candidate of IMPORT_PATHS[importKind]) {
|
|
141
|
+
const fullPath = candidate.startsWith(".") ? path.resolve(cwd, candidate) : candidate;
|
|
142
|
+
if (fs.existsSync(fullPath))
|
|
143
|
+
return fullPath;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function extractServers(config, kind) {
|
|
148
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
149
|
+
return {};
|
|
150
|
+
const obj = config;
|
|
151
|
+
const servers = kind === "cursor" || kind === "windsurf" || kind === "vscode"
|
|
152
|
+
? obj.mcpServers ?? obj["mcp-servers"]
|
|
153
|
+
: obj.mcpServers;
|
|
154
|
+
return servers && typeof servers === "object" && !Array.isArray(servers) ? servers : {};
|
|
155
|
+
}
|
|
156
|
+
function resolveDirectToolNames(config, cache, defaultPrefix, envOverride) {
|
|
157
|
+
const names = [];
|
|
158
|
+
const seenNames = new Set();
|
|
159
|
+
const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
|
|
160
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
161
|
+
if (definition.disabled)
|
|
162
|
+
continue;
|
|
163
|
+
const serverCache = cache.servers[serverName];
|
|
164
|
+
if (!isServerCacheValid(serverCache, definition))
|
|
165
|
+
continue;
|
|
166
|
+
const prefix = definition.toolPrefix ?? defaultPrefix;
|
|
167
|
+
const toolFilter = selectedServers.has(serverName)
|
|
168
|
+
? true
|
|
169
|
+
: selectedTools.get(serverName);
|
|
170
|
+
if (!toolFilter)
|
|
171
|
+
continue;
|
|
172
|
+
for (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {
|
|
173
|
+
if (typeof tool?.name !== "string" || !tool.name)
|
|
174
|
+
continue;
|
|
175
|
+
if (toolFilter !== true && !toolFilter.has(tool.name))
|
|
176
|
+
continue;
|
|
177
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools))
|
|
178
|
+
continue;
|
|
179
|
+
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
180
|
+
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName))
|
|
181
|
+
continue;
|
|
182
|
+
seenNames.add(prefixedName);
|
|
183
|
+
names.push(prefixedName);
|
|
184
|
+
}
|
|
185
|
+
if (definition.exposeResources === false)
|
|
186
|
+
continue;
|
|
187
|
+
for (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {
|
|
188
|
+
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri)
|
|
189
|
+
continue;
|
|
190
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
191
|
+
if (toolFilter !== true && !toolFilter.has(baseName))
|
|
192
|
+
continue;
|
|
193
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools))
|
|
194
|
+
continue;
|
|
195
|
+
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
196
|
+
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName))
|
|
197
|
+
continue;
|
|
198
|
+
seenNames.add(prefixedName);
|
|
199
|
+
names.push(prefixedName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return names;
|
|
203
|
+
}
|
|
204
|
+
function parseSelections(selections) {
|
|
205
|
+
const servers = new Set();
|
|
206
|
+
const tools = new Map();
|
|
207
|
+
for (let item of selections) {
|
|
208
|
+
item = item.replace(/\/+$/, "");
|
|
209
|
+
if (item.includes("/")) {
|
|
210
|
+
const [server, tool] = item.split("/", 2);
|
|
211
|
+
if (server && tool) {
|
|
212
|
+
if (!tools.has(server))
|
|
213
|
+
tools.set(server, new Set());
|
|
214
|
+
tools.get(server).add(tool);
|
|
215
|
+
}
|
|
216
|
+
else if (server) {
|
|
217
|
+
servers.add(server);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (item) {
|
|
221
|
+
servers.add(item);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { servers, tools };
|
|
225
|
+
}
|
|
226
|
+
function isServerCacheValid(entry, definition) {
|
|
227
|
+
if (!entry || entry.configHash !== computeMcpServerHash(definition))
|
|
228
|
+
return false;
|
|
229
|
+
if (!entry.cachedAt || typeof entry.cachedAt !== "number")
|
|
230
|
+
return false;
|
|
231
|
+
return Date.now() - entry.cachedAt <= CACHE_MAX_AGE_MS;
|
|
232
|
+
}
|
|
233
|
+
function computeMcpServerHash(definition) {
|
|
234
|
+
const identity = {
|
|
235
|
+
command: definition.command,
|
|
236
|
+
args: definition.args,
|
|
237
|
+
socket: resolveConfigPath(definition.socket),
|
|
238
|
+
env: interpolateEnvRecord(definition.env),
|
|
239
|
+
cwd: resolveConfigPath(definition.cwd),
|
|
240
|
+
url: definition.url ? interpolateEnvVars(definition.url) : definition.url,
|
|
241
|
+
headers: interpolateEnvRecord(definition.headers),
|
|
242
|
+
auth: definition.auth,
|
|
243
|
+
bearerToken: resolveBearerToken(definition),
|
|
244
|
+
bearerTokenEnv: definition.bearerTokenEnv,
|
|
245
|
+
exposeResources: definition.exposeResources,
|
|
246
|
+
includeTools: definition.includeTools,
|
|
247
|
+
excludeTools: definition.excludeTools,
|
|
248
|
+
};
|
|
249
|
+
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
250
|
+
}
|
|
251
|
+
function getToolPrefix(value) {
|
|
252
|
+
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
253
|
+
}
|
|
254
|
+
function isImportKind(value) {
|
|
255
|
+
return typeof value === "string" && Object.hasOwn(IMPORT_PATHS, value);
|
|
256
|
+
}
|
|
257
|
+
function getServerPrefix(serverName, mode) {
|
|
258
|
+
if (mode === "none")
|
|
259
|
+
return "";
|
|
260
|
+
if (mode === "short") {
|
|
261
|
+
const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
|
|
262
|
+
return short || "mcp";
|
|
263
|
+
}
|
|
264
|
+
return serverName.replace(/-/g, "_");
|
|
265
|
+
}
|
|
266
|
+
function formatToolName(toolName, serverName, prefix) {
|
|
267
|
+
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
268
|
+
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
269
|
+
}
|
|
270
|
+
function isToolExcluded(toolName, serverName, prefix, excludeTools) {
|
|
271
|
+
if (!Array.isArray(excludeTools) || excludeTools.length === 0)
|
|
272
|
+
return false;
|
|
273
|
+
const candidates = new Set([
|
|
274
|
+
normalizeToolName(toolName),
|
|
275
|
+
normalizeToolName(formatToolName(toolName, serverName, prefix)),
|
|
276
|
+
normalizeToolName(formatToolName(toolName, serverName, "server")),
|
|
277
|
+
normalizeToolName(formatToolName(toolName, serverName, "short")),
|
|
278
|
+
]);
|
|
279
|
+
return excludeTools.some((excluded) => typeof excluded === "string" && candidates.has(normalizeToolName(excluded)));
|
|
280
|
+
}
|
|
281
|
+
function normalizeToolName(value) {
|
|
282
|
+
return value.replace(/-/g, "_");
|
|
283
|
+
}
|
|
284
|
+
function resourceNameToToolName(name) {
|
|
285
|
+
let result = name
|
|
286
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
287
|
+
.replace(/_+/g, "_")
|
|
288
|
+
.replace(/^_+/, "")
|
|
289
|
+
.replace(/_+$/, "")
|
|
290
|
+
.toLowerCase();
|
|
291
|
+
if (!result || /^\d/.test(result))
|
|
292
|
+
result = `resource${result ? `_${result}` : ""}`;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function interpolateEnvRecord(values) {
|
|
296
|
+
if (!values || typeof values !== "object" || Array.isArray(values))
|
|
297
|
+
return undefined;
|
|
298
|
+
const resolved = {};
|
|
299
|
+
for (const [key, value] of Object.entries(values)) {
|
|
300
|
+
if (typeof value === "string")
|
|
301
|
+
resolved[key] = interpolateEnvVars(value);
|
|
302
|
+
}
|
|
303
|
+
return resolved;
|
|
304
|
+
}
|
|
305
|
+
function interpolateEnvVars(value) {
|
|
306
|
+
return value
|
|
307
|
+
.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
|
|
308
|
+
.replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
|
|
309
|
+
}
|
|
310
|
+
function resolveConfigPath(value) {
|
|
311
|
+
if (typeof value !== "string")
|
|
312
|
+
return undefined;
|
|
313
|
+
const resolved = interpolateEnvVars(value);
|
|
314
|
+
if (resolved === "~")
|
|
315
|
+
return os.homedir();
|
|
316
|
+
if (resolved.startsWith("~/") || resolved.startsWith("~\\"))
|
|
317
|
+
return path.join(os.homedir(), resolved.slice(2));
|
|
318
|
+
return resolved;
|
|
319
|
+
}
|
|
320
|
+
function resolveBearerToken(definition) {
|
|
321
|
+
if (typeof definition.bearerToken === "string")
|
|
322
|
+
return interpolateEnvVars(definition.bearerToken);
|
|
323
|
+
return typeof definition.bearerTokenEnv === "string" ? process.env[definition.bearerTokenEnv] : undefined;
|
|
324
|
+
}
|
|
325
|
+
function stableStringify(value) {
|
|
326
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
327
|
+
const serialized = JSON.stringify(value);
|
|
328
|
+
return serialized === undefined ? "undefined" : serialized;
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(value))
|
|
331
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
332
|
+
const obj = value;
|
|
333
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
334
|
+
}
|