context-mode 1.0.120 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +4 -4
- package/build/adapters/claude-code/hooks.d.ts +16 -1
- package/build/adapters/claude-code/hooks.js +16 -0
- package/build/adapters/claude-code/index.js +2 -11
- package/build/adapters/codex/hooks.d.ts +19 -0
- package/build/adapters/codex/hooks.js +22 -0
- package/build/adapters/codex/index.js +8 -1
- package/build/adapters/copilot-base.d.ts +17 -1
- package/build/adapters/copilot-base.js +18 -2
- package/build/adapters/cursor/hooks.d.ts +14 -1
- package/build/adapters/cursor/hooks.js +14 -0
- package/build/adapters/detect.d.ts +12 -2
- package/build/adapters/detect.js +70 -3
- package/build/adapters/gemini-cli/hooks.d.ts +16 -0
- package/build/adapters/gemini-cli/hooks.js +19 -0
- package/build/adapters/gemini-cli/index.js +4 -2
- package/build/adapters/kiro/hooks.d.ts +16 -1
- package/build/adapters/kiro/hooks.js +19 -0
- package/build/adapters/pi/extension.d.ts +9 -0
- package/build/adapters/pi/extension.js +47 -0
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.js +6 -0
- package/build/cli.js +44 -1
- package/build/executor.js +18 -3
- package/build/lifecycle.d.ts +15 -0
- package/build/lifecycle.js +24 -1
- package/build/runtime.js +34 -13
- package/build/session/extract.js +150 -48
- package/build/session/snapshot.js +46 -0
- package/cli.bundle.mjs +137 -136
- package/configs/codex/hooks.json +1 -1
- package/configs/cursor/hooks.json +1 -1
- package/configs/kiro/agent.json +1 -1
- package/hooks/core/routing.mjs +56 -1
- package/hooks/cursor/hooks.json +1 -1
- package/hooks/ensure-deps.mjs +22 -3
- package/hooks/hooks.json +9 -0
- package/hooks/routing-block.mjs +5 -0
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-snapshot.bundle.mjs +21 -20
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -4
- package/scripts/heal-better-sqlite3.mjs +188 -10
- package/scripts/heal-installed-plugins.mjs +111 -0
- package/scripts/postinstall.mjs +37 -10
- package/server.bundle.mjs +111 -111
- package/skills/.ignore +7 -0
- package/start.mjs +14 -1
- 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
|
-
|
|
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 (
|
|
252
|
+
if (fsExistsSync(bindingPath)) {
|
|
83
253
|
return { healed: true, reason: "package-installed" };
|
|
84
254
|
}
|
|
85
|
-
if (!
|
|
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 (
|
|
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 (
|
|
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:
|
|
291
|
+
{ cwd: bsqRoot, stdio: "pipe", timeout: 120000, env: childEnv },
|
|
122
292
|
);
|
|
123
|
-
if (r.status === 0 &&
|
|
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 (
|
|
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
|
+
}
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -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
|
|
145
|
+
process.stderr.write("context-mode: healed mcpServers args (Issues #523 + #531)\n");
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
} catch { /* never block install */ }
|
|
@@ -271,11 +287,22 @@ try { healBetterSqlite3Binding(pkgRoot); } catch { /* best effort — don't bloc
|
|
|
271
287
|
// PATH lookup failure). start.mjs normalizes on every MCP boot, but normalizing
|
|
272
288
|
// here too closes the gap for the very first hook fire after a fresh install
|
|
273
289
|
// (before any MCP server has run).
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
290
|
+
//
|
|
291
|
+
// Guard: /ctx-upgrade clones the repo to `<tmpdir>/context-mode-upgrade-<epoch>/`
|
|
292
|
+
// and runs `npm install` there before `cpSync`-ing files into the real pluginRoot
|
|
293
|
+
// (src/cli.ts). If we normalize here, pkgRoot is the tmpdir → hooks.json gets
|
|
294
|
+
// the tmpdir's absolute paths baked in → cpSync copies that poisoned hooks.json
|
|
295
|
+
// into the real plugin dir → tmpdir is later cleaned → every hook fires with
|
|
296
|
+
// `MODULE_NOT_FOUND`. Detect the upgrade staging path and skip; start.mjs will
|
|
297
|
+
// normalize correctly on the next MCP boot from the real pluginRoot.
|
|
298
|
+
const TMPDIR_UPGRADE_RE = /[/\\]context-mode-upgrade-\d+[/\\]?$/;
|
|
299
|
+
if (!TMPDIR_UPGRADE_RE.test(pkgRoot)) {
|
|
300
|
+
try {
|
|
301
|
+
const { normalizeHooksOnStartup } = await import("../hooks/normalize-hooks.mjs");
|
|
302
|
+
normalizeHooksOnStartup({
|
|
303
|
+
pluginRoot: pkgRoot,
|
|
304
|
+
nodePath: process.execPath,
|
|
305
|
+
platform: process.platform,
|
|
306
|
+
});
|
|
307
|
+
} catch { /* best effort — never block install */ }
|
|
308
|
+
}
|