context-mode 1.0.162 → 1.0.164
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/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +149 -30
- package/bin/statusline.mjs +24 -4
- package/build/adapters/antigravity/index.d.ts +1 -1
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/claude-code/hooks.d.ts +1 -0
- package/build/adapters/claude-code/hooks.js +3 -0
- package/build/adapters/claude-code/index.js +24 -5
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +5 -1
- package/build/adapters/codex/hooks.js +5 -1
- package/build/adapters/codex/index.d.ts +9 -1
- package/build/adapters/codex/index.js +87 -5
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +71 -3
- package/build/adapters/openclaw/mcp-tools.js +1 -1
- package/build/adapters/opencode/index.js +31 -17
- package/build/adapters/opencode/zod3tov4.js +27 -6
- package/build/adapters/pi/extension.d.ts +2 -12
- package/build/adapters/pi/extension.js +128 -109
- package/build/adapters/types.d.ts +5 -4
- package/build/adapters/types.js +4 -3
- package/build/cache-heal.d.ts +48 -0
- package/build/cache-heal.js +150 -0
- package/build/cli.js +37 -97
- package/build/executor.d.ts +25 -0
- package/build/executor.js +143 -22
- package/build/lifecycle.d.ts +48 -0
- package/build/lifecycle.js +111 -0
- package/build/opencode-plugin.js +5 -2
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +0 -36
- package/build/runtime.js +107 -27
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/security.d.ts +73 -3
- package/build/security.js +293 -33
- package/build/server.d.ts +14 -0
- package/build/server.js +441 -354
- package/build/session/analytics.d.ts +1 -1
- package/build/session/analytics.js +5 -1
- package/build/session/db.js +23 -3
- package/build/session/extract.js +78 -0
- package/build/store.d.ts +1 -1
- package/build/store.js +139 -25
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/cli.bundle.mjs +253 -250
- package/configs/antigravity/GEMINI.md +2 -2
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +2 -2
- package/configs/codex/AGENTS.md +2 -2
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/gemini-cli/GEMINI.md +2 -2
- package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
- package/configs/kilo/AGENTS.md +2 -2
- package/configs/kiro/KIRO.md +2 -2
- package/configs/omp/SYSTEM.md +2 -2
- package/configs/openclaw/AGENTS.md +2 -2
- package/configs/opencode/AGENTS.md +2 -2
- package/configs/qwen-code/QWEN.md +2 -2
- package/configs/vscode-copilot/copilot-instructions.md +2 -2
- package/configs/zed/AGENTS.md +2 -2
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/codex/pretooluse.mjs +14 -4
- package/hooks/codex/stop.mjs +12 -4
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +158 -7
- package/hooks/core/mcp-ready.mjs +37 -8
- package/hooks/core/routing.mjs +94 -8
- package/hooks/core/tool-naming.mjs +3 -0
- package/hooks/hooks.json +12 -1
- package/hooks/pretooluse.mjs +6 -2
- package/hooks/routing-block.mjs +3 -4
- package/hooks/security.bundle.mjs +2 -1
- package/hooks/session-db.bundle.mjs +5 -5
- package/hooks/session-directive.mjs +88 -20
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +21 -0
- package/hooks/sessionstart.mjs +37 -5
- package/hooks/stop.mjs +49 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -10
- package/server.bundle.mjs +206 -200
- package/skills/ctx-insight/SKILL.md +12 -17
- package/build/util/db-lock.d.ts +0 -65
- package/build/util/db-lock.js +0 -166
- package/insight/index.html +0 -13
- package/insight/package.json +0 -55
- package/insight/server.mjs +0 -1265
- package/insight/src/components/analytics.tsx +0 -112
- package/insight/src/components/ui/badge.tsx +0 -52
- package/insight/src/components/ui/button.tsx +0 -58
- package/insight/src/components/ui/card.tsx +0 -103
- package/insight/src/components/ui/chart.tsx +0 -371
- package/insight/src/components/ui/collapsible.tsx +0 -19
- package/insight/src/components/ui/input.tsx +0 -20
- package/insight/src/components/ui/progress.tsx +0 -83
- package/insight/src/components/ui/scroll-area.tsx +0 -55
- package/insight/src/components/ui/separator.tsx +0 -23
- package/insight/src/components/ui/table.tsx +0 -114
- package/insight/src/components/ui/tabs.tsx +0 -82
- package/insight/src/components/ui/tooltip.tsx +0 -64
- package/insight/src/lib/api.ts +0 -144
- package/insight/src/lib/utils.ts +0 -6
- package/insight/src/main.tsx +0 -22
- package/insight/src/routeTree.gen.ts +0 -189
- package/insight/src/router.tsx +0 -19
- package/insight/src/routes/__root.tsx +0 -55
- package/insight/src/routes/enterprise.tsx +0 -316
- package/insight/src/routes/index.tsx +0 -1482
- package/insight/src/routes/knowledge.tsx +0 -221
- package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
- package/insight/src/routes/search.tsx +0 -97
- package/insight/src/routes/sessions.tsx +0 -179
- package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
- package/insight/src/styles.css +0 -104
- package/insight/tsconfig.json +0 -29
- 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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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("
|
|
175
|
+
resolve("kilo.json"),
|
|
163
176
|
resolve(".kilo", "kilo.jsonc"),
|
|
164
|
-
resolve(".
|
|
177
|
+
resolve(".kilo", "kilo.json"),
|
|
165
178
|
resolve(".kilocode", "kilo.jsonc"),
|
|
166
|
-
|
|
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("
|
|
186
|
+
resolve("opencode.json"),
|
|
174
187
|
resolve(".opencode", "opencode.jsonc"),
|
|
175
|
-
|
|
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
|
-
//
|
|
101
|
-
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
140
|
-
*
|
|
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
|
|
286
|
+
// ── Pi MCP bridge lazy bootstrap (#534, #809) ───────────
|
|
284
287
|
//
|
|
285
|
-
// Pi
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
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
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
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 ?? "");
|
|
@@ -542,11 +565,22 @@ export default function piExtension(pi) {
|
|
|
542
565
|
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
543
566
|
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
544
567
|
// budget loop if the helper is unavailable.
|
|
545
|
-
|
|
568
|
+
//
|
|
569
|
+
// Issue #856 — do NOT re-inject `role` as a standing behavioral_directive
|
|
570
|
+
// on every turn. A casual past phrase that classified as a role would
|
|
571
|
+
// otherwise be pinned and replayed each turn ("since you said 'that's
|
|
572
|
+
// fine for now', I'll leave it"), producing a do-nothing loop. Defense in
|
|
573
|
+
// depth: even if a stale `role` event exists (from an older build, or a
|
|
574
|
+
// genuine persona the user has since moved past), it must not become an
|
|
575
|
+
// inescapable per-turn standing order. Role events stay in the DB and
|
|
576
|
+
// remain queryable via ctx_search(source: "session-events"); intent,
|
|
577
|
+
// skills, decisions, and the resume snapshot are unaffected.
|
|
578
|
+
const activeEvents = db
|
|
579
|
+
.getEvents(_sessionId, {
|
|
546
580
|
minPriority: 3,
|
|
547
581
|
limit: 50,
|
|
548
|
-
})
|
|
549
|
-
|
|
582
|
+
})
|
|
583
|
+
.filter((e) => String(e.category ?? "") !== "role");
|
|
550
584
|
if (activeEvents.length > 0) {
|
|
551
585
|
const buildAuto = await getAutoInjection(pluginRoot);
|
|
552
586
|
let memoryContext = "";
|
|
@@ -555,14 +589,6 @@ export default function piExtension(pi) {
|
|
|
555
589
|
category: String(e.category ?? ""),
|
|
556
590
|
data: String(e.data ?? ""),
|
|
557
591
|
})));
|
|
558
|
-
const bdMatch = memoryContext.match(/(<behavioral_directive>\n[^<]*\n<\/behavioral_directive>)/);
|
|
559
|
-
if (bdMatch) {
|
|
560
|
-
behavioralDirective = bdMatch[1];
|
|
561
|
-
memoryContext = memoryContext.replace(bdMatch[1], "");
|
|
562
|
-
if (memoryContext.match(/^<session_state[^>]*>\s*<\/session_state>\s*$/)) {
|
|
563
|
-
memoryContext = "";
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
592
|
}
|
|
567
593
|
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
568
594
|
if (!memoryContext) {
|
|
@@ -588,18 +614,45 @@ export default function piExtension(pi) {
|
|
|
588
614
|
parts.push(resume.snapshot);
|
|
589
615
|
db.markResumeConsumed(_sessionId);
|
|
590
616
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
//
|
|
617
|
+
// Store extra context (routing anchor, active_memory, resume, behavioralDirective)
|
|
618
|
+
// for injection via the 'context' hook as a message, NOT as a systemPrompt
|
|
619
|
+
// modification. Mutating systemPrompt breaks prefix prompt caching on
|
|
620
|
+
// DeepSeek/Anthropic/OpenAI because the system message sits at messages[0]
|
|
621
|
+
// and any change invalidates the entire cache chain.
|
|
594
622
|
const baseLen = existingPrompt ? 1 : 0;
|
|
595
623
|
if (parts.length > baseLen) {
|
|
596
|
-
|
|
624
|
+
const extraParts = parts.slice(baseLen);
|
|
625
|
+
_pendingContext = extraParts.join("\n\n");
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
_pendingContext = "";
|
|
597
629
|
}
|
|
598
630
|
}
|
|
599
631
|
catch {
|
|
632
|
+
_pendingContext = ""; // Reset — ensure no stale data escapes
|
|
600
633
|
// best effort — never break agent start
|
|
601
634
|
}
|
|
602
635
|
});
|
|
636
|
+
// ── 4a2. context — Inject active_memory + resume + behavioralDirective as message ──
|
|
637
|
+
// Uses the 'context' hook (like hindsight does) to append context at the END of
|
|
638
|
+
// messages rather than mutating systemPrompt at the beginning. This preserves
|
|
639
|
+
// prefix prompt cache for DeepSeek, Anthropic, and OpenAI.
|
|
640
|
+
pi.on("context", (event) => {
|
|
641
|
+
try {
|
|
642
|
+
if (!_pendingContext)
|
|
643
|
+
return;
|
|
644
|
+
const ctx = _pendingContext;
|
|
645
|
+
_pendingContext = "";
|
|
646
|
+
event.messages.push({
|
|
647
|
+
role: "user",
|
|
648
|
+
content: ctx,
|
|
649
|
+
});
|
|
650
|
+
return { messages: event.messages };
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
// best effort — never break context assembly
|
|
654
|
+
}
|
|
655
|
+
});
|
|
603
656
|
// ── 4b. before_provider_response — capture response metadata ───
|
|
604
657
|
// Pi-2: Register the missing event so providers can record latency,
|
|
605
658
|
// model, and token usage when Pi exposes them. Best-effort only;
|
|
@@ -676,12 +729,13 @@ export default function piExtension(pi) {
|
|
|
676
729
|
catch {
|
|
677
730
|
// best effort — never throw during shutdown
|
|
678
731
|
}
|
|
679
|
-
// Race fix (#472 round-3): if shutdown fires while
|
|
680
|
-
// is still in flight, _mcpBridge
|
|
681
|
-
//
|
|
682
|
-
// resolves
|
|
683
|
-
//
|
|
684
|
-
|
|
732
|
+
// Race fix (#472 round-3 + #809 lazy follow-up): if shutdown fires while
|
|
733
|
+
// bridge bootstrap is still in flight, _mcpBridge may be null at this
|
|
734
|
+
// point. Invalidate this bootstrap generation before waiting so any handle
|
|
735
|
+
// that resolves after the 2s ceiling self-shuts down instead of publishing
|
|
736
|
+
// a stale child handle after session shutdown.
|
|
737
|
+
mcpBridgeGeneration++;
|
|
738
|
+
mcpBridgeStarted = false;
|
|
685
739
|
try {
|
|
686
740
|
await Promise.race([
|
|
687
741
|
_mcpBridgeReady,
|
|
@@ -701,6 +755,7 @@ export default function piExtension(pi) {
|
|
|
701
755
|
}
|
|
702
756
|
_mcpBridge = null;
|
|
703
757
|
}
|
|
758
|
+
_mcpBridgeReady = Promise.resolve();
|
|
704
759
|
});
|
|
705
760
|
// ── 8. Slash commands ──────────────────────────────────
|
|
706
761
|
pi.registerCommand("ctx-stats", {
|
|
@@ -747,45 +802,9 @@ export default function piExtension(pi) {
|
|
|
747
802
|
});
|
|
748
803
|
// ── 9. MCP tool bridge (#426) ───────────────────────────
|
|
749
804
|
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
|
|
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
|
-
}
|
|
805
|
+
// Intentionally no eager bootstrap here. `before_agent_start` is the first
|
|
806
|
+
// lifecycle signal that proves Pi is about to run a model call; starting the
|
|
807
|
+
// bridge there keeps ctx_* available for real agent turns while package/help
|
|
808
|
+
// commands that only load extensions never spawn a long-lived child (#809).
|
|
809
|
+
_mcpBridgeReady = Promise.resolve();
|
|
791
810
|
}
|
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A) JSON stdin/stdout — Claude Code, Gemini/Qwen family CLIs, Copilot/Codex/Kimi,
|
|
7
|
+
* Cursor, Kiro, Antigravity CLI (`agy`)
|
|
8
|
+
* B) TS Plugin Functions — OpenCode, 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. */
|
package/build/adapters/types.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A) JSON stdin/stdout — Claude Code, Gemini/Qwen family CLIs, Copilot/Codex/Kimi,
|
|
7
|
+
* Cursor, Kiro, Antigravity CLI (`agy`)
|
|
8
|
+
* B) TS Plugin Functions — OpenCode, 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;
|