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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/runPool.d.ts +36 -0
  47. package/build/runPool.js +51 -0
  48. package/build/runtime.js +64 -5
  49. package/build/search/auto-memory.js +6 -4
  50. package/build/security.js +30 -10
  51. package/build/server.d.ts +23 -1
  52. package/build/server.js +662 -182
  53. package/build/session/analytics.d.ts +404 -1
  54. package/build/session/analytics.js +1347 -42
  55. package/build/session/db.d.ts +114 -5
  56. package/build/session/db.js +275 -27
  57. package/build/session/event-emit.d.ts +48 -0
  58. package/build/session/event-emit.js +101 -0
  59. package/build/session/extract.d.ts +1 -0
  60. package/build/session/extract.js +79 -12
  61. package/build/session/purge.d.ts +111 -0
  62. package/build/session/purge.js +138 -0
  63. package/build/store.d.ts +7 -0
  64. package/build/store.js +69 -6
  65. package/build/util/claude-config.d.ts +26 -0
  66. package/build/util/claude-config.js +91 -0
  67. package/build/util/hook-config.d.ts +4 -0
  68. package/build/util/hook-config.js +39 -0
  69. package/build/util/project-dir.d.ts +49 -0
  70. package/build/util/project-dir.js +67 -0
  71. package/cli.bundle.mjs +411 -208
  72. package/configs/antigravity/GEMINI.md +0 -3
  73. package/configs/claude-code/CLAUDE.md +1 -4
  74. package/configs/codex/AGENTS.md +1 -4
  75. package/configs/codex/config.toml +3 -0
  76. package/configs/codex/hooks.json +8 -0
  77. package/configs/cursor/context-mode.mdc +0 -3
  78. package/configs/gemini-cli/GEMINI.md +0 -3
  79. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  80. package/configs/kilo/AGENTS.md +0 -3
  81. package/configs/kiro/KIRO.md +0 -3
  82. package/configs/omp/SYSTEM.md +85 -0
  83. package/configs/omp/mcp.json +7 -0
  84. package/configs/openclaw/AGENTS.md +0 -3
  85. package/configs/opencode/AGENTS.md +0 -3
  86. package/configs/pi/AGENTS.md +0 -3
  87. package/configs/qwen-code/QWEN.md +1 -4
  88. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  89. package/configs/zed/AGENTS.md +0 -3
  90. package/hooks/codex/posttooluse.mjs +9 -2
  91. package/hooks/codex/precompact.mjs +69 -0
  92. package/hooks/codex/sessionstart.mjs +13 -9
  93. package/hooks/codex/stop.mjs +1 -2
  94. package/hooks/codex/userpromptsubmit.mjs +1 -2
  95. package/hooks/core/routing.mjs +237 -18
  96. package/hooks/cursor/afteragentresponse.mjs +1 -1
  97. package/hooks/cursor/hooks.json +31 -0
  98. package/hooks/cursor/posttooluse.mjs +1 -1
  99. package/hooks/cursor/sessionstart.mjs +5 -5
  100. package/hooks/cursor/stop.mjs +1 -1
  101. package/hooks/ensure-deps.mjs +12 -13
  102. package/hooks/gemini-cli/aftertool.mjs +1 -1
  103. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  104. package/hooks/gemini-cli/precompress.mjs +3 -2
  105. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  106. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  107. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  108. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  109. package/hooks/kiro/agentspawn.mjs +5 -5
  110. package/hooks/kiro/posttooluse.mjs +2 -2
  111. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  112. package/hooks/posttooluse.mjs +45 -0
  113. package/hooks/precompact.mjs +17 -0
  114. package/hooks/pretooluse.mjs +23 -0
  115. package/hooks/routing-block.mjs +0 -12
  116. package/hooks/run-hook.mjs +16 -3
  117. package/hooks/session-db.bundle.mjs +27 -18
  118. package/hooks/session-extract.bundle.mjs +2 -2
  119. package/hooks/session-helpers.mjs +101 -64
  120. package/hooks/sessionstart.mjs +51 -2
  121. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  122. package/hooks/vscode-copilot/precompact.mjs +3 -2
  123. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  124. package/openclaw.plugin.json +1 -1
  125. package/package.json +14 -8
  126. package/server.bundle.mjs +349 -147
  127. package/start.mjs +16 -4
  128. package/skills/UPSTREAM-CREDITS.md +0 -51
  129. package/skills/context-mode-ops/SKILL.md +0 -299
  130. package/skills/context-mode-ops/agent-teams.md +0 -198
  131. package/skills/context-mode-ops/communication.md +0 -224
  132. package/skills/context-mode-ops/marketing.md +0 -124
  133. package/skills/context-mode-ops/release.md +0 -214
  134. package/skills/context-mode-ops/review-pr.md +0 -269
  135. package/skills/context-mode-ops/tdd.md +0 -329
  136. package/skills/context-mode-ops/triage-issue.md +0 -266
  137. package/skills/context-mode-ops/validation.md +0 -307
  138. package/skills/diagnose/SKILL.md +0 -122
  139. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  140. package/skills/grill-me/SKILL.md +0 -15
  141. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  142. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  143. package/skills/grill-with-docs/SKILL.md +0 -93
  144. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  145. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  146. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  147. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  148. package/skills/tdd/SKILL.md +0 -114
  149. package/skills/tdd/deep-modules.md +0 -33
  150. package/skills/tdd/interface-design.md +0 -31
  151. package/skills/tdd/mocking.md +0 -59
  152. package/skills/tdd/refactoring.md +0 -10
  153. 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
- /** Directory where session data is stored. */
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. */