context-mode 1.0.161 → 1.0.163

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/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +142 -28
  7. package/bin/statusline.mjs +24 -4
  8. package/build/adapters/antigravity/index.d.ts +1 -1
  9. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  10. package/build/adapters/antigravity-cli/index.js +341 -0
  11. package/build/adapters/claude-code/hooks.d.ts +1 -0
  12. package/build/adapters/claude-code/hooks.js +3 -0
  13. package/build/adapters/claude-code/index.js +24 -5
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +5 -1
  16. package/build/adapters/codex/hooks.js +5 -1
  17. package/build/adapters/codex/index.d.ts +9 -1
  18. package/build/adapters/codex/index.js +87 -5
  19. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  20. package/build/adapters/copilot-cli/hooks.js +64 -0
  21. package/build/adapters/copilot-cli/index.d.ts +48 -0
  22. package/build/adapters/copilot-cli/index.js +341 -0
  23. package/build/adapters/detect.d.ts +1 -1
  24. package/build/adapters/detect.js +71 -3
  25. package/build/adapters/openclaw/mcp-tools.js +1 -1
  26. package/build/adapters/opencode/index.js +31 -17
  27. package/build/adapters/opencode/zod3tov4.js +27 -6
  28. package/build/adapters/pi/extension.d.ts +2 -12
  29. package/build/adapters/pi/extension.js +114 -96
  30. package/build/adapters/types.d.ts +5 -4
  31. package/build/adapters/types.js +4 -3
  32. package/build/cache-heal.d.ts +48 -0
  33. package/build/cache-heal.js +150 -0
  34. package/build/cli.js +37 -97
  35. package/build/executor.d.ts +25 -0
  36. package/build/executor.js +143 -22
  37. package/build/opencode-plugin.js +5 -2
  38. package/build/routing-block.d.ts +8 -0
  39. package/build/routing-block.js +86 -0
  40. package/build/runtime.d.ts +0 -36
  41. package/build/runtime.js +107 -27
  42. package/build/search/flood-guard.d.ts +57 -0
  43. package/build/search/flood-guard.js +80 -0
  44. package/build/security.d.ts +8 -3
  45. package/build/security.js +155 -29
  46. package/build/server.d.ts +14 -0
  47. package/build/server.js +368 -350
  48. package/build/session/analytics.d.ts +8 -8
  49. package/build/session/analytics.js +18 -13
  50. package/build/session/db.d.ts +1 -0
  51. package/build/session/db.js +37 -4
  52. package/build/session/extract.d.ts +46 -0
  53. package/build/session/extract.js +764 -13
  54. package/build/session/project-attribution.js +14 -0
  55. package/build/store.d.ts +1 -1
  56. package/build/store.js +139 -25
  57. package/build/tool-naming.d.ts +4 -0
  58. package/build/tool-naming.js +24 -0
  59. package/build/util/jsonc.d.ts +14 -0
  60. package/build/util/jsonc.js +104 -0
  61. package/cli.bundle.mjs +260 -254
  62. package/configs/antigravity/GEMINI.md +2 -2
  63. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  64. package/configs/antigravity-cli/hooks.json +37 -0
  65. package/configs/antigravity-cli/mcp_config.json +10 -0
  66. package/configs/antigravity-cli/plugin.json +14 -0
  67. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  68. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  69. package/configs/claude-code/CLAUDE.md +2 -2
  70. package/configs/codex/AGENTS.md +2 -2
  71. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  72. package/configs/copilot-cli/.mcp.json +12 -0
  73. package/configs/copilot-cli/README.md +47 -0
  74. package/configs/copilot-cli/hooks.json +41 -0
  75. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  76. package/configs/gemini-cli/GEMINI.md +2 -2
  77. package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
  78. package/configs/kilo/AGENTS.md +2 -2
  79. package/configs/kiro/KIRO.md +2 -2
  80. package/configs/omp/SYSTEM.md +2 -2
  81. package/configs/openclaw/AGENTS.md +2 -2
  82. package/configs/opencode/AGENTS.md +2 -2
  83. package/configs/qwen-code/QWEN.md +2 -2
  84. package/configs/vscode-copilot/copilot-instructions.md +2 -2
  85. package/configs/zed/AGENTS.md +2 -2
  86. package/hooks/antigravity-cli/payload.mjs +98 -0
  87. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  88. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  89. package/hooks/antigravity-cli/stop.mjs +58 -0
  90. package/hooks/codex/pretooluse.mjs +14 -4
  91. package/hooks/codex/stop.mjs +12 -4
  92. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  93. package/hooks/copilot-cli/precompact.mjs +66 -0
  94. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  95. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  96. package/hooks/copilot-cli/stop.mjs +59 -0
  97. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  98. package/hooks/core/codex-caps.mjs +112 -0
  99. package/hooks/core/formatters.mjs +158 -7
  100. package/hooks/core/mcp-ready.mjs +37 -8
  101. package/hooks/core/routing.mjs +94 -8
  102. package/hooks/core/tool-naming.mjs +3 -0
  103. package/hooks/hooks.json +12 -1
  104. package/hooks/pretooluse.mjs +6 -2
  105. package/hooks/routing-block.mjs +2 -2
  106. package/hooks/security.bundle.mjs +2 -1
  107. package/hooks/session-db.bundle.mjs +11 -7
  108. package/hooks/session-directive.mjs +88 -20
  109. package/hooks/session-extract.bundle.mjs +2 -2
  110. package/hooks/session-helpers.mjs +21 -0
  111. package/hooks/session-loaders.mjs +8 -5
  112. package/hooks/sessionstart.mjs +53 -7
  113. package/hooks/stop.mjs +49 -0
  114. package/hooks/userpromptsubmit.mjs +9 -2
  115. package/openclaw.plugin.json +1 -1
  116. package/package.json +4 -10
  117. package/scripts/install-antigravity-cli-plugin.mjs +141 -0
  118. package/server.bundle.mjs +214 -205
  119. package/skills/ctx-insight/SKILL.md +12 -17
  120. package/build/util/db-lock.d.ts +0 -65
  121. package/build/util/db-lock.js +0 -166
  122. package/insight/index.html +0 -13
  123. package/insight/package.json +0 -55
  124. package/insight/server.mjs +0 -1265
  125. package/insight/src/components/analytics.tsx +0 -112
  126. package/insight/src/components/ui/badge.tsx +0 -52
  127. package/insight/src/components/ui/button.tsx +0 -58
  128. package/insight/src/components/ui/card.tsx +0 -103
  129. package/insight/src/components/ui/chart.tsx +0 -371
  130. package/insight/src/components/ui/collapsible.tsx +0 -19
  131. package/insight/src/components/ui/input.tsx +0 -20
  132. package/insight/src/components/ui/progress.tsx +0 -83
  133. package/insight/src/components/ui/scroll-area.tsx +0 -55
  134. package/insight/src/components/ui/separator.tsx +0 -23
  135. package/insight/src/components/ui/table.tsx +0 -114
  136. package/insight/src/components/ui/tabs.tsx +0 -82
  137. package/insight/src/components/ui/tooltip.tsx +0 -64
  138. package/insight/src/lib/api.ts +0 -144
  139. package/insight/src/lib/utils.ts +0 -6
  140. package/insight/src/main.tsx +0 -22
  141. package/insight/src/routeTree.gen.ts +0 -189
  142. package/insight/src/router.tsx +0 -19
  143. package/insight/src/routes/__root.tsx +0 -55
  144. package/insight/src/routes/enterprise.tsx +0 -316
  145. package/insight/src/routes/index.tsx +0 -1482
  146. package/insight/src/routes/knowledge.tsx +0 -221
  147. package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
  148. package/insight/src/routes/search.tsx +0 -97
  149. package/insight/src/routes/sessions.tsx +0 -179
  150. package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
  151. package/insight/src/styles.css +0 -104
  152. package/insight/tsconfig.json +0 -29
  153. package/insight/vite.config.ts +0 -19
@@ -15,17 +15,11 @@
15
15
  * - Config: opencode.json plugin array, .opencode/plugins/*.ts
16
16
  * - Session dir: ~/.config/opencode/context-mode/sessions/
17
17
  */
18
- /** Strip JSONC comments (// and /* *​/) and trailing commas for JSON.parse. */
19
- function stripJsonComments(str) {
20
- return str
21
- .replace(/\/\/.*$/gm, "")
22
- .replace(/\/\*[\s\S]*?\*\//g, "")
23
- .replace(/,(\s*[}\]])/g, "$1");
24
- }
25
- import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, constants, } from "node:fs";
18
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, existsSync, constants, } from "node:fs";
26
19
  import { resolve, join } from "node:path";
27
20
  import { homedir } from "node:os";
28
21
  import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
22
+ import { stripJsonComments } from "../../util/jsonc.js";
29
23
  // ─────────────────────────────────────────────────────────
30
24
  // Hook constants (re-exported from hooks.ts)
31
25
  // ─────────────────────────────────────────────────────────
@@ -147,33 +141,53 @@ export class OpenCodeAdapter extends BaseAdapter {
147
141
  }
148
142
  // ── Configuration ──────────────────────────────────────
149
143
  getSettingsPath() {
150
- // OpenCode uses opencode.json in the project root or .opencode/opencode.json
151
- return this.settingsPath ?? resolve(`${this.platform}.json`);
144
+ if (this.settingsPath)
145
+ return this.settingsPath;
146
+ // Edge case (#849): writeSettings() called without a prior readSettings()
147
+ // (which is what populates this.settingsPath). Never create a `.json` that
148
+ // would shadow an existing `.jsonc` — OpenCode merges the project-root
149
+ // `.jsonc` LAST, so it is the authoritative config
150
+ // (refs/platforms/opencode/packages/opencode/src/config/config.ts:406-408
151
+ // + paths.ts:15-22). Prefer the existing `.jsonc` as the write target.
152
+ const jsoncPath = resolve(`${this.platform}.jsonc`);
153
+ if (existsSync(jsoncPath))
154
+ return jsoncPath;
155
+ return resolve(`${this.platform}.json`);
152
156
  }
153
157
  paths() {
158
+ // `.jsonc` is listed BEFORE `.json` so it is selected as the write target
159
+ // when both exist (#849). OpenCode loads the project-root `.json` then
160
+ // `.jsonc` and merges `.jsonc` last — scalars override-last, arrays concat
161
+ // (refs/platforms/opencode/packages/opencode/src/config/config.ts:406-408,
162
+ // 258-260 + paths.ts:15-22). The `.jsonc` is therefore the authoritative
163
+ // file holding the user's real config; a `.json` is often an empty/auto-
164
+ // generated placeholder. Writing into the placeholder would create a
165
+ // file that shadows the user's real `.jsonc`. Preferring `.jsonc` for the
166
+ // write target avoids that silent config destruction. Read order is
167
+ // irrelevant for the plugin early-return (hasContextModePlugin) path.
154
168
  if (this.platform === "kilo") {
155
169
  // Kilo runtime accepts `.kilo/`, `.kilocode/`, and `.opencode/` as
156
170
  // project config dirs (refs/platforms/kilo/packages/opencode/src/
157
171
  // kilocode/config/config.ts:50,408). Mirror that here so context-mode
158
172
  // discovers config regardless of which suffix the user adopted.
159
173
  return [
160
- resolve("kilo.json"),
161
174
  resolve("kilo.jsonc"),
162
- resolve(".kilo", "kilo.json"),
175
+ resolve("kilo.json"),
163
176
  resolve(".kilo", "kilo.jsonc"),
164
- resolve(".kilocode", "kilo.json"),
177
+ resolve(".kilo", "kilo.json"),
165
178
  resolve(".kilocode", "kilo.jsonc"),
166
- join(homedir(), ".config", "kilo", "kilo.json"),
179
+ resolve(".kilocode", "kilo.json"),
167
180
  join(homedir(), ".config", "kilo", "kilo.jsonc"),
181
+ join(homedir(), ".config", "kilo", "kilo.json"),
168
182
  ];
169
183
  }
170
184
  return [
171
- resolve("opencode.json"),
172
185
  resolve("opencode.jsonc"),
173
- resolve(".opencode", "opencode.json"),
186
+ resolve("opencode.json"),
174
187
  resolve(".opencode", "opencode.jsonc"),
175
- join(homedir(), ".config", "opencode", "opencode.json"),
188
+ resolve(".opencode", "opencode.json"),
176
189
  join(homedir(), ".config", "opencode", "opencode.jsonc"),
190
+ join(homedir(), ".config", "opencode", "opencode.json"),
177
191
  ];
178
192
  }
179
193
  getSessionDir() {
@@ -29,13 +29,13 @@ function zod3ToV4(v, depth = 0) {
29
29
  let result;
30
30
  switch (def.typeName) {
31
31
  case "ZodString":
32
- result = z.string();
32
+ result = def.coerce === true ? z.coerce.string() : z.string();
33
33
  break;
34
34
  case "ZodNumber":
35
- result = z.number();
35
+ result = def.coerce === true ? z.coerce.number() : z.number();
36
36
  break;
37
37
  case "ZodBoolean":
38
- result = z.boolean();
38
+ result = def.coerce === true ? z.coerce.boolean() : z.boolean();
39
39
  break;
40
40
  case "ZodAny":
41
41
  result = z.any();
@@ -96,10 +96,31 @@ function zod3ToV4(v, depth = 0) {
96
96
  result = z.union(opts.map(o => zod3ToV4(o, depth + 1)));
97
97
  break;
98
98
  }
99
- case "ZodEffects":
100
- // Host schema only. Original Zod 3 schema still parses in execute().
101
- result = zod3ToV4(def.schema, depth + 1);
99
+ case "ZodEffects": {
100
+ // Zod v3 has two patterns for coercion:
101
+ //
102
+ // 1. Native coercion (z.coerce.number()): typeName is ZodNumber
103
+ // with _def.coerce=true. Handled in the ZodNumber case above.
104
+ //
105
+ // 2. Custom preprocess (z.preprocess(fn, schema)): typeName is
106
+ // ZodEffects with effect.type="preprocess". The transform is
107
+ // plain JS and works identically in Zod v4's z.preprocess().
108
+ //
109
+ // We never map ZodEffects→native coerce here because native
110
+ // coerce is already a ZodType (not ZodEffects), and custom
111
+ // transforms can't be safely replaced (e.g., z.coerce.boolean()
112
+ // in v4 converts "false"→true via Boolean(), which is wrong).
113
+ const effect = def.effect;
114
+ if (effect?.type === "preprocess" && typeof effect?.transform === "function") {
115
+ const innerSchema = zod3ToV4(def.schema, depth + 1);
116
+ result = z.preprocess(effect.transform, innerSchema);
117
+ }
118
+ else {
119
+ // Refinement / transform effects — host schema only.
120
+ result = zod3ToV4(def.schema, depth + 1);
121
+ }
102
122
  break;
123
+ }
103
124
  default:
104
125
  // Never leak raw Zod 3 schemas back to a v4 host.
105
126
  result = z.unknown();
@@ -45,20 +45,10 @@ export declare function isSafeCurlWget(segment: string): boolean;
45
45
  * can `await` the wiring deterministically without relying on internal
46
46
  * timing or `setImmediate` polling.
47
47
  *
48
- * Reset to a fresh promise on every `piExtension(pi)` call so repeated
49
- * registrations in one test process don't see a stale resolution from
50
- * a prior load.
48
+ * Starts as an already-settled promise because the bridge is now bootstrapped
49
+ * lazily from `before_agent_start`, not during extension discovery.
51
50
  */
52
51
  export declare let _mcpBridgeReady: Promise<void>;
53
- /**
54
- * Returns true iff `argv` matches a Pi top-level short-circuit invocation
55
- * (help or version). Only argv[0] is inspected — Pi's runCli only checks
56
- * the first token, and subcommand-level `--help` (e.g. `pi stats --help`)
57
- * still spins up a real session, so we must NOT skip bootstrap there.
58
- *
59
- * Exported for unit tests.
60
- */
61
- export declare function isPiShortCircuitArgv(argv: readonly string[]): boolean;
62
52
  /**
63
53
  * Issue #545 — Pi workspace resolver.
64
54
  *
@@ -136,13 +136,16 @@ let _mcpBridge = null;
136
136
  * can `await` the wiring deterministically without relying on internal
137
137
  * timing or `setImmediate` polling.
138
138
  *
139
- * Reset to a fresh promise on every `piExtension(pi)` call so repeated
140
- * registrations in one test process don't see a stale resolution from
141
- * a prior load.
139
+ * Starts as an already-settled promise because the bridge is now bootstrapped
140
+ * lazily from `before_agent_start`, not during extension discovery.
142
141
  */
143
142
  export let _mcpBridgeReady = Promise.resolve();
144
143
  // Cached buildAutoInjection (500-token cap, prioritized).
145
144
  let _buildAutoInjection = undefined;
145
+ // Pending context to inject via the 'context' hook (avoiding systemPrompt mutation
146
+ // which breaks prefix prompt cache on DeepSeek/Anthropic/OpenAI).
147
+ // See: https://github.com/mksglu/context-mode/issues/598
148
+ let _pendingContext = "";
146
149
  async function getAutoInjection(pluginRoot) {
147
150
  if (_buildAutoInjection !== undefined)
148
151
  return _buildAutoInjection;
@@ -280,40 +283,52 @@ function handleCommandText(text, ctx) {
280
283
  }
281
284
  return { text };
282
285
  }
283
- // ── Pi short-circuit argv detection (#534) ───────────────
286
+ // ── Pi MCP bridge lazy bootstrap (#534, #809) ───────────
284
287
  //
285
- // Pi's runtime loads every extension during module discovery, BEFORE its
286
- // `runCli()` decides whether the invocation is a real session or a
287
- // short-lived help / version print. Without this guard, even `pi --help`
288
- // causes us to spawn `server.bundle.mjs` as a long-lived stdio child
289
- // which is then reparented to PID 1 the moment Pi's `--help` handler
290
- // returns. The MCP SDK's StdioServerTransport CPU-spins on the half-closed
291
- // pipe until the 30 s ppid poll catches up, accumulating multi-hour orphans
292
- // (see issue #534, plus the historical #311 / #388 fixes that only addressed
293
- // the *recovery* path — not the *prevention* path).
288
+ // Pi loads extensions in several CLI paths that never dispatch an agent turn:
289
+ // top-level help/version, package management (`install`, `list`, ...), config,
290
+ // metadata commands, and project-trust probing. Bootstrapping the MCP bridge
291
+ // during extension discovery is therefore the wrong signal: there may be no
292
+ // real agent lifecycle and no `session_shutdown` cleanup. That caused both the
293
+ // #534 short-lived help/version orphan class and the #809 package-command hang
294
+ // (the bridge child's stdio handles kept Pi alive after `install`/`list`).
294
295
  //
295
- // Token set verified against the Pi 14.x source — specifically:
296
- // refs/platforms/oh-my-pi/packages/coding-agent/src/cli.ts:runCli
297
- //
298
- // if (first === "--help" || first === "-h" || first === "--version"
299
- // || first === "-v" || first === "help") { /* short-circuit */ }
300
- //
301
- // We mirror it exactly — no inferred flags, no `-V` (Pi uses lowercase `-v`),
302
- // no `--no-help`. Anything else (including `pi stats --help`) routes through
303
- // the normal launch path and the bridge bootstraps as usual.
304
- const PI_SHORT_CIRCUIT_TOKENS = new Set(["--help", "-h", "--version", "-v", "help"]);
305
- /**
306
- * Returns true iff `argv` matches a Pi top-level short-circuit invocation
307
- * (help or version). Only argv[0] is inspected — Pi's runCli only checks
308
- * the first token, and subcommand-level `--help` (e.g. `pi stats --help`)
309
- * still spins up a real session, so we must NOT skip bootstrap there.
310
- *
311
- * Exported for unit tests.
312
- */
313
- export function isPiShortCircuitArgv(argv) {
314
- if (argv.length === 0)
315
- return false;
316
- return PI_SHORT_CIRCUIT_TOKENS.has(argv[0]);
296
+ // The robust signal is Pi's agent lifecycle itself. `before_agent_start` fires
297
+ // only for invocations that are about to dispatch a model call, including
298
+ // print-mode subagents (`pi --mode json -p --no-session`). We start and await
299
+ // the bridge from that hook so ctx_* tools are present before Pi snapshots the
300
+ // tool registry, while CLI-only commands never spawn a bridge at all.
301
+ function startPiMCPBridge(pi, serverBundle, shouldKeepHandle) {
302
+ if (existsSync(serverBundle)) {
303
+ _mcpBridgeReady = bootstrapMCPTools(pi, serverBundle).then((handle) => {
304
+ if (shouldKeepHandle()) {
305
+ _mcpBridge = handle;
306
+ }
307
+ else {
308
+ // Bootstrap completed after this extension registration had already
309
+ // shut down or superseded the attempt. Do not publish a stale handle;
310
+ // immediately reclaim the child that bootstrap just spawned.
311
+ try {
312
+ handle.shutdown();
313
+ }
314
+ catch {
315
+ // best effort — never throw from best-effort bridge cleanup
316
+ }
317
+ }
318
+ }, (err) => {
319
+ if (!shouldKeepHandle())
320
+ return;
321
+ const msg = err instanceof Error ? err.message : String(err);
322
+ process.stderr.write(`[context-mode] WARNING: failed to bridge MCP tools to Pi (${msg}). ` +
323
+ `ctx_* tools will not be callable from this session.\n`);
324
+ });
325
+ }
326
+ else {
327
+ // No bundle on disk → nothing to await. Tests can still rely on
328
+ // _mcpBridgeReady being a settled promise.
329
+ _mcpBridgeReady = Promise.resolve();
330
+ }
331
+ return _mcpBridgeReady;
317
332
  }
318
333
  /**
319
334
  * Issue #545 — Pi workspace resolver.
@@ -364,6 +379,16 @@ export function resolvePiWorkspaceDir(opts) {
364
379
  export default function piExtension(pi) {
365
380
  const buildDir = dirname(fileURLToPath(import.meta.url));
366
381
  const pluginRoot = resolve(buildDir, "..", "..", "..");
382
+ const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
383
+ let mcpBridgeStarted = false;
384
+ let mcpBridgeGeneration = 0;
385
+ const ensureMCPBridge = () => {
386
+ if (mcpBridgeStarted)
387
+ return _mcpBridgeReady;
388
+ mcpBridgeStarted = true;
389
+ const generation = ++mcpBridgeGeneration;
390
+ return startPiMCPBridge(pi, serverBundle, () => mcpBridgeStarted && mcpBridgeGeneration === generation);
391
+ };
367
392
  // Issue #545 — Pi workspace resolver. PI_CONFIG_DIR is Pi's CONFIG dir
368
393
  // (~/.pi), NOT the user's workspace; using it as the project anchor
369
394
  // collapsed every Pi session into a single phantom workspace. The
@@ -504,18 +529,16 @@ export default function piExtension(pi) {
504
529
  // ── 4. before_agent_start — Routing + active_memory + resume injection ─
505
530
  pi.on("before_agent_start", async (event) => {
506
531
  try {
507
- // Block first agent start until the MCP bridge bootstrap has
508
- // settled so the LLM call dispatched right after this handler
509
- // sees the ctx_* tools in Pi's registry. Each subagent starts
510
- // a fresh `pi --mode json -p --no-session` process whose only
511
- // window to register tools is the gap between piExtension(pi)
512
- // returning and the first before_agent_start firing that gap
513
- // is too small for the spawn initialize tools/list
514
- // pi.registerTool round-trip, so without this await the first
515
- // (and often only) prompt of a subagent goes out with an empty
516
- // ctx_* registry and the routing block becomes dead weight.
517
- // Resolves on bootstrap failure too — the bridge is best-effort.
518
- await _mcpBridgeReady;
532
+ _pendingContext = ""; // Reset will be filled below if events exist
533
+ // Lazily start and await the MCP bridge only when Pi is about to
534
+ // dispatch a real agent turn. This is the non-brittle #534/#809 guard:
535
+ // help/version/package/config CLI paths may load the extension, but they
536
+ // never fire before_agent_start, so they never spawn server.bundle.mjs.
537
+ // Subagents (`pi --mode json -p --no-session`) do fire this hook; awaiting
538
+ // here ensures ctx_* tools are registered before Pi snapshots the tool
539
+ // registry for the model call. Resolves on bootstrap failure too the
540
+ // bridge is best-effort.
541
+ await ensureMCPBridge();
519
542
  if (!_sessionId)
520
543
  return;
521
544
  const prompt = String(event?.prompt ?? "");
@@ -590,16 +613,45 @@ export default function piExtension(pi) {
590
613
  }
591
614
  if (behavioralDirective)
592
615
  parts.push(behavioralDirective);
593
- // Return modified systemPrompt only if we added something beyond existing.
616
+ // Store extra context (routing anchor, active_memory, resume, behavioralDirective)
617
+ // for injection via the 'context' hook as a message, NOT as a systemPrompt
618
+ // modification. Mutating systemPrompt breaks prefix prompt caching on
619
+ // DeepSeek/Anthropic/OpenAI because the system message sits at messages[0]
620
+ // and any change invalidates the entire cache chain.
594
621
  const baseLen = existingPrompt ? 1 : 0;
595
622
  if (parts.length > baseLen) {
596
- return { systemPrompt: parts.join("\n\n") };
623
+ const extraParts = parts.slice(baseLen);
624
+ _pendingContext = extraParts.join("\n\n");
625
+ }
626
+ else {
627
+ _pendingContext = "";
597
628
  }
598
629
  }
599
630
  catch {
631
+ _pendingContext = ""; // Reset — ensure no stale data escapes
600
632
  // best effort — never break agent start
601
633
  }
602
634
  });
635
+ // ── 4a2. context — Inject active_memory + resume + behavioralDirective as message ──
636
+ // Uses the 'context' hook (like hindsight does) to append context at the END of
637
+ // messages rather than mutating systemPrompt at the beginning. This preserves
638
+ // prefix prompt cache for DeepSeek, Anthropic, and OpenAI.
639
+ pi.on("context", (event) => {
640
+ try {
641
+ if (!_pendingContext)
642
+ return;
643
+ const ctx = _pendingContext;
644
+ _pendingContext = "";
645
+ event.messages.push({
646
+ role: "user",
647
+ content: ctx,
648
+ });
649
+ return { messages: event.messages };
650
+ }
651
+ catch {
652
+ // best effort — never break context assembly
653
+ }
654
+ });
603
655
  // ── 4b. before_provider_response — capture response metadata ───
604
656
  // Pi-2: Register the missing event so providers can record latency,
605
657
  // model, and token usage when Pi exposes them. Best-effort only;
@@ -676,12 +728,13 @@ export default function piExtension(pi) {
676
728
  catch {
677
729
  // best effort — never throw during shutdown
678
730
  }
679
- // Race fix (#472 round-3): if shutdown fires while bridge bootstrap
680
- // is still in flight, _mcpBridge is null at this point and the
681
- // freshly-spawned MCP child gets orphaned once bootstrap eventually
682
- // resolves. Await the bootstrap up to a 2s ceiling so we see the
683
- // real handle, then call shutdown() on it. The ceiling prevents a
684
- // hung bootstrap (e.g. broken bundle) from blocking session exit.
731
+ // Race fix (#472 round-3 + #809 lazy follow-up): if shutdown fires while
732
+ // bridge bootstrap is still in flight, _mcpBridge may be null at this
733
+ // point. Invalidate this bootstrap generation before waiting so any handle
734
+ // that resolves after the 2s ceiling self-shuts down instead of publishing
735
+ // a stale child handle after session shutdown.
736
+ mcpBridgeGeneration++;
737
+ mcpBridgeStarted = false;
685
738
  try {
686
739
  await Promise.race([
687
740
  _mcpBridgeReady,
@@ -701,6 +754,7 @@ export default function piExtension(pi) {
701
754
  }
702
755
  _mcpBridge = null;
703
756
  }
757
+ _mcpBridgeReady = Promise.resolve();
704
758
  });
705
759
  // ── 8. Slash commands ──────────────────────────────────
706
760
  pi.registerCommand("ctx-stats", {
@@ -747,45 +801,9 @@ export default function piExtension(pi) {
747
801
  });
748
802
  // ── 9. MCP tool bridge (#426) ───────────────────────────
749
803
  //
750
- // Pi 0.73.x has no native MCP support. Without bridging here, the
751
- // routing block tells the LLM to call ctx_execute / ctx_search / etc.
752
- // but those tools never appear in Pi's tool list and the LLM cannot
753
- // reach them context-mode becomes a pure cost (~2.5K tokens of
754
- // system-prompt overhead, 0 actual ctx_* calls).
755
- //
756
- // Spawn server.bundle.mjs as a long-lived MCP child and register
757
- // each of its tools via pi.registerTool() so they enter the Pi
758
- // tool list under their bare names — same names the routing block
759
- // emits for the Pi platform (per hooks/core/tool-naming.mjs).
760
- //
761
- // Best-effort: a missing bundle or a spawn failure must NOT prevent
762
- // the rest of the extension (session capture, hooks, slash commands)
763
- // from initializing. We log to stderr and continue.
764
- // Short-circuit guard (#534): skip the MCP bridge bootstrap for
765
- // `pi --help` / `pi --version` / `pi help` and similar. Pi prints and
766
- // exits within milliseconds, but the bridge child would otherwise live
767
- // long enough to be reparented to PID 1, half-close stdin, and pin a CPU
768
- // core via the MCP SDK's stdio loop. We use process.argv directly so the
769
- // guard works for any caller that boots Pi with a short-circuit token,
770
- // regardless of how the runtime wires its CLI parser.
771
- const piArgv = process.argv.slice(2);
772
- if (isPiShortCircuitArgv(piArgv)) {
773
- _mcpBridgeReady = Promise.resolve();
774
- return;
775
- }
776
- const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
777
- if (existsSync(serverBundle)) {
778
- _mcpBridgeReady = bootstrapMCPTools(pi, serverBundle).then((handle) => {
779
- _mcpBridge = handle;
780
- }, (err) => {
781
- const msg = err instanceof Error ? err.message : String(err);
782
- process.stderr.write(`[context-mode] WARNING: failed to bridge MCP tools to Pi (${msg}). ` +
783
- `ctx_* tools will not be callable from this session.\n`);
784
- });
785
- }
786
- else {
787
- // No bundle on disk → nothing to await. Tests can still rely on
788
- // _mcpBridgeReady being a settled promise.
789
- _mcpBridgeReady = Promise.resolve();
790
- }
804
+ // Intentionally no eager bootstrap here. `before_agent_start` is the first
805
+ // lifecycle signal that proves Pi is about to run a model call; starting the
806
+ // bridge there keeps ctx_* available for real agent turns while package/help
807
+ // commands that only load extensions never spawn a long-lived child (#809).
808
+ _mcpBridgeReady = Promise.resolve();
791
809
  }
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Defines the contract that each platform adapter must implement.
5
5
  * Three paradigms exist across supported platforms:
6
- * A) JSON stdin/stdout — Claude Code, Gemini CLI, VS Code Copilot, Copilot CLI, Cursor
7
- * B) TS Plugin Functions — OpenCode
8
- * C) MCP-only (no hooks)Codex CLI
6
+ * A) JSON stdin/stdout — Claude Code, Gemini/Qwen family CLIs, Copilot/Codex/Kimi,
7
+ * Cursor, Kiro, Antigravity CLI (`agy`)
8
+ * B) TS Plugin FunctionsOpenCode, KiloCode, OpenClaw
9
+ * C) MCP-only (no hooks) — Antigravity IDE, Zed, Pi/OMP MCP-only paths
9
10
  *
10
11
  * The MCP server layer is 100% portable and needs no adapter.
11
12
  * Only the hook layer requires platform-specific adapters.
@@ -347,7 +348,7 @@ export declare const JS_RUNTIMES: ReadonlySet<string>;
347
348
  export declare const IN_PROCESS_PLUGIN_PLATFORMS: ReadonlySet<string>;
348
349
  export declare function isInProcessPluginPlatform(p: string | undefined): boolean;
349
350
  /** Supported platform identifiers. */
350
- export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "kimi" | "zed" | "qwen-code" | "unknown";
351
+ export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "copilot-cli" | "cursor" | "antigravity" | "antigravity-cli" | "kiro" | "pi" | "omp" | "kimi" | "zed" | "qwen-code" | "unknown";
351
352
  /** Detection signal used to identify which platform is running. */
352
353
  export interface DetectionSignal {
353
354
  /** Platform identifier. */
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Defines the contract that each platform adapter must implement.
5
5
  * Three paradigms exist across supported platforms:
6
- * A) JSON stdin/stdout — Claude Code, Gemini CLI, VS Code Copilot, Copilot CLI, Cursor
7
- * B) TS Plugin Functions — OpenCode
8
- * C) MCP-only (no hooks)Codex CLI
6
+ * A) JSON stdin/stdout — Claude Code, Gemini/Qwen family CLIs, Copilot/Codex/Kimi,
7
+ * Cursor, Kiro, Antigravity CLI (`agy`)
8
+ * B) TS Plugin FunctionsOpenCode, KiloCode, OpenClaw
9
+ * C) MCP-only (no hooks) — Antigravity IDE, Zed, Pi/OMP MCP-only paths
9
10
  *
10
11
  * The MCP server layer is 100% portable and needs no adapter.
11
12
  * Only the hook layer requires platform-specific adapters.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Plugin cache self-heal — fixes broken CLAUDE_PLUGIN_ROOT references.
3
+ *
4
+ * Claude Code's plugin auto-update can leave installed_plugins.json pointing
5
+ * to a non-existent directory (anthropics/claude-code#46915). This module
6
+ * detects and repairs the mismatch by creating symlinks.
7
+ *
8
+ * 4-layer defense:
9
+ * 1. start.mjs startup — reverse heal (registry → symlink to us)
10
+ * 2. server.ts first tool call — mid-session heal
11
+ * 3. postinstall.mjs — backward symlink on new install
12
+ * 4. global hook auto-deploy — survives total plugin cache breakage
13
+ */
14
+ export interface HealResult {
15
+ healed: boolean;
16
+ action?: "symlink" | "global-hook" | "none";
17
+ from?: string;
18
+ to?: string;
19
+ }
20
+ /**
21
+ * Core heal: if installed_plugins.json points to a non-existent directory,
22
+ * create a symlink from that path to our actual directory.
23
+ *
24
+ * @param currentDir - The directory we're actually running from
25
+ * @param installedPluginsPath - Path to installed_plugins.json (injectable for testing)
26
+ */
27
+ export declare function healRegistryMismatch(currentDir: string, installedPluginsPath?: string): HealResult;
28
+ /**
29
+ * Deploy a global SessionStart hook that heals plugin cache mismatches.
30
+ * This hook lives outside the plugin directory, so it survives cache breakage.
31
+ *
32
+ * Written to ~/.claude/hooks/context-mode-cache-heal.sh
33
+ */
34
+ export declare function deployGlobalHealHook(): HealResult;
35
+ /**
36
+ * Backward symlink: during postinstall, if the registry points to a
37
+ * non-existent OLD path, create a symlink from old → new (our directory).
38
+ * Same as healRegistryMismatch but called from postinstall context.
39
+ */
40
+ export { healRegistryMismatch as healBackwardCompat };
41
+ /**
42
+ * Mid-session heal — call on first MCP tool invocation.
43
+ * Checks if registry path differs from our running directory.
44
+ * Creates symlink if needed. Runs only once per process.
45
+ */
46
+ export declare function healMidSession(currentDir: string): HealResult;
47
+ /** Reset mid-session flag (for testing only) */
48
+ export declare function _resetMidSession(): void;