peaks-loop 4.1.2 → 4.1.3
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/.claude-plugin/marketplace.json +136 -47
- package/CHANGELOG.md +56 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/contracts/readonly-argv-whitelist.json +211 -0
- package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
- package/dist/cli/commands/audit-artifact-runners.js +170 -0
- package/dist/cli/commands/audit-command-shared.d.ts +97 -0
- package/dist/cli/commands/audit-command-shared.js +60 -0
- package/dist/cli/commands/audit-commands.d.ts +7 -31
- package/dist/cli/commands/audit-commands.js +64 -473
- package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
- package/dist/cli/commands/audit-goal-runners.js +114 -0
- package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
- package/dist/cli/commands/audit-red-lines-runners.js +43 -0
- package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
- package/dist/cli/commands/audit-refusal-data.js +69 -0
- package/dist/cli/commands/audit-static-runners.d.ts +13 -0
- package/dist/cli/commands/audit-static-runners.js +139 -0
- package/dist/cli/commands/await-runners.d.ts +11 -0
- package/dist/cli/commands/await-runners.js +148 -0
- package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
- package/dist/cli/commands/code-auto-compact-command.js +124 -0
- package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
- package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
- package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
- package/dist/cli/commands/code-context-audit-command.js +68 -0
- package/dist/cli/commands/code-context-now-command.d.ts +3 -0
- package/dist/cli/commands/code-context-now-command.js +124 -0
- package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
- package/dist/cli/commands/code-context-now-envelope.js +136 -0
- package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
- package/dist/cli/commands/code-context-now-mapping.js +43 -0
- package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
- package/dist/cli/commands/code-emit-handoff-command.js +99 -0
- package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
- package/dist/cli/commands/code-gate-step-08-command.js +139 -0
- package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
- package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
- package/dist/cli/commands/code-runtime-commands.js +20 -672
- package/dist/cli/commands/code-runtime-session.d.ts +1 -0
- package/dist/cli/commands/code-runtime-session.js +15 -0
- package/dist/cli/commands/compact-command.d.ts +6 -2
- package/dist/cli/commands/compact-command.js +12 -559
- package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
- package/dist/cli/commands/compact-dry-run-command.js +87 -0
- package/dist/cli/commands/compact-force-command.d.ts +3 -0
- package/dist/cli/commands/compact-force-command.js +98 -0
- package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
- package/dist/cli/commands/compact-harness-window-command.js +174 -0
- package/dist/cli/commands/compact-history-command.d.ts +3 -0
- package/dist/cli/commands/compact-history-command.js +89 -0
- package/dist/cli/commands/compact-read-commands.d.ts +3 -0
- package/dist/cli/commands/compact-read-commands.js +123 -0
- package/dist/cli/commands/compact-settle-command.d.ts +3 -0
- package/dist/cli/commands/compact-settle-command.js +130 -0
- package/dist/cli/commands/compact-shared.d.ts +9 -0
- package/dist/cli/commands/compact-shared.js +26 -0
- package/dist/cli/commands/core/skill-command.d.ts +1 -44
- package/dist/cli/commands/core/skill-command.js +13 -541
- package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
- package/dist/cli/commands/core/skill-context-verdict.js +63 -0
- package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
- package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
- package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-commands.js +123 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
- package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-registry-commands.js +117 -0
- package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
- package/dist/cli/commands/core/skill-runbook-command.js +29 -0
- package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
- package/dist/cli/commands/dispatch-command-spec.js +62 -0
- package/dist/cli/commands/dispatch-commands.js +159 -775
- package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
- package/dist/cli/commands/dispatch-detached-branch.js +58 -0
- package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
- package/dist/cli/commands/dispatch-envelope.js +104 -0
- package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
- package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
- package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
- package/dist/cli/commands/dispatch-preflight.js +105 -0
- package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
- package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
- package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
- package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
- package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
- package/dist/cli/commands/dispatch-record-write.js +96 -0
- package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
- package/dist/cli/commands/dispatch-tool-call.js +128 -0
- package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
- package/dist/cli/commands/evolution-command-shared.js +107 -0
- package/dist/cli/commands/evolution-commands.d.ts +11 -1
- package/dist/cli/commands/evolution-commands.js +19 -350
- package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
- package/dist/cli/commands/evolution-evaluate-command.js +138 -0
- package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
- package/dist/cli/commands/evolution-propose-command.js +145 -0
- package/dist/cli/commands/evolution-status-command.d.ts +17 -0
- package/dist/cli/commands/evolution-status-command.js +41 -0
- package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
- package/dist/cli/commands/evolution-verdict-commands.js +100 -0
- package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
- package/dist/cli/commands/finalize-accumulator.js +55 -0
- package/dist/cli/commands/finalize-branches.d.ts +34 -0
- package/dist/cli/commands/finalize-branches.js +119 -0
- package/dist/cli/commands/finalize-runners.d.ts +3 -0
- package/dist/cli/commands/finalize-runners.js +92 -0
- package/dist/cli/commands/finalize-selection.d.ts +65 -0
- package/dist/cli/commands/finalize-selection.js +35 -0
- package/dist/cli/commands/gate-commands.js +26 -8
- package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
- package/dist/cli/commands/job-checkpoint-commands.js +157 -0
- package/dist/cli/commands/job-command-shared.d.ts +109 -0
- package/dist/cli/commands/job-command-shared.js +72 -0
- package/dist/cli/commands/job-commands.d.ts +4 -64
- package/dist/cli/commands/job-commands.js +42 -552
- package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
- package/dist/cli/commands/job-cost-check-command.js +41 -0
- package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
- package/dist/cli/commands/job-init-status-commands.js +149 -0
- package/dist/cli/commands/job-run-commands.d.ts +3 -0
- package/dist/cli/commands/job-run-commands.js +97 -0
- package/dist/cli/commands/job-state-root.d.ts +35 -0
- package/dist/cli/commands/job-state-root.js +76 -0
- package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
- package/dist/cli/commands/loop-eval-capture-index.js +44 -0
- package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
- package/dist/cli/commands/loop-eval-commands.js +12 -626
- package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-eval-command.js +168 -0
- package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
- package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
- package/dist/cli/commands/loop-eval-parents.js +20 -0
- package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
- package/dist/cli/commands/loop-eval-run-code.js +40 -0
- package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-run-command.js +113 -0
- package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
- package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
- package/dist/cli/commands/mcp-commands.d.ts +10 -0
- package/dist/cli/commands/mcp-commands.js +157 -0
- package/dist/cli/commands/request-command-options.d.ts +49 -0
- package/dist/cli/commands/request-command-options.js +1 -0
- package/dist/cli/commands/request-commands.d.ts +3 -9
- package/dist/cli/commands/request-commands.js +17 -522
- package/dist/cli/commands/request-init-command.d.ts +3 -0
- package/dist/cli/commands/request-init-command.js +100 -0
- package/dist/cli/commands/request-lint-command.d.ts +3 -0
- package/dist/cli/commands/request-lint-command.js +50 -0
- package/dist/cli/commands/request-list-command.d.ts +10 -0
- package/dist/cli/commands/request-list-command.js +53 -0
- package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
- package/dist/cli/commands/request-repair-status-command.js +61 -0
- package/dist/cli/commands/request-show-command.d.ts +3 -0
- package/dist/cli/commands/request-show-command.js +56 -0
- package/dist/cli/commands/request-transition-command.d.ts +3 -0
- package/dist/cli/commands/request-transition-command.js +122 -0
- package/dist/cli/commands/request-transition-failures.d.ts +8 -0
- package/dist/cli/commands/request-transition-failures.js +90 -0
- package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
- package/dist/cli/commands/request-transition-hooks.js +115 -0
- package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
- package/dist/cli/commands/request-transition-preflight.js +107 -0
- package/dist/cli/commands/scan-command-shared.d.ts +96 -0
- package/dist/cli/commands/scan-command-shared.js +83 -0
- package/dist/cli/commands/scan-commands.d.ts +3 -2
- package/dist/cli/commands/scan-commands.js +23 -463
- package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
- package/dist/cli/commands/scan-inventory-commands.js +96 -0
- package/dist/cli/commands/scan-project-commands.d.ts +3 -0
- package/dist/cli/commands/scan-project-commands.js +65 -0
- package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
- package/dist/cli/commands/scan-scope-commands.js +149 -0
- package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
- package/dist/cli/commands/scan-scope-helpers.js +54 -0
- package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
- package/dist/cli/commands/scan-structural-commands.js +113 -0
- package/dist/cli/commands/share-commands.d.ts +12 -68
- package/dist/cli/commands/share-commands.js +47 -556
- package/dist/cli/commands/share-error-hints.d.ts +14 -0
- package/dist/cli/commands/share-error-hints.js +38 -0
- package/dist/cli/commands/share-identity.d.ts +13 -0
- package/dist/cli/commands/share-identity.js +22 -0
- package/dist/cli/commands/share-record-paths.d.ts +37 -0
- package/dist/cli/commands/share-record-paths.js +68 -0
- package/dist/cli/commands/share-runners.d.ts +3 -0
- package/dist/cli/commands/share-runners.js +158 -0
- package/dist/cli/commands/shared-read-runners.d.ts +3 -0
- package/dist/cli/commands/shared-read-runners.js +51 -0
- package/dist/cli/commands/slice-check-command.d.ts +3 -0
- package/dist/cli/commands/slice-check-command.js +54 -0
- package/dist/cli/commands/slice-commands.d.ts +2 -22
- package/dist/cli/commands/slice-commands.js +18 -517
- package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
- package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
- package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
- package/dist/cli/commands/slice-decompose-command.js +123 -0
- package/dist/cli/commands/slice-list-commands.d.ts +3 -0
- package/dist/cli/commands/slice-list-commands.js +182 -0
- package/dist/cli/commands/slice-pick-command.d.ts +3 -0
- package/dist/cli/commands/slice-pick-command.js +60 -0
- package/dist/cli/commands/slice-plan-command.d.ts +24 -0
- package/dist/cli/commands/slice-plan-command.js +111 -0
- package/dist/cli/commands/workflow-commands.d.ts +10 -1
- package/dist/cli/commands/workflow-commands.js +6 -594
- package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-gate-commands.js +116 -0
- package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
- package/dist/cli/commands/workflow-plan-helpers.js +103 -0
- package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-planning-commands.js +56 -0
- package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
- package/dist/cli/commands/workflow-swarm-actions.js +109 -0
- package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-swarm-commands.js +138 -0
- package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
- package/dist/cli/commands/workflow-tech-actions.js +150 -0
- package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
- package/dist/cli/commands/workspace/init-actions.js +199 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
- package/dist/cli/commands/workspace/init-command.d.ts +8 -124
- package/dist/cli/commands/workspace/init-command.js +59 -623
- package/dist/cli/commands/workspace/init-context.d.ts +58 -0
- package/dist/cli/commands/workspace/init-context.js +121 -0
- package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
- package/dist/cli/commands/workspace/init-envelope.js +34 -0
- package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
- package/dist/cli/commands/workspace/init-failures.js +84 -0
- package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
- package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
- package/dist/cli/commands/workspace/init-options.d.ts +102 -0
- package/dist/cli/commands/workspace/init-options.js +1 -0
- package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
- package/dist/cli/commands/worktree-lease-commands.js +42 -512
- package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
- package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-git.js +48 -0
- package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-list-command.js +100 -0
- package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
- package/dist/cli/commands/worktree-lease-load.js +28 -0
- package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
- package/dist/cli/commands/worktree-lease-release-command.js +105 -0
- package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
- package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-session.js +29 -0
- package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
- package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-status-command.js +86 -0
- package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
- package/dist/cli/commands/worktree-lease-store.js +24 -0
- package/dist/cli/program.js +5 -0
- package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
- package/dist/services/distribution/mcp-install-runner.js +59 -0
- package/dist/services/distribution/mcp-install.d.ts +95 -0
- package/dist/services/distribution/mcp-install.js +179 -0
- package/dist/services/distribution/plugin-manifest.d.ts +97 -0
- package/dist/services/distribution/plugin-manifest.js +126 -0
- package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
- package/dist/services/hooks/mcp-surface-gate.js +87 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
- package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
- package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
- package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
- package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
- package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
- package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
- package/dist/services/ide/ide-mcp-install-types.js +104 -0
- package/dist/services/ide/ide-types.d.ts +45 -1
- package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
- package/dist/services/ide/mcp-tool-matcher.js +84 -0
- package/dist/services/lint/npx-resolver.js +7 -0
- package/dist/services/mcp/cli-executor.d.ts +36 -0
- package/dist/services/mcp/cli-executor.js +97 -0
- package/dist/services/mcp/json-rpc.d.ts +81 -0
- package/dist/services/mcp/json-rpc.js +116 -0
- package/dist/services/mcp/memory-tool.d.ts +3 -0
- package/dist/services/mcp/memory-tool.js +62 -0
- package/dist/services/mcp/server-main.d.ts +1 -0
- package/dist/services/mcp/server-main.js +66 -0
- package/dist/services/mcp/server.d.ts +22 -0
- package/dist/services/mcp/server.js +152 -0
- package/dist/services/mcp/surface.d.ts +62 -0
- package/dist/services/mcp/surface.js +198 -0
- package/dist/services/mcp/tool-core.d.ts +42 -0
- package/dist/services/mcp/tool-core.js +137 -0
- package/dist/services/mcp/tools.d.ts +13 -0
- package/dist/services/mcp/tools.js +58 -0
- package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
- package/dist/services/readonly-surface/argv-guard.js +164 -0
- package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
- package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
- package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
- package/dist/services/readonly-surface/whitelist-generator.js +212 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
- package/dist/services/skills/hooks-settings-service.js +10 -2
- package/package.json +17 -6
- package/scripts/install-skills-bundles.mjs +233 -0
- package/scripts/install-skills-fs.mjs +148 -0
- package/scripts/install-skills-ide.mjs +279 -0
- package/scripts/install-skills-output-style-default.mjs +196 -0
- package/scripts/install-skills-output-styles.mjs +121 -0
- package/scripts/install-skills-report.mjs +42 -0
- package/scripts/install-skills-user-config.mjs +198 -0
- package/scripts/install-skills.mjs +36 -1066
- package/scripts/watch-tree.mjs +140 -0
- package/scripts/watch.mjs +4 -126
- package/skills/peaks-status/SKILL.md +31 -13
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Ceiling on one CLI execution. Generous for a read, far short of "forever". */
|
|
2
|
+
export declare const DEFAULT_EXECUTION_TIMEOUT_MS = 15000;
|
|
3
|
+
/** How to start this tree's own CLI. */
|
|
4
|
+
export interface CliLaunch {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly argsPrefix: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the platform-neutral launch shape. Uses the shared daemon-supervisor
|
|
10
|
+
* resolvers rather than a second copy of them: the entry path and the
|
|
11
|
+
* TypeScript-loader flags have to agree with every other self-spawn in this
|
|
12
|
+
* tree, and the file that owns them says so in as many words.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveCliLaunch(): CliLaunch;
|
|
15
|
+
export interface CliExecution {
|
|
16
|
+
/** The concrete argv that was handed to the CLI, without the launch prefix. */
|
|
17
|
+
readonly argv: readonly string[];
|
|
18
|
+
/** Exit status, or `null` when the process died on a signal or was killed. */
|
|
19
|
+
readonly exitCode: number | null;
|
|
20
|
+
readonly stdout: string;
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
/** True when the deadline fired and the child was killed. */
|
|
23
|
+
readonly timedOut: boolean;
|
|
24
|
+
/** Set ONLY when the process could not be started at all. Never set on timeout. */
|
|
25
|
+
readonly launchError?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ExecuteOptions {
|
|
28
|
+
readonly launch?: CliLaunch;
|
|
29
|
+
readonly timeoutMs?: number;
|
|
30
|
+
readonly cwd?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run one argv. Always resolves: a failure is a described outcome, never a
|
|
34
|
+
* rejection, because the caller has to report it to the harness either way.
|
|
35
|
+
*/
|
|
36
|
+
export declare function executeCliArgv(argv: readonly string[], options?: ExecuteOptions): Promise<CliExecution>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/services/mcp/cli-executor.ts
|
|
2
|
+
//
|
|
3
|
+
// The one place the MCP server touches the outside world: it runs a concrete
|
|
4
|
+
// argv against the peaks CLI and hands back what happened.
|
|
5
|
+
//
|
|
6
|
+
// THREE RULES, EACH MEASURED INTO THE SHAPE BELOW.
|
|
7
|
+
//
|
|
8
|
+
// 1. NO SHELL, NO PLATFORM BRANCH (spec §8.2). The launch is
|
|
9
|
+
// `node <this tree's CLI entry>` — `process.execPath` plus the argument list
|
|
10
|
+
// that makes this Node interpret that entry. Naming the CLI as a command
|
|
11
|
+
// (`peaks`) is measurably wrong on a platform whose CLI is a batch shim:
|
|
12
|
+
// Node refuses to spawn `.cmd`/`.bat` without a shell, and with a shell the
|
|
13
|
+
// argv array stops being an argv array. `process.execPath` needs neither, so
|
|
14
|
+
// one shape serves every platform and no `if (platform)` exists.
|
|
15
|
+
//
|
|
16
|
+
// 2. THE TIMEOUT IS MANDATORY, NOT OPTIONAL. `peaks job status --watch` never
|
|
17
|
+
// returns. It is outside the read-only surface, but it proves the class: a
|
|
18
|
+
// server that waits forever on a hung child gives its caller no recovery
|
|
19
|
+
// path. A timeout therefore returns a NAMED failure, never an empty success
|
|
20
|
+
// (spec §7.2 + §9 rule 3).
|
|
21
|
+
//
|
|
22
|
+
// 3. THE CHILD GETS NO STDIN. Our stdin is the JSON-RPC transport; a child that
|
|
23
|
+
// inherited it would read our protocol stream and answer the harness itself.
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
25
|
+
import { readonlySpawnOptions } from '../readonly-surface/argv-guard.js';
|
|
26
|
+
import { cliEntryPath, interpreterArgs } from '../web/daemon-supervisor.js';
|
|
27
|
+
/** Ceiling on one CLI execution. Generous for a read, far short of "forever". */
|
|
28
|
+
export const DEFAULT_EXECUTION_TIMEOUT_MS = 15_000;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the platform-neutral launch shape. Uses the shared daemon-supervisor
|
|
31
|
+
* resolvers rather than a second copy of them: the entry path and the
|
|
32
|
+
* TypeScript-loader flags have to agree with every other self-spawn in this
|
|
33
|
+
* tree, and the file that owns them says so in as many words.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveCliLaunch() {
|
|
36
|
+
return { command: process.execPath, argsPrefix: interpreterArgs(cliEntryPath()) };
|
|
37
|
+
}
|
|
38
|
+
/** The spawn + drain half: one child, one settlement, whatever happens to it. */
|
|
39
|
+
function runChild(launch, argv, cwd, timeoutMs) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let stdout = '';
|
|
42
|
+
let stderr = '';
|
|
43
|
+
let timedOut = false;
|
|
44
|
+
let settled = false;
|
|
45
|
+
let launchError;
|
|
46
|
+
const child = spawn(launch.command, [...launch.argsPrefix, ...argv], {
|
|
47
|
+
...readonlySpawnOptions(),
|
|
48
|
+
cwd,
|
|
49
|
+
// stdin must stay ours; stdout/stderr are read as data.
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
// Windows contract: a spawned child must not allocate a console window.
|
|
52
|
+
windowsHide: true
|
|
53
|
+
});
|
|
54
|
+
const deadline = setTimeout(() => {
|
|
55
|
+
timedOut = true;
|
|
56
|
+
child.kill();
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
const settle = (exitCode) => {
|
|
59
|
+
if (settled)
|
|
60
|
+
return;
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(deadline);
|
|
63
|
+
const outcome = { argv, exitCode, stdout, stderr, timedOut };
|
|
64
|
+
resolve(launchError === undefined ? outcome : { ...outcome, launchError });
|
|
65
|
+
};
|
|
66
|
+
child.stdout.setEncoding('utf8');
|
|
67
|
+
child.stdout.on('data', (chunk) => {
|
|
68
|
+
stdout += chunk;
|
|
69
|
+
});
|
|
70
|
+
child.stderr.setEncoding('utf8');
|
|
71
|
+
child.stderr.on('data', (chunk) => {
|
|
72
|
+
stderr += chunk;
|
|
73
|
+
});
|
|
74
|
+
child.on('error', (error) => {
|
|
75
|
+
launchError = error.message;
|
|
76
|
+
});
|
|
77
|
+
// `close` (not `exit`) so both pipes are drained before we read them.
|
|
78
|
+
child.on('close', (code) => {
|
|
79
|
+
settle(code);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Run one argv. Always resolves: a failure is a described outcome, never a
|
|
85
|
+
* rejection, because the caller has to report it to the harness either way.
|
|
86
|
+
*/
|
|
87
|
+
export async function executeCliArgv(argv, options = {}) {
|
|
88
|
+
let launch;
|
|
89
|
+
try {
|
|
90
|
+
launch = options.launch ?? resolveCliLaunch();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
return { argv, exitCode: null, stdout: '', stderr: '', timedOut: false, launchError: message };
|
|
95
|
+
}
|
|
96
|
+
return runChild(launch, argv, options.cwd ?? process.cwd(), options.timeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS);
|
|
97
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** The only JSON-RPC version this server speaks. */
|
|
2
|
+
export declare const JSON_RPC_VERSION: "2.0";
|
|
3
|
+
/** A request/response correlation id. `null` is legal and means "unknown". */
|
|
4
|
+
export type JsonRpcId = string | number | null;
|
|
5
|
+
export interface JsonRpcErrorObject {
|
|
6
|
+
readonly code: number;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly data?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface JsonRpcRequest {
|
|
11
|
+
readonly jsonrpc: typeof JSON_RPC_VERSION;
|
|
12
|
+
readonly id: JsonRpcId;
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly params?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** A request WITHOUT an id. JSON-RPC forbids replying to one. */
|
|
17
|
+
export interface JsonRpcNotification {
|
|
18
|
+
readonly jsonrpc: typeof JSON_RPC_VERSION;
|
|
19
|
+
readonly method: string;
|
|
20
|
+
readonly params?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface JsonRpcSuccess {
|
|
23
|
+
readonly jsonrpc: typeof JSON_RPC_VERSION;
|
|
24
|
+
readonly id: JsonRpcId;
|
|
25
|
+
readonly result: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface JsonRpcFailure {
|
|
28
|
+
readonly jsonrpc: typeof JSON_RPC_VERSION;
|
|
29
|
+
readonly id: JsonRpcId;
|
|
30
|
+
readonly error: JsonRpcErrorObject;
|
|
31
|
+
}
|
|
32
|
+
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;
|
|
33
|
+
/** Standard JSON-RPC codes. The server adds no private codes of its own. */
|
|
34
|
+
export declare const JSON_RPC_PARSE_ERROR = -32700;
|
|
35
|
+
export declare const JSON_RPC_INVALID_REQUEST = -32600;
|
|
36
|
+
export declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
37
|
+
export declare const JSON_RPC_INVALID_PARAMS = -32602;
|
|
38
|
+
export declare const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
39
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
40
|
+
/** What one decoded line turned out to be. */
|
|
41
|
+
export type DecodedMessage = {
|
|
42
|
+
readonly kind: 'request';
|
|
43
|
+
readonly request: JsonRpcRequest;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'notification';
|
|
46
|
+
readonly notification: JsonRpcNotification;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'invalid';
|
|
49
|
+
readonly id: JsonRpcId;
|
|
50
|
+
readonly error: JsonRpcErrorObject;
|
|
51
|
+
};
|
|
52
|
+
export declare function makeError(code: number, message: string, data?: unknown): JsonRpcErrorObject;
|
|
53
|
+
export declare function successResponse(id: JsonRpcId, result: unknown): JsonRpcSuccess;
|
|
54
|
+
export declare function failureResponse(id: JsonRpcId, error: JsonRpcErrorObject): JsonRpcFailure;
|
|
55
|
+
/**
|
|
56
|
+
* Decode one frame.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` means "nothing to answer" — an empty line, which is legal padding
|
|
59
|
+
* and must not be reported as a parse error (an empty line is not a message the
|
|
60
|
+
* peer sent; it is whitespace between messages).
|
|
61
|
+
*/
|
|
62
|
+
export declare function decodeMessage(line: string): DecodedMessage | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Encode one frame for the wire. `JSON.stringify` escapes every newline inside
|
|
65
|
+
* a string, which is what keeps one message on one line.
|
|
66
|
+
*/
|
|
67
|
+
export declare function encodeMessage(message: JsonRpcResponse | JsonRpcNotification): string;
|
|
68
|
+
/**
|
|
69
|
+
* The buffer that turns an arbitrary chunking of bytes into whole lines.
|
|
70
|
+
*
|
|
71
|
+
* Needed because a pipe delivers no framing of its own: one `data` event may
|
|
72
|
+
* carry half a message or three of them, and a server that parsed each event as
|
|
73
|
+
* a message would work on a fast local pipe and fail on a loaded one.
|
|
74
|
+
*/
|
|
75
|
+
export declare class LineDecoder {
|
|
76
|
+
private buffer;
|
|
77
|
+
/** Feed a chunk; get back the complete lines it completed (no trailing newline). */
|
|
78
|
+
push(chunk: string): string[];
|
|
79
|
+
/** Whatever is left when the stream ends. A peer that never sent a newline still sent a message. */
|
|
80
|
+
flush(): string[];
|
|
81
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/services/mcp/json-rpc.ts
|
|
2
|
+
//
|
|
3
|
+
// JSON-RPC 2.0 message shapes plus the stdio framing MCP uses, implemented in
|
|
4
|
+
// this tree rather than pulled in as a dependency.
|
|
5
|
+
//
|
|
6
|
+
// WHY NOT THE SDK. peaks-loop ships 12 runtime dependencies to every user
|
|
7
|
+
// globally, and the protocol surface this server needs is four methods and five
|
|
8
|
+
// error codes — all frozen since the first MCP revision. A dependency would be
|
|
9
|
+
// the larger commitment. See the slice report for the exact surface list.
|
|
10
|
+
//
|
|
11
|
+
// THE FRAMING IS NEWLINE-DELIMITED JSON, not LSP's `Content-Length` headers.
|
|
12
|
+
// That is the MCP stdio transport's rule, and it is also why a message may not
|
|
13
|
+
// contain a raw newline: this module therefore serializes with `JSON.stringify`
|
|
14
|
+
// (which escapes newlines inside strings) and never pretty-prints.
|
|
15
|
+
/** The only JSON-RPC version this server speaks. */
|
|
16
|
+
export const JSON_RPC_VERSION = '2.0';
|
|
17
|
+
/** Standard JSON-RPC codes. The server adds no private codes of its own. */
|
|
18
|
+
export const JSON_RPC_PARSE_ERROR = -32700;
|
|
19
|
+
export const JSON_RPC_INVALID_REQUEST = -32600;
|
|
20
|
+
export const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
21
|
+
export const JSON_RPC_INVALID_PARAMS = -32602;
|
|
22
|
+
export const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
23
|
+
export function isPlainObject(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isJsonRpcId(value) {
|
|
27
|
+
return typeof value === 'string' || typeof value === 'number' || value === null;
|
|
28
|
+
}
|
|
29
|
+
export function makeError(code, message, data) {
|
|
30
|
+
return data === undefined ? { code, message } : { code, message, data };
|
|
31
|
+
}
|
|
32
|
+
export function successResponse(id, result) {
|
|
33
|
+
return { jsonrpc: JSON_RPC_VERSION, id, result };
|
|
34
|
+
}
|
|
35
|
+
export function failureResponse(id, error) {
|
|
36
|
+
return { jsonrpc: JSON_RPC_VERSION, id, error };
|
|
37
|
+
}
|
|
38
|
+
function invalid(id, code, message) {
|
|
39
|
+
return { kind: 'invalid', id, error: makeError(code, message) };
|
|
40
|
+
}
|
|
41
|
+
/** The message's id when it carries a legal one, else the protocol's `null`. */
|
|
42
|
+
function idOf(parsed) {
|
|
43
|
+
const id = parsed['id'];
|
|
44
|
+
return isJsonRpcId(id) ? id : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the message from a well-formed envelope. The presence of the `id` KEY is
|
|
48
|
+
* the only thing separating a request from a notification — `id: null` is a legal
|
|
49
|
+
* id, so this cannot be written as a null check.
|
|
50
|
+
*/
|
|
51
|
+
function buildMessage(parsed, method) {
|
|
52
|
+
const params = parsed['params'];
|
|
53
|
+
const head = { jsonrpc: JSON_RPC_VERSION, method, ...(params === undefined ? {} : { params }) };
|
|
54
|
+
return 'id' in parsed
|
|
55
|
+
? { kind: 'request', request: { ...head, id: idOf(parsed) } }
|
|
56
|
+
: { kind: 'notification', notification: head };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decode one frame.
|
|
60
|
+
*
|
|
61
|
+
* `undefined` means "nothing to answer" — an empty line, which is legal padding
|
|
62
|
+
* and must not be reported as a parse error (an empty line is not a message the
|
|
63
|
+
* peer sent; it is whitespace between messages).
|
|
64
|
+
*/
|
|
65
|
+
export function decodeMessage(line) {
|
|
66
|
+
if (line.trim().length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(line);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return invalid(null, JSON_RPC_PARSE_ERROR, 'Invalid JSON received on the transport.');
|
|
74
|
+
}
|
|
75
|
+
if (!isPlainObject(parsed) || parsed['jsonrpc'] !== JSON_RPC_VERSION) {
|
|
76
|
+
const id = isPlainObject(parsed) ? idOf(parsed) : null;
|
|
77
|
+
return invalid(id, JSON_RPC_INVALID_REQUEST, `Messages must declare jsonrpc "${JSON_RPC_VERSION}".`);
|
|
78
|
+
}
|
|
79
|
+
const method = parsed['method'];
|
|
80
|
+
if (typeof method !== 'string' || method.length === 0) {
|
|
81
|
+
return invalid(idOf(parsed), JSON_RPC_INVALID_REQUEST, 'Messages must carry a non-empty "method".');
|
|
82
|
+
}
|
|
83
|
+
return buildMessage(parsed, method);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Encode one frame for the wire. `JSON.stringify` escapes every newline inside
|
|
87
|
+
* a string, which is what keeps one message on one line.
|
|
88
|
+
*/
|
|
89
|
+
export function encodeMessage(message) {
|
|
90
|
+
return `${JSON.stringify(message)}\n`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The buffer that turns an arbitrary chunking of bytes into whole lines.
|
|
94
|
+
*
|
|
95
|
+
* Needed because a pipe delivers no framing of its own: one `data` event may
|
|
96
|
+
* carry half a message or three of them, and a server that parsed each event as
|
|
97
|
+
* a message would work on a fast local pipe and fail on a loaded one.
|
|
98
|
+
*/
|
|
99
|
+
export class LineDecoder {
|
|
100
|
+
buffer = '';
|
|
101
|
+
/** Feed a chunk; get back the complete lines it completed (no trailing newline). */
|
|
102
|
+
push(chunk) {
|
|
103
|
+
this.buffer += chunk;
|
|
104
|
+
const lines = this.buffer.split('\n');
|
|
105
|
+
// The last element is either an incomplete line or '' when the chunk ended
|
|
106
|
+
// on a newline. Either way it stays buffered for the next push.
|
|
107
|
+
this.buffer = lines.pop() ?? '';
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
/** Whatever is left when the stream ends. A peer that never sent a newline still sent a message. */
|
|
111
|
+
flush() {
|
|
112
|
+
const remainder = this.buffer.trim().length > 0 ? [this.buffer] : [];
|
|
113
|
+
this.buffer = '';
|
|
114
|
+
return remainder;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { McpToolDefinition } from './surface.js';
|
|
2
|
+
import { type ToolCallContext, type ToolCallResult } from './tool-core.js';
|
|
3
|
+
export declare function callMemorySearchTool(tool: McpToolDefinition, args: Readonly<Record<string, unknown>>, context: ToolCallContext): Promise<ToolCallResult>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/services/mcp/memory-tool.ts
|
|
2
|
+
//
|
|
3
|
+
// `peaks_memory_search`: one argv, a bounded reply (§7.1).
|
|
4
|
+
//
|
|
5
|
+
// It answers "is there anything, and what is it called" — never "what does it
|
|
6
|
+
// say". The raw CLI output is measured in megabytes, so the reply is bounded
|
|
7
|
+
// twice over: a hard ceiling on how many matches come back, and a truncated
|
|
8
|
+
// excerpt per match. Both are on §3.2's allowed list for a composite tool
|
|
9
|
+
// (field selection and truncation), and neither concludes anything: the count,
|
|
10
|
+
// the names and the first characters are all the CLI's.
|
|
11
|
+
import { buildArgv } from '../readonly-surface/argv-guard.js';
|
|
12
|
+
import { MEMORY_EXCERPT_CHARS, MEMORY_MAX_RETURNED, rejectedResult, resultOf, runArgv, valuesForEntry } from './tool-core.js';
|
|
13
|
+
/** Project one raw CLI match down to the fields §7.1 allows, with a bounded excerpt. */
|
|
14
|
+
function boundMatch(raw) {
|
|
15
|
+
const match = typeof raw === 'object' && raw !== null ? raw : {};
|
|
16
|
+
const description = typeof match['description'] === 'string' ? match['description'] : '';
|
|
17
|
+
return {
|
|
18
|
+
name: match['name'],
|
|
19
|
+
kind: match['kind'],
|
|
20
|
+
excerpt: description.slice(0, MEMORY_EXCERPT_CHARS)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function callMemorySearchTool(tool, args, context) {
|
|
24
|
+
const entry = tool.entries[0];
|
|
25
|
+
if (entry === undefined) {
|
|
26
|
+
return { content: [{ type: 'text', text: '{"reason":"no entry"}' }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
const values = valuesForEntry(entry, args, context);
|
|
29
|
+
if (values === undefined) {
|
|
30
|
+
return rejectedResult(entry, 'A search query is required.');
|
|
31
|
+
}
|
|
32
|
+
const built = buildArgv(entry, values);
|
|
33
|
+
if (!built.ok) {
|
|
34
|
+
return rejectedResult(entry, `Parameter ${built.param} was refused: ${built.message}`);
|
|
35
|
+
}
|
|
36
|
+
const outcome = await runArgv(entry.id, built.argv, context);
|
|
37
|
+
if (outcome.failure !== undefined)
|
|
38
|
+
return resultOf([outcome]);
|
|
39
|
+
const envelope = outcome.envelope;
|
|
40
|
+
const record = typeof envelope === 'object' && envelope !== null ? envelope : {};
|
|
41
|
+
const matches = record['matches'];
|
|
42
|
+
if (!Array.isArray(matches)) {
|
|
43
|
+
// No `matches` array is not "nothing matched" — it is a shape this tool does
|
|
44
|
+
// not understand, and reporting it as empty is the failure §9 rule 3 names.
|
|
45
|
+
const argv = outcome.execution.argv.join(' ');
|
|
46
|
+
return resultOf([
|
|
47
|
+
{ ...outcome, failure: `peaks memory search returned no matches array: peaks ${argv}` }
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: JSON.stringify({
|
|
55
|
+
query: record['query'],
|
|
56
|
+
total: record['total'],
|
|
57
|
+
matches: matches.slice(0, MEMORY_MAX_RETURNED).map(boundMatch)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/services/mcp/server-main.ts
|
|
2
|
+
//
|
|
3
|
+
// The process entry: a stdin pipe, a stdout pipe, and spec §6's L4 check before
|
|
4
|
+
// either is used.
|
|
5
|
+
//
|
|
6
|
+
// STDOUT CARRIES PROTOCOL AND NOTHING ELSE. Any diagnostic goes to stderr,
|
|
7
|
+
// because a harness reads stdout as the message stream and one stray line is a
|
|
8
|
+
// parse error that is indistinguishable from a broken server. That is also why
|
|
9
|
+
// the startup refusal below writes to stderr and exits non-zero rather than
|
|
10
|
+
// printing a JSON error: at that point there is no session to answer in.
|
|
11
|
+
//
|
|
12
|
+
// L4 REFUSES TO START. If the surface cannot be served as proven — a tool with
|
|
13
|
+
// no description, an argv the whitelist does not carry, a whitelisted argv
|
|
14
|
+
// reachable from no tool, or a tool with no handler — this process exits 1.
|
|
15
|
+
// A server that started anyway would be serving something nobody proved, and the
|
|
16
|
+
// failure would surface as data rather than as a refusal (spec §6, L4).
|
|
17
|
+
import { loadReadOnlyWhitelist } from '../readonly-surface/readonly-whitelist.js';
|
|
18
|
+
import { LineDecoder } from './json-rpc.js';
|
|
19
|
+
import { buildToolDefinitions, validateSurface } from './surface.js';
|
|
20
|
+
import { createMcpServer } from './server.js';
|
|
21
|
+
import { missingHandlers } from './tools.js';
|
|
22
|
+
function refuse(reason) {
|
|
23
|
+
process.stderr.write(`peaks mcp: ${reason}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
function start() {
|
|
27
|
+
let tools;
|
|
28
|
+
try {
|
|
29
|
+
const whitelist = loadReadOnlyWhitelist();
|
|
30
|
+
tools = buildToolDefinitions(whitelist);
|
|
31
|
+
validateSurface(whitelist, tools);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
refuse(`refusing to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
const unserved = missingHandlers(tools);
|
|
37
|
+
if (unserved.length > 0) {
|
|
38
|
+
refuse(`refusing to start: no handler for tool(s) ${unserved.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const server = createMcpServer({ tools });
|
|
41
|
+
const decoder = new LineDecoder();
|
|
42
|
+
const write = (line) => {
|
|
43
|
+
if (line !== undefined)
|
|
44
|
+
process.stdout.write(line);
|
|
45
|
+
};
|
|
46
|
+
const consume = (line) => {
|
|
47
|
+
void server.handleLine(line).then(write);
|
|
48
|
+
};
|
|
49
|
+
process.stdin.setEncoding('utf8');
|
|
50
|
+
process.stdin.on('data', (chunk) => {
|
|
51
|
+
for (const line of decoder.push(chunk))
|
|
52
|
+
consume(line);
|
|
53
|
+
});
|
|
54
|
+
process.stdin.on('end', () => {
|
|
55
|
+
for (const line of decoder.flush())
|
|
56
|
+
consume(line);
|
|
57
|
+
});
|
|
58
|
+
// A read error on our own transport is not recoverable and not ours to
|
|
59
|
+
// interpret: report it and stop, so the harness sees a dead server instead of
|
|
60
|
+
// a live one that answers nothing.
|
|
61
|
+
process.stdin.on('error', (error) => {
|
|
62
|
+
process.stderr.write(`peaks mcp: stdin error: ${error.message}\n`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
start();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { McpToolDefinition } from './surface.js';
|
|
2
|
+
import { type ToolCallContext } from './tool-core.js';
|
|
3
|
+
/** Used when the client names no protocol version. */
|
|
4
|
+
export declare const DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
5
|
+
/** The server's own name, as it appears in `initialize`. */
|
|
6
|
+
export declare const SERVER_NAME = "peaks";
|
|
7
|
+
export declare const METHOD_INITIALIZE = "initialize";
|
|
8
|
+
export declare const METHOD_INITIALIZED = "notifications/initialized";
|
|
9
|
+
export declare const METHOD_TOOLS_LIST = "tools/list";
|
|
10
|
+
export declare const METHOD_TOOLS_CALL = "tools/call";
|
|
11
|
+
export interface McpServerOptions {
|
|
12
|
+
readonly tools: readonly McpToolDefinition[];
|
|
13
|
+
readonly context?: ToolCallContext;
|
|
14
|
+
readonly serverVersion?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface McpServer {
|
|
17
|
+
/** Answer one line. A notification (or blank line) yields `undefined`. */
|
|
18
|
+
handleLine(line: string): Promise<string | undefined>;
|
|
19
|
+
/** The tools, for a caller that wants to assert what it built. */
|
|
20
|
+
readonly tools: readonly McpToolDefinition[];
|
|
21
|
+
}
|
|
22
|
+
export declare function createMcpServer(options: McpServerOptions): McpServer;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/services/mcp/server.ts
|
|
2
|
+
//
|
|
3
|
+
// The protocol core: one decoded line in, at most one encoded line out.
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS SERVER IS. A table lookup, an execution, and the CLI's own answer —
|
|
6
|
+
// nothing else (spec §3, component 3: "there is no fourth responsibility").
|
|
7
|
+
// It does not know what a gate is, does not cache, keeps no state between
|
|
8
|
+
// messages, and never rewrites a CLI result. `handleLine` is a pure function of
|
|
9
|
+
// (line, surface, context), which is also what makes it testable without a pipe.
|
|
10
|
+
//
|
|
11
|
+
// THE PROTOCOL SURFACE IS DELIBERATELY SMALL and is listed in full here so it can
|
|
12
|
+
// be checked line by line rather than inferred:
|
|
13
|
+
//
|
|
14
|
+
// initialize -> protocolVersion, capabilities.tools, serverInfo
|
|
15
|
+
// notifications/initialized -> accepted, no reply (it is a notification)
|
|
16
|
+
// tools/list -> tools[] {name, description, inputSchema}
|
|
17
|
+
// tools/call -> params {name, arguments} -> content[] | isError
|
|
18
|
+
// (anything else, request) -> -32601 METHOD_NOT_FOUND
|
|
19
|
+
// (anything else, notification) -> ignored; JSON-RPC forbids replying
|
|
20
|
+
// malformed JSON -> -32700 wrong envelope -> -32600
|
|
21
|
+
// bad tools/call params -> -32602 handler crash -> -32603
|
|
22
|
+
//
|
|
23
|
+
// NOT IMPLEMENTED, on purpose: `ping`, `resources/*`, `prompts/*`, `logging/*`,
|
|
24
|
+
// `completion/*`, `notifications/*` beyond `initialized`, progress tokens,
|
|
25
|
+
// cancellation, and resource subscriptions. Any of them arrives as a
|
|
26
|
+
// METHOD_NOT_FOUND rather than as silence.
|
|
27
|
+
//
|
|
28
|
+
// NO HANDSHAKE STATE. The server does not track whether `initialize` has been
|
|
29
|
+
// seen; every method is answerable on any line. MCP asks clients to initialize
|
|
30
|
+
// first, and this server simply does not depend on it — a state machine here
|
|
31
|
+
// would be state kept for no decision.
|
|
32
|
+
//
|
|
33
|
+
// PROTOCOL VERSION. The client's requested version is echoed when it names one,
|
|
34
|
+
// and a constant is used otherwise. Echoing is honest for this surface and not a
|
|
35
|
+
// claim of unlimited support: the four methods above have had identical request
|
|
36
|
+
// and response shapes across every published revision, so there is no version
|
|
37
|
+
// whose meaning this server would get wrong.
|
|
38
|
+
import { CLI_VERSION } from 'peaks-loop-shared/version';
|
|
39
|
+
import { decodeMessage, encodeMessage, failureResponse, isPlainObject, makeError, successResponse, JSON_RPC_INTERNAL_ERROR, JSON_RPC_INVALID_PARAMS, JSON_RPC_METHOD_NOT_FOUND } from './json-rpc.js';
|
|
40
|
+
import { handlerFor } from './tools.js';
|
|
41
|
+
/** Used when the client names no protocol version. */
|
|
42
|
+
export const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
|
|
43
|
+
/** The server's own name, as it appears in `initialize`. */
|
|
44
|
+
export const SERVER_NAME = 'peaks';
|
|
45
|
+
export const METHOD_INITIALIZE = 'initialize';
|
|
46
|
+
export const METHOD_INITIALIZED = 'notifications/initialized';
|
|
47
|
+
export const METHOD_TOOLS_LIST = 'tools/list';
|
|
48
|
+
export const METHOD_TOOLS_CALL = 'tools/call';
|
|
49
|
+
/** The tool shape that goes on the wire — behaviour without the whitelist entries. */
|
|
50
|
+
function wireTool(tool) {
|
|
51
|
+
return {
|
|
52
|
+
name: tool.name,
|
|
53
|
+
description: tool.description,
|
|
54
|
+
inputSchema: tool.inputSchema
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function requestedVersion(params) {
|
|
58
|
+
if (!isPlainObject(params))
|
|
59
|
+
return undefined;
|
|
60
|
+
const version = params['protocolVersion'];
|
|
61
|
+
return typeof version === 'string' && version.length > 0 ? version : undefined;
|
|
62
|
+
}
|
|
63
|
+
function callArguments(params) {
|
|
64
|
+
const args = params['arguments'];
|
|
65
|
+
if (args === undefined)
|
|
66
|
+
return {};
|
|
67
|
+
return isPlainObject(args) ? args : undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reject an argument the tool does not declare, BEFORE anything runs. The tool
|
|
71
|
+
* schemas set `additionalProperties: false`; enforcing it here is what makes
|
|
72
|
+
* that a promise rather than a hint, and it is the boundary at which an
|
|
73
|
+
* injection-shaped key ("--apply") stops being an argument.
|
|
74
|
+
*/
|
|
75
|
+
function unknownArgument(tool, args) {
|
|
76
|
+
const declared = new Set(Object.keys(tool.inputSchema.properties));
|
|
77
|
+
return Object.keys(args).find((name) => !declared.has(name));
|
|
78
|
+
}
|
|
79
|
+
function initializeResult(params, version) {
|
|
80
|
+
return {
|
|
81
|
+
protocolVersion: requestedVersion(params) ?? DEFAULT_PROTOCOL_VERSION,
|
|
82
|
+
capabilities: { tools: { listChanged: false } },
|
|
83
|
+
serverInfo: { name: SERVER_NAME, version }
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function fail(id, message) {
|
|
87
|
+
return encodeMessage(failureResponse(id, makeError(JSON_RPC_INVALID_PARAMS, message)));
|
|
88
|
+
}
|
|
89
|
+
async function toolsCallResult(tools, context, id, params) {
|
|
90
|
+
if (!isPlainObject(params)) {
|
|
91
|
+
return fail(id, 'tools/call requires an object of parameters.');
|
|
92
|
+
}
|
|
93
|
+
const name = params['name'];
|
|
94
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
95
|
+
return fail(id, 'tools/call requires a tool "name".');
|
|
96
|
+
}
|
|
97
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
98
|
+
const handler = tool === undefined ? undefined : handlerFor(tool.name);
|
|
99
|
+
if (tool === undefined || handler === undefined) {
|
|
100
|
+
return fail(id, `Unknown tool: ${name}`);
|
|
101
|
+
}
|
|
102
|
+
const args = callArguments(params);
|
|
103
|
+
if (args === undefined) {
|
|
104
|
+
return fail(id, 'tools/call "arguments" must be an object.');
|
|
105
|
+
}
|
|
106
|
+
const unexpected = unknownArgument(tool, args);
|
|
107
|
+
if (unexpected !== undefined) {
|
|
108
|
+
return fail(id, `Tool ${name} declares no argument "${unexpected}".`);
|
|
109
|
+
}
|
|
110
|
+
return encodeMessage(successResponse(id, await handler(tool, args, context)));
|
|
111
|
+
}
|
|
112
|
+
export function createMcpServer(options) {
|
|
113
|
+
const tools = options.tools;
|
|
114
|
+
const context = options.context ?? {};
|
|
115
|
+
const version = options.serverVersion ?? CLI_VERSION;
|
|
116
|
+
const respond = async (id, method, params) => {
|
|
117
|
+
switch (method) {
|
|
118
|
+
case METHOD_INITIALIZE:
|
|
119
|
+
return encodeMessage(successResponse(id, initializeResult(params, version)));
|
|
120
|
+
case METHOD_TOOLS_LIST:
|
|
121
|
+
return encodeMessage(successResponse(id, { tools: tools.map(wireTool) }));
|
|
122
|
+
case METHOD_TOOLS_CALL:
|
|
123
|
+
return toolsCallResult(tools, context, id, params);
|
|
124
|
+
default:
|
|
125
|
+
return encodeMessage(failureResponse(id, makeError(JSON_RPC_METHOD_NOT_FOUND, `Unknown method: ${method}`)));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const handleLine = async (line) => {
|
|
129
|
+
const decoded = decodeMessage(line);
|
|
130
|
+
if (decoded === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
if (decoded.kind === 'invalid') {
|
|
133
|
+
return encodeMessage(failureResponse(decoded.id, decoded.error));
|
|
134
|
+
}
|
|
135
|
+
if (decoded.kind === 'notification') {
|
|
136
|
+
// Notifications are answered by not answering. `initialized` needs no
|
|
137
|
+
// branch of its own for that reason — but an unknown one is silently
|
|
138
|
+
// dropped, which is the protocol's rule, not this server's leniency.
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await respond(decoded.request.id, decoded.request.method, decoded.request.params);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// A handler crash is reported, never swallowed: the caller has to know the
|
|
146
|
+
// answer is missing rather than empty.
|
|
147
|
+
const message = error instanceof Error ? error.message : 'Internal error';
|
|
148
|
+
return encodeMessage(failureResponse(decoded.request.id, makeError(JSON_RPC_INTERNAL_ERROR, message)));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
return { tools, handleLine };
|
|
152
|
+
}
|