context-mode 1.0.111 → 1.0.113
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/index.ts +3 -2
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +152 -34
- package/bin/statusline.mjs +144 -127
- package/build/adapters/base.d.ts +8 -5
- package/build/adapters/base.js +8 -18
- package/build/adapters/claude-code/index.d.ts +24 -3
- package/build/adapters/claude-code/index.js +44 -11
- package/build/adapters/codex/hooks.d.ts +10 -5
- package/build/adapters/codex/hooks.js +10 -5
- package/build/adapters/codex/index.d.ts +17 -5
- package/build/adapters/codex/index.js +337 -37
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/cursor/index.d.ts +6 -0
- package/build/adapters/cursor/index.js +83 -2
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +29 -6
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +75 -0
- package/build/adapters/omp/plugin.js +220 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +198 -0
- package/build/adapters/openclaw/plugin.d.ts +130 -0
- package/build/adapters/openclaw/plugin.js +629 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/plugin.d.ts +145 -0
- package/build/adapters/opencode/plugin.js +457 -0
- package/build/adapters/pi/extension.d.ts +26 -0
- package/build/adapters/pi/extension.js +552 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +113 -0
- package/build/adapters/pi/mcp-bridge.js +251 -0
- package/build/adapters/types.d.ts +11 -6
- package/build/cli.js +186 -170
- package/build/db-base.d.ts +15 -2
- package/build/db-base.js +50 -5
- package/build/executor.d.ts +2 -0
- package/build/executor.js +15 -2
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.js +64 -5
- package/build/search/auto-memory.js +6 -4
- package/build/security.js +30 -10
- package/build/server.d.ts +23 -1
- package/build/server.js +662 -182
- package/build/session/analytics.d.ts +404 -1
- package/build/session/analytics.js +1347 -42
- package/build/session/db.d.ts +114 -5
- package/build/session/db.js +275 -27
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +1 -0
- package/build/session/extract.js +79 -12
- package/build/session/purge.d.ts +111 -0
- package/build/session/purge.js +138 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +69 -6
- package/build/util/claude-config.d.ts +26 -0
- package/build/util/claude-config.js +91 -0
- package/build/util/hook-config.d.ts +4 -0
- package/build/util/hook-config.js +39 -0
- package/build/util/project-dir.d.ts +49 -0
- package/build/util/project-dir.js +67 -0
- package/cli.bundle.mjs +411 -208
- package/configs/antigravity/GEMINI.md +0 -3
- package/configs/claude-code/CLAUDE.md +1 -4
- package/configs/codex/AGENTS.md +1 -4
- package/configs/codex/config.toml +3 -0
- package/configs/codex/hooks.json +8 -0
- package/configs/cursor/context-mode.mdc +0 -3
- package/configs/gemini-cli/GEMINI.md +0 -3
- package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
- package/configs/kilo/AGENTS.md +0 -3
- package/configs/kiro/KIRO.md +0 -3
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +0 -3
- package/configs/opencode/AGENTS.md +0 -3
- package/configs/pi/AGENTS.md +0 -3
- package/configs/qwen-code/QWEN.md +1 -4
- package/configs/vscode-copilot/copilot-instructions.md +0 -3
- package/configs/zed/AGENTS.md +0 -3
- package/hooks/codex/posttooluse.mjs +9 -2
- package/hooks/codex/precompact.mjs +69 -0
- package/hooks/codex/sessionstart.mjs +13 -9
- package/hooks/codex/stop.mjs +1 -2
- package/hooks/codex/userpromptsubmit.mjs +1 -2
- package/hooks/core/routing.mjs +237 -18
- package/hooks/cursor/afteragentresponse.mjs +1 -1
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +1 -1
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +1 -1
- package/hooks/ensure-deps.mjs +12 -13
- package/hooks/gemini-cli/aftertool.mjs +1 -1
- package/hooks/gemini-cli/beforeagent.mjs +1 -1
- package/hooks/gemini-cli/precompress.mjs +3 -2
- package/hooks/gemini-cli/sessionstart.mjs +9 -9
- package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
- package/hooks/jetbrains-copilot/precompact.mjs +3 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
- package/hooks/kiro/agentspawn.mjs +5 -5
- package/hooks/kiro/posttooluse.mjs +2 -2
- package/hooks/kiro/userpromptsubmit.mjs +1 -1
- package/hooks/posttooluse.mjs +45 -0
- package/hooks/precompact.mjs +17 -0
- package/hooks/pretooluse.mjs +23 -0
- package/hooks/routing-block.mjs +0 -12
- package/hooks/run-hook.mjs +16 -3
- package/hooks/session-db.bundle.mjs +27 -18
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +101 -64
- package/hooks/sessionstart.mjs +51 -2
- package/hooks/vscode-copilot/posttooluse.mjs +1 -1
- package/hooks/vscode-copilot/precompact.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +14 -8
- package/server.bundle.mjs +349 -147
- package/start.mjs +16 -4
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/context-mode-ops/SKILL.md +0 -299
- package/skills/context-mode-ops/agent-teams.md +0 -198
- package/skills/context-mode-ops/communication.md +0 -224
- package/skills/context-mode-ops/marketing.md +0 -124
- package/skills/context-mode-ops/release.md +0 -214
- package/skills/context-mode-ops/review-pr.md +0 -269
- package/skills/context-mode-ops/tdd.md +0 -329
- package/skills/context-mode-ops/triage-issue.md +0 -266
- package/skills/context-mode-ops/validation.md +0 -307
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- package/skills/tdd/tests.md +0 -61
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/pi — Pi coding agent platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Implements HookAdapter for Pi's MCP-only paradigm at the adapter layer.
|
|
5
|
+
*
|
|
6
|
+
* Pi hook specifics:
|
|
7
|
+
* - NO JSON-stdio hooks. Pi exposes a JS-callback runtime API
|
|
8
|
+
* (`pi.on("session_start", fn)`, `pi.on("tool_call", fn)`, …) which is
|
|
9
|
+
* wired DIRECTLY by `src/adapters/pi/extension.ts`. The HookAdapter
|
|
10
|
+
* contract here intentionally reports `mcp-only` and all-false
|
|
11
|
+
* capabilities so harness paths that walk the JSON-stdio matrix do not
|
|
12
|
+
* try to register stdio hooks for Pi.
|
|
13
|
+
* - Config root: ~/.pi/
|
|
14
|
+
* - Settings: ~/.pi/settings.json (kept lightweight — Pi does not
|
|
15
|
+
* prescribe a canonical settings file, but several internal tools
|
|
16
|
+
* write one; using settings.json keeps parity with Claude Code).
|
|
17
|
+
* - Session dir: ~/.pi/context-mode/sessions/ (parallel to ~/.claude/,
|
|
18
|
+
* ~/.omp/) — this is the data-isolation contract from issue #473.
|
|
19
|
+
* - Instruction file: AGENTS.md (per configs/pi/AGENTS.md).
|
|
20
|
+
*
|
|
21
|
+
* Why a dedicated adapter is mandatory:
|
|
22
|
+
* Before this adapter existed, `getAdapter("pi")` fell through to the
|
|
23
|
+
* `default` arm of the switch in `src/adapters/detect.ts` and returned a
|
|
24
|
+
* ClaudeCodeAdapter. Pi sessions therefore wrote DBs and event logs into
|
|
25
|
+
* `~/.claude/context-mode/sessions/`, contaminating Claude Code state and
|
|
26
|
+
* silently leaking Pi user data into the wrong storage root (issue #473
|
|
27
|
+
* follow-up). The OMP adapter fixed the same class of bug for OMP; this
|
|
28
|
+
* adapter closes the gap for Pi.
|
|
29
|
+
*/
|
|
30
|
+
import { readFileSync, writeFileSync, mkdirSync, } from "node:fs";
|
|
31
|
+
import { resolve, dirname } from "node:path";
|
|
32
|
+
import { homedir } from "node:os";
|
|
33
|
+
import { BaseAdapter } from "../base.js";
|
|
34
|
+
// ─────────────────────────────────────────────────────────
|
|
35
|
+
// Adapter implementation
|
|
36
|
+
// ─────────────────────────────────────────────────────────
|
|
37
|
+
export class PiAdapter extends BaseAdapter {
|
|
38
|
+
constructor() {
|
|
39
|
+
super([".pi"]);
|
|
40
|
+
}
|
|
41
|
+
name = "Pi";
|
|
42
|
+
paradigm = "mcp-only";
|
|
43
|
+
capabilities = {
|
|
44
|
+
preToolUse: false,
|
|
45
|
+
postToolUse: false,
|
|
46
|
+
preCompact: false,
|
|
47
|
+
sessionStart: false,
|
|
48
|
+
canModifyArgs: false,
|
|
49
|
+
canModifyOutput: false,
|
|
50
|
+
canInjectSessionContext: false,
|
|
51
|
+
};
|
|
52
|
+
// ── Input parsing ──────────────────────────────────────
|
|
53
|
+
// Pi does not feed the adapter via JSON-stdio. These methods exist to
|
|
54
|
+
// satisfy the HookAdapter contract and throw if the harness mistakenly
|
|
55
|
+
// routes a JSON-stdio event through the adapter.
|
|
56
|
+
parsePreToolUseInput(_raw) {
|
|
57
|
+
throw new Error("Pi does not support JSON-stdio hooks (wired via extension.ts)");
|
|
58
|
+
}
|
|
59
|
+
parsePostToolUseInput(_raw) {
|
|
60
|
+
throw new Error("Pi does not support JSON-stdio hooks (wired via extension.ts)");
|
|
61
|
+
}
|
|
62
|
+
parsePreCompactInput(_raw) {
|
|
63
|
+
throw new Error("Pi does not support JSON-stdio hooks (wired via extension.ts)");
|
|
64
|
+
}
|
|
65
|
+
parseSessionStartInput(_raw) {
|
|
66
|
+
throw new Error("Pi does not support JSON-stdio hooks (wired via extension.ts)");
|
|
67
|
+
}
|
|
68
|
+
// ── Response formatting ────────────────────────────────
|
|
69
|
+
// No JSON-stdio path — return undefined to satisfy the contract.
|
|
70
|
+
formatPreToolUseResponse(_response) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
formatPostToolUseResponse(_response) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
formatPreCompactResponse(_response) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
formatSessionStartResponse(_response) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
// ── Configuration ──────────────────────────────────────
|
|
83
|
+
getSettingsPath() {
|
|
84
|
+
return resolve(homedir(), ".pi", "settings.json");
|
|
85
|
+
}
|
|
86
|
+
getInstructionFiles() {
|
|
87
|
+
return ["AGENTS.md"];
|
|
88
|
+
}
|
|
89
|
+
generateHookConfig(_pluginRoot) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
readSettings() {
|
|
93
|
+
try {
|
|
94
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
writeSettings(settings) {
|
|
102
|
+
const settingsPath = this.getSettingsPath();
|
|
103
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
104
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
105
|
+
}
|
|
106
|
+
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
107
|
+
validateHooks(_pluginRoot) {
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
check: "Hook support",
|
|
111
|
+
status: "pass",
|
|
112
|
+
message: "Pi hooks are wired via the context-mode Pi extension " +
|
|
113
|
+
"(~/.pi/extensions/context-mode/), not via JSON-stdio.",
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
checkPluginRegistration() {
|
|
118
|
+
// Pi registers extensions by directory presence; the version-sync
|
|
119
|
+
// script writes ~/.pi/extensions/context-mode/package.json. We treat
|
|
120
|
+
// that file as the registration signal.
|
|
121
|
+
const pkgPath = resolve(homedir(), ".pi", "extensions", "context-mode", "package.json");
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
124
|
+
if (pkg?.name === "context-mode") {
|
|
125
|
+
return {
|
|
126
|
+
check: "Pi extension registration",
|
|
127
|
+
status: "pass",
|
|
128
|
+
message: `context-mode extension installed at ${pkgPath}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
check: "Pi extension registration",
|
|
133
|
+
status: "warn",
|
|
134
|
+
message: `Unexpected package at ${pkgPath}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return {
|
|
139
|
+
check: "Pi extension registration",
|
|
140
|
+
status: "fail",
|
|
141
|
+
message: `context-mode not found at ${pkgPath}`,
|
|
142
|
+
fix: "Run: context-mode upgrade",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
getInstalledVersion() {
|
|
147
|
+
try {
|
|
148
|
+
const pkgPath = resolve(homedir(), ".pi", "extensions", "context-mode", "package.json");
|
|
149
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
150
|
+
return pkg.version ?? "unknown";
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return "not installed";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ── Upgrade ────────────────────────────────────────────
|
|
157
|
+
// Pi does NOT use settings.json hook entries. The extension is the
|
|
158
|
+
// integration point — there is nothing for the harness to register
|
|
159
|
+
// beyond copying the extension into ~/.pi/extensions/context-mode/.
|
|
160
|
+
configureAllHooks(_pluginRoot) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
setHookPermissions(_pluginRoot) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
updatePluginRegistry(_pluginRoot, _version) {
|
|
167
|
+
// Pi extension version is managed by scripts/version-sync.mjs writing
|
|
168
|
+
// to ~/.pi/extensions/context-mode/package.json. No-op here.
|
|
169
|
+
}
|
|
170
|
+
getRoutingInstructions() {
|
|
171
|
+
return "# context-mode\n\nUse context-mode MCP tools (ctx_execute, ctx_execute_file, ctx_batch_execute, ctx_fetch_and_index, ctx_search) instead of inline shell/HTTP calls for data-heavy operations.";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-stdio bridge for the Pi Coding Agent extension.
|
|
3
|
+
*
|
|
4
|
+
* Pi 0.73.x has no native MCP support — its README is explicit:
|
|
5
|
+
* > "No MCP. Build CLI tools with READMEs (see Skills), or build an
|
|
6
|
+
* > extension that adds MCP support."
|
|
7
|
+
*
|
|
8
|
+
* Without this bridge, the routing block tells the LLM to call
|
|
9
|
+
* `ctx_execute`, `ctx_search`, etc. — but those tools never enter Pi's
|
|
10
|
+
* tool list, so the LLM cannot reach them. context-mode then becomes a
|
|
11
|
+
* pure cost on Pi (~2.5K tokens of system-prompt overhead with 0
|
|
12
|
+
* actual ctx_* calls). Reported in mksglu/context-mode#426.
|
|
13
|
+
*
|
|
14
|
+
* The bridge spawns `server.bundle.mjs` as a long-lived child via stdio
|
|
15
|
+
* JSON-RPC, performs the MCP handshake, calls `tools/list` once, and
|
|
16
|
+
* registers each returned tool through `pi.registerTool({ … })`. Each
|
|
17
|
+
* tool's `execute()` forwards into the child via `tools/call` — same
|
|
18
|
+
* code path Claude Code, Gemini CLI, and the other adapters use, so
|
|
19
|
+
* Pi behavior matches the rest of the platform suite.
|
|
20
|
+
*
|
|
21
|
+
* No external dependencies — pure node:child_process + JSON line frames.
|
|
22
|
+
*/
|
|
23
|
+
export interface MCPTool {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
inputSchema?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export interface MCPCallResult {
|
|
29
|
+
content?: Array<{
|
|
30
|
+
type?: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
}>;
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Minimal stdio JSON-RPC client targeting the context-mode MCP server.
|
|
37
|
+
*
|
|
38
|
+
* Implementation notes:
|
|
39
|
+
* - One outstanding ID per request; results matched by `id` from the
|
|
40
|
+
* returned envelope. Notifications (no id) are sent fire-and-forget.
|
|
41
|
+
* - Buffer is split on `\n` because the MCP server writes one
|
|
42
|
+
* newline-delimited JSON message per `console.log` / `stdout.write`
|
|
43
|
+
* invocation — this is the standard MCP stdio transport framing.
|
|
44
|
+
* - On child exit / error, every in-flight request is rejected so
|
|
45
|
+
* callers do not hang forever.
|
|
46
|
+
*/
|
|
47
|
+
export declare class MCPStdioClient {
|
|
48
|
+
private readonly serverScript;
|
|
49
|
+
private readonly env;
|
|
50
|
+
private child;
|
|
51
|
+
private requestId;
|
|
52
|
+
private readonly pending;
|
|
53
|
+
private buffer;
|
|
54
|
+
private initialized;
|
|
55
|
+
private exited;
|
|
56
|
+
constructor(serverScript: string, env?: NodeJS.ProcessEnv);
|
|
57
|
+
/** Spawn the MCP child. Idempotent. */
|
|
58
|
+
start(): void;
|
|
59
|
+
private onExit;
|
|
60
|
+
private onData;
|
|
61
|
+
request<T = unknown>(method: string, params: unknown, timeoutMs?: number): Promise<T>;
|
|
62
|
+
notify(method: string, params: unknown): void;
|
|
63
|
+
initialize(): Promise<void>;
|
|
64
|
+
listTools(): Promise<MCPTool[]>;
|
|
65
|
+
callTool(name: string, args: unknown): Promise<MCPCallResult>;
|
|
66
|
+
shutdown(): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Subset of the Pi ExtensionAPI we touch. Typed structurally so we don't
|
|
70
|
+
* pull `@earendil-works/pi-coding-agent` as a build dependency — keeps
|
|
71
|
+
* the bundle size unchanged and matches the existing pi-extension.ts
|
|
72
|
+
* style (which also types `pi` as `any`).
|
|
73
|
+
*/
|
|
74
|
+
export interface PiToolRegistration {
|
|
75
|
+
name: string;
|
|
76
|
+
label: string;
|
|
77
|
+
description: string;
|
|
78
|
+
parameters: unknown;
|
|
79
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
|
|
80
|
+
content: Array<{
|
|
81
|
+
type: "text";
|
|
82
|
+
text: string;
|
|
83
|
+
}>;
|
|
84
|
+
details: Record<string, unknown>;
|
|
85
|
+
isError?: boolean;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
export interface PiLikeAPI {
|
|
89
|
+
registerTool: (tool: PiToolRegistration) => void;
|
|
90
|
+
}
|
|
91
|
+
/** Result of bootstrapping the bridge. */
|
|
92
|
+
export interface BridgeHandle {
|
|
93
|
+
/** Names of tools registered with Pi (for diagnostics / tests). */
|
|
94
|
+
tools: string[];
|
|
95
|
+
/** Idempotent shutdown — terminates the MCP child. */
|
|
96
|
+
shutdown: () => void;
|
|
97
|
+
/** Underlying client, exposed for tests / advanced callers. */
|
|
98
|
+
client: MCPStdioClient;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Spawn the MCP server and register each of its tools with Pi via
|
|
102
|
+
* `pi.registerTool()`. The same JSON Schema returned by `tools/list` is
|
|
103
|
+
* passed straight through as `parameters` — TypeBox emits JSON-Schema
|
|
104
|
+
* compatible objects, so any Pi runtime that validates JSON Schema
|
|
105
|
+
* accepts this shape (verified against pi 0.73.x).
|
|
106
|
+
*
|
|
107
|
+
* Errors during MCP `tools/call` are translated to a `throw` from the
|
|
108
|
+
* `execute()` callback — Pi's contract is "throw to mark the tool call
|
|
109
|
+
* failed", which lets the LLM see and adapt.
|
|
110
|
+
*/
|
|
111
|
+
export declare function bootstrapMCPTools(pi: PiLikeAPI, serverScript: string, options?: {
|
|
112
|
+
env?: NodeJS.ProcessEnv;
|
|
113
|
+
}): Promise<BridgeHandle>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-stdio bridge for the Pi Coding Agent extension.
|
|
3
|
+
*
|
|
4
|
+
* Pi 0.73.x has no native MCP support — its README is explicit:
|
|
5
|
+
* > "No MCP. Build CLI tools with READMEs (see Skills), or build an
|
|
6
|
+
* > extension that adds MCP support."
|
|
7
|
+
*
|
|
8
|
+
* Without this bridge, the routing block tells the LLM to call
|
|
9
|
+
* `ctx_execute`, `ctx_search`, etc. — but those tools never enter Pi's
|
|
10
|
+
* tool list, so the LLM cannot reach them. context-mode then becomes a
|
|
11
|
+
* pure cost on Pi (~2.5K tokens of system-prompt overhead with 0
|
|
12
|
+
* actual ctx_* calls). Reported in mksglu/context-mode#426.
|
|
13
|
+
*
|
|
14
|
+
* The bridge spawns `server.bundle.mjs` as a long-lived child via stdio
|
|
15
|
+
* JSON-RPC, performs the MCP handshake, calls `tools/list` once, and
|
|
16
|
+
* registers each returned tool through `pi.registerTool({ … })`. Each
|
|
17
|
+
* tool's `execute()` forwards into the child via `tools/call` — same
|
|
18
|
+
* code path Claude Code, Gemini CLI, and the other adapters use, so
|
|
19
|
+
* Pi behavior matches the rest of the platform suite.
|
|
20
|
+
*
|
|
21
|
+
* No external dependencies — pure node:child_process + JSON line frames.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
25
|
+
// Tools/call may run shell commands or fetch URLs — wider window than
|
|
26
|
+
// initialize/list, but still bounded so a hung server can't block Pi.
|
|
27
|
+
const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
28
|
+
/**
|
|
29
|
+
* Minimal stdio JSON-RPC client targeting the context-mode MCP server.
|
|
30
|
+
*
|
|
31
|
+
* Implementation notes:
|
|
32
|
+
* - One outstanding ID per request; results matched by `id` from the
|
|
33
|
+
* returned envelope. Notifications (no id) are sent fire-and-forget.
|
|
34
|
+
* - Buffer is split on `\n` because the MCP server writes one
|
|
35
|
+
* newline-delimited JSON message per `console.log` / `stdout.write`
|
|
36
|
+
* invocation — this is the standard MCP stdio transport framing.
|
|
37
|
+
* - On child exit / error, every in-flight request is rejected so
|
|
38
|
+
* callers do not hang forever.
|
|
39
|
+
*/
|
|
40
|
+
export class MCPStdioClient {
|
|
41
|
+
serverScript;
|
|
42
|
+
env;
|
|
43
|
+
child = null;
|
|
44
|
+
requestId = 0;
|
|
45
|
+
pending = new Map();
|
|
46
|
+
buffer = "";
|
|
47
|
+
initialized = false;
|
|
48
|
+
exited = false;
|
|
49
|
+
constructor(serverScript, env = process.env) {
|
|
50
|
+
this.serverScript = serverScript;
|
|
51
|
+
this.env = env;
|
|
52
|
+
}
|
|
53
|
+
/** Spawn the MCP child. Idempotent. */
|
|
54
|
+
start() {
|
|
55
|
+
if (this.child)
|
|
56
|
+
return;
|
|
57
|
+
this.exited = false;
|
|
58
|
+
this.child = spawn(process.execPath, [this.serverScript], {
|
|
59
|
+
// Pipe stderr (#472 round-3): swallowing it via "ignore" hides
|
|
60
|
+
// server crash diagnostics — the user only saw "ctx_* tools will
|
|
61
|
+
// not be callable" with no clue WHY. Forwarding to process.stderr
|
|
62
|
+
// with a [mcp-bridge] prefix lets ops grep across session noise.
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
env: this.env,
|
|
65
|
+
});
|
|
66
|
+
this.child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
67
|
+
this.child.stderr?.on("data", (chunk) => {
|
|
68
|
+
const text = chunk.toString("utf-8");
|
|
69
|
+
// Preserve original line breaks; prefix every non-empty line so
|
|
70
|
+
// multi-line traces stay grep-friendly.
|
|
71
|
+
const prefixed = text
|
|
72
|
+
.split(/\r?\n/)
|
|
73
|
+
.map((line, i, arr) => i === arr.length - 1 && line === "" ? "" : `[mcp-bridge] ${line}`)
|
|
74
|
+
.join("\n");
|
|
75
|
+
process.stderr.write(prefixed);
|
|
76
|
+
});
|
|
77
|
+
this.child.on("exit", () => this.onExit());
|
|
78
|
+
this.child.on("error", () => this.onExit());
|
|
79
|
+
}
|
|
80
|
+
onExit() {
|
|
81
|
+
if (this.exited)
|
|
82
|
+
return;
|
|
83
|
+
this.exited = true;
|
|
84
|
+
const err = new Error("MCP server exited");
|
|
85
|
+
for (const [, p] of this.pending)
|
|
86
|
+
p.reject(err);
|
|
87
|
+
this.pending.clear();
|
|
88
|
+
}
|
|
89
|
+
onData(chunk) {
|
|
90
|
+
this.buffer += chunk.toString("utf-8");
|
|
91
|
+
let idx;
|
|
92
|
+
while ((idx = this.buffer.indexOf("\n")) >= 0) {
|
|
93
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
94
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
95
|
+
if (!line)
|
|
96
|
+
continue;
|
|
97
|
+
let msg;
|
|
98
|
+
try {
|
|
99
|
+
msg = JSON.parse(line);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue; // skip non-JSON noise (e.g. stray log lines)
|
|
103
|
+
}
|
|
104
|
+
if (typeof msg.id !== "number" || !this.pending.has(msg.id))
|
|
105
|
+
continue;
|
|
106
|
+
const handler = this.pending.get(msg.id);
|
|
107
|
+
this.pending.delete(msg.id);
|
|
108
|
+
if (msg.error)
|
|
109
|
+
handler.reject(msg.error);
|
|
110
|
+
else
|
|
111
|
+
handler.resolve(msg.result);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
115
|
+
if (!this.child)
|
|
116
|
+
throw new Error("MCP client not started");
|
|
117
|
+
if (this.exited)
|
|
118
|
+
return Promise.reject(new Error("MCP server has exited"));
|
|
119
|
+
const id = ++this.requestId;
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
if (!this.pending.has(id))
|
|
123
|
+
return;
|
|
124
|
+
this.pending.delete(id);
|
|
125
|
+
reject(new Error(`MCP request timeout after ${timeoutMs}ms: ${method}`));
|
|
126
|
+
}, timeoutMs);
|
|
127
|
+
this.pending.set(id, {
|
|
128
|
+
resolve: (v) => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
resolve(v);
|
|
131
|
+
},
|
|
132
|
+
reject: (e) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
reject(e);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
138
|
+
this.child.stdin?.write(frame + "\n");
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
notify(method, params) {
|
|
142
|
+
if (!this.child)
|
|
143
|
+
return;
|
|
144
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
145
|
+
this.child.stdin?.write(frame + "\n");
|
|
146
|
+
}
|
|
147
|
+
async initialize() {
|
|
148
|
+
if (this.initialized)
|
|
149
|
+
return;
|
|
150
|
+
await this.request("initialize", {
|
|
151
|
+
protocolVersion: "2025-06-18",
|
|
152
|
+
capabilities: { tools: {} },
|
|
153
|
+
clientInfo: {
|
|
154
|
+
name: "pi-coding-agent-context-mode-bridge",
|
|
155
|
+
version: "1.0",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
this.notify("notifications/initialized", {});
|
|
159
|
+
this.initialized = true;
|
|
160
|
+
}
|
|
161
|
+
async listTools() {
|
|
162
|
+
const result = await this.request("tools/list", {});
|
|
163
|
+
return Array.isArray(result.tools) ? result.tools : [];
|
|
164
|
+
}
|
|
165
|
+
async callTool(name, args) {
|
|
166
|
+
return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
|
|
167
|
+
}
|
|
168
|
+
shutdown() {
|
|
169
|
+
if (!this.child)
|
|
170
|
+
return;
|
|
171
|
+
const child = this.child;
|
|
172
|
+
try {
|
|
173
|
+
child.kill("SIGTERM");
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// best effort
|
|
177
|
+
}
|
|
178
|
+
// SIGKILL fallback (#472 round-3): a child that ignores SIGTERM
|
|
179
|
+
// (e.g. installed handler that swallows the signal, or stuck in
|
|
180
|
+
// an uninterruptible syscall) becomes a zombie because we null
|
|
181
|
+
// the handle immediately. Schedule a hard kill bounded at 5s; the
|
|
182
|
+
// .unref() prevents this timer from keeping the parent alive after
|
|
183
|
+
// legitimate work is done.
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
try {
|
|
186
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
187
|
+
child.kill("SIGKILL");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// best effort
|
|
192
|
+
}
|
|
193
|
+
}, 5000).unref();
|
|
194
|
+
this.child = null;
|
|
195
|
+
this.initialized = false;
|
|
196
|
+
this.exited = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Spawn the MCP server and register each of its tools with Pi via
|
|
201
|
+
* `pi.registerTool()`. The same JSON Schema returned by `tools/list` is
|
|
202
|
+
* passed straight through as `parameters` — TypeBox emits JSON-Schema
|
|
203
|
+
* compatible objects, so any Pi runtime that validates JSON Schema
|
|
204
|
+
* accepts this shape (verified against pi 0.73.x).
|
|
205
|
+
*
|
|
206
|
+
* Errors during MCP `tools/call` are translated to a `throw` from the
|
|
207
|
+
* `execute()` callback — Pi's contract is "throw to mark the tool call
|
|
208
|
+
* failed", which lets the LLM see and adapt.
|
|
209
|
+
*/
|
|
210
|
+
export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
211
|
+
const client = new MCPStdioClient(serverScript, options.env);
|
|
212
|
+
client.start();
|
|
213
|
+
await client.initialize();
|
|
214
|
+
const tools = await client.listTools();
|
|
215
|
+
const registered = [];
|
|
216
|
+
for (const tool of tools) {
|
|
217
|
+
pi.registerTool({
|
|
218
|
+
name: tool.name,
|
|
219
|
+
label: tool.name,
|
|
220
|
+
description: tool.description ?? "",
|
|
221
|
+
// MCP tools/list returns JSON Schema; Pi validates against JSON
|
|
222
|
+
// Schema (TypeBox is just JSON Schema with extra Symbol metadata
|
|
223
|
+
// for type inference). Empty-object fallback keeps tools that
|
|
224
|
+
// declare no parameters callable.
|
|
225
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
226
|
+
async execute(_toolCallId, params) {
|
|
227
|
+
const result = await client.callTool(tool.name, params ?? {});
|
|
228
|
+
const text = (result.content ?? [])
|
|
229
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
230
|
+
.map((c) => c.text)
|
|
231
|
+
.join("\n");
|
|
232
|
+
if (result.isError) {
|
|
233
|
+
// Throw is the Pi contract for "tool failed". The text body
|
|
234
|
+
// becomes the error message visible to the LLM, so it sees
|
|
235
|
+
// the same diagnostic the MCP server emitted.
|
|
236
|
+
throw new Error(text || `${tool.name} returned an error`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text }],
|
|
240
|
+
details: {},
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
registered.push(tool.name);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
tools: registered,
|
|
248
|
+
shutdown: () => client.shutdown(),
|
|
249
|
+
client,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -154,12 +154,17 @@ export interface HookAdapter {
|
|
|
154
154
|
formatSessionStartResponse?(response: SessionStartResponse): unknown;
|
|
155
155
|
/** Path to the platform's settings file (e.g., ~/.claude/settings.json). */
|
|
156
156
|
getSettingsPath(): string;
|
|
157
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Directory where session data is stored.
|
|
159
|
+
*
|
|
160
|
+
* NOTE — C2 narrowing (2026-05): this is the ONLY storage-path concern an
|
|
161
|
+
* adapter exposes. Per-project DB paths are derived by callers via
|
|
162
|
+
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
|
163
|
+
* (see `src/session/db.ts`). Per-project events.md paths follow the same
|
|
164
|
+
* `<sessionDir>/<hash><suffix>-events.md` shape and are computed inline at
|
|
165
|
+
* the small number of call sites that need them (server.ts, hooks).
|
|
166
|
+
*/
|
|
158
167
|
getSessionDir(): string;
|
|
159
|
-
/** Compute per-project session DB path. */
|
|
160
|
-
getSessionDBPath(projectDir: string): string;
|
|
161
|
-
/** Compute per-project session events file path. */
|
|
162
|
-
getSessionEventsPath(projectDir: string): string;
|
|
163
168
|
/**
|
|
164
169
|
* Platform config directory.
|
|
165
170
|
*
|
|
@@ -239,7 +244,7 @@ export interface DiagnosticResult {
|
|
|
239
244
|
*/
|
|
240
245
|
export declare function buildNodeCommand(scriptPath: string): string;
|
|
241
246
|
/** Supported platform identifiers. */
|
|
242
|
-
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "zed" | "qwen-code" | "unknown";
|
|
247
|
+
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "zed" | "qwen-code" | "unknown";
|
|
243
248
|
/** Detection signal used to identify which platform is running. */
|
|
244
249
|
export interface DetectionSignal {
|
|
245
250
|
/** Platform identifier. */
|