context-mode 1.0.150 → 1.0.152
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/mcp.json +5 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +16 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +89 -3
- package/build/adapters/claude-code/hooks.js +2 -2
- package/build/adapters/claude-code/index.js +14 -13
- package/build/adapters/client-map.js +3 -0
- package/build/adapters/detect.js +13 -1
- package/build/adapters/gemini-cli/hooks.d.ts +10 -0
- package/build/adapters/gemini-cli/hooks.js +12 -2
- package/build/adapters/gemini-cli/index.d.ts +21 -1
- package/build/adapters/gemini-cli/index.js +37 -1
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +1 -0
- package/build/adapters/kimi/paths.js +12 -0
- package/build/adapters/kiro/hooks.js +2 -2
- package/build/adapters/openclaw/plugin.d.ts +14 -13
- package/build/adapters/openclaw/plugin.js +140 -40
- package/build/adapters/opencode/plugin.js +4 -3
- package/build/adapters/opencode/zod3tov4.js +8 -8
- package/build/adapters/pi/extension.js +9 -24
- package/build/adapters/pi/mcp-bridge.js +37 -0
- package/build/adapters/qwen-code/index.js +7 -7
- package/build/adapters/types.d.ts +39 -2
- package/build/adapters/types.js +55 -2
- package/build/adapters/vscode-copilot/index.js +13 -1
- package/build/cli.js +433 -25
- package/build/executor.js +6 -3
- package/build/runtime.d.ts +81 -1
- package/build/runtime.js +195 -9
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/unified.d.ts +12 -0
- package/build/search/unified.js +17 -2
- package/build/server.d.ts +2 -1
- package/build/server.js +378 -97
- package/build/session/analytics.d.ts +36 -13
- package/build/session/analytics.js +123 -26
- package/build/session/db.d.ts +24 -0
- package/build/session/db.js +41 -0
- package/build/session/extract.js +30 -0
- package/build/session/snapshot.js +24 -0
- package/build/store.d.ts +12 -1
- package/build/store.js +72 -20
- package/build/types.d.ts +7 -0
- package/build/util/project-dir.d.ts +19 -16
- package/build/util/project-dir.js +80 -45
- package/cli.bundle.mjs +371 -320
- package/configs/kimi/hooks.json +54 -0
- package/configs/pi/AGENTS.md +3 -85
- package/hooks/cache-heal-utils.mjs +148 -0
- package/hooks/core/formatters.mjs +26 -0
- package/hooks/core/routing.mjs +9 -1
- package/hooks/core/stdin.mjs +74 -3
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +61 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/normalize-hooks.mjs +66 -12
- package/hooks/routing-block.mjs +8 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-db.bundle.mjs +6 -4
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +93 -3
- package/hooks/session-snapshot.bundle.mjs +20 -19
- package/hooks/sessionstart.mjs +64 -0
- package/insight/server.mjs +15 -3
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +31 -10
- package/scripts/postinstall.mjs +10 -0
- package/server.bundle.mjs +206 -157
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/start.mjs +84 -11
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/concurrency/runPool.d.ts +0 -36
- package/build/concurrency/runPool.js +0 -51
- package/build/openclaw/mcp-tools.d.ts +0 -54
- package/build/openclaw/mcp-tools.js +0 -198
- package/build/openclaw/workspace-router.d.ts +0 -29
- package/build/openclaw/workspace-router.js +0 -64
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -375
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- 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
package/build/runtime.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface RuntimeInfo {
|
|
|
7
7
|
preferred: boolean;
|
|
8
8
|
}
|
|
9
9
|
export interface RuntimeMap {
|
|
10
|
-
javascript: string;
|
|
10
|
+
javascript: string | null;
|
|
11
11
|
typescript: string | null;
|
|
12
12
|
python: string | null;
|
|
13
13
|
shell: string;
|
|
@@ -20,8 +20,88 @@ export interface RuntimeMap {
|
|
|
20
20
|
elixir: string | null;
|
|
21
21
|
csharp: string | null;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the JavaScript runtime used by PolyglotExecutor.
|
|
25
|
+
*
|
|
26
|
+
* PR #190 (f69b0d2) made `process.execPath` the default so snap-Node
|
|
27
|
+
* envs would not re-invoke the snap wrapper via PATH. That assumed
|
|
28
|
+
* `process.execPath` always points at a JS runtime — true on Node,
|
|
29
|
+
* tsx, and snap-Node, but FALSE when context-mode runs in-process
|
|
30
|
+
* inside a bun-compiled self-contained binary (OpenCode, Kilo, …).
|
|
31
|
+
* In those hosts, `process.execPath` resolves to `opencode.exe` /
|
|
32
|
+
* `opencode` (NOT node), and spawning that with a `.js` argument
|
|
33
|
+
* triggers the yargs "Failed to change directory" error (#731).
|
|
34
|
+
*
|
|
35
|
+
* Fix: gate `process.execPath` on the existing `JS_RUNTIMES`
|
|
36
|
+
* allowlist (single source of truth — same set used by
|
|
37
|
+
* `buildNodeCommand()` in src/adapters/types.ts since PR #708). When
|
|
38
|
+
* the execPath basename is not a known JS runtime, fall back to a
|
|
39
|
+
* PATH-resolved `node`. If neither is reachable, return `null` and
|
|
40
|
+
* let ctx_doctor surface an actionable error.
|
|
41
|
+
*
|
|
42
|
+
* The cross-OS guard is the allowlist itself — NOT a `win32` check.
|
|
43
|
+
* OpenCode ships self-contained binaries on macOS and Linux too,
|
|
44
|
+
* and the bug reproduces identically there.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveJavascriptRuntime(bun: string | null, deps?: {
|
|
47
|
+
execPath?: string;
|
|
48
|
+
commandExists?: (cmd: string) => boolean;
|
|
49
|
+
}): string | null;
|
|
23
50
|
export declare function detectRuntimes(): RuntimeMap;
|
|
24
51
|
export declare function hasBunRuntime(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Resolved JS runtime for hook spawn commands. `path` is the absolute (or
|
|
54
|
+
* bare-name on POSIX where PATH resolution is reliable) binary path.
|
|
55
|
+
* `isBun` is true only when we successfully probed a Bun ≥1.0 install.
|
|
56
|
+
*/
|
|
57
|
+
export interface HookRuntime {
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly isBun: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reset the hook-runtime resolution cache. Test-only — production code
|
|
63
|
+
* should never call this. Vitest mocks `node:child_process`/`node:fs`
|
|
64
|
+
* per-test, so the per-process cache from a previous test would otherwise
|
|
65
|
+
* mask the mock and yield the host's real bun/node detection result.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resetHookRuntimeCache(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the JS runtime to use for spawning hook scripts (issue #738).
|
|
70
|
+
*
|
|
71
|
+
* Returns Bun when:
|
|
72
|
+
* - a bun binary is located via {@link bunCommand} (already handles the
|
|
73
|
+
* Windows .cmd shim trap from #506 + absolute path fallbacks), AND
|
|
74
|
+
* - `bun --version` exits 0 within the probe timeout, AND
|
|
75
|
+
* - the reported semver major is ≥1.
|
|
76
|
+
*
|
|
77
|
+
* Returns Node (`process.execPath`) on every other path — missing bun,
|
|
78
|
+
* version probe failure, version <1, malformed version banner. Silent
|
|
79
|
+
* fallback: never throws, never logs to stderr (a noisy log would clutter
|
|
80
|
+
* the same MCP boot output that #719 tightened up).
|
|
81
|
+
*
|
|
82
|
+
* Result is cached at module load so the cost is amortised across every
|
|
83
|
+
* hook command emission for the lifetime of the process. The cache also
|
|
84
|
+
* keeps the behaviour deterministic — if the user `brew uninstall bun`
|
|
85
|
+
* mid-session, the cached resolution stays valid for that session and the
|
|
86
|
+
* next MCP boot re-detects.
|
|
87
|
+
*
|
|
88
|
+
* Why bun ≥1.0 instead of "any bun":
|
|
89
|
+
* - Bun 0.x had multiple ESM/module-resolution regressions that broke
|
|
90
|
+
* dynamic `import()` inside hooks (and our hooks do ~7 dynamic imports
|
|
91
|
+
* in `pretooluse.mjs`).
|
|
92
|
+
* - 1.0 ships stable npm-compat that our better-sqlite3-adjacent code
|
|
93
|
+
* relies on indirectly (hooks share `ensure-deps.mjs` which is
|
|
94
|
+
* bun-safe past 1.0 but not 0.x).
|
|
95
|
+
*
|
|
96
|
+
* NOT used by:
|
|
97
|
+
* - `buildNodeCommand` — kept on `process.execPath` for openclaw doctor /
|
|
98
|
+
* upgrade hints which must invoke the better-sqlite3-loading CLI on
|
|
99
|
+
* Node (#543: bun cannot dlopen better-sqlite3's prebuilt .node).
|
|
100
|
+
* - `ensure-deps.mjs` — separate path, must stay on Node for the same
|
|
101
|
+
* reason.
|
|
102
|
+
* - `ctx_upgrade` — separate path, must stay on Node for the same reason.
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveHookRuntime(): HookRuntime;
|
|
25
105
|
export declare function getRuntimeSummary(runtimes: RuntimeMap): string;
|
|
26
106
|
export declare function getAvailableLanguages(runtimes: RuntimeMap): Language[];
|
|
27
107
|
export declare function buildCommand(runtimes: RuntimeMap, language: Language, filePath: string): string[];
|
package/build/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { JS_RUNTIMES } from "./adapters/types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Allowlist for SHELL env override. Only POSIX shells + Windows shells permit
|
|
5
6
|
* arbitrary command interpretation; anything else (e.g., /usr/bin/python set
|
|
@@ -21,6 +22,11 @@ export function isAllowlistedShell(shellPath) {
|
|
|
21
22
|
// Cross-OS basename: split on either separator, take the last segment.
|
|
22
23
|
return ALLOWED_SHELL_BASENAMES.test(runtimeBasename(shellPath));
|
|
23
24
|
}
|
|
25
|
+
function isWindowsWslBash(shellPath) {
|
|
26
|
+
const lower = shellPath.toLowerCase().replace(/\//g, "\\");
|
|
27
|
+
return /\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(lower) ||
|
|
28
|
+
/\\microsoft\\windowsapps\\bash\.exe$/.test(lower);
|
|
29
|
+
}
|
|
24
30
|
const isWindows = process.platform === "win32";
|
|
25
31
|
function commandExists(cmd) {
|
|
26
32
|
try {
|
|
@@ -189,24 +195,71 @@ function getVersion(cmd, args = ["--version"]) {
|
|
|
189
195
|
return "unknown";
|
|
190
196
|
}
|
|
191
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Resolve the JavaScript runtime used by PolyglotExecutor.
|
|
200
|
+
*
|
|
201
|
+
* PR #190 (f69b0d2) made `process.execPath` the default so snap-Node
|
|
202
|
+
* envs would not re-invoke the snap wrapper via PATH. That assumed
|
|
203
|
+
* `process.execPath` always points at a JS runtime — true on Node,
|
|
204
|
+
* tsx, and snap-Node, but FALSE when context-mode runs in-process
|
|
205
|
+
* inside a bun-compiled self-contained binary (OpenCode, Kilo, …).
|
|
206
|
+
* In those hosts, `process.execPath` resolves to `opencode.exe` /
|
|
207
|
+
* `opencode` (NOT node), and spawning that with a `.js` argument
|
|
208
|
+
* triggers the yargs "Failed to change directory" error (#731).
|
|
209
|
+
*
|
|
210
|
+
* Fix: gate `process.execPath` on the existing `JS_RUNTIMES`
|
|
211
|
+
* allowlist (single source of truth — same set used by
|
|
212
|
+
* `buildNodeCommand()` in src/adapters/types.ts since PR #708). When
|
|
213
|
+
* the execPath basename is not a known JS runtime, fall back to a
|
|
214
|
+
* PATH-resolved `node`. If neither is reachable, return `null` and
|
|
215
|
+
* let ctx_doctor surface an actionable error.
|
|
216
|
+
*
|
|
217
|
+
* The cross-OS guard is the allowlist itself — NOT a `win32` check.
|
|
218
|
+
* OpenCode ships self-contained binaries on macOS and Linux too,
|
|
219
|
+
* and the bug reproduces identically there.
|
|
220
|
+
*/
|
|
221
|
+
export function resolveJavascriptRuntime(bun, deps = {}) {
|
|
222
|
+
if (bun)
|
|
223
|
+
return bun;
|
|
224
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
225
|
+
const cmdExists = deps.commandExists ?? commandExists;
|
|
226
|
+
// Cross-OS basename: split on either separator, strip optional `.exe`.
|
|
227
|
+
const base = execPath
|
|
228
|
+
.split(/[\\/]/)
|
|
229
|
+
.pop()
|
|
230
|
+
.replace(/\.exe$/i, "");
|
|
231
|
+
if (JS_RUNTIMES.has(base)) {
|
|
232
|
+
// Real JS runtime (node, bun, deno) — preserves #190 snap-Node fix
|
|
233
|
+
// because the snap wrapper's binary is literally named `node`.
|
|
234
|
+
return execPath;
|
|
235
|
+
}
|
|
236
|
+
// Host binary (opencode/kilo/etc.) — fall back to node on PATH.
|
|
237
|
+
if (cmdExists("node"))
|
|
238
|
+
return "node";
|
|
239
|
+
// No usable runtime — doctor + summary must handle null gracefully.
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
192
242
|
export function detectRuntimes() {
|
|
193
243
|
const hasBun = bunExists();
|
|
194
244
|
const bun = hasBun ? bunCommand() : null;
|
|
195
245
|
// Honor SHELL env var when it points at a real binary AND the basename is
|
|
196
|
-
// an allowlisted shell. Lets users with non-standard setups (
|
|
197
|
-
//
|
|
246
|
+
// an allowlisted shell. Lets users with non-standard setups (custom bash,
|
|
247
|
+
// msys2, pwsh) pin context-mode to their preferred shell.
|
|
198
248
|
//
|
|
199
249
|
// Allowlist (PR #401 ops review): basename must match
|
|
200
|
-
// /^(bash|sh|zsh|dash|pwsh|cmd)(\.exe)?$/. Without this guard, an attacker
|
|
250
|
+
// /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/. Without this guard, an attacker
|
|
201
251
|
// who controls SHELL (e.g., supply-chain compromise of a profile script)
|
|
202
252
|
// could redirect the executor to /usr/bin/python or any arbitrary binary.
|
|
203
253
|
const userShell = process.env.SHELL;
|
|
204
|
-
const
|
|
254
|
+
const isWin = process.platform === "win32";
|
|
255
|
+
const shellOverride = userShell &&
|
|
256
|
+
existsSync(userShell) &&
|
|
257
|
+
isAllowlistedShell(userShell) &&
|
|
258
|
+
!(isWin && isWindowsWslBash(userShell))
|
|
205
259
|
? userShell
|
|
206
260
|
: null;
|
|
207
|
-
const isWin = process.platform === "win32";
|
|
208
261
|
return {
|
|
209
|
-
javascript: bun
|
|
262
|
+
javascript: resolveJavascriptRuntime(bun),
|
|
210
263
|
typescript: bun
|
|
211
264
|
? bun
|
|
212
265
|
: commandExists("tsx")
|
|
@@ -241,10 +294,130 @@ export function detectRuntimes() {
|
|
|
241
294
|
export function hasBunRuntime() {
|
|
242
295
|
return bunExists();
|
|
243
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Cached result of {@link resolveHookRuntime}. Populated on first call so the
|
|
299
|
+
* relatively expensive `bun --version` probe runs at most once per process.
|
|
300
|
+
* Reset via {@link resetHookRuntimeCache} (test-only).
|
|
301
|
+
*/
|
|
302
|
+
let _hookRuntimeCache = null;
|
|
303
|
+
/**
|
|
304
|
+
* Reset the hook-runtime resolution cache. Test-only — production code
|
|
305
|
+
* should never call this. Vitest mocks `node:child_process`/`node:fs`
|
|
306
|
+
* per-test, so the per-process cache from a previous test would otherwise
|
|
307
|
+
* mask the mock and yield the host's real bun/node detection result.
|
|
308
|
+
*/
|
|
309
|
+
export function resetHookRuntimeCache() {
|
|
310
|
+
_hookRuntimeCache = null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse a `bun --version` stdout string and return true when the version is
|
|
314
|
+
* ≥1.0.0. Anything that doesn't match `MAJOR.MINOR.PATCH` (with optional
|
|
315
|
+
* pre-release suffix) returns false — we refuse to trust runtimes whose
|
|
316
|
+
* version we can't read because the failure mode is silent miscompare
|
|
317
|
+
* (e.g. a banner line getting interpreted as "0.0.0").
|
|
318
|
+
*/
|
|
319
|
+
function bunVersionAtLeast1(versionOutput) {
|
|
320
|
+
const trimmed = versionOutput.trim();
|
|
321
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
|
|
322
|
+
if (!m)
|
|
323
|
+
return false;
|
|
324
|
+
const major = Number(m[1]);
|
|
325
|
+
return Number.isFinite(major) && major >= 1;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Resolve the JS runtime to use for spawning hook scripts (issue #738).
|
|
329
|
+
*
|
|
330
|
+
* Returns Bun when:
|
|
331
|
+
* - a bun binary is located via {@link bunCommand} (already handles the
|
|
332
|
+
* Windows .cmd shim trap from #506 + absolute path fallbacks), AND
|
|
333
|
+
* - `bun --version` exits 0 within the probe timeout, AND
|
|
334
|
+
* - the reported semver major is ≥1.
|
|
335
|
+
*
|
|
336
|
+
* Returns Node (`process.execPath`) on every other path — missing bun,
|
|
337
|
+
* version probe failure, version <1, malformed version banner. Silent
|
|
338
|
+
* fallback: never throws, never logs to stderr (a noisy log would clutter
|
|
339
|
+
* the same MCP boot output that #719 tightened up).
|
|
340
|
+
*
|
|
341
|
+
* Result is cached at module load so the cost is amortised across every
|
|
342
|
+
* hook command emission for the lifetime of the process. The cache also
|
|
343
|
+
* keeps the behaviour deterministic — if the user `brew uninstall bun`
|
|
344
|
+
* mid-session, the cached resolution stays valid for that session and the
|
|
345
|
+
* next MCP boot re-detects.
|
|
346
|
+
*
|
|
347
|
+
* Why bun ≥1.0 instead of "any bun":
|
|
348
|
+
* - Bun 0.x had multiple ESM/module-resolution regressions that broke
|
|
349
|
+
* dynamic `import()` inside hooks (and our hooks do ~7 dynamic imports
|
|
350
|
+
* in `pretooluse.mjs`).
|
|
351
|
+
* - 1.0 ships stable npm-compat that our better-sqlite3-adjacent code
|
|
352
|
+
* relies on indirectly (hooks share `ensure-deps.mjs` which is
|
|
353
|
+
* bun-safe past 1.0 but not 0.x).
|
|
354
|
+
*
|
|
355
|
+
* NOT used by:
|
|
356
|
+
* - `buildNodeCommand` — kept on `process.execPath` for openclaw doctor /
|
|
357
|
+
* upgrade hints which must invoke the better-sqlite3-loading CLI on
|
|
358
|
+
* Node (#543: bun cannot dlopen better-sqlite3's prebuilt .node).
|
|
359
|
+
* - `ensure-deps.mjs` — separate path, must stay on Node for the same
|
|
360
|
+
* reason.
|
|
361
|
+
* - `ctx_upgrade` — separate path, must stay on Node for the same reason.
|
|
362
|
+
*/
|
|
363
|
+
export function resolveHookRuntime() {
|
|
364
|
+
if (_hookRuntimeCache)
|
|
365
|
+
return _hookRuntimeCache;
|
|
366
|
+
const nodeFallback = { path: process.execPath, isBun: false };
|
|
367
|
+
try {
|
|
368
|
+
if (!bunExists()) {
|
|
369
|
+
_hookRuntimeCache = nodeFallback;
|
|
370
|
+
return _hookRuntimeCache;
|
|
371
|
+
}
|
|
372
|
+
const bun = bunCommand();
|
|
373
|
+
// Re-use the same probe shape as getVersion (POSIX execFile, Windows
|
|
374
|
+
// execSync quoted string for DEP0190 compliance).
|
|
375
|
+
let versionOutput;
|
|
376
|
+
try {
|
|
377
|
+
if (process.platform === "win32") {
|
|
378
|
+
const out = execSync(`"${bun}" --version`, {
|
|
379
|
+
encoding: "utf-8",
|
|
380
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
381
|
+
timeout: 5000,
|
|
382
|
+
});
|
|
383
|
+
versionOutput = String(out);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const out = execFileSync(bun, ["--version"], {
|
|
387
|
+
encoding: "utf-8",
|
|
388
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
389
|
+
timeout: 5000,
|
|
390
|
+
});
|
|
391
|
+
versionOutput = String(out);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
_hookRuntimeCache = nodeFallback;
|
|
396
|
+
return _hookRuntimeCache;
|
|
397
|
+
}
|
|
398
|
+
if (!bunVersionAtLeast1(versionOutput)) {
|
|
399
|
+
_hookRuntimeCache = nodeFallback;
|
|
400
|
+
return _hookRuntimeCache;
|
|
401
|
+
}
|
|
402
|
+
_hookRuntimeCache = { path: bun, isBun: true };
|
|
403
|
+
return _hookRuntimeCache;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
_hookRuntimeCache = nodeFallback;
|
|
407
|
+
return _hookRuntimeCache;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
244
410
|
export function getRuntimeSummary(runtimes) {
|
|
245
411
|
const lines = [];
|
|
246
412
|
const bunPreferred = runtimes.javascript?.endsWith("bun") ?? false;
|
|
247
|
-
|
|
413
|
+
if (runtimes.javascript) {
|
|
414
|
+
lines.push(` JavaScript: ${runtimes.javascript} (${getVersion(runtimes.javascript)})${bunPreferred ? " ⚡" : ""}`);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
// #731: host binary (opencode/kilo) AND no PATH-resolvable node.
|
|
418
|
+
// Surface actionable guidance instead of rendering literal `null`.
|
|
419
|
+
lines.push(` JavaScript: not available (install node or bun — host process is not a JS runtime)`);
|
|
420
|
+
}
|
|
248
421
|
if (runtimes.typescript) {
|
|
249
422
|
lines.push(` TypeScript: ${runtimes.typescript} (${getVersion(runtimes.typescript)})`);
|
|
250
423
|
}
|
|
@@ -308,6 +481,12 @@ export function getAvailableLanguages(runtimes) {
|
|
|
308
481
|
export function buildCommand(runtimes, language, filePath) {
|
|
309
482
|
switch (language) {
|
|
310
483
|
case "javascript":
|
|
484
|
+
if (!runtimes.javascript) {
|
|
485
|
+
// #731: in-process plugin host (opencode/kilo binary) AND no
|
|
486
|
+
// PATH-resolvable node. Refuse early with an actionable error
|
|
487
|
+
// instead of spawning the host binary (the original bug shape).
|
|
488
|
+
throw new Error("No JavaScript runtime available. Install Node.js or Bun on PATH (the host process is not itself a JS runtime).");
|
|
489
|
+
}
|
|
311
490
|
return BUN_BASENAME.test(runtimeBasename(runtimes.javascript))
|
|
312
491
|
? [runtimes.javascript, "run", filePath]
|
|
313
492
|
: [runtimes.javascript, filePath];
|
|
@@ -341,9 +520,16 @@ export function buildCommand(runtimes, language, filePath) {
|
|
|
341
520
|
return [runtimes.shell, "-c", `source '${escaped}'`];
|
|
342
521
|
}
|
|
343
522
|
if (shellName.includes("powershell") || shellName.includes("pwsh")) {
|
|
344
|
-
|
|
523
|
+
// Windows PowerShell defaults to Restricted when no execution policy
|
|
524
|
+
// is configured. Use process-scoped Bypass so generated temp scripts
|
|
525
|
+
// run without changing machine/user policy.
|
|
526
|
+
return [runtimes.shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", filePath];
|
|
527
|
+
}
|
|
528
|
+
const shellBase = shellName.split(/[\\/]/).pop() ?? shellName;
|
|
529
|
+
if (shellBase === "cmd" || shellBase === "cmd.exe") {
|
|
530
|
+
return [runtimes.shell, "/d", "/s", "/c", filePath];
|
|
345
531
|
}
|
|
346
|
-
//
|
|
532
|
+
// Other Windows shells: direct file.
|
|
347
533
|
}
|
|
348
534
|
return [runtimes.shell, filePath];
|
|
349
535
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search input-schema builder and project-scope resolver.
|
|
3
|
+
*
|
|
4
|
+
* Issue #737 introduces the optional `project:` parameter used by callers
|
|
5
|
+
* running in the shared-DB mode (`CONTEXT_MODE_PROJECT_DIR` is set). The
|
|
6
|
+
* field is registered conditionally so that in the default per-project DB
|
|
7
|
+
* mode the LLM physically cannot pass it — the parameter does not exist
|
|
8
|
+
* in the tool schema at all, which is a stronger guarantee than runtime
|
|
9
|
+
* validation that depends on the model honouring documentation.
|
|
10
|
+
*
|
|
11
|
+
* The handler in `src/server.ts` consumes both exports:
|
|
12
|
+
* - {@link buildCtxSearchInputSchema} composes the Zod object used at
|
|
13
|
+
* `registerTool` time, spreading the conditional `project` field only
|
|
14
|
+
* when `isSharedMode` is true.
|
|
15
|
+
* - {@link resolveProjectScope} normalises the raw param into the
|
|
16
|
+
* three-state contract consumed by `searchAllSources`:
|
|
17
|
+
* undefined → no filter
|
|
18
|
+
* null → explicit cross-project recall (no filter)
|
|
19
|
+
* string → restrict to that project directory
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
/**
|
|
23
|
+
* Build the Zod object passed to `server.registerTool("ctx_search", …)`.
|
|
24
|
+
*
|
|
25
|
+
* The base fields (`queries`, `limit`, `source`, `contentType`, `sort`)
|
|
26
|
+
* are always present and mirror today's contract exactly. The `project`
|
|
27
|
+
* field is only spread in when `isSharedMode` is true. When the host runs
|
|
28
|
+
* with the default per-project DB layout the schema does not expose the
|
|
29
|
+
* field at all, which keeps the tool surface honest about what is
|
|
30
|
+
* actionable in that mode.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildCtxSearchInputSchema(isSharedMode: boolean): z.ZodObject<{
|
|
33
|
+
queries: z.ZodEffects<z.ZodOptional<z.ZodArray<z.ZodString, "many">>, string[] | undefined, unknown>;
|
|
34
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
35
|
+
source: z.ZodOptional<z.ZodString>;
|
|
36
|
+
contentType: z.ZodOptional<z.ZodEnum<["code", "prose"]>>;
|
|
37
|
+
sort: z.ZodDefault<z.ZodOptional<z.ZodEnum<["relevance", "timeline"]>>>;
|
|
38
|
+
} | {
|
|
39
|
+
project: z.ZodOptional<z.ZodString>;
|
|
40
|
+
queries: z.ZodEffects<z.ZodOptional<z.ZodArray<z.ZodString, "many">>, string[] | undefined, unknown>;
|
|
41
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
42
|
+
source: z.ZodOptional<z.ZodString>;
|
|
43
|
+
contentType: z.ZodOptional<z.ZodEnum<["code", "prose"]>>;
|
|
44
|
+
sort: z.ZodDefault<z.ZodOptional<z.ZodEnum<["relevance", "timeline"]>>>;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
sort: "relevance" | "timeline";
|
|
47
|
+
limit: number;
|
|
48
|
+
source?: string | undefined;
|
|
49
|
+
contentType?: "code" | "prose" | undefined;
|
|
50
|
+
queries?: string[] | undefined;
|
|
51
|
+
} | {
|
|
52
|
+
sort: "relevance" | "timeline";
|
|
53
|
+
limit: number;
|
|
54
|
+
source?: string | undefined;
|
|
55
|
+
contentType?: "code" | "prose" | undefined;
|
|
56
|
+
queries?: string[] | undefined;
|
|
57
|
+
project?: unknown;
|
|
58
|
+
}, {
|
|
59
|
+
sort?: "relevance" | "timeline" | undefined;
|
|
60
|
+
source?: string | undefined;
|
|
61
|
+
limit?: number | undefined;
|
|
62
|
+
contentType?: "code" | "prose" | undefined;
|
|
63
|
+
queries?: unknown;
|
|
64
|
+
} | {
|
|
65
|
+
sort?: "relevance" | "timeline" | undefined;
|
|
66
|
+
source?: string | undefined;
|
|
67
|
+
limit?: number | undefined;
|
|
68
|
+
contentType?: "code" | "prose" | undefined;
|
|
69
|
+
queries?: unknown;
|
|
70
|
+
project?: unknown;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Normalise the raw `project` value into the three-state contract consumed
|
|
74
|
+
* by {@link searchAllSources}.
|
|
75
|
+
*
|
|
76
|
+
* - shared mode OFF → `undefined` (param ignored)
|
|
77
|
+
* - shared mode ON, param `undefined` → current project (`getProjectDirFn()`)
|
|
78
|
+
* - shared mode ON, param `"global"` → `null` (no filter — cross-project)
|
|
79
|
+
* - shared mode ON, param `<string>` → that string verbatim
|
|
80
|
+
*
|
|
81
|
+
* The function is pure so it stays trivially testable without spinning up
|
|
82
|
+
* the MCP server.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveProjectScope(raw: string | undefined, isSharedMode: boolean, getProjectDirFn: () => string): string | null | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Module-load snapshot of `CONTEXT_MODE_PROJECT_DIR`. Captured once so the
|
|
87
|
+
* tool schema registered with `server.registerTool` reflects the launch
|
|
88
|
+
* environment — the LLM-visible surface should never flip mid-session.
|
|
89
|
+
*/
|
|
90
|
+
export declare const CTX_SEARCH_SHARED_MODE: boolean;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx_search input-schema builder and project-scope resolver.
|
|
3
|
+
*
|
|
4
|
+
* Issue #737 introduces the optional `project:` parameter used by callers
|
|
5
|
+
* running in the shared-DB mode (`CONTEXT_MODE_PROJECT_DIR` is set). The
|
|
6
|
+
* field is registered conditionally so that in the default per-project DB
|
|
7
|
+
* mode the LLM physically cannot pass it — the parameter does not exist
|
|
8
|
+
* in the tool schema at all, which is a stronger guarantee than runtime
|
|
9
|
+
* validation that depends on the model honouring documentation.
|
|
10
|
+
*
|
|
11
|
+
* The handler in `src/server.ts` consumes both exports:
|
|
12
|
+
* - {@link buildCtxSearchInputSchema} composes the Zod object used at
|
|
13
|
+
* `registerTool` time, spreading the conditional `project` field only
|
|
14
|
+
* when `isSharedMode` is true.
|
|
15
|
+
* - {@link resolveProjectScope} normalises the raw param into the
|
|
16
|
+
* three-state contract consumed by `searchAllSources`:
|
|
17
|
+
* undefined → no filter
|
|
18
|
+
* null → explicit cross-project recall (no filter)
|
|
19
|
+
* string → restrict to that project directory
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
/**
|
|
23
|
+
* Helper that mirrors the Zod coercer used elsewhere in the server for
|
|
24
|
+
* array-shaped tool args. Kept inline so this module has no runtime
|
|
25
|
+
* dependency on `server.ts` (which would create a cycle).
|
|
26
|
+
*
|
|
27
|
+
* Behaviour mirrors `coerceJsonArray` in `server.ts`:
|
|
28
|
+
* 1. Empty / whitespace string → returned untouched so Zod surfaces the
|
|
29
|
+
* "non-empty" error rather than masquerading as `[""]`.
|
|
30
|
+
* 2. Valid JSON array string → parsed and returned.
|
|
31
|
+
* 3. Any other plain string (a bare single query) → lifted to a
|
|
32
|
+
* single-element array. Fixes #627 for the native OpenCode plugin
|
|
33
|
+
* path where some providers deliver `queries: "search term"`.
|
|
34
|
+
*/
|
|
35
|
+
function coerceJsonArray(val) {
|
|
36
|
+
if (typeof val === "string") {
|
|
37
|
+
const trimmed = val.trim();
|
|
38
|
+
if (trimmed.length === 0)
|
|
39
|
+
return val;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(val);
|
|
42
|
+
if (Array.isArray(parsed))
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* fall through — not JSON, treat as bare-string lift */
|
|
47
|
+
}
|
|
48
|
+
return [val];
|
|
49
|
+
}
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the Zod object passed to `server.registerTool("ctx_search", …)`.
|
|
54
|
+
*
|
|
55
|
+
* The base fields (`queries`, `limit`, `source`, `contentType`, `sort`)
|
|
56
|
+
* are always present and mirror today's contract exactly. The `project`
|
|
57
|
+
* field is only spread in when `isSharedMode` is true. When the host runs
|
|
58
|
+
* with the default per-project DB layout the schema does not expose the
|
|
59
|
+
* field at all, which keeps the tool surface honest about what is
|
|
60
|
+
* actionable in that mode.
|
|
61
|
+
*/
|
|
62
|
+
export function buildCtxSearchInputSchema(isSharedMode) {
|
|
63
|
+
const projectField = isSharedMode
|
|
64
|
+
? {
|
|
65
|
+
project: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Project scope. " +
|
|
69
|
+
"Default (omit): this session's project — auto-resolved from the host adapter. " +
|
|
70
|
+
"'global': span every project in the shared store (cross-project recall). " +
|
|
71
|
+
"<absolute-path>: scope to that specific project directory."),
|
|
72
|
+
}
|
|
73
|
+
: {};
|
|
74
|
+
return z.object({
|
|
75
|
+
queries: z.preprocess(coerceJsonArray, z
|
|
76
|
+
.array(z.string())
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Array of search queries. Batch ALL questions in one call.")),
|
|
79
|
+
// limit: z.coerce.number() (not z.number()) — OpenCode's native
|
|
80
|
+
// plugin path delivers tool args straight from the LLM provider's
|
|
81
|
+
// tool-call JSON, where several providers stringify primitives
|
|
82
|
+
// (limit:"4" instead of limit:4). Since v1.0.139 / #621 we run
|
|
83
|
+
// inputSchema.parse() on that path, so a plain z.number() rejects
|
|
84
|
+
// "4" with "Expected number, received string". z.coerce mirrors what
|
|
85
|
+
// ctx_batch_execute / ctx_fetch_and_index / ctx_execute already do.
|
|
86
|
+
// Fixes #627.
|
|
87
|
+
limit: z
|
|
88
|
+
.coerce.number()
|
|
89
|
+
.optional()
|
|
90
|
+
.default(3)
|
|
91
|
+
.describe("Results per query (default: 3)"),
|
|
92
|
+
source: z
|
|
93
|
+
.string()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Filter to a specific indexed source (partial match)."),
|
|
96
|
+
contentType: z
|
|
97
|
+
.enum(["code", "prose"])
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Filter results by content type: 'code' or 'prose'."),
|
|
100
|
+
sort: z
|
|
101
|
+
.enum(["relevance", "timeline"])
|
|
102
|
+
.optional()
|
|
103
|
+
.default("relevance")
|
|
104
|
+
.describe("Sort mode. 'relevance' (default): BM25 ranked, current session only. " +
|
|
105
|
+
"'timeline': chronological across current session, prior sessions, and auto-memory."),
|
|
106
|
+
...projectField,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalise the raw `project` value into the three-state contract consumed
|
|
111
|
+
* by {@link searchAllSources}.
|
|
112
|
+
*
|
|
113
|
+
* - shared mode OFF → `undefined` (param ignored)
|
|
114
|
+
* - shared mode ON, param `undefined` → current project (`getProjectDirFn()`)
|
|
115
|
+
* - shared mode ON, param `"global"` → `null` (no filter — cross-project)
|
|
116
|
+
* - shared mode ON, param `<string>` → that string verbatim
|
|
117
|
+
*
|
|
118
|
+
* The function is pure so it stays trivially testable without spinning up
|
|
119
|
+
* the MCP server.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveProjectScope(raw, isSharedMode, getProjectDirFn) {
|
|
122
|
+
if (!isSharedMode)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (raw === undefined)
|
|
125
|
+
return getProjectDirFn();
|
|
126
|
+
if (raw === "global")
|
|
127
|
+
return null;
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Module-load snapshot of `CONTEXT_MODE_PROJECT_DIR`. Captured once so the
|
|
132
|
+
* tool schema registered with `server.registerTool` reflects the launch
|
|
133
|
+
* environment — the LLM-visible surface should never flip mid-session.
|
|
134
|
+
*/
|
|
135
|
+
export const CTX_SEARCH_SHARED_MODE = !!process.env.CONTEXT_MODE_PROJECT_DIR;
|
|
@@ -31,6 +31,18 @@ export interface SearchAllSourcesOpts {
|
|
|
31
31
|
configDir?: string;
|
|
32
32
|
/** Detected platform adapter — used for adapter-aware auto-memory. */
|
|
33
33
|
adapter?: AutoMemoryAdapter;
|
|
34
|
+
/**
|
|
35
|
+
* Per-project scope for the ContentStore filter (#737). Only honoured
|
|
36
|
+
* when a `sessionDB` is also supplied (the 2-step IN-clause needs the
|
|
37
|
+
* SessionDB to translate `project_dir` → list of session ids).
|
|
38
|
+
*
|
|
39
|
+
* - `undefined` — no project filter, today's behaviour.
|
|
40
|
+
* - `null` — cross-project recall in shared-DB mode (also no filter).
|
|
41
|
+
* - `string` — restrict ContentStore results to chunks attributed to
|
|
42
|
+
* session ids whose events match this `project_dir`,
|
|
43
|
+
* plus legacy `session_id=''` chunks (public surface).
|
|
44
|
+
*/
|
|
45
|
+
projectScope?: string | null;
|
|
34
46
|
}
|
|
35
47
|
/**
|
|
36
48
|
* Search across all available sources.
|
package/build/search/unified.js
CHANGED
|
@@ -20,14 +20,29 @@ const DEBUG = process.env.DEBUG?.includes("context-mode");
|
|
|
20
20
|
* are always returned.
|
|
21
21
|
*/
|
|
22
22
|
export function searchAllSources(opts) {
|
|
23
|
-
const { query, limit, store, sort = "relevance", source, contentType, sessionDB, projectDir, configDir, adapter, } = opts;
|
|
23
|
+
const { query, limit, store, sort = "relevance", source, contentType, sessionDB, projectDir, configDir, adapter, projectScope, } = opts;
|
|
24
24
|
const results = [];
|
|
25
25
|
// Capture session start time once — used as proxy for ContentStore items
|
|
26
26
|
// (we don't know exact indexing time, but all content is from current session)
|
|
27
27
|
const sessionStartTime = new Date().toISOString();
|
|
28
|
+
// ── Project scope (#737) ──
|
|
29
|
+
// Resolve the per-project session-id allow-set ONCE, before the
|
|
30
|
+
// ContentStore call. `projectScope === null` means cross-project recall —
|
|
31
|
+
// an explicit "no filter" choice surfaced by the ctx_search caller — and
|
|
32
|
+
// `undefined` falls back to today's unfiltered behaviour.
|
|
33
|
+
let sessionIdAllowSet;
|
|
34
|
+
if (typeof projectScope === "string" && sessionDB) {
|
|
35
|
+
try {
|
|
36
|
+
sessionIdAllowSet = new Set(sessionDB.getSessionIdsForProject(projectScope));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (DEBUG)
|
|
40
|
+
process.stderr.write(`[ctx] getSessionIdsForProject failed: ${e}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
// ── Source 1: ContentStore (always, both modes) ──
|
|
29
44
|
try {
|
|
30
|
-
const storeResults = store.searchWithFallback(query, limit, source, contentType);
|
|
45
|
+
const storeResults = store.searchWithFallback(query, limit, source, contentType, "like", sessionIdAllowSet);
|
|
31
46
|
results.push(...storeResults.map((r) => ({
|
|
32
47
|
title: r.title,
|
|
33
48
|
content: r.content,
|
package/build/server.d.ts
CHANGED
|
@@ -97,7 +97,8 @@ export declare function getProjectDir(): string;
|
|
|
97
97
|
*/
|
|
98
98
|
export declare function positionsFromHighlight(highlighted: string): number[];
|
|
99
99
|
export declare function extractSnippet(content: string, query: string, maxLen?: number, highlighted?: string): string;
|
|
100
|
-
export
|
|
100
|
+
export type BatchQueryScope = "batch" | "global";
|
|
101
|
+
export declare function formatBatchQueryResults(store: ContentStore, queries: string[], source: string, maxOutput?: number, scope?: BatchQueryScope): string[];
|
|
101
102
|
export interface BatchCommand {
|
|
102
103
|
label: string;
|
|
103
104
|
command: string;
|