context-mode 1.0.118 → 1.0.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/openclaw/mcp-tools.js +10 -1
- package/build/adapters/pi/mcp-bridge.d.ts +28 -3
- package/build/adapters/pi/mcp-bridge.js +127 -14
- package/build/adapters/qwen-code/index.js +6 -2
- package/build/cli.js +93 -5
- package/build/opencode-plugin.js +2 -5
- package/build/server.js +104 -30
- package/build/session/purge.d.ts +27 -0
- package/build/session/purge.js +105 -3
- package/build/util/project-dir.js +9 -5
- package/cli.bundle.mjs +195 -164
- package/hooks/core/routing.mjs +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -6
- package/scripts/heal-better-sqlite3.mjs +53 -6
- package/scripts/heal-installed-plugins.mjs +104 -0
- package/scripts/postinstall.mjs +35 -1
- package/server.bundle.mjs +135 -113
- package/skills/UPSTREAM-CREDITS.md +51 -0
- package/skills/ctx-purge/SKILL.md +23 -9
- package/skills/diagnose/SKILL.md +122 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/grill-me/SKILL.md +15 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +93 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/skills/tdd/SKILL.md +114 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
- package/start.mjs +25 -1
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- package/build/tool-naming.js +0 -24
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.120"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.120",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.120",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.120",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.120",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -175,7 +175,16 @@ export const OPENCLAW_TOOL_DEFS = [
|
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
name: "ctx_purge",
|
|
178
|
-
description: "
|
|
178
|
+
description: "DESTRUCTIVE — permanently delete indexed content. CANNOT be undone.\n\n" +
|
|
179
|
+
"MUST specify exactly ONE scope:\n" +
|
|
180
|
+
" • {confirm:true, sessionId:\"<uuid>\"} → wipes ONLY that session's events + chunks; preserves stats and other sessions\n" +
|
|
181
|
+
" • {confirm:true, scope:\"project\"} → wipes ENTIRE project: FTS5 KB + every session DB + stats file\n\n" +
|
|
182
|
+
"REFUSED:\n" +
|
|
183
|
+
" • confirm:false → 'purge cancelled'\n" +
|
|
184
|
+
" • sessionId AND scope:\"project\" together → 'ambiguous — pick one'\n" +
|
|
185
|
+
" • scope:\"session\" without sessionId → throws\n" +
|
|
186
|
+
" • bare {confirm:true} → DEPRECATED: maps to scope:\"project\" with stderr warning\n\n" +
|
|
187
|
+
"Use sessionId for clearing one conversation. Use scope:\"project\" only when the user explicitly resets everything. NEVER call with bare {confirm:true}.",
|
|
179
188
|
parameters: {
|
|
180
189
|
type: "object",
|
|
181
190
|
properties: {},
|
|
@@ -20,6 +20,21 @@
|
|
|
20
20
|
*
|
|
21
21
|
* No external dependencies — pure node:child_process + JSON line frames.
|
|
22
22
|
*/
|
|
23
|
+
export interface ResolveDeps {
|
|
24
|
+
detect?: () => {
|
|
25
|
+
javascript: string | null;
|
|
26
|
+
};
|
|
27
|
+
which?: (cmd: string) => string | null;
|
|
28
|
+
execPath?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a JS runtime safe to spawn the MCP server with.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` when no real runtime is reachable (caller must skip
|
|
34
|
+
* the bridge gracefully — see bootstrapMCPTools). Pi-named binaries are
|
|
35
|
+
* explicitly rejected at every step to prevent the #516 fork bomb.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveJsRuntimeForBridge(deps?: ResolveDeps): string | null;
|
|
23
38
|
export interface MCPTool {
|
|
24
39
|
name: string;
|
|
25
40
|
description?: string;
|
|
@@ -47,13 +62,20 @@ export interface MCPCallResult {
|
|
|
47
62
|
export declare class MCPStdioClient {
|
|
48
63
|
private readonly serverScript;
|
|
49
64
|
private readonly env;
|
|
65
|
+
private readonly runtimeOverride;
|
|
50
66
|
private child;
|
|
51
67
|
private requestId;
|
|
52
68
|
private readonly pending;
|
|
53
69
|
private buffer;
|
|
54
70
|
private initialized;
|
|
55
71
|
private exited;
|
|
56
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Live env passed to the spawned child — exposed (read-only intent)
|
|
74
|
+
* so tests can pin the fork-bomb-prevention env counter (#516)
|
|
75
|
+
* without needing to attach a process-tree probe.
|
|
76
|
+
*/
|
|
77
|
+
_spawnEnv: NodeJS.ProcessEnv | null;
|
|
78
|
+
constructor(serverScript: string, env?: NodeJS.ProcessEnv, runtimeOverride?: string | null);
|
|
57
79
|
/** Spawn the MCP child. Idempotent. */
|
|
58
80
|
start(): void;
|
|
59
81
|
private onExit;
|
|
@@ -108,6 +130,9 @@ export interface BridgeHandle {
|
|
|
108
130
|
* `execute()` callback — Pi's contract is "throw to mark the tool call
|
|
109
131
|
* failed", which lets the LLM see and adapt.
|
|
110
132
|
*/
|
|
111
|
-
export
|
|
133
|
+
export interface BootstrapOptions {
|
|
112
134
|
env?: NodeJS.ProcessEnv;
|
|
113
|
-
|
|
135
|
+
/** DI hook for tests: override the runtime resolver entirely. */
|
|
136
|
+
_resolveJsRuntime?: () => string | null;
|
|
137
|
+
}
|
|
138
|
+
export declare function bootstrapMCPTools(pi: PiLikeAPI, serverScript: string, options?: BootstrapOptions): Promise<BridgeHandle>;
|
|
@@ -20,7 +20,77 @@
|
|
|
20
20
|
*
|
|
21
21
|
* No external dependencies — pure node:child_process + JSON line frames.
|
|
22
22
|
*/
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
23
|
+
import { spawn, execSync } from "node:child_process";
|
|
24
|
+
import { detectRuntimes } from "../../runtime.js";
|
|
25
|
+
// ── Fork-bomb prevention (#516) ──────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// Original bug: `spawn(process.execPath, [serverScript])` recursively
|
|
28
|
+
// re-executed the Pi binary on Bun-only systems where `process.execPath`
|
|
29
|
+
// IS pi itself. Each spawn re-loaded context-mode → spawned again →
|
|
30
|
+
// took the box down.
|
|
31
|
+
//
|
|
32
|
+
// Defence in depth:
|
|
33
|
+
// 1. resolveJsRuntimeForBridge() refuses pi-named binaries even when
|
|
34
|
+
// detectRuntimes() returns one, falling back to PATH-resolved
|
|
35
|
+
// node/bun.
|
|
36
|
+
// 2. Spawn passes CONTEXT_MODE_BRIDGE_DEPTH=1 in child env so any
|
|
37
|
+
// transitive bridge load can detect the recursion via env counter.
|
|
38
|
+
// 3. bootstrapMCPTools() aborts if CONTEXT_MODE_BRIDGE_DEPTH > 0 in
|
|
39
|
+
// its own env — catches recursion that bypasses the binary-name
|
|
40
|
+
// check (e.g. a `node` shim that re-execs Pi).
|
|
41
|
+
const PI_BINARY_BASENAME = /^pi(\.exe)?$/i;
|
|
42
|
+
const BRIDGE_DEPTH_ENV = "CONTEXT_MODE_BRIDGE_DEPTH";
|
|
43
|
+
const isWindows = process.platform === "win32";
|
|
44
|
+
function basename(p) {
|
|
45
|
+
const segs = p.split(/[\\/]/);
|
|
46
|
+
return segs[segs.length - 1] ?? "";
|
|
47
|
+
}
|
|
48
|
+
function whichOnPath(cmd) {
|
|
49
|
+
try {
|
|
50
|
+
const probe = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
|
|
51
|
+
const out = execSync(probe, { encoding: "utf-8", stdio: "pipe" })
|
|
52
|
+
.trim()
|
|
53
|
+
.split(/\r?\n/)[0]
|
|
54
|
+
?.trim();
|
|
55
|
+
return out && out.length > 0 ? out : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a JS runtime safe to spawn the MCP server with.
|
|
63
|
+
*
|
|
64
|
+
* Returns `null` when no real runtime is reachable (caller must skip
|
|
65
|
+
* the bridge gracefully — see bootstrapMCPTools). Pi-named binaries are
|
|
66
|
+
* explicitly rejected at every step to prevent the #516 fork bomb.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveJsRuntimeForBridge(deps = {}) {
|
|
69
|
+
const detect = deps.detect ?? (() => detectRuntimes());
|
|
70
|
+
const which = deps.which ?? whichOnPath;
|
|
71
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
72
|
+
const isPi = (p) => !!p && PI_BINARY_BASENAME.test(basename(p));
|
|
73
|
+
// 1. Prefer detectRuntimes().javascript when it is NOT pi.
|
|
74
|
+
let candidate = null;
|
|
75
|
+
try {
|
|
76
|
+
candidate = detect().javascript ?? null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
candidate = null;
|
|
80
|
+
}
|
|
81
|
+
if (candidate && !isPi(candidate))
|
|
82
|
+
return candidate;
|
|
83
|
+
// 2. Fall back to PATH-resolved node, then bun.
|
|
84
|
+
for (const cmd of ["node", "bun"]) {
|
|
85
|
+
const resolved = which(cmd);
|
|
86
|
+
if (resolved && !isPi(resolved))
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
// 3. Last resort: process.execPath only if it is not pi.
|
|
90
|
+
if (execPath && !isPi(execPath))
|
|
91
|
+
return execPath;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
24
94
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
25
95
|
// Tools/call may run shell commands or fetch URLs — wider window than
|
|
26
96
|
// initialize/list, but still bounded so a hung server can't block Pi.
|
|
@@ -40,28 +110,49 @@ const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
|
40
110
|
export class MCPStdioClient {
|
|
41
111
|
serverScript;
|
|
42
112
|
env;
|
|
113
|
+
runtimeOverride;
|
|
43
114
|
child = null;
|
|
44
115
|
requestId = 0;
|
|
45
116
|
pending = new Map();
|
|
46
117
|
buffer = "";
|
|
47
118
|
initialized = false;
|
|
48
119
|
exited = false;
|
|
49
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Live env passed to the spawned child — exposed (read-only intent)
|
|
122
|
+
* so tests can pin the fork-bomb-prevention env counter (#516)
|
|
123
|
+
* without needing to attach a process-tree probe.
|
|
124
|
+
*/
|
|
125
|
+
_spawnEnv = null;
|
|
126
|
+
constructor(serverScript, env = process.env, runtimeOverride = null) {
|
|
50
127
|
this.serverScript = serverScript;
|
|
51
128
|
this.env = env;
|
|
129
|
+
this.runtimeOverride = runtimeOverride;
|
|
52
130
|
}
|
|
53
131
|
/** Spawn the MCP child. Idempotent. */
|
|
54
132
|
start() {
|
|
55
133
|
if (this.child)
|
|
56
134
|
return;
|
|
57
135
|
this.exited = false;
|
|
58
|
-
|
|
136
|
+
// Pick a JS runtime that is NOT the host process (#516). When Pi
|
|
137
|
+
// is the host binary, process.execPath would re-exec Pi and fork
|
|
138
|
+
// bomb the box. resolveJsRuntimeForBridge prefers bun/node and
|
|
139
|
+
// explicitly rejects pi-named binaries.
|
|
140
|
+
const runtime = this.runtimeOverride ?? resolveJsRuntimeForBridge() ?? process.execPath;
|
|
141
|
+
// Increment the depth counter so any transitive bridge load inside
|
|
142
|
+
// the child can short-circuit before spawning yet another server.
|
|
143
|
+
const depth = Number.parseInt(this.env[BRIDGE_DEPTH_ENV] ?? "0", 10);
|
|
144
|
+
const childEnv = {
|
|
145
|
+
...this.env,
|
|
146
|
+
[BRIDGE_DEPTH_ENV]: String(Number.isFinite(depth) ? depth + 1 : 1),
|
|
147
|
+
};
|
|
148
|
+
this._spawnEnv = childEnv;
|
|
149
|
+
this.child = spawn(runtime, [this.serverScript], {
|
|
59
150
|
// Pipe stderr (#472 round-3): swallowing it via "ignore" hides
|
|
60
151
|
// server crash diagnostics — the user only saw "ctx_* tools will
|
|
61
152
|
// not be callable" with no clue WHY. Forwarding to process.stderr
|
|
62
153
|
// with a [mcp-bridge] prefix lets ops grep across session noise.
|
|
63
154
|
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
-
env:
|
|
155
|
+
env: childEnv,
|
|
65
156
|
});
|
|
66
157
|
this.child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
67
158
|
this.child.stderr?.on("data", (chunk) => {
|
|
@@ -197,18 +288,40 @@ export class MCPStdioClient {
|
|
|
197
288
|
}
|
|
198
289
|
}
|
|
199
290
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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.
|
|
291
|
+
* Empty-but-valid handle returned when bootstrap is skipped (#516).
|
|
292
|
+
* Keeps the shutdown contract intact so callers do not need null checks.
|
|
209
293
|
*/
|
|
294
|
+
function skippedBridge() {
|
|
295
|
+
return {
|
|
296
|
+
tools: [],
|
|
297
|
+
shutdown: () => {
|
|
298
|
+
/* nothing to shut down */
|
|
299
|
+
},
|
|
300
|
+
client: new MCPStdioClient("/dev/null"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
210
303
|
export async function bootstrapMCPTools(pi, serverScript, options = {}) {
|
|
211
|
-
const
|
|
304
|
+
const env = options.env ?? process.env;
|
|
305
|
+
// Recursion guard (#516): if an ancestor bridge already incremented
|
|
306
|
+
// the depth counter, refuse to spawn another child — even if the
|
|
307
|
+
// binary-name check would let us through. Catches `node` shims that
|
|
308
|
+
// re-exec Pi and other host swaps that bypass basename detection.
|
|
309
|
+
const depth = Number.parseInt(env[BRIDGE_DEPTH_ENV] ?? "0", 10);
|
|
310
|
+
if (Number.isFinite(depth) && depth > 0) {
|
|
311
|
+
process.stderr.write(`[context-mode] WARNING: skipping MCP bridge — ${BRIDGE_DEPTH_ENV}=${depth} ` +
|
|
312
|
+
`indicates recursion (fork-bomb guard, #516). ctx_* tools will not be callable.\n`);
|
|
313
|
+
return skippedBridge();
|
|
314
|
+
}
|
|
315
|
+
// Runtime guard (#516): when neither node nor bun is on PATH and the
|
|
316
|
+
// host process is pi, there is no safe binary to spawn. Log once and
|
|
317
|
+
// return an empty handle — the rest of the extension keeps working.
|
|
318
|
+
const runtime = (options._resolveJsRuntime ?? resolveJsRuntimeForBridge)();
|
|
319
|
+
if (runtime === null) {
|
|
320
|
+
process.stderr.write(`[context-mode] WARNING: no JS runtime found (need node or bun on PATH). ` +
|
|
321
|
+
`Skipping MCP bridge to avoid fork bomb (#516). ctx_* tools will not be callable.\n`);
|
|
322
|
+
return skippedBridge();
|
|
323
|
+
}
|
|
324
|
+
const client = new MCPStdioClient(serverScript, env, runtime);
|
|
212
325
|
client.start();
|
|
213
326
|
await client.initialize();
|
|
214
327
|
const tools = await client.listTools();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - MCP clientInfo: qwen-cli-mcp-client-* (pattern)
|
|
13
13
|
* - 12 hook events (superset of Claude's 5, but context-mode uses the shared 5)
|
|
14
14
|
*/
|
|
15
|
-
import { readFileSync, existsSync, } from "node:fs";
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, } from "node:fs";
|
|
16
16
|
import { resolve, join } from "node:path";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
@@ -110,7 +110,11 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
writeSettings(settings) {
|
|
113
|
-
|
|
113
|
+
// Issue #511: use top-level static import (line 18) — never inline
|
|
114
|
+
// `require("node:fs")` in ESM-bundled sources. esbuild rewrites them to
|
|
115
|
+
// a `__require` shim that throws `Dynamic require of "node:fs" is not
|
|
116
|
+
// supported` under Node ESM/Bun (this adapter is pulled into both
|
|
117
|
+
// server.bundle.mjs and cli.bundle.mjs via adapter detect).
|
|
114
118
|
writeFileSync(this.getSettingsPath(), JSON.stringify(settings, null, 2));
|
|
115
119
|
}
|
|
116
120
|
// ── Diagnostics (doctor) ───────────────────────────────
|
package/build/cli.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as p from "@clack/prompts";
|
|
15
15
|
import color from "picocolors";
|
|
16
|
-
import { execFileSync, execFile as nodeExecFile } from "node:child_process";
|
|
16
|
+
import { execFileSync, execSync, execFile as nodeExecFile } from "node:child_process";
|
|
17
17
|
import { readFileSync, writeFileSync, cpSync, accessSync, existsSync, rmSync, closeSync, openSync, chmodSync, constants } from "node:fs";
|
|
18
18
|
import { request as httpsRequest } from "node:https";
|
|
19
19
|
import { resolve, dirname, join } from "node:path";
|
|
@@ -22,6 +22,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
22
22
|
import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
|
|
23
23
|
import { getHookScriptPaths } from "./util/hook-config.js";
|
|
24
24
|
import { resolveClaudeConfigDir } from "./util/claude-config.js";
|
|
25
|
+
// v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
|
|
26
|
+
// mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
|
|
27
|
+
// @ts-expect-error — JS module, no TS declarations
|
|
28
|
+
import { healPluginJsonMcpServers } from "../scripts/heal-installed-plugins.mjs";
|
|
25
29
|
// Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
|
|
26
30
|
// duplicated here so the cli bundle does not pull server.ts top-level boot side effects.
|
|
27
31
|
// Keep in sync — pure data, no I/O.
|
|
@@ -163,11 +167,16 @@ export function npmExecFile(args, opts = {}) {
|
|
|
163
167
|
});
|
|
164
168
|
}
|
|
165
169
|
export function npmExec(command, opts = {}) {
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
// Issue #511: use top-level static import (line 17) — never inline `require("node:...")`
|
|
171
|
+
// in ESM-bundled sources. esbuild rewrites them to a `__require` shim that throws
|
|
172
|
+
// `Dynamic require of "node:child_process" is not supported` under Node ESM/Bun.
|
|
173
|
+
// Cast preserves the prior `require()`-as-`any` shape; `shell: true` is the documented
|
|
174
|
+
// Node behavior even though @types/node typed `shell` as `string | undefined`.
|
|
175
|
+
const execOpts = {
|
|
168
176
|
...opts,
|
|
169
177
|
...(isWin ? { shell: true } : {}),
|
|
170
|
-
}
|
|
178
|
+
};
|
|
179
|
+
execSync(isWin ? command.replace(/^npm /, "npm.cmd ") : command, execOpts);
|
|
171
180
|
}
|
|
172
181
|
export function openInBrowser(url, platform = process.platform, runner = nodeExecFile) {
|
|
173
182
|
const opts = { stdio: "ignore" };
|
|
@@ -398,7 +407,28 @@ async function doctor() {
|
|
|
398
407
|
}
|
|
399
408
|
catch (err) {
|
|
400
409
|
const message = err instanceof Error ? err.message : String(err);
|
|
401
|
-
|
|
410
|
+
// Distinguish package-missing from binding-missing (#514). Both
|
|
411
|
+
// throw with similar shapes from `import("better-sqlite3")` but the
|
|
412
|
+
// recovery commands differ:
|
|
413
|
+
// - package-missing → `npm install better-sqlite3 --no-optional`
|
|
414
|
+
// (npm@7+ silently drops optionalDependencies on engine
|
|
415
|
+
// mismatch, e.g. Node 26 vs better-sqlite3@12.x — we name the
|
|
416
|
+
// package explicitly + flip the optional filter to recover)
|
|
417
|
+
// - binding-missing → `npm rebuild better-sqlite3` (#408 flow,
|
|
418
|
+
// Windows + missing prebuild-install shim)
|
|
419
|
+
const pluginRootForDoctor = getPluginRoot();
|
|
420
|
+
const bsqPackageDir = resolve(pluginRootForDoctor, "node_modules", "better-sqlite3");
|
|
421
|
+
const packageMissing = !existsSync(bsqPackageDir);
|
|
422
|
+
if (packageMissing) {
|
|
423
|
+
criticalFails++;
|
|
424
|
+
p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
|
|
425
|
+
color.dim(" — package-missing") +
|
|
426
|
+
color.dim(`\n Path: ${bsqPackageDir}` +
|
|
427
|
+
"\n Root cause: npm silently skipped better-sqlite3 because the package's `engines` field excluded the running Node (issue #514, e.g. Node 26 vs better-sqlite3@12.x)." +
|
|
428
|
+
`\n Try (primary): cd "${pluginRootForDoctor}" && npm install better-sqlite3 --no-optional` +
|
|
429
|
+
"\n Try (fallback): /context-mode:ctx-upgrade"));
|
|
430
|
+
}
|
|
431
|
+
else if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
|
|
402
432
|
p.log.warn(color.yellow("FTS5 / better-sqlite3: SKIP") + color.dim(" — module not available (restart session after upgrade)"));
|
|
403
433
|
}
|
|
404
434
|
else {
|
|
@@ -732,6 +762,30 @@ async function upgrade() {
|
|
|
732
762
|
const message = err instanceof Error ? err.message : String(err);
|
|
733
763
|
throw new Error(`Registry consistency check failed: ${message}`);
|
|
734
764
|
}
|
|
765
|
+
// v1.0.119 — Issue #523 — Layer 5 heal: assert .claude-plugin/plugin.json's
|
|
766
|
+
// mcpServers["context-mode"].args[0] is the literal ${CLAUDE_PLUGIN_ROOT}/start.mjs
|
|
767
|
+
// placeholder, not a tmpdir-prefixed absolute path. cli.ts already wrote .mcp.json
|
|
768
|
+
// with the placeholder (#411 fix), but plugin.json was never touched here — and
|
|
769
|
+
// start.mjs's normalize-hooks (Windows + #378) can bake in absolute paths that
|
|
770
|
+
// become stale across upgrades. We call the shared heal twice: first call cleans
|
|
771
|
+
// any drift; second call MUST return healed:[] or we throw. Single source of
|
|
772
|
+
// truth shared with start.mjs HEAL block + postinstall.
|
|
773
|
+
try {
|
|
774
|
+
const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
|
|
775
|
+
const pluginKey = "context-mode@context-mode";
|
|
776
|
+
const firstPass = healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKey });
|
|
777
|
+
if (firstPass && firstPass.error) {
|
|
778
|
+
throw new Error(firstPass.error);
|
|
779
|
+
}
|
|
780
|
+
const secondPass = healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKey });
|
|
781
|
+
if (secondPass && Array.isArray(secondPass.healed) && secondPass.healed.length > 0) {
|
|
782
|
+
throw new Error(`Plugin manifest drift: plugin.json mcpServers.args still poisoned after first heal pass (healed=${secondPass.healed.join(",")})`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (err) {
|
|
786
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
787
|
+
throw new Error(`plugin.json drift check failed: ${message}`);
|
|
788
|
+
}
|
|
735
789
|
// v1.0.114 hotfix — marketplace post-pull assertion: clone (if
|
|
736
790
|
// present) MUST be on newVersion. Mert's case showed marketplace
|
|
737
791
|
// stuck at v1.0.89 — the sync block above swallowed that silently.
|
|
@@ -784,6 +838,40 @@ async function upgrade() {
|
|
|
784
838
|
` — ${message}` +
|
|
785
839
|
color.dim(`\n Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
|
|
786
840
|
}
|
|
841
|
+
// ── Post-install binding verifier (#514) ────────────────────
|
|
842
|
+
// npm@7+ silently drops optionalDependencies whose engines
|
|
843
|
+
// field excludes the running Node (e.g. Node 26 vs
|
|
844
|
+
// better-sqlite3@12.x). On a silent skip the package directory
|
|
845
|
+
// is missing entirely and ensure-deps cannot recover. Fail
|
|
846
|
+
// loud so /ctx-upgrade no longer reports success while the
|
|
847
|
+
// knowledge base is unusable.
|
|
848
|
+
const bsqBindingPath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
849
|
+
if (!existsSync(bsqBindingPath)) {
|
|
850
|
+
// Try one last self-heal — explicit, named install bypasses
|
|
851
|
+
// the optionalDependency silent-skip path even if the dep
|
|
852
|
+
// somehow regressed back to optional.
|
|
853
|
+
try {
|
|
854
|
+
const healPath = resolve(pluginRoot, "scripts", "heal-better-sqlite3.mjs");
|
|
855
|
+
if (existsSync(healPath)) {
|
|
856
|
+
const mod = await import(`${pathToFileURL(healPath).href}?upgrade=${Date.now()}`);
|
|
857
|
+
if (typeof mod.healBetterSqlite3Binding === "function") {
|
|
858
|
+
mod.healBetterSqlite3Binding(pluginRoot);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch { /* best effort — verifier below will fail loud */ }
|
|
863
|
+
}
|
|
864
|
+
if (!existsSync(bsqBindingPath)) {
|
|
865
|
+
// Mark the upgrade process for a non-zero exit at completion.
|
|
866
|
+
// Stays in scope only for the rest of upgrade(); the actual
|
|
867
|
+
// exit-code wiring sits below the top-level changes report.
|
|
868
|
+
process.exitCode = 1;
|
|
869
|
+
p.log.error(color.red("better-sqlite3 native binding: MISSING") +
|
|
870
|
+
color.dim(`\n Path: ${bsqBindingPath}`) +
|
|
871
|
+
color.dim("\n Cause: npm silently skipped the package (Node engine mismatch, issue #514)") +
|
|
872
|
+
color.dim(`\n Try (primary): cd "${pluginRoot}" && npm install better-sqlite3 --no-optional`) +
|
|
873
|
+
color.dim("\n Try (fallback): /context-mode:ctx-doctor"));
|
|
874
|
+
}
|
|
787
875
|
}
|
|
788
876
|
// Update global npm
|
|
789
877
|
s.start("Updating npm global package");
|
package/build/opencode-plugin.js
CHANGED
|
@@ -179,7 +179,7 @@ async function createContextModePlugin(ctx) {
|
|
|
179
179
|
const toolInput = output.args ?? {};
|
|
180
180
|
let decision;
|
|
181
181
|
try {
|
|
182
|
-
decision = routing.routePreToolUse(toolName, toolInput, projectDir,
|
|
182
|
+
decision = routing.routePreToolUse(toolName, toolInput, projectDir, getPlatform());
|
|
183
183
|
}
|
|
184
184
|
catch {
|
|
185
185
|
return; // Routing failure → allow passthrough
|
|
@@ -194,10 +194,7 @@ async function createContextModePlugin(ctx) {
|
|
|
194
194
|
// Mutate output.args — OpenCode reads the mutated output object
|
|
195
195
|
Object.assign(output.args, decision.updatedInput);
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
// Mutate output.args — OpenCode reads the mutated output object
|
|
199
|
-
output.args.additionalContext = decision.additionalContext;
|
|
200
|
-
}
|
|
197
|
+
// "context" action → no-op (OpenCode doesn't support context injection)
|
|
201
198
|
},
|
|
202
199
|
// ── PostToolUse: Session event capture ──────────────
|
|
203
200
|
"tool.execute.after": async (input, output) => {
|