context-mode 1.0.137 → 1.0.139

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.137"
9
+ "version": "1.0.139"
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.137",
16
+ "version": "1.0.139",
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.137",
3
+ "version": "1.0.139",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.137",
3
+ "version": "1.0.139",
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.137",
6
+ "version": "1.0.139",
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.137",
3
+ "version": "1.0.139",
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",
package/README.md CHANGED
@@ -439,6 +439,8 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
439
439
 
440
440
  **Verify:** In the OpenCode session, type `ctx stats`. Context-mode tools should appear and respond.
441
441
 
442
+ **Upgrade note:** If an existing config still has `mcp.context-mode`, run `context-mode upgrade`. OpenCode now gets `ctx_*` tools from the plugin; the upgrade removes only `mcp.context-mode` and preserves any other MCP servers.
443
+
442
444
  **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
443
445
 
444
446
  > **Note:** OpenCode lacks a real SessionStart hook ([#14808](https://github.com/sst/opencode/issues/14808), [#5409](https://github.com/sst/opencode/issues/5409)). The plugin uses `experimental.chat.system.transform` as a surrogate — it injects both the routing block and resume snapshots into the system prompt. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
@@ -481,6 +483,8 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
481
483
 
482
484
  **Verify:** In the KiloCode session, type `ctx stats`. Context-mode tools should appear and respond.
483
485
 
486
+ **Upgrade note:** If an existing config still has `mcp.context-mode`, run `context-mode upgrade`. KiloCode now gets `ctx_*` tools from the plugin; the upgrade removes only `mcp.context-mode` and preserves any other MCP servers.
487
+
484
488
  **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
485
489
 
486
490
  > **Note:** KiloCode shares the same plugin architecture as OpenCode, using the OpenCodeAdapter with platform-specific configuration paths (`kilo.json` instead of `opencode.json`, `~/.config/kilo/` instead of `~/.config/opencode/`). Like OpenCode, it lacks a real SessionStart hook — the plugin uses `experimental.chat.system.transform` as a surrogate. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
@@ -1190,7 +1194,7 @@ Tool call output can be collapsed/expanded with the default Pi's default keybind
1190
1194
 
1191
1195
  | Feature | Claude Code | Qwen Code | Gemini CLI | VS Code Copilot | JetBrains Copilot | Cursor | OpenCode | KiloCode | OpenClaw | Codex CLI | Antigravity | Kiro | Zed | Pi | OMP |
1192
1196
  |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
1193
- | MCP Server | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
1197
+ | MCP Server / Native Tools | Yes | Yes | Yes | Yes | Yes | Yes | Native plugin | Native plugin | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
1194
1198
  | PreToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
1195
1199
  | PostToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
1196
1200
  | SessionStart Hook | Yes | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | Yes (extension) | Plugin |
@@ -45,7 +45,15 @@ export declare function isContextModeHook(entry: {
45
45
  }, hookType: HookType): boolean;
46
46
  /**
47
47
  * Build the hook command string for a given hook type.
48
- * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
49
- * Falls back to CLI dispatcher if pluginRoot is not provided.
48
+ *
49
+ * Always emits the CLI dispatcher form
50
+ * (`context-mode hook jetbrains-copilot <event>`) — the `pluginRoot`
51
+ * argument is accepted for API compatibility but intentionally ignored.
52
+ *
53
+ * Same Tier C contract as VS Code Copilot (Issue #613):
54
+ * `.github/hooks/context-mode.json` is workspace-committed (team-shared
55
+ * via git). Embedding `process.execPath` or absolute pluginRoot paths
56
+ * leaks PII and breaks cross-machine portability. See
57
+ * src/adapters/vscode-copilot/hooks.ts for the full archaeology.
50
58
  */
51
- export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
59
+ export declare function buildHookCommand(hookType: HookType, _pluginRoot?: string): string;
@@ -1,4 +1,3 @@
1
- import { buildNodeCommand } from "../types.js";
2
1
  /**
3
2
  * adapters/jetbrains-copilot/hooks — JetBrains Copilot hook definitions and matchers.
4
3
  *
@@ -68,16 +67,21 @@ export function isContextModeHook(entry, hookType) {
68
67
  }
69
68
  /**
70
69
  * Build the hook command string for a given hook type.
71
- * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
72
- * Falls back to CLI dispatcher if pluginRoot is not provided.
70
+ *
71
+ * Always emits the CLI dispatcher form
72
+ * (`context-mode hook jetbrains-copilot <event>`) — the `pluginRoot`
73
+ * argument is accepted for API compatibility but intentionally ignored.
74
+ *
75
+ * Same Tier C contract as VS Code Copilot (Issue #613):
76
+ * `.github/hooks/context-mode.json` is workspace-committed (team-shared
77
+ * via git). Embedding `process.execPath` or absolute pluginRoot paths
78
+ * leaks PII and breaks cross-machine portability. See
79
+ * src/adapters/vscode-copilot/hooks.ts for the full archaeology.
73
80
  */
74
- export function buildHookCommand(hookType, pluginRoot) {
81
+ export function buildHookCommand(hookType, _pluginRoot) {
75
82
  const scriptName = HOOK_SCRIPTS[hookType];
76
83
  if (!scriptName) {
77
84
  throw new Error(`No script defined for hook type: ${hookType}`);
78
85
  }
79
- if (pluginRoot) {
80
- return buildNodeCommand(`${pluginRoot}/hooks/jetbrains-copilot/${scriptName}`);
81
- }
82
86
  return `context-mode hook jetbrains-copilot ${hookType.toLowerCase()}`;
83
87
  }
@@ -251,11 +251,18 @@ async function createContextModePlugin(ctx) {
251
251
  const tools = {};
252
252
  for (const registered of mod.REGISTERED_CTX_TOOLS) {
253
253
  const config = registered.config;
254
- const schema = config.inputSchema;
255
- const shape = typeof schema?.shape === "object" && schema.shape !== null
256
- ? schema.shape
257
- : typeof schema?._def?.shape === "function"
258
- ? schema._def.shape()
254
+ // Zod schema object that the MCP framework normally calls
255
+ // safeParseAsync() on before invoking the handler. The native
256
+ // OpenCode plugin path bypasses MCP's transport layer entirely
257
+ // (refs/platforms/opencode/packages/opencode/src/tool/registry.ts:127),
258
+ // so we must parse args here too — otherwise z.preprocess() coercions
259
+ // (coerceCommandsArray / coerceJsonArray in server.ts) and defaults
260
+ // never fire. Fixes #621.
261
+ const inputSchema = config.inputSchema;
262
+ const shape = typeof inputSchema?.shape === "object" && inputSchema.shape !== null
263
+ ? inputSchema.shape
264
+ : typeof inputSchema?._def?.shape === "function"
265
+ ? inputSchema._def.shape()
259
266
  : {};
260
267
  tools[registered.name] = {
261
268
  description: String(config.description ?? ""),
@@ -263,7 +270,24 @@ async function createContextModePlugin(ctx) {
263
270
  async execute(args, toolCtx) {
264
271
  toolCtx.metadata?.({ title: String(config.title ?? registered.name) });
265
272
  const project = toolCtx.directory || projectDir;
266
- const result = await mod.withProjectDirOverride({ projectDir: project, sessionId: toolCtx.sessionID }, async () => registered.handler(args ?? {}));
273
+ // Run the registered Zod schema BEFORE the handler same contract
274
+ // as the MCP SDK (server/mcp.js safeParseAsync at line 174). This
275
+ // applies z.preprocess() coercions, populates .default() values,
276
+ // and produces the validation error the handler expects (#621).
277
+ let parsedArgs = args ?? {};
278
+ if (typeof inputSchema?.parse === "function") {
279
+ try {
280
+ parsedArgs = inputSchema.parse(args ?? {});
281
+ }
282
+ catch (err) {
283
+ // Surface validation failures with a clear, actionable message
284
+ // (mirrors MCP SDK error format) instead of a downstream
285
+ // "x.map is not a function" crash.
286
+ const message = err instanceof Error ? err.message : String(err);
287
+ throw new Error(`Invalid arguments for ${registered.name}: ${message}`);
288
+ }
289
+ }
290
+ const result = await mod.withProjectDirOverride({ projectDir: project, sessionId: toolCtx.sessionID }, async () => registered.handler(parsedArgs));
267
291
  const r = result;
268
292
  const text = Array.isArray(r?.content)
269
293
  ? r.content
@@ -41,7 +41,31 @@ export declare function isContextModeHook(entry: {
41
41
  }, hookType: HookType): boolean;
42
42
  /**
43
43
  * Build the hook command string for a given hook type.
44
- * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
45
- * Falls back to CLI dispatcher if pluginRoot is not provided.
44
+ *
45
+ * Always emits the CLI dispatcher form
46
+ * (`context-mode hook vscode-copilot <event>`) — the `pluginRoot` argument
47
+ * is accepted for API compatibility but intentionally ignored.
48
+ *
49
+ * Why the dispatcher form is mandatory here (Issue #613 — Tier C contract):
50
+ * `.github/hooks/context-mode.json` is a **workspace-committed** file
51
+ * (upstream: refs/platforms/vscode-copilot/assets/prompts/skills/
52
+ * agent-customization/references/hooks.md line 7 — "Workspace
53
+ * (team-shared)"). It lands in every teammate's `git status`. Embedding
54
+ * `process.execPath` or any absolute pluginRoot path here:
55
+ * - Leaks PII (username, `C:/Users/<user>/...` paths).
56
+ * - Breaks cross-machine portability (fnm/nvm/volta/brew shims are
57
+ * per-shell-session ephemeral; the path goes stale immediately on
58
+ * Windows + fnm).
59
+ *
60
+ * Commit `f5c9d02` (2026-03-06) added an absolute-path branch when a
61
+ * pluginRoot was passed. It solved a real PATH-availability bug on
62
+ * Brew/nvm setups by going too far — the CLI then always passes
63
+ * pluginRoot, so the portable form became unreachable in production
64
+ * and every `/ctx-upgrade` baked a non-portable command into the
65
+ * committed config. This reverts to the pre-`f5c9d02` shape.
66
+ *
67
+ * For users without a global install, the recovery path is the same as
68
+ * every other CLI-dispatcher adapter (cursor, codex):
69
+ * `npm install -g context-mode`
46
70
  */
47
- export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
71
+ export declare function buildHookCommand(hookType: HookType, _pluginRoot?: string): string;
@@ -1,4 +1,3 @@
1
- import { buildNodeCommand } from "../types.js";
2
1
  /**
3
2
  * adapters/vscode-copilot/hooks — VS Code Copilot hook definitions and matchers.
4
3
  *
@@ -63,21 +62,37 @@ export function isContextModeHook(entry, hookType) {
63
62
  }
64
63
  /**
65
64
  * Build the hook command string for a given hook type.
66
- * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
67
- * Falls back to CLI dispatcher if pluginRoot is not provided.
65
+ *
66
+ * Always emits the CLI dispatcher form
67
+ * (`context-mode hook vscode-copilot <event>`) — the `pluginRoot` argument
68
+ * is accepted for API compatibility but intentionally ignored.
69
+ *
70
+ * Why the dispatcher form is mandatory here (Issue #613 — Tier C contract):
71
+ * `.github/hooks/context-mode.json` is a **workspace-committed** file
72
+ * (upstream: refs/platforms/vscode-copilot/assets/prompts/skills/
73
+ * agent-customization/references/hooks.md line 7 — "Workspace
74
+ * (team-shared)"). It lands in every teammate's `git status`. Embedding
75
+ * `process.execPath` or any absolute pluginRoot path here:
76
+ * - Leaks PII (username, `C:/Users/<user>/...` paths).
77
+ * - Breaks cross-machine portability (fnm/nvm/volta/brew shims are
78
+ * per-shell-session ephemeral; the path goes stale immediately on
79
+ * Windows + fnm).
80
+ *
81
+ * Commit `f5c9d02` (2026-03-06) added an absolute-path branch when a
82
+ * pluginRoot was passed. It solved a real PATH-availability bug on
83
+ * Brew/nvm setups by going too far — the CLI then always passes
84
+ * pluginRoot, so the portable form became unreachable in production
85
+ * and every `/ctx-upgrade` baked a non-portable command into the
86
+ * committed config. This reverts to the pre-`f5c9d02` shape.
87
+ *
88
+ * For users without a global install, the recovery path is the same as
89
+ * every other CLI-dispatcher adapter (cursor, codex):
90
+ * `npm install -g context-mode`
68
91
  */
69
- export function buildHookCommand(hookType, pluginRoot) {
92
+ export function buildHookCommand(hookType, _pluginRoot) {
70
93
  const scriptName = HOOK_SCRIPTS[hookType];
71
94
  if (!scriptName) {
72
95
  throw new Error(`No script defined for hook type: ${hookType}`);
73
96
  }
74
- if (pluginRoot) {
75
- // v1.0.107 fix — was `${pluginRoot}/hooks/${scriptName}` which resolved to
76
- // the Claude-Code generic hook (`hooks/pretooluse.mjs`) instead of the
77
- // VSCode-specific wrapper at `hooks/vscode-copilot/pretooluse.mjs`. JetBrains
78
- // adapter already had the correct subdir (jetbrains-copilot/hooks.ts:98)
79
- // so this brings VSCode to parity.
80
- return buildNodeCommand(`${pluginRoot}/hooks/vscode-copilot/${scriptName}`);
81
- }
82
97
  return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
83
98
  }
package/build/cli.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import * as p from "@clack/prompts";
15
15
  import color from "picocolors";
16
16
  import { execFileSync, execSync, execFile as nodeExecFile } from "node:child_process";
17
- import { readFileSync, writeFileSync, cpSync, accessSync, existsSync, rmSync, closeSync, openSync, chmodSync, constants } from "node:fs";
17
+ import { readFileSync, cpSync, accessSync, existsSync, readdirSync, 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";
20
20
  import { tmpdir, devNull, homedir } from "node:os";
@@ -27,7 +27,7 @@ import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mc
27
27
  // v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
28
28
  // mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
29
29
  // @ts-expect-error — JS module, no TS declarations
30
- import { healPluginJsonMcpServers, healMcpJsonArgs } from "../scripts/heal-installed-plugins.mjs";
30
+ import { healPluginJsonMcpServers, sweepStaleMcpJson } from "../scripts/heal-installed-plugins.mjs";
31
31
  // @ts-expect-error — JS module, no TS declarations
32
32
  import { detectWindowsVsYear } from "../scripts/heal-better-sqlite3.mjs";
33
33
  // Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
@@ -454,6 +454,166 @@ async function doctor() {
454
454
  p.log.warn(color.yellow("Plugin enabled: WARN") +
455
455
  ` — ${pluginCheck.message}`);
456
456
  }
457
+ // ── Issue #613 — proactive Tier C absolute-path detection ───────────
458
+ // PR #620 fixed `buildHookCommand` for vscode-copilot + jetbrains-copilot
459
+ // so future writes are CLI-dispatcher-shape. But users who ran
460
+ // /ctx-upgrade on v1.0.136 or earlier are still carrying poisoned
461
+ // committable files in their workspace:
462
+ // - `.github/hooks/context-mode.json` (vscode-copilot, team-shared)
463
+ // - `.jetbrains/copilot/hooks.json` (jetbrains-copilot, team-shared)
464
+ // - `.cursor/hooks.json` (cursor, team-shared)
465
+ // Per ISSUE-613-VERDICT §6.1 these are Tier C — workspace-committed
466
+ // cross-machine config. Doctor scans them for absolute paths and
467
+ // fnm_multishells shims; if found, FAIL with `ctx_upgrade` remediation.
468
+ // Per ISSUE-604-VERDICT §11 ("silent-green doctor while hooks are dead
469
+ // is itself a P0 trust bug") — surface poison BEFORE the user hits a
470
+ // runtime failure.
471
+ p.log.step("Checking team-shared hook configs in your workspace...");
472
+ {
473
+ const projectDir = process.cwd();
474
+ const tierCFiles = [
475
+ ".github/hooks/context-mode.json",
476
+ ".cursor/hooks.json",
477
+ ".jetbrains/copilot/hooks.json",
478
+ ];
479
+ let tierCFails = 0;
480
+ let tierCChecked = 0;
481
+ // Detect absolute-path patterns that should never appear in a
482
+ // workspace-committed config. Per Mert's standing Windows-safety rule:
483
+ // handle both `/` and `\\` separators.
484
+ function isAbsoluteOrShimPath(s) {
485
+ // unix absolute
486
+ if (s.startsWith("/"))
487
+ return true;
488
+ // Windows drive-letter absolute (e.g. C:/, C:\)
489
+ if (/^[A-Za-z]:[/\\]/.test(s))
490
+ return true;
491
+ // Windows UNC or escaped-backslash absolute fragments
492
+ if (s.includes("\\\\"))
493
+ return true;
494
+ // fnm shim hint — issue #613 reporter's exact stderr shape
495
+ if (s.includes("fnm_multishells"))
496
+ return true;
497
+ // process.execPath literal baked into JSON
498
+ if (s.includes("process.execPath"))
499
+ return true;
500
+ return false;
501
+ }
502
+ function recurseStrings(node, hit) {
503
+ if (typeof node === "string") {
504
+ hit(node);
505
+ }
506
+ else if (Array.isArray(node)) {
507
+ for (const item of node)
508
+ recurseStrings(item, hit);
509
+ }
510
+ else if (node && typeof node === "object") {
511
+ for (const v of Object.values(node))
512
+ recurseStrings(v, hit);
513
+ }
514
+ }
515
+ for (const rel of tierCFiles) {
516
+ const abs = resolve(projectDir, rel);
517
+ if (!existsSync(abs))
518
+ continue; // missing config → SKIP, no false fail
519
+ tierCChecked++;
520
+ try {
521
+ const parsed = JSON.parse(readFileSync(abs, "utf-8"));
522
+ const offenders = [];
523
+ recurseStrings(parsed, (s) => {
524
+ if (isAbsoluteOrShimPath(s))
525
+ offenders.push(s);
526
+ });
527
+ if (offenders.length > 0) {
528
+ criticalFails++;
529
+ tierCFails++;
530
+ // Truncate to one example to keep output readable; show count.
531
+ const example = offenders[0].length > 100
532
+ ? offenders[0].slice(0, 97) + "..."
533
+ : offenders[0];
534
+ p.log.error(color.red(`Hook config: FAIL`) +
535
+ ` — ${rel} has your machine's local paths baked in` +
536
+ color.dim("\n This file is committed to git, so teammates and CI will get your path and the hooks will break for them." +
537
+ `\n Found ${offenders.length} hard-coded path(s), e.g.: ${example}` +
538
+ "\n Fix: run /context-mode:ctx-upgrade — it rewrites the file to a portable form that works on every machine." +
539
+ "\n Details: https://github.com/mksglu/context-mode/issues/613"));
540
+ }
541
+ else {
542
+ p.log.success(color.green("Hook config: PASS") +
543
+ color.dim(` — ${rel} is portable (no hard-coded paths)`));
544
+ }
545
+ }
546
+ catch (err) {
547
+ // Malformed JSON should not crash doctor; warn and move on.
548
+ const msg = err instanceof Error ? err.message : String(err);
549
+ p.log.warn(color.yellow(`Hook config: WARN`) +
550
+ ` — ${rel} is not valid JSON` +
551
+ color.dim("\n Doctor cannot scan it for portability issues until the file parses." +
552
+ "\n Fix: open the file and check it in a JSON validator, or delete it and run /context-mode:ctx-upgrade to regenerate." +
553
+ `\n Parser said: ${msg.slice(0, 160)}`));
554
+ }
555
+ }
556
+ if (tierCChecked === 0) {
557
+ p.log.info(color.dim("Hook config: SKIP — no team-shared hook configs found in this workspace"));
558
+ }
559
+ else if (tierCFails === 0) {
560
+ // already individual PASS messages above; no need for a summary
561
+ }
562
+ }
563
+ // ── Issue #609 — proactive stale `.mcp.json` detection ──────────────
564
+ // PR #620 deleted the per-version cache `.mcp.json` write from cli.ts
565
+ // and shipped `sweepStaleMcpJson` to clean up any pre-existing copies.
566
+ // But users on the field may still have stale `.mcp.json` files left
567
+ // by /ctx-upgrade flows that ran before PR #620 (or by Claude Code's
568
+ // native auto-update copying a poisoned file forward). Surface those
569
+ // as WARN (recoverable — next ctx_upgrade sweeps them) so the user
570
+ // knows what to do instead of being told everything is green while
571
+ // the file lingers on disk.
572
+ // Per ISSUE-604-VERDICT §11 same trust contract as Tier C check above.
573
+ p.log.step("Checking for leftover .mcp.json files from older versions...");
574
+ {
575
+ const cacheRoot = join(homedir(), ".claude", "plugins", "cache", "context-mode", "context-mode");
576
+ if (!existsSync(cacheRoot)) {
577
+ p.log.info(color.dim("Leftover .mcp.json check: SKIP — no plugin cache exists yet (Claude Code has not installed context-mode here)"));
578
+ }
579
+ else {
580
+ let staleCount = 0;
581
+ const staleVersions = [];
582
+ try {
583
+ const versionDirs = readdirSync(cacheRoot);
584
+ for (const v of versionDirs) {
585
+ const candidate = join(cacheRoot, v, ".mcp.json");
586
+ if (existsSync(candidate)) {
587
+ staleCount++;
588
+ if (staleVersions.length < 5)
589
+ staleVersions.push(v);
590
+ }
591
+ }
592
+ }
593
+ catch (err) {
594
+ const msg = err instanceof Error ? err.message : String(err);
595
+ p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
596
+ ` — could not read the plugin cache directory` +
597
+ color.dim(`\n Path: ${cacheRoot}` +
598
+ `\n Reason: ${msg.slice(0, 160)}` +
599
+ "\n Fix: check that the directory is readable, then re-run doctor. If the issue persists, run /context-mode:ctx-upgrade."));
600
+ staleCount = 0;
601
+ }
602
+ if (staleCount === 0) {
603
+ p.log.success(color.green("Leftover .mcp.json check: PASS") +
604
+ color.dim(" — no old .mcp.json files in the plugin cache"));
605
+ }
606
+ else {
607
+ // WARN, not FAIL — per architect spec this is recoverable.
608
+ p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
609
+ ` — found ${staleCount} old .mcp.json file(s) left over from previous context-mode versions` +
610
+ color.dim("\n These are harmless but should be cleaned up so they cannot confuse Claude Code after an auto-update." +
611
+ `\n Versions affected: ${staleVersions.join(", ")}${staleCount > staleVersions.length ? ", ..." : ""}` +
612
+ "\n Fix: run /context-mode:ctx-upgrade — it sweeps these files automatically on the next run." +
613
+ "\n Details: https://github.com/mksglu/context-mode/issues/609"));
614
+ }
615
+ }
616
+ }
457
617
  // FTS5 / SQLite
458
618
  p.log.step("Checking FTS5 / SQLite...");
459
619
  try {
@@ -798,20 +958,24 @@ async function upgrade(opts) {
798
958
  }
799
959
  catch { /* some files may not exist in source */ }
800
960
  }
801
- // Write .mcp.json with CLAUDE_PLUGIN_ROOT placeholder (fixes #411).
802
- // Absolute paths bake-in the current pluginRoot dir, which sessionstart.mjs
803
- // (#181) deletes after upgrade breaking MCP server resolution. The literal
804
- // ${CLAUDE_PLUGIN_ROOT} placeholder is resolved by Claude at load-time and
805
- // stays valid across version cleanups. Matches .claude-plugin/plugin.json.
806
- const mcpConfig = {
807
- mcpServers: {
808
- "context-mode": {
809
- command: "node",
810
- args: ["${CLAUDE_PLUGIN_ROOT}/start.mjs"],
811
- },
812
- },
813
- };
814
- writeFileSync(resolve(pluginRoot, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
961
+ // Issue #609 — DO NOT write `.mcp.json` into the plugin cache dir.
962
+ //
963
+ // Historical context: #411 fixed an absolute-path bake by writing the
964
+ // ${CLAUDE_PLUGIN_ROOT} placeholder form here. #531 (commit 9261377)
965
+ // removed `.mcp.json` from `package.json files[]` so the npm tarball
966
+ // stopped shipping it. But the cli-side write persisted, so every
967
+ // /ctx-upgrade re-baked one. When Claude Code's native plugin manager
968
+ // auto-update later carries a previous version's `.mcp.json` forward
969
+ // into a fresh version dir, the stale start.mjs absolute path goes
970
+ // with it → MODULE_NOT_FOUND on every MCP boot.
971
+ //
972
+ // Architectural fix: Claude Code reads `.claude-plugin/plugin.json`
973
+ // .mcpServers as the canonical source (upstream:
974
+ // refs/platforms/claude-code/src/utils/plugins/mcpPluginIntegration.ts:131-212).
975
+ // `.mcp.json` is a redundant per-version artifact whose only role
976
+ // historically was to be a write-time poison vector. Don't write it.
977
+ // The post-bump cache-sweep below removes any pre-existing copies so
978
+ // the previous-version-carry vector cannot replay.
815
979
  // Normalize hooks.json + plugin.json against the REAL pluginRoot now that
816
980
  // files have been copied. Two reasons:
817
981
  // 1. If a prior buggy postinstall (or any future regression) baked the
@@ -908,30 +1072,33 @@ async function upgrade(opts) {
908
1072
  const message = err instanceof Error ? err.message : String(err);
909
1073
  throw new Error(`plugin.json drift check failed: ${message}`);
910
1074
  }
911
- // v1.0.122 — Issue #531 — Layer 6 heal: assert .mcp.json's
912
- // mcpServers["context-mode"].args[0] is the literal ${CLAUDE_PLUGIN_ROOT}/start.mjs
913
- // placeholder. Asymmetric-heal sibling of the plugin.json assertion above.
914
- // cli.ts writes .mcp.json at ~line 829-845 with the placeholder, but never
915
- // asserted the on-disk shape afterwards if a future regression dropped
916
- // the placeholder write or a parallel normalize baked in an absolute path,
917
- // upgrade() would declare success on a poisoned tree. Belt-and-braces:
918
- // first call cleans any drift; second call MUST return healed:[] or throw.
919
- // Single source of truth shared with start.mjs HEAL block + postinstall.
1075
+ // Issue #609 — Layer 6 replacement: sweep stale `.mcp.json` files from
1076
+ // every per-version cache dir. Supersedes the previous healMcpJsonArgs
1077
+ // drift-check block (v1.0.122) that block existed because cli.ts
1078
+ // itself wrote `.mcp.json`. With the write gone (above), the only
1079
+ // remaining `.mcp.json` files are stale carry-forwards from earlier
1080
+ // versions. Sweep them so Claude Code's auto-update can't replay them
1081
+ // into a fresh version dir.
1082
+ //
1083
+ // Belt-and-braces: a second sweep call MUST report removed:[] or we
1084
+ // throw — same architectural-lock pattern as the plugin.json drift
1085
+ // check above. Single source of truth shared with start.mjs HEAL
1086
+ // block + postinstall.
920
1087
  try {
921
1088
  const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
922
1089
  const pluginKey = "context-mode@context-mode";
923
- const firstPass = healMcpJsonArgs({ pluginRoot, pluginCacheRoot, pluginKey });
924
- if (firstPass && firstPass.error) {
925
- throw new Error(firstPass.error);
1090
+ const firstSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
1091
+ if (firstSweep && firstSweep.removed && firstSweep.removed.length > 0) {
1092
+ p.log.info(color.dim(` Swept ${firstSweep.removed.length} stale .mcp.json file(s) from cache`));
926
1093
  }
927
- const secondPass = healMcpJsonArgs({ pluginRoot, pluginCacheRoot, pluginKey });
928
- if (secondPass && Array.isArray(secondPass.healed) && secondPass.healed.length > 0) {
929
- throw new Error(`.mcp.json drift: mcpServers.args still poisoned after first heal pass (healed=${secondPass.healed.join(",")})`);
1094
+ const secondSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
1095
+ if (secondSweep && Array.isArray(secondSweep.removed) && secondSweep.removed.length > 0) {
1096
+ throw new Error(`.mcp.json sweep drift: ${secondSweep.removed.length} file(s) still present after first pass`);
930
1097
  }
931
1098
  }
932
1099
  catch (err) {
933
1100
  const message = err instanceof Error ? err.message : String(err);
934
- throw new Error(`.mcp.json drift check failed: ${message}`);
1101
+ throw new Error(`.mcp.json sweep check failed: ${message}`);
935
1102
  }
936
1103
  // v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
937
1104
  // registrations that point to old context-mode version dirs.