context-mode 1.0.151 → 1.0.153
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/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 -3
- package/build/session/analytics.js +88 -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 +370 -319
- 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/cli.js
CHANGED
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
import * as p from "@clack/prompts";
|
|
17
17
|
import color from "picocolors";
|
|
18
18
|
import { execFileSync, execSync, execFile as nodeExecFile } from "node:child_process";
|
|
19
|
-
import { readFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, chmodSync, constants } from "node:fs";
|
|
19
|
+
import { readFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, chmodSync, lstatSync, realpathSync, statSync, constants } from "node:fs";
|
|
20
20
|
import { request as httpsRequest } from "node:https";
|
|
21
|
-
import { resolve, dirname, join } from "node:path";
|
|
21
|
+
import { resolve, dirname, join, sep, basename, isAbsolute } from "node:path";
|
|
22
22
|
import { tmpdir, devNull, homedir } from "node:os";
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
24
|
import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
|
|
25
25
|
import { getHookScriptPaths } from "./util/hook-config.js";
|
|
26
26
|
import { resolveClaudeConfigDir } from "./util/claude-config.js";
|
|
27
27
|
import { ensureWritableStorageDir, formatStorageDirectoryError, resolveContentStorageDir, resolveSessionStorageDir, resolveStatsStorageDir, StorageDirectoryError, } from "./session/db.js";
|
|
28
|
+
import { ContentStore } from "./store.js";
|
|
29
|
+
import { readToolDenyPatterns, evaluateFilePath } from "./security.js";
|
|
28
30
|
// v1.0.128 — Issue #559 sibling MCP kill helpers (see PR-559-560-FIX-DESIGN.md).
|
|
29
31
|
import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mcp.js";
|
|
30
32
|
// v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
|
|
@@ -49,6 +51,7 @@ function browserOpenArgv(url, platform) {
|
|
|
49
51
|
}
|
|
50
52
|
// ── Adapter imports ──────────────────────────────────────
|
|
51
53
|
import { detectPlatform, getAdapter } from "./adapters/detect.js";
|
|
54
|
+
import { isInProcessPluginPlatform } from "./adapters/types.js";
|
|
52
55
|
/* -------------------------------------------------------
|
|
53
56
|
* Hook dispatcher — `context-mode hook <platform> <event>`
|
|
54
57
|
* ------------------------------------------------------- */
|
|
@@ -98,6 +101,15 @@ const HOOK_MAP = {
|
|
|
98
101
|
precompact: "hooks/jetbrains-copilot/precompact.mjs",
|
|
99
102
|
sessionstart: "hooks/jetbrains-copilot/sessionstart.mjs",
|
|
100
103
|
},
|
|
104
|
+
"kimi": {
|
|
105
|
+
pretooluse: "hooks/kimi/pretooluse.mjs",
|
|
106
|
+
posttooluse: "hooks/kimi/posttooluse.mjs",
|
|
107
|
+
precompact: "hooks/kimi/precompact.mjs",
|
|
108
|
+
sessionstart: "hooks/kimi/sessionstart.mjs",
|
|
109
|
+
sessionend: "hooks/kimi/sessionend.mjs",
|
|
110
|
+
userpromptsubmit: "hooks/kimi/userpromptsubmit.mjs",
|
|
111
|
+
stop: "hooks/kimi/stop.mjs",
|
|
112
|
+
},
|
|
101
113
|
"qwen-code": {
|
|
102
114
|
pretooluse: "hooks/pretooluse.mjs",
|
|
103
115
|
posttooluse: "hooks/posttooluse.mjs",
|
|
@@ -128,18 +140,35 @@ async function hookDispatch(platform, event) {
|
|
|
128
140
|
/* -------------------------------------------------------
|
|
129
141
|
* Entry point
|
|
130
142
|
* ------------------------------------------------------- */
|
|
131
|
-
const IN_PROCESS_PLUGIN_PLATFORMS = new Set(["opencode", "kilo"]);
|
|
132
|
-
const isInProcessPluginPlatform = (p) => p ? IN_PROCESS_PLUGIN_PLATFORMS.has(p) : false;
|
|
133
143
|
const args = process.argv.slice(2);
|
|
134
144
|
function printHelp() {
|
|
135
145
|
console.log([
|
|
136
146
|
"Usage:",
|
|
137
147
|
" context-mode Start MCP server (stdio)",
|
|
148
|
+
" context-mode index <path> Index a file or directory into the FTS5 knowledge base",
|
|
149
|
+
" context-mode search <query...> Search the current project's FTS5 knowledge base",
|
|
138
150
|
" context-mode doctor Diagnose runtime issues, hooks, FTS5, version",
|
|
139
151
|
" context-mode upgrade Fix hooks, permissions, and settings",
|
|
140
152
|
" context-mode hook <platform> <event> Dispatch a configured hook script",
|
|
141
153
|
" context-mode statusline Print Claude Code status line",
|
|
142
154
|
"",
|
|
155
|
+
"Index options:",
|
|
156
|
+
" --source <label> Source label (default: project:<directory-name> or path)",
|
|
157
|
+
" --project <path> Project identity for the content DB (default: indexed dir or cwd)",
|
|
158
|
+
" --max-depth <n> Directory recursion depth (default: 5)",
|
|
159
|
+
" --max-files <n> Directory file cap (default: 200)",
|
|
160
|
+
" --ext <.ts,.md> Comma-separated extension allowlist",
|
|
161
|
+
" --include <glob> Directory include pattern (repeatable)",
|
|
162
|
+
" --exclude <glob> Directory exclude pattern (repeatable)",
|
|
163
|
+
" --no-gitignore Do not apply .gitignore during directory walks",
|
|
164
|
+
" --follow-symlinks Follow directory symlinks inside the root",
|
|
165
|
+
"",
|
|
166
|
+
"Search options:",
|
|
167
|
+
" --project <path> Project identity for the content DB (default: cwd)",
|
|
168
|
+
" --source <label> Filter to a source label (partial match)",
|
|
169
|
+
" --limit <n> Results to show (default: 3)",
|
|
170
|
+
" --type <code|prose> Filter by content type",
|
|
171
|
+
"",
|
|
143
172
|
"Environment:",
|
|
144
173
|
" CONTEXT_MODE_DIR=/absolute/path Override sessions/content storage root; empty is ignored, non-empty must be absolute",
|
|
145
174
|
].join("\n"));
|
|
@@ -147,6 +176,12 @@ function printHelp() {
|
|
|
147
176
|
if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
148
177
|
printHelp();
|
|
149
178
|
}
|
|
179
|
+
else if (args[0] === "index") {
|
|
180
|
+
indexCommand(args.slice(1)).then((code) => process.exit(code));
|
|
181
|
+
}
|
|
182
|
+
else if (args[0] === "search") {
|
|
183
|
+
searchCommand(args.slice(1)).then((code) => process.exit(code));
|
|
184
|
+
}
|
|
150
185
|
else if (args[0] === "doctor") {
|
|
151
186
|
doctor().then((code) => process.exit(code));
|
|
152
187
|
}
|
|
@@ -300,6 +335,206 @@ async function fetchLatestVersion() {
|
|
|
300
335
|
function describeStorageSource(dir) {
|
|
301
336
|
return dir.envVar ? dir.envVar : "adapter default";
|
|
302
337
|
}
|
|
338
|
+
function parseFlags(argv) {
|
|
339
|
+
const positional = [];
|
|
340
|
+
const flags = {};
|
|
341
|
+
for (let i = 0; i < argv.length; i++) {
|
|
342
|
+
const arg = argv[i];
|
|
343
|
+
if (!arg.startsWith("--") || arg === "--") {
|
|
344
|
+
positional.push(arg);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const raw = arg.slice(2);
|
|
348
|
+
const eq = raw.indexOf("=");
|
|
349
|
+
const key = eq >= 0 ? raw.slice(0, eq) : raw;
|
|
350
|
+
const inlineValue = eq >= 0 ? raw.slice(eq + 1) : undefined;
|
|
351
|
+
const next = argv[i + 1];
|
|
352
|
+
const value = inlineValue !== undefined
|
|
353
|
+
? inlineValue
|
|
354
|
+
: next && !next.startsWith("--")
|
|
355
|
+
? (i++, next)
|
|
356
|
+
: true;
|
|
357
|
+
if (key === "include" || key === "exclude") {
|
|
358
|
+
const prev = flags[key];
|
|
359
|
+
flags[key] = Array.isArray(prev) ? [...prev, String(value)] : [String(value)];
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
flags[key] = value;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { positional, flags };
|
|
366
|
+
}
|
|
367
|
+
function stringFlag(flags, key) {
|
|
368
|
+
const v = flags[key];
|
|
369
|
+
if (typeof v === "string" && v.length > 0)
|
|
370
|
+
return v;
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
function boolFlag(flags, key) {
|
|
374
|
+
return flags[key] === true || flags[key] === "true";
|
|
375
|
+
}
|
|
376
|
+
function stringListFlag(flags, key) {
|
|
377
|
+
const v = flags[key];
|
|
378
|
+
if (Array.isArray(v))
|
|
379
|
+
return v.filter(Boolean);
|
|
380
|
+
if (typeof v === "string" && v.length > 0)
|
|
381
|
+
return [v];
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
function numberFlag(flags, key, opts = {}) {
|
|
385
|
+
const raw = stringFlag(flags, key);
|
|
386
|
+
if (!raw)
|
|
387
|
+
return undefined;
|
|
388
|
+
const n = Number(raw);
|
|
389
|
+
const min = opts.min ?? 1;
|
|
390
|
+
if (!Number.isInteger(n) || n < min)
|
|
391
|
+
throw new Error(`--${key} must be an integer >= ${min}`);
|
|
392
|
+
return n;
|
|
393
|
+
}
|
|
394
|
+
function extFlag(flags) {
|
|
395
|
+
const raw = stringFlag(flags, "ext") ?? stringFlag(flags, "extensions");
|
|
396
|
+
if (!raw)
|
|
397
|
+
return undefined;
|
|
398
|
+
const exts = raw
|
|
399
|
+
.split(",")
|
|
400
|
+
.map((x) => x.trim())
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.map((x) => (x.startsWith(".") ? x : `.${x}`));
|
|
403
|
+
return exts.length > 0 ? exts : undefined;
|
|
404
|
+
}
|
|
405
|
+
function resolveCliProjectDir(projectFlag, fallback) {
|
|
406
|
+
if (projectFlag)
|
|
407
|
+
return resolve(projectFlag);
|
|
408
|
+
return resolve(fallback);
|
|
409
|
+
}
|
|
410
|
+
async function openCliContentStore(projectDir) {
|
|
411
|
+
const adapter = await getAdapter(detectPlatform().platform);
|
|
412
|
+
const contentStorage = resolveContentStorageDir(() => adapter.getSessionDir());
|
|
413
|
+
const contentDir = ensureWritableStorageDir(contentStorage);
|
|
414
|
+
const { resolveContentStorePath } = await import("./session/db.js");
|
|
415
|
+
const dbPath = resolveContentStorePath({ projectDir, contentDir });
|
|
416
|
+
return { store: new ContentStore(dbPath), dbPath, contentDir };
|
|
417
|
+
}
|
|
418
|
+
function defaultSourceForPath(absPath) {
|
|
419
|
+
try {
|
|
420
|
+
if (statSync(absPath).isDirectory())
|
|
421
|
+
return `project:${basename(absPath) || absPath}`;
|
|
422
|
+
}
|
|
423
|
+
catch { /* path errors are reported by the index command */ }
|
|
424
|
+
return absPath;
|
|
425
|
+
}
|
|
426
|
+
function assertReadAllowed(path, projectDir) {
|
|
427
|
+
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
428
|
+
const denied = evaluateFilePath(path, denyGlobs, process.platform === "win32", projectDir);
|
|
429
|
+
if (denied.denied) {
|
|
430
|
+
throw new Error(`Read denied by policy: ${path}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function indexCommand(argv) {
|
|
434
|
+
try {
|
|
435
|
+
const parsed = parseFlags(argv);
|
|
436
|
+
const target = parsed.positional[0];
|
|
437
|
+
if (!target || target === "-h" || target === "--help") {
|
|
438
|
+
console.log("Usage: context-mode index <path> [--source label] [--project path] [--max-files n] [--max-depth n] [--ext .ts,.md]");
|
|
439
|
+
return target ? 0 : 1;
|
|
440
|
+
}
|
|
441
|
+
const absPath = isAbsolute(target) ? resolve(target) : resolve(process.cwd(), target);
|
|
442
|
+
if (!existsSync(absPath))
|
|
443
|
+
throw new Error(`Path does not exist: ${absPath}`);
|
|
444
|
+
const st = statSync(absPath);
|
|
445
|
+
const projectDir = resolveCliProjectDir(stringFlag(parsed.flags, "project"), st.isDirectory() ? absPath : dirname(absPath));
|
|
446
|
+
const source = stringFlag(parsed.flags, "source") ?? defaultSourceForPath(absPath);
|
|
447
|
+
const { store, dbPath } = await openCliContentStore(projectDir);
|
|
448
|
+
try {
|
|
449
|
+
assertReadAllowed(absPath, projectDir);
|
|
450
|
+
if (st.isDirectory()) {
|
|
451
|
+
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
452
|
+
const result = store.indexDirectory({
|
|
453
|
+
path: absPath,
|
|
454
|
+
source,
|
|
455
|
+
include: stringListFlag(parsed.flags, "include"),
|
|
456
|
+
exclude: stringListFlag(parsed.flags, "exclude"),
|
|
457
|
+
maxDepth: numberFlag(parsed.flags, "max-depth", { min: 0 }),
|
|
458
|
+
maxFiles: numberFlag(parsed.flags, "max-files"),
|
|
459
|
+
extensions: extFlag(parsed.flags),
|
|
460
|
+
respectGitignore: !boolFlag(parsed.flags, "no-gitignore"),
|
|
461
|
+
followSymlinks: boolFlag(parsed.flags, "follow-symlinks"),
|
|
462
|
+
perFileDeny: (filePath) => {
|
|
463
|
+
try {
|
|
464
|
+
return evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir).denied;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
const cap = result.capped ? ` (cap reached at ${result.filesIndexed} files)` : "";
|
|
472
|
+
const denied = result.denied > 0 ? `; ${result.denied} denied` : "";
|
|
473
|
+
const failed = result.failed > 0 ? `; ${result.failed} failed` : "";
|
|
474
|
+
console.log(`Indexed ${result.filesIndexed} files (${result.totalChunks} sections) from ${absPath}${cap}${denied}${failed}`);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const result = store.index({ path: absPath, source });
|
|
478
|
+
console.log(`Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from ${absPath}`);
|
|
479
|
+
}
|
|
480
|
+
console.log(`Source: ${source}`);
|
|
481
|
+
console.log(`Project: ${projectDir}`);
|
|
482
|
+
console.log(`DB: ${dbPath}`);
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
store.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
491
|
+
console.error(`context-mode index: ${message}`);
|
|
492
|
+
return 1;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function searchCommand(argv) {
|
|
496
|
+
try {
|
|
497
|
+
const parsed = parseFlags(argv);
|
|
498
|
+
const query = parsed.positional.join(" ").trim();
|
|
499
|
+
if (!query || query === "-h" || query === "--help") {
|
|
500
|
+
console.log("Usage: context-mode search <query...> [--source label] [--project path] [--limit n] [--type code|prose]");
|
|
501
|
+
return query ? 0 : 1;
|
|
502
|
+
}
|
|
503
|
+
const projectDir = resolveCliProjectDir(stringFlag(parsed.flags, "project"), process.cwd());
|
|
504
|
+
const { store, dbPath } = await openCliContentStore(projectDir);
|
|
505
|
+
try {
|
|
506
|
+
const limit = numberFlag(parsed.flags, "limit") ?? 3;
|
|
507
|
+
const type = stringFlag(parsed.flags, "type");
|
|
508
|
+
if (type && type !== "code" && type !== "prose")
|
|
509
|
+
throw new Error("--type must be code or prose");
|
|
510
|
+
const results = store.searchWithFallback(query, limit, stringFlag(parsed.flags, "source"), type);
|
|
511
|
+
if (results.length === 0) {
|
|
512
|
+
console.log(`No matches for: ${query}`);
|
|
513
|
+
console.log(`Project: ${projectDir}`);
|
|
514
|
+
console.log(`DB: ${dbPath}`);
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
for (const [i, r] of results.entries()) {
|
|
518
|
+
const content = r.content.replace(/\s+/g, " ").trim();
|
|
519
|
+
const snippet = content.length > 500 ? `${content.slice(0, 500)}...` : content;
|
|
520
|
+
console.log(`## ${i + 1}. ${r.title}`);
|
|
521
|
+
console.log(`Source: ${r.source}`);
|
|
522
|
+
console.log(`Type: ${r.contentType}`);
|
|
523
|
+
console.log(snippet);
|
|
524
|
+
console.log("");
|
|
525
|
+
}
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
store.close();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
534
|
+
console.error(`context-mode search: ${message}`);
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
303
538
|
function logStorageDir(dir) {
|
|
304
539
|
try {
|
|
305
540
|
ensureWritableStorageDir(dir);
|
|
@@ -1019,12 +1254,55 @@ async function upgrade(opts) {
|
|
|
1019
1254
|
...(clonedPkg.files || []),
|
|
1020
1255
|
"src", "package.json",
|
|
1021
1256
|
];
|
|
1257
|
+
// Supply-chain containment on items[]. A compromised upstream tag
|
|
1258
|
+
// shipping files: ["../../.ssh/authorized_keys"] or an absolute
|
|
1259
|
+
// path would, without a guard, hand rmSync+cpSync an arbitrary
|
|
1260
|
+
// destination under the user's UID. resolve(P, "/abs") discards P,
|
|
1261
|
+
// so the absolute-path variant escapes too. Reject items whose
|
|
1262
|
+
// resolved path escapes either srcDir or pluginRoot. Mirrors the
|
|
1263
|
+
// pattern hooks/heal-partial-install.mjs already uses for its own
|
|
1264
|
+
// files[] expansion (PR #699).
|
|
1265
|
+
//
|
|
1266
|
+
// Also refuse to copy any symlink encountered anywhere under a
|
|
1267
|
+
// source item. cpSync's default is to preserve source symlinks as
|
|
1268
|
+
// destination symlinks; a compromised upstream tag committing a
|
|
1269
|
+
// symlink to /etc inside src/ would plant that link in pluginRoot,
|
|
1270
|
+
// and the next Claude Code session that loads pluginRoot/src/*
|
|
1271
|
+
// would dereference through to the attacker target. Filtering at
|
|
1272
|
+
// copy time keeps pluginRoot symlink-free regardless of what the
|
|
1273
|
+
// clone shipped.
|
|
1274
|
+
const pluginRootWithSep = resolve(pluginRoot) + sep;
|
|
1275
|
+
const srcDirWithSep = resolve(srcDir) + sep;
|
|
1276
|
+
const refuseSymlinks = (src) => {
|
|
1277
|
+
try {
|
|
1278
|
+
return !lstatSync(src).isSymbolicLink();
|
|
1279
|
+
}
|
|
1280
|
+
catch {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1022
1284
|
for (const item of items) {
|
|
1285
|
+
const from = resolve(srcDir, item);
|
|
1286
|
+
const to = resolve(pluginRoot, item);
|
|
1287
|
+
if (!(to + sep).startsWith(pluginRootWithSep))
|
|
1288
|
+
continue;
|
|
1289
|
+
if (!(from + sep).startsWith(srcDirWithSep))
|
|
1290
|
+
continue;
|
|
1291
|
+
if (!refuseSymlinks(from))
|
|
1292
|
+
continue;
|
|
1293
|
+
// Existence-check the source BEFORE the rm so a `files[]` entry that
|
|
1294
|
+
// doesn't exist in srcDir can never delete-without-replace at
|
|
1295
|
+
// pluginRoot. The catch-all below swallows cpSync failures too, and
|
|
1296
|
+
// a swallowed cp after a successful rm is exactly how a partial
|
|
1297
|
+
// install lands silently. Mirrors the safe pattern in
|
|
1298
|
+
// server.ts's inline-fallback upgrade path (PR #699).
|
|
1299
|
+
if (!existsSync(from))
|
|
1300
|
+
continue;
|
|
1023
1301
|
try {
|
|
1024
|
-
rmSync(
|
|
1025
|
-
cpSync(
|
|
1302
|
+
rmSync(to, { recursive: true, force: true });
|
|
1303
|
+
cpSync(from, to, { recursive: true, filter: refuseSymlinks });
|
|
1026
1304
|
}
|
|
1027
|
-
catch { /*
|
|
1305
|
+
catch { /* best effort, next /ctx-upgrade retries */ }
|
|
1028
1306
|
}
|
|
1029
1307
|
// Issue #609 — DO NOT write `.mcp.json` into the plugin cache dir.
|
|
1030
1308
|
//
|
|
@@ -1044,24 +1322,78 @@ async function upgrade(opts) {
|
|
|
1044
1322
|
// historically was to be a write-time poison vector. Don't write it.
|
|
1045
1323
|
// The post-bump cache-sweep below removes any pre-existing copies so
|
|
1046
1324
|
// the previous-version-carry vector cannot replay.
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1325
|
+
// Issue #711 + #414 split: normalize hooks.json (only) here.
|
|
1326
|
+
//
|
|
1327
|
+
// - plugin.json must NOT be normalized during /ctx-upgrade — Claude
|
|
1328
|
+
// Code carries it forward into new versioned cache dirs on
|
|
1329
|
+
// auto-update, so baked absolute paths go stale (#711).
|
|
1330
|
+
// - hooks/hooks.json MUST be normalized during /ctx-upgrade on
|
|
1331
|
+
// Windows + Git Bash — Claude Code fires SessionStart / PreToolUse
|
|
1332
|
+
// BEFORE the MCP server boots, so the unresolved
|
|
1333
|
+
// `${CLAUDE_PLUGIN_ROOT}` placeholder yields MODULE_NOT_FOUND for
|
|
1334
|
+
// the first hook fire after upgrade (#414, originally wired in
|
|
1335
|
+
// 13d1342 / #528).
|
|
1336
|
+
//
|
|
1337
|
+
// The narrow `normalizeHooksJsonOnly` helper preserves both invariants.
|
|
1338
|
+
// start.mjs continues to call the full `normalizeHooksOnStartup` at the
|
|
1339
|
+
// next MCP boot to re-heal plugin.json against the live __dirname.
|
|
1054
1340
|
try {
|
|
1341
|
+
// #738: pass the resolved Bun ≥1.0 path so /ctx-upgrade's hooks.json
|
|
1342
|
+
// rewrite gains the same cold-start win as the boot-time rewrite.
|
|
1343
|
+
// Probe failures fall through to nodePath default.
|
|
1344
|
+
let jsRuntimePath;
|
|
1345
|
+
try {
|
|
1346
|
+
const { resolveHookRuntime } = await import("./runtime.js");
|
|
1347
|
+
const r = resolveHookRuntime();
|
|
1348
|
+
if (r.isBun)
|
|
1349
|
+
jsRuntimePath = r.path;
|
|
1350
|
+
}
|
|
1351
|
+
catch { /* best effort */ }
|
|
1055
1352
|
const mod =
|
|
1056
1353
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1057
1354
|
(await import("../hooks/normalize-hooks.mjs"));
|
|
1058
|
-
mod.
|
|
1355
|
+
mod.normalizeHooksJsonOnly({
|
|
1059
1356
|
pluginRoot,
|
|
1060
1357
|
nodePath: process.execPath,
|
|
1358
|
+
jsRuntimePath,
|
|
1061
1359
|
platform: process.platform,
|
|
1062
1360
|
});
|
|
1063
1361
|
}
|
|
1064
1362
|
catch { /* best effort — never block upgrade */ }
|
|
1363
|
+
// Issue #710 — Layer 1: rewrite stale shell-snapshot PATH entries.
|
|
1364
|
+
//
|
|
1365
|
+
// Claude Code's per-session shell snapshot
|
|
1366
|
+
// (~/.claude/shell-snapshots/snapshot-*.sh, baked at session boot —
|
|
1367
|
+
// refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336)
|
|
1368
|
+
// is `source`d before every Bash tool call. It contains an
|
|
1369
|
+
// `export PATH='…'` line including the context-mode `bin/` for the
|
|
1370
|
+
// version active at session start. /ctx-upgrade deletes the old
|
|
1371
|
+
// cache dir mid-session — the snapshot still points at it, so every
|
|
1372
|
+
// Bash call fails with "Plugin directory does not exist" until the
|
|
1373
|
+
// session restarts. Layer 1 fixes the active session immediately;
|
|
1374
|
+
// Layer 2 (sessionstart.mjs) heals any session that started before
|
|
1375
|
+
// /ctx-upgrade ran.
|
|
1376
|
+
//
|
|
1377
|
+
// claude-code only — no other adapter uses shell-snapshots. Skip
|
|
1378
|
+
// when running under a non-claude-code adapter (Codex/Cursor/Gemini
|
|
1379
|
+
// etc. spawn Bash differently and have no `~/.claude/shell-snapshots`
|
|
1380
|
+
// tree). Best-effort, idempotent, never throws.
|
|
1381
|
+
try {
|
|
1382
|
+
if (detection.platform === "claude-code") {
|
|
1383
|
+
const { rewriteShellSnapshots } = await import(
|
|
1384
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1385
|
+
"../hooks/cache-heal-utils.mjs");
|
|
1386
|
+
const snapshotsDir = resolve(resolveClaudeConfigDir(), "shell-snapshots");
|
|
1387
|
+
const result = rewriteShellSnapshots({
|
|
1388
|
+
snapshotsDir,
|
|
1389
|
+
currentVersion: newVersion,
|
|
1390
|
+
});
|
|
1391
|
+
if (result.rewritten.length > 0) {
|
|
1392
|
+
p.log.info(color.dim(` Healed ${result.rewritten.length} stale shell snapshot(s) — Bash tool calls in the active session will pick up v${newVersion} immediately`));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
catch { /* best effort — never block upgrade */ }
|
|
1065
1397
|
s.stop(color.green(`Updated in-place to v${newVersion}`));
|
|
1066
1398
|
// v1.0.114 hotfix — pre-flight: verify the in-place copy actually
|
|
1067
1399
|
// wrote a plugin.json carrying newVersion BEFORE we tell the
|
|
@@ -1292,19 +1624,61 @@ async function upgrade(opts) {
|
|
|
1292
1624
|
// Issue #460 round-3: honor $CLAUDE_CONFIG_DIR so the registry lookup
|
|
1293
1625
|
// tracks relocated CC config trees.
|
|
1294
1626
|
try {
|
|
1295
|
-
const
|
|
1627
|
+
const claudeRoot = resolveClaudeConfigDir();
|
|
1628
|
+
const registryPath = resolve(claudeRoot, "plugins", "installed_plugins.json");
|
|
1296
1629
|
if (existsSync(registryPath)) {
|
|
1630
|
+
// The registry's installPath fields are written by Claude Code under
|
|
1631
|
+
// <claudeRoot>/plugins/cache/<marketplace>/<plugin>/<version>. Any other
|
|
1632
|
+
// shape means the registry has been tampered with by a co-resident
|
|
1633
|
+
// plugin, a malicious postinstall script, or another local actor.
|
|
1634
|
+
// Without containment, cpSync would happily recursive-write the in-repo
|
|
1635
|
+
// skills/ tree to /etc/skills, ~/.ssh/skills, or wherever the attacker
|
|
1636
|
+
// pointed. server.ts:790 (healCacheMidSession) already gates the same
|
|
1637
|
+
// field this way; the symmetric guard belongs here too.
|
|
1638
|
+
//
|
|
1639
|
+
// The lexical resolve+startsWith check rejects ".."-escapes and
|
|
1640
|
+
// absolute paths outside cacheRoot, but path.resolve doesn't
|
|
1641
|
+
// dereference symlinks. A same-uid actor who can plant a symlink
|
|
1642
|
+
// AT <cacheRoot>/<owner>/<plugin>/<version> targeting an attacker
|
|
1643
|
+
// dir gets past the lexical guard, then cpSync follows the link at
|
|
1644
|
+
// FS-write time. Re-check via realpathSync so a planted symlink
|
|
1645
|
+
// anchor fails the gate.
|
|
1646
|
+
const cacheRoot = resolve(claudeRoot, "plugins", "cache");
|
|
1647
|
+
let cacheRootCanon;
|
|
1648
|
+
try {
|
|
1649
|
+
cacheRootCanon = realpathSync(cacheRoot);
|
|
1650
|
+
}
|
|
1651
|
+
catch {
|
|
1652
|
+
cacheRootCanon = cacheRoot;
|
|
1653
|
+
}
|
|
1654
|
+
const cacheRootWithSep = cacheRootCanon + sep;
|
|
1297
1655
|
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
1298
1656
|
const entries = registry?.plugins?.["context-mode@context-mode"];
|
|
1299
1657
|
if (Array.isArray(entries)) {
|
|
1300
1658
|
for (const entry of entries) {
|
|
1301
|
-
const installPath = entry
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1659
|
+
const installPath = entry?.installPath;
|
|
1660
|
+
if (typeof installPath !== "string" || !installPath)
|
|
1661
|
+
continue;
|
|
1662
|
+
if (installPath === pluginRoot)
|
|
1663
|
+
continue;
|
|
1664
|
+
const resolvedInstallPath = resolve(installPath);
|
|
1665
|
+
if (!(resolvedInstallPath + sep).startsWith(cacheRootWithSep))
|
|
1666
|
+
continue;
|
|
1667
|
+
if (!existsSync(resolvedInstallPath))
|
|
1668
|
+
continue;
|
|
1669
|
+
let realInstallPath;
|
|
1670
|
+
try {
|
|
1671
|
+
realInstallPath = realpathSync(resolvedInstallPath);
|
|
1672
|
+
}
|
|
1673
|
+
catch {
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (!(realInstallPath + sep).startsWith(cacheRootWithSep))
|
|
1677
|
+
continue;
|
|
1678
|
+
const srcSkills = resolve(srcDir, "skills");
|
|
1679
|
+
if (existsSync(srcSkills)) {
|
|
1680
|
+
cpSync(srcSkills, resolve(realInstallPath, "skills"), { recursive: true });
|
|
1681
|
+
changes.push(`Synced skills to active install path`);
|
|
1308
1682
|
}
|
|
1309
1683
|
}
|
|
1310
1684
|
}
|
|
@@ -1452,14 +1826,48 @@ function statuslineForward() {
|
|
|
1452
1826
|
try {
|
|
1453
1827
|
const registryPath = resolve(claudeRoot, "plugins", "installed_plugins.json");
|
|
1454
1828
|
if (existsSync(registryPath)) {
|
|
1829
|
+
// Same trust boundary as the cpSync site in upgrade() and as
|
|
1830
|
+
// server.ts:790's healCacheMidSession: only honor installPath values
|
|
1831
|
+
// that resolve under <claudeRoot>/plugins/cache. A stray /etc or
|
|
1832
|
+
// ~/.ssh entry written by another local actor must not become the
|
|
1833
|
+
// script the statusline forwarder imports, since statusline re-fires
|
|
1834
|
+
// several times per second and would hand the attacker durable RCE
|
|
1835
|
+
// on the user's behalf.
|
|
1836
|
+
//
|
|
1837
|
+
// path.resolve is purely lexical, so a same-uid actor who can plant
|
|
1838
|
+
// a symlink at <cacheRoot>/<owner>/<plugin>/<version> targeting an
|
|
1839
|
+
// attacker dir would pass the lexical gate. Re-check via
|
|
1840
|
+
// realpathSync so the dynamic-import target's actual on-disk
|
|
1841
|
+
// location also stays under cacheRoot.
|
|
1842
|
+
const cacheRoot = resolve(claudeRoot, "plugins", "cache");
|
|
1843
|
+
let cacheRootCanon;
|
|
1844
|
+
try {
|
|
1845
|
+
cacheRootCanon = realpathSync(cacheRoot);
|
|
1846
|
+
}
|
|
1847
|
+
catch {
|
|
1848
|
+
cacheRootCanon = cacheRoot;
|
|
1849
|
+
}
|
|
1850
|
+
const cacheRootWithSep = cacheRootCanon + sep;
|
|
1455
1851
|
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
1456
1852
|
const entries = registry?.plugins?.["context-mode@context-mode"];
|
|
1457
1853
|
if (Array.isArray(entries)) {
|
|
1458
1854
|
for (const entry of entries) {
|
|
1459
1855
|
const installPath = entry?.installPath;
|
|
1460
|
-
if (typeof installPath
|
|
1461
|
-
|
|
1856
|
+
if (typeof installPath !== "string" || !installPath)
|
|
1857
|
+
continue;
|
|
1858
|
+
const resolvedInstallPath = resolve(installPath);
|
|
1859
|
+
if (!(resolvedInstallPath + sep).startsWith(cacheRootWithSep))
|
|
1860
|
+
continue;
|
|
1861
|
+
let realInstallPath;
|
|
1862
|
+
try {
|
|
1863
|
+
realInstallPath = realpathSync(resolvedInstallPath);
|
|
1864
|
+
}
|
|
1865
|
+
catch {
|
|
1866
|
+
continue;
|
|
1462
1867
|
}
|
|
1868
|
+
if (!(realInstallPath + sep).startsWith(cacheRootWithSep))
|
|
1869
|
+
continue;
|
|
1870
|
+
candidates.push(resolve(realInstallPath, "bin", "statusline.mjs"));
|
|
1463
1871
|
}
|
|
1464
1872
|
}
|
|
1465
1873
|
}
|
package/build/executor.js
CHANGED
|
@@ -29,9 +29,12 @@ const SCRIPT_EXT = {
|
|
|
29
29
|
export function buildScriptFilename(language, platform, shellPath) {
|
|
30
30
|
if (platform === "win32" && language === "shell") {
|
|
31
31
|
const shellName = shellPath?.toLowerCase() ?? "";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (shellName.includes("powershell") || shellName.includes("pwsh"))
|
|
33
|
+
return "script.ps1";
|
|
34
|
+
const shellBase = shellName.split(/[\\/]/).pop() ?? shellName;
|
|
35
|
+
if (shellBase === "cmd" || shellBase === "cmd.exe")
|
|
36
|
+
return "script.cmd";
|
|
37
|
+
return "script";
|
|
35
38
|
}
|
|
36
39
|
return `script.${SCRIPT_EXT[language]}`;
|
|
37
40
|
}
|