context-mode 1.0.121 → 1.0.122

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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +4 -4
  6. package/build/adapters/claude-code/hooks.d.ts +16 -1
  7. package/build/adapters/claude-code/hooks.js +16 -0
  8. package/build/adapters/claude-code/index.js +2 -11
  9. package/build/adapters/codex/hooks.d.ts +19 -0
  10. package/build/adapters/codex/hooks.js +22 -0
  11. package/build/adapters/codex/index.js +8 -1
  12. package/build/adapters/copilot-base.d.ts +17 -1
  13. package/build/adapters/copilot-base.js +18 -2
  14. package/build/adapters/cursor/hooks.d.ts +14 -1
  15. package/build/adapters/cursor/hooks.js +14 -0
  16. package/build/adapters/detect.d.ts +12 -2
  17. package/build/adapters/detect.js +70 -3
  18. package/build/adapters/gemini-cli/hooks.d.ts +16 -0
  19. package/build/adapters/gemini-cli/hooks.js +19 -0
  20. package/build/adapters/gemini-cli/index.js +4 -2
  21. package/build/adapters/kiro/hooks.d.ts +16 -1
  22. package/build/adapters/kiro/hooks.js +19 -0
  23. package/build/adapters/pi/extension.d.ts +9 -0
  24. package/build/adapters/pi/extension.js +47 -0
  25. package/build/adapters/qwen-code/hooks.d.ts +26 -0
  26. package/build/adapters/qwen-code/hooks.js +29 -0
  27. package/build/adapters/qwen-code/index.js +6 -0
  28. package/build/cli.js +26 -1
  29. package/build/executor.js +18 -3
  30. package/build/lifecycle.d.ts +15 -0
  31. package/build/lifecycle.js +24 -1
  32. package/build/runtime.js +34 -13
  33. package/build/session/extract.js +150 -48
  34. package/build/session/snapshot.js +46 -0
  35. package/cli.bundle.mjs +137 -136
  36. package/configs/codex/hooks.json +1 -1
  37. package/configs/cursor/hooks.json +1 -1
  38. package/configs/kiro/agent.json +1 -1
  39. package/hooks/core/routing.mjs +56 -1
  40. package/hooks/cursor/hooks.json +1 -1
  41. package/hooks/ensure-deps.mjs +22 -3
  42. package/hooks/hooks.json +9 -0
  43. package/hooks/routing-block.mjs +5 -0
  44. package/hooks/session-extract.bundle.mjs +2 -2
  45. package/hooks/session-snapshot.bundle.mjs +21 -20
  46. package/openclaw.plugin.json +1 -1
  47. package/package.json +3 -3
  48. package/scripts/heal-better-sqlite3.mjs +188 -10
  49. package/scripts/heal-installed-plugins.mjs +111 -0
  50. package/scripts/postinstall.mjs +18 -2
  51. package/server.bundle.mjs +111 -111
  52. package/start.mjs +14 -1
  53. package/.mcp.json +0 -8
@@ -12,6 +12,12 @@
12
12
  * without Visual Studio C++ tooling. We bypass that by spawning
13
13
  * prebuild-install JS directly with `process.execPath`.
14
14
  *
15
+ * On macOS / Linux, when conda's `python3` is first on PATH (very
16
+ * common data-science setup), node-gyp picks it up via its `python3`
17
+ * PATH fallback and fails to build on Node 26 (arm64). We defend by
18
+ * pinning PYTHON + npm_config_python to a "safe" interpreter and
19
+ * stripping CONDA_* keys before shelling out to npm / node-gyp (#533).
20
+ *
15
21
  * Layered heal:
16
22
  * A. Spawn prebuild-install via process.execPath — bypasses PATH/MSVC.
17
23
  * B. `npm install better-sqlite3` (re-resolves tree, NOT `npm rebuild`).
@@ -23,13 +29,151 @@
23
29
  * heal could not produce a working binding.
24
30
  *
25
31
  * @see https://github.com/mksglu/context-mode/issues/408
32
+ * @see https://github.com/mksglu/context-mode/issues/533
26
33
  */
27
34
 
28
- import { existsSync } from "node:fs";
35
+ import { existsSync as fsExistsSync } from "node:fs";
29
36
  import { execSync, execFileSync, spawnSync } from "node:child_process";
30
37
  import { resolve } from "node:path";
31
38
  import { createRequire } from "node:module";
32
39
 
40
+ /**
41
+ * Conda installation path prefixes that must NEVER be selected as the
42
+ * Python interpreter for node-gyp. Conda's Python ships environment
43
+ * activation hooks and a custom site-packages layout that breaks
44
+ * better-sqlite3's native build on Node 26 arm64 (#533).
45
+ */
46
+ const CONDA_PATH_PATTERNS = [
47
+ /^\/opt\/anaconda/i,
48
+ /^\/opt\/miniconda/i,
49
+ /\/miniforge\d*\//i,
50
+ /\/anaconda\d*\//i,
51
+ /\/miniconda\d*\//i,
52
+ /\/\.conda\//i,
53
+ /\/conda\//i,
54
+ ];
55
+
56
+ /**
57
+ * CONDA_* environment keys that must be stripped from the child env
58
+ * before spawning npm / node-gyp. Even after pinning PYTHON, leaving
59
+ * CONDA_PREFIX intact causes npm lifecycle scripts to re-activate
60
+ * conda's shims via .npmrc / shell rc files.
61
+ */
62
+ const CONDA_ENV_KEYS = [
63
+ "CONDA_PREFIX",
64
+ "CONDA_DEFAULT_ENV",
65
+ "CONDA_EXE",
66
+ "CONDA_PROMPT_MODIFIER",
67
+ "CONDA_SHLVL",
68
+ "CONDA_PYTHON_EXE",
69
+ ];
70
+
71
+ /**
72
+ * Decide whether a candidate python path is "safe" — i.e. not under
73
+ * any known conda installation prefix.
74
+ *
75
+ * @param {string} candidate - absolute path to a python interpreter
76
+ * @returns {boolean}
77
+ */
78
+ function isSafePythonPath(candidate) {
79
+ if (!candidate) return false;
80
+ return !CONDA_PATH_PATTERNS.some((rx) => rx.test(candidate));
81
+ }
82
+
83
+ /**
84
+ * Resolve a "safe" python interpreter that node-gyp can drive without
85
+ * conda activation noise. Pure / side-effect-free / dependency-injected
86
+ * so the unit test can exercise it on any host.
87
+ *
88
+ * Strategy:
89
+ * - darwin: prefer /usr/bin/python3 (Apple's system Python, ships
90
+ * with every macOS 10.15+ install). If absent, return null.
91
+ * - linux: scan PATH for the first python3 that is not under a
92
+ * conda prefix. If none, return null.
93
+ * - win32: not affected — node-gyp uses the py launcher, not PATH.
94
+ *
95
+ * @param {object} [deps]
96
+ * @param {string} [deps.platform] - process.platform override
97
+ * @param {NodeJS.ProcessEnv} [deps.env] - environment to inspect
98
+ * @param {(p: string) => boolean} [deps.existsSync] - fs probe override
99
+ * @returns {string | null}
100
+ */
101
+ export function resolveSafePython({
102
+ platform = process.platform,
103
+ env = process.env,
104
+ existsSync = fsExistsSync,
105
+ } = {}) {
106
+ if (platform === "darwin") {
107
+ // Apple-shipped Python is the safe choice — it is outside any
108
+ // conda prefix by definition and node-gyp builds against it
109
+ // cleanly on arm64.
110
+ return existsSync("/usr/bin/python3") ? "/usr/bin/python3" : null;
111
+ }
112
+ if (platform === "linux") {
113
+ const pathEntries = (env.PATH || "").split(":").filter(Boolean);
114
+ for (const dir of pathEntries) {
115
+ const candidate = `${dir}/python3`;
116
+ if (existsSync(candidate) && isSafePythonPath(candidate)) {
117
+ return candidate;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ // Windows / other — no override needed.
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Detect whether the current environment is conda-activated. Used to
128
+ * decide whether to emit the override breadcrumb (we don't spam stderr
129
+ * for users who never had conda interference).
130
+ *
131
+ * @param {NodeJS.ProcessEnv} [env]
132
+ * @returns {boolean}
133
+ */
134
+ function isCondaActive(env = process.env) {
135
+ if (env.CONDA_PREFIX || env.CONDA_DEFAULT_ENV) return true;
136
+ const pathEntries = (env.PATH || "").split(process.platform === "win32" ? ";" : ":");
137
+ return pathEntries.some((dir) => !isSafePythonPath(dir + "/python3"));
138
+ }
139
+
140
+ /**
141
+ * Build the child-process env for an npm/node-gyp invocation. Pins
142
+ * PYTHON + npm_config_python to the resolved safe interpreter, strips
143
+ * CONDA_* keys, and prepends /usr/bin to PATH on darwin so any
144
+ * downstream PATH-based python3 lookup also resolves to system python.
145
+ *
146
+ * @param {string | null} safePython - output of resolveSafePython()
147
+ * @param {NodeJS.ProcessEnv} [base] - starting env (defaults to process.env)
148
+ * @returns {NodeJS.ProcessEnv}
149
+ */
150
+ function buildSafeEnv(safePython, base = process.env) {
151
+ const env = { ...base };
152
+ if (safePython) {
153
+ // node-gyp reads env.PYTHON (see lib/find-python.js — second slot in
154
+ // its `checks` array, before any PATH-based fallback).
155
+ env.PYTHON = safePython;
156
+ // npm passes npm_config_python through to node-gyp as --python,
157
+ // which sits in the FIRST slot of node-gyp's `checks` array — even
158
+ // higher priority than env.PYTHON. Set both for belt-and-suspenders.
159
+ env.npm_config_python = safePython;
160
+ }
161
+ // Wipe every CONDA_* key so npm lifecycle scripts can't re-shim
162
+ // python3 via shell activation hooks.
163
+ for (const key of CONDA_ENV_KEYS) {
164
+ delete env[key];
165
+ }
166
+ // Prepend /usr/bin on darwin so any sub-script that does `python3`
167
+ // unqualified still resolves to /usr/bin/python3.
168
+ if (process.platform === "darwin" && env.PATH) {
169
+ const parts = env.PATH.split(":");
170
+ if (parts[0] !== "/usr/bin") {
171
+ env.PATH = "/usr/bin:" + parts.filter((p) => p !== "/usr/bin").join(":");
172
+ }
173
+ }
174
+ return env;
175
+ }
176
+
33
177
  /**
34
178
  * Self-heal a missing better_sqlite3.node binding.
35
179
  *
@@ -42,7 +186,26 @@ export function healBetterSqlite3Binding(pkgRoot) {
42
186
  const bindingPath = resolve(bsqRoot, "build", "Release", "better_sqlite3.node");
43
187
  const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
44
188
 
45
- if (!existsSync(bsqRoot)) {
189
+ // ── Conda defense (#533) ─────────────────────────────────────────
190
+ // Resolve once up front; reuse across all child spawns. The probe
191
+ // is cheap (one stat call on darwin, a PATH walk on linux).
192
+ const safePython = resolveSafePython();
193
+ const condaActive = isCondaActive();
194
+ const childEnv = buildSafeEnv(safePython, process.env);
195
+
196
+ if (condaActive && safePython) {
197
+ // Emit a single breadcrumb so support requests are
198
+ // self-diagnosing. Best-effort: stderr may be unavailable in
199
+ // postinstall captured by npm logs.
200
+ try {
201
+ process.stderr.write(
202
+ `[context-mode] conda python detected on PATH — overriding with ` +
203
+ `PYTHON=${safePython} for better-sqlite3 build (#533).\n`,
204
+ );
205
+ } catch { /* stderr unavailable — proceed silently */ }
206
+ }
207
+
208
+ if (!fsExistsSync(bsqRoot)) {
46
209
  // ── Package itself missing (#514) ───────────────────────────────
47
210
  // npm@7+ silently drops optionalDependencies whose engines field
48
211
  // does not match the running Node version (Node 26 vs
@@ -53,6 +216,12 @@ export function healBetterSqlite3Binding(pkgRoot) {
53
216
  // install the package by name with --no-optional, which forces
54
217
  // npm to install the named package even if it would otherwise
55
218
  // be filtered out as an optional dep.
219
+ if (condaActive && !safePython) {
220
+ // Conda is active AND we couldn't find a system fallback. The
221
+ // install will almost certainly fail. Surface a distinct reason
222
+ // code so /ctx-upgrade can print conda-specific remediation.
223
+ return { healed: false, reason: "python-conda-blocked" };
224
+ }
56
225
  try {
57
226
  execFileSync(
58
227
  npmBin,
@@ -69,6 +238,7 @@ export function healBetterSqlite3Binding(pkgRoot) {
69
238
  stdio: "pipe",
70
239
  timeout: 180000,
71
240
  shell: process.platform === "win32",
241
+ env: childEnv,
72
242
  },
73
243
  );
74
244
  } catch {
@@ -79,10 +249,10 @@ export function healBetterSqlite3Binding(pkgRoot) {
79
249
  // Re-check after install. If npm wrote the package AND its
80
250
  // postinstall produced the binding, we're done. Otherwise fall
81
251
  // through into the binding-missing flow below.
82
- if (existsSync(bindingPath)) {
252
+ if (fsExistsSync(bindingPath)) {
83
253
  return { healed: true, reason: "package-installed" };
84
254
  }
85
- if (!existsSync(bsqRoot)) {
255
+ if (!fsExistsSync(bsqRoot)) {
86
256
  // npm reported success but the directory is still absent.
87
257
  // This indicates the engine-mismatch silent-skip is still in
88
258
  // effect (e.g. npm < 7 or pnpm without --shamefully-hoist).
@@ -93,7 +263,7 @@ export function healBetterSqlite3Binding(pkgRoot) {
93
263
  // install / actionable-stderr.
94
264
  }
95
265
 
96
- if (existsSync(bindingPath)) {
266
+ if (fsExistsSync(bindingPath)) {
97
267
  return { healed: true, reason: "binding-present" };
98
268
  }
99
269
 
@@ -111,16 +281,16 @@ export function healBetterSqlite3Binding(pkgRoot) {
111
281
  resolve(pkgRoot, "node_modules", "prebuild-install", "bin.js"),
112
282
  ];
113
283
  for (const c of candidates) {
114
- if (existsSync(c)) { prebuildBin = c; break; }
284
+ if (fsExistsSync(c)) { prebuildBin = c; break; }
115
285
  }
116
286
  }
117
287
  if (prebuildBin) {
118
288
  const r = spawnSync(
119
289
  process.execPath,
120
290
  [prebuildBin, "--target", process.versions.node, "--runtime", "node"],
121
- { cwd: bsqRoot, stdio: "pipe", timeout: 120000, env: { ...process.env } },
291
+ { cwd: bsqRoot, stdio: "pipe", timeout: 120000, env: childEnv },
122
292
  );
123
- if (r.status === 0 && existsSync(bindingPath)) {
293
+ if (r.status === 0 && fsExistsSync(bindingPath)) {
124
294
  return { healed: true, reason: "prebuild-install" };
125
295
  }
126
296
  }
@@ -132,21 +302,29 @@ export function healBetterSqlite3Binding(pkgRoot) {
132
302
  try {
133
303
  execSync(
134
304
  `${npmBin} install better-sqlite3 --no-package-lock --no-save --silent`,
135
- { cwd: pkgRoot, stdio: "pipe", timeout: 120000, shell: true },
305
+ { cwd: pkgRoot, stdio: "pipe", timeout: 120000, shell: true, env: childEnv },
136
306
  );
137
- if (existsSync(bindingPath)) {
307
+ if (fsExistsSync(bindingPath)) {
138
308
  return { healed: true, reason: "npm-install" };
139
309
  }
140
310
  } catch { /* best effort — fall through to Layer C */ }
141
311
 
142
312
  // ── Layer C: actionable stderr — give the user a real next step ──
143
313
  try {
314
+ const condaHint = condaActive && !safePython
315
+ ? " Conda python detected on PATH and no system /usr/bin/python3 fallback found (#533).\n" +
316
+ " Deactivate conda (`conda deactivate`) or install system python3, then retry.\n"
317
+ : "";
144
318
  process.stderr.write(
145
319
  "\n[context-mode] better-sqlite3 native binding could not be installed automatically.\n" +
146
320
  " This is a known issue on Windows when prebuild-install is not on PATH (#408).\n" +
321
+ condaHint +
147
322
  " Workaround: run `npm install better-sqlite3` from the plugin directory.\n\n",
148
323
  );
149
324
  } catch { /* stderr unavailable — give up silently */ }
325
+ if (condaActive && !safePython) {
326
+ return { healed: false, reason: "python-conda-blocked" };
327
+ }
150
328
  return { healed: false, reason: "manual-required" };
151
329
  } catch {
152
330
  // Outermost guard — never throw, never block the caller.
@@ -283,3 +283,114 @@ export function healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKe
283
283
 
284
284
  return { healed };
285
285
  }
286
+
287
+ // ─────────────────────────────────────────────────────────────────────────
288
+ // Issue #531 (v1.0.122) — Layer 6 heal: .mcp.json mcpServers args
289
+ //
290
+ // Asymmetric-heal sibling of healPluginJsonMcpServers (#523). The regression
291
+ // that broke `.mcp.json` was commit aea633c (PR #253, 2026-04-13): the shipped
292
+ // `.mcp.json` template at repo root used a bare relative `./start.mjs` arg.
293
+ // Claude Code spawns the MCP child with session CWD inherited (not pluginRoot)
294
+ // so fresh npm marketplace installs throw MODULE_NOT_FOUND on every ctx_* tool.
295
+ // v1.0.119 added healPluginJsonMcpServers for the `.claude-plugin/plugin.json`
296
+ // sibling but missed `.mcp.json` — same plugin, same drift class, different
297
+ // file. This module is the asymmetric-heal sibling.
298
+ //
299
+ // Same regex, same placeholder, same traversal guard as #523. Only difference:
300
+ // - Target: `<pluginRoot>/.mcp.json` (flat shape, no `.claude-plugin/` subdir)
301
+ // - Structure: `.mcpServers.<pluginName>.args[]`
302
+ // - Additional drift shape: bare relative `./start.mjs` (the #253 regression)
303
+ // that healPluginJsonMcpServers's tmpdir-only check would not catch.
304
+ //
305
+ // Single source of truth shared by:
306
+ // - `start.mjs` HEAL 5b (every MCP boot)
307
+ // - `scripts/postinstall.mjs` (every `npm install -g context-mode`)
308
+ // - `src/cli.ts` upgrade() (post-bump)
309
+ // ─────────────────────────────────────────────────────────────────────────
310
+
311
+ /**
312
+ * Heal `<pluginRoot>/.mcp.json` mcpServers args.
313
+ *
314
+ * Detects two drift shapes:
315
+ * 1. Bare relative `./start.mjs` (#253 regression — fresh-install class).
316
+ * 2. Tmpdir-prefixed `<...>/context-mode-upgrade-<digits>/start.mjs`
317
+ * (mirrors healPluginJsonMcpServers's #523 tmpdir class).
318
+ * Both rewrite to the literal `${CLAUDE_PLUGIN_ROOT}/start.mjs` placeholder
319
+ * Claude Code resolves at load-time.
320
+ *
321
+ * @param {{
322
+ * pluginRoot: string,
323
+ * pluginCacheRoot: string,
324
+ * pluginKey: string,
325
+ * }} opts
326
+ * @returns {HealResult}
327
+ */
328
+ export function healMcpJsonArgs({ pluginRoot, pluginCacheRoot, pluginKey }) {
329
+ if (!pluginRoot || !pluginCacheRoot || !pluginKey) {
330
+ return { healed: [], skipped: "missing-args" };
331
+ }
332
+
333
+ // Path-traversal guard: refuse to touch a plugin root that escapes the
334
+ // declared cache root. Mirrors healPluginJsonMcpServers + HEAL 3.
335
+ const resolvedRoot = resolve(pluginRoot);
336
+ const cacheRootWithSep = resolve(pluginCacheRoot) + sep;
337
+ if (!resolvedRoot.startsWith(cacheRootWithSep)) {
338
+ return { healed: [], skipped: "outside-cache-root" };
339
+ }
340
+
341
+ // `.mcp.json` lives at pluginRoot/.mcp.json (flat), NOT under .claude-plugin/.
342
+ const mcpJsonPath = resolve(pluginRoot, ".mcp.json");
343
+ if (!existsSync(mcpJsonPath)) {
344
+ return { healed: [], skipped: "no-mcp-json" };
345
+ }
346
+
347
+ let raw;
348
+ try { raw = readFileSync(mcpJsonPath, "utf-8"); }
349
+ catch (err) { return { healed: [], error: `read-failed: ${(err && err.message) || err}` }; }
350
+
351
+ let parsed;
352
+ try { parsed = JSON.parse(raw); }
353
+ catch (err) { return { healed: [], error: `parse-failed: ${(err && err.message) || err}` }; }
354
+
355
+ const servers = parsed && parsed.mcpServers;
356
+ if (!servers || typeof servers !== "object") {
357
+ return { healed: [], skipped: "no-mcp-servers" };
358
+ }
359
+
360
+ // Derive our server name from pluginKey ("context-mode@context-mode" → "context-mode").
361
+ const ourServerName = pluginKey.split("@")[0];
362
+ const ours = servers[ourServerName];
363
+ if (!ours || typeof ours !== "object" || !Array.isArray(ours.args)) {
364
+ return { healed: [], skipped: "no-our-server" };
365
+ }
366
+
367
+ /** @type {string[]} */
368
+ const healed = [];
369
+ const before = ours.args;
370
+ const after = before.map((a) => {
371
+ if (typeof a !== "string") return a;
372
+ // Drift shape #1 (issue #531 / commit aea633c): bare relative `./start.mjs`.
373
+ // Resolved against session CWD (not pluginRoot) → MODULE_NOT_FOUND.
374
+ if (a === "./start.mjs" || a === "start.mjs") {
375
+ return PLACEHOLDER_ARG;
376
+ }
377
+ // Drift shape #2 (mirror of healPluginJsonMcpServers / #523):
378
+ // tmpdir-prefixed `<...>/context-mode-upgrade-<digits>/start.mjs`.
379
+ if (TMPDIR_UPGRADE_RE.test(a) && /[/\\]start\.mjs$/.test(a)) {
380
+ return PLACEHOLDER_ARG;
381
+ }
382
+ return a;
383
+ });
384
+ const changed = after.some((v, i) => v !== before[i]);
385
+ if (changed) {
386
+ ours.args = after;
387
+ healed.push("mcp-json-args");
388
+ try {
389
+ writeFileSync(mcpJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
390
+ } catch (err) {
391
+ return { healed: [], error: `write-failed: ${(err && err.message) || err}` };
392
+ }
393
+ }
394
+
395
+ return { healed };
396
+ }
@@ -14,7 +14,7 @@ import { dirname, resolve, join, sep } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { homedir } from "node:os";
16
16
  import { healBetterSqlite3Binding } from "./heal-better-sqlite3.mjs";
17
- import { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServers } from "./heal-installed-plugins.mjs";
17
+ import { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServers, healMcpJsonArgs } from "./heal-installed-plugins.mjs";
18
18
 
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
20
  const pkgRoot = resolve(__dirname, "..");
@@ -123,10 +123,26 @@ if (isGlobalInstall()) {
123
123
  healedAny = true;
124
124
  }
125
125
  } catch { /* per-entry best effort */ }
126
+ // v1.0.122 — Issue #531 — Layer 6: asymmetric-heal sibling for
127
+ // .mcp.json. The #253/aea633c regression shipped a bare `./start.mjs`
128
+ // arg that Claude Code resolves against session CWD (not pluginRoot)
129
+ // → MODULE_NOT_FOUND on every ctx_* tool. When MCP is dead, the
130
+ // only escape hatch is `npm install -g context-mode` whose
131
+ // postinstall MUST run this heal too.
132
+ try {
133
+ const r = healMcpJsonArgs({
134
+ pluginRoot: installPath,
135
+ pluginCacheRoot: cacheRoot,
136
+ pluginKey: "context-mode@context-mode",
137
+ });
138
+ if (r && Array.isArray(r.healed) && r.healed.length > 0) {
139
+ healedAny = true;
140
+ }
141
+ } catch { /* per-entry best effort */ }
126
142
  }
127
143
  }
128
144
  if (healedAny) {
129
- process.stderr.write("context-mode: healed plugin.json mcpServers args (Issue #523)\n");
145
+ process.stderr.write("context-mode: healed mcpServers args (Issues #523 + #531)\n");
130
146
  }
131
147
  }
132
148
  } catch { /* never block install */ }