context-mode 1.0.121 → 1.0.123

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 (55) 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/client-map.js +6 -0
  10. package/build/adapters/codex/hooks.d.ts +19 -0
  11. package/build/adapters/codex/hooks.js +22 -0
  12. package/build/adapters/codex/index.js +8 -1
  13. package/build/adapters/copilot-base.d.ts +17 -1
  14. package/build/adapters/copilot-base.js +18 -2
  15. package/build/adapters/cursor/hooks.d.ts +14 -1
  16. package/build/adapters/cursor/hooks.js +14 -0
  17. package/build/adapters/detect.d.ts +12 -2
  18. package/build/adapters/detect.js +96 -13
  19. package/build/adapters/gemini-cli/hooks.d.ts +16 -0
  20. package/build/adapters/gemini-cli/hooks.js +19 -0
  21. package/build/adapters/gemini-cli/index.js +4 -2
  22. package/build/adapters/kiro/hooks.d.ts +16 -1
  23. package/build/adapters/kiro/hooks.js +19 -0
  24. package/build/adapters/pi/extension.d.ts +9 -0
  25. package/build/adapters/pi/extension.js +52 -1
  26. package/build/adapters/qwen-code/hooks.d.ts +26 -0
  27. package/build/adapters/qwen-code/hooks.js +29 -0
  28. package/build/adapters/qwen-code/index.js +6 -0
  29. package/build/cli.js +46 -5
  30. package/build/executor.js +18 -3
  31. package/build/lifecycle.d.ts +15 -0
  32. package/build/lifecycle.js +24 -1
  33. package/build/runtime.js +34 -13
  34. package/build/server.js +17 -2
  35. package/build/session/extract.js +150 -48
  36. package/build/session/snapshot.js +46 -0
  37. package/cli.bundle.mjs +151 -150
  38. package/configs/codex/hooks.json +1 -1
  39. package/configs/cursor/hooks.json +1 -1
  40. package/configs/kiro/agent.json +1 -1
  41. package/hooks/core/routing.mjs +56 -1
  42. package/hooks/cursor/hooks.json +1 -1
  43. package/hooks/ensure-deps.mjs +45 -10
  44. package/hooks/hooks.json +9 -0
  45. package/hooks/routing-block.mjs +5 -0
  46. package/hooks/session-extract.bundle.mjs +2 -2
  47. package/hooks/session-snapshot.bundle.mjs +21 -20
  48. package/openclaw.plugin.json +1 -1
  49. package/package.json +3 -3
  50. package/scripts/heal-better-sqlite3.mjs +188 -10
  51. package/scripts/heal-installed-plugins.mjs +111 -0
  52. package/scripts/postinstall.mjs +35 -9
  53. package/server.bundle.mjs +118 -118
  54. package/start.mjs +14 -1
  55. package/.mcp.json +0 -8
@@ -25,7 +25,7 @@ import { BaseAdapter } from "../base.js";
25
25
  // ─────────────────────────────────────────────────────────
26
26
  // Hook constants (re-exported from hooks.ts)
27
27
  // ─────────────────────────────────────────────────────────
28
- import { HOOK_TYPES as GEMINI_HOOK_NAMES, HOOK_SCRIPTS as GEMINI_HOOK_SCRIPTS, buildHookCommand as buildGeminiHookCommand, } from "./hooks.js";
28
+ import { HOOK_TYPES as GEMINI_HOOK_NAMES, HOOK_SCRIPTS as GEMINI_HOOK_SCRIPTS, buildHookCommand as buildGeminiHookCommand, EXTERNAL_MCP_MATCHER_PATTERN, } from "./hooks.js";
29
29
  // ─────────────────────────────────────────────────────────
30
30
  // Adapter implementation
31
31
  // ─────────────────────────────────────────────────────────
@@ -178,7 +178,9 @@ export class GeminiCLIAdapter extends BaseAdapter {
178
178
  ],
179
179
  [GEMINI_HOOK_NAMES.BEFORE_TOOL]: [
180
180
  {
181
- matcher: "run_shell_command|read_file|read_many_files|grep_search|search_file_content|web_fetch|activate_skill|mcp__plugin_context-mode",
181
+ // Gemini native tools + context-mode own MCP (both canonical and Claude
182
+ // shim prefixes) + external MCP catch-all (#529).
183
+ matcher: `run_shell_command|read_file|read_many_files|grep_search|search_file_content|web_fetch|activate_skill|mcp__plugin_context-mode|mcp__context-mode|${EXTERNAL_MCP_MATCHER_PATTERN}`,
182
184
  hooks: [
183
185
  {
184
186
  type: "command",
@@ -18,6 +18,21 @@ export declare const HOOK_TYPES: {
18
18
  };
19
19
  export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
20
20
  export declare const HOOK_SCRIPTS: Record<string, string>;
21
+ /**
22
+ * Negative-lookahead matcher for external MCP tool namespaces on Kiro (#529).
23
+ *
24
+ * Kiro MCP wire shape: `@<server>/<tool>` (verified in
25
+ * hooks/core/tool-naming.mjs — context-mode's own tools surface as
26
+ * `@context-mode/<tool>`). This pattern fires PreToolUse for any external
27
+ * `@<server>/<tool>` whose server segment is NOT `context-mode`. Without it,
28
+ * large payloads from slack / telegram / gdrive / notion-style MCPs bypass
29
+ * the routing nudge and flood the model's context window — PostToolUse runs
30
+ * too late to keep the raw data out.
31
+ *
32
+ * Routing.mjs `isExternalMcpTool` is extended to recognise the `@<server>/`
33
+ * prefix shape so the routing branch returns external-MCP guidance.
34
+ */
35
+ export declare const EXTERNAL_MCP_MATCHER_PATTERN = "@(?!context-mode/)";
21
36
  /**
22
37
  * Tools that context-mode's PreToolUse hook intercepts on Kiro.
23
38
  *
@@ -26,7 +41,7 @@ export declare const HOOK_SCRIPTS: Record<string, string>;
26
41
  *
27
42
  * MCP tools surface as @context-mode/ctx_* in Kiro.
28
43
  */
29
- export declare const PRE_TOOL_USE_MATCHERS: readonly ["execute_bash", "fs_read", "@context-mode/ctx_execute", "@context-mode/ctx_execute_file", "@context-mode/ctx_batch_execute"];
44
+ export declare const PRE_TOOL_USE_MATCHERS: readonly ["execute_bash", "fs_read", "@context-mode/ctx_execute", "@context-mode/ctx_execute_file", "@context-mode/ctx_batch_execute", "@(?!context-mode/)"];
30
45
  /**
31
46
  * Combined matcher pattern for Kiro hook config (pipe-separated).
32
47
  * Used by generateHookConfig and configureAllHooks.
@@ -24,6 +24,24 @@ export const HOOK_SCRIPTS = {
24
24
  [HOOK_TYPES.AGENT_SPAWN]: "agentspawn.mjs",
25
25
  };
26
26
  // ─────────────────────────────────────────────────────────
27
+ // External MCP routing matcher (#529)
28
+ // ─────────────────────────────────────────────────────────
29
+ /**
30
+ * Negative-lookahead matcher for external MCP tool namespaces on Kiro (#529).
31
+ *
32
+ * Kiro MCP wire shape: `@<server>/<tool>` (verified in
33
+ * hooks/core/tool-naming.mjs — context-mode's own tools surface as
34
+ * `@context-mode/<tool>`). This pattern fires PreToolUse for any external
35
+ * `@<server>/<tool>` whose server segment is NOT `context-mode`. Without it,
36
+ * large payloads from slack / telegram / gdrive / notion-style MCPs bypass
37
+ * the routing nudge and flood the model's context window — PostToolUse runs
38
+ * too late to keep the raw data out.
39
+ *
40
+ * Routing.mjs `isExternalMcpTool` is extended to recognise the `@<server>/`
41
+ * prefix shape so the routing branch returns external-MCP guidance.
42
+ */
43
+ export const EXTERNAL_MCP_MATCHER_PATTERN = "@(?!context-mode/)";
44
+ // ─────────────────────────────────────────────────────────
27
45
  // PreToolUse matchers
28
46
  // ─────────────────────────────────────────────────────────
29
47
  /**
@@ -40,6 +58,7 @@ export const PRE_TOOL_USE_MATCHERS = [
40
58
  "@context-mode/ctx_execute",
41
59
  "@context-mode/ctx_execute_file",
42
60
  "@context-mode/ctx_batch_execute",
61
+ EXTERNAL_MCP_MATCHER_PATTERN,
43
62
  ];
44
63
  /**
45
64
  * Combined matcher pattern for Kiro hook config (pipe-separated).
@@ -22,5 +22,14 @@
22
22
  * a prior load.
23
23
  */
24
24
  export declare let _mcpBridgeReady: Promise<void>;
25
+ /**
26
+ * Returns true iff `argv` matches a Pi top-level short-circuit invocation
27
+ * (help or version). Only argv[0] is inspected — Pi's runCli only checks
28
+ * the first token, and subcommand-level `--help` (e.g. `pi stats --help`)
29
+ * still spins up a real session, so we must NOT skip bootstrap there.
30
+ *
31
+ * Exported for unit tests.
32
+ */
33
+ export declare function isPiShortCircuitArgv(argv: readonly string[]): boolean;
25
34
  /** Pi extension default export. Called once by Pi runtime with the extension API. */
26
35
  export default function piExtension(pi: any): void;
@@ -193,12 +193,51 @@ function handleCommandText(text, ctx) {
193
193
  }
194
194
  return { text };
195
195
  }
196
+ // ── Pi short-circuit argv detection (#534) ───────────────
197
+ //
198
+ // Pi's runtime loads every extension during module discovery, BEFORE its
199
+ // `runCli()` decides whether the invocation is a real session or a
200
+ // short-lived help / version print. Without this guard, even `pi --help`
201
+ // causes us to spawn `server.bundle.mjs` as a long-lived stdio child —
202
+ // which is then reparented to PID 1 the moment Pi's `--help` handler
203
+ // returns. The MCP SDK's StdioServerTransport CPU-spins on the half-closed
204
+ // pipe until the 30 s ppid poll catches up, accumulating multi-hour orphans
205
+ // (see issue #534, plus the historical #311 / #388 fixes that only addressed
206
+ // the *recovery* path — not the *prevention* path).
207
+ //
208
+ // Token set verified against the Pi 14.x source — specifically:
209
+ // refs/platforms/oh-my-pi/packages/coding-agent/src/cli.ts:runCli
210
+ //
211
+ // if (first === "--help" || first === "-h" || first === "--version"
212
+ // || first === "-v" || first === "help") { /* short-circuit */ }
213
+ //
214
+ // We mirror it exactly — no inferred flags, no `-V` (Pi uses lowercase `-v`),
215
+ // no `--no-help`. Anything else (including `pi stats --help`) routes through
216
+ // the normal launch path and the bridge bootstraps as usual.
217
+ const PI_SHORT_CIRCUIT_TOKENS = new Set(["--help", "-h", "--version", "-v", "help"]);
218
+ /**
219
+ * Returns true iff `argv` matches a Pi top-level short-circuit invocation
220
+ * (help or version). Only argv[0] is inspected — Pi's runCli only checks
221
+ * the first token, and subcommand-level `--help` (e.g. `pi stats --help`)
222
+ * still spins up a real session, so we must NOT skip bootstrap there.
223
+ *
224
+ * Exported for unit tests.
225
+ */
226
+ export function isPiShortCircuitArgv(argv) {
227
+ if (argv.length === 0)
228
+ return false;
229
+ return PI_SHORT_CIRCUIT_TOKENS.has(argv[0]);
230
+ }
196
231
  // ── Extension entry point ────────────────────────────────
197
232
  /** Pi extension default export. Called once by Pi runtime with the extension API. */
198
233
  export default function piExtension(pi) {
199
234
  const buildDir = dirname(fileURLToPath(import.meta.url));
200
235
  const pluginRoot = resolve(buildDir, "..", "..", "..");
201
- const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
236
+ // Issue #542 Pi's runtime sets PI_CONFIG_DIR (not PI_PROJECT_DIR).
237
+ // PI_PROJECT_DIR remains supported as a legacy override for callers
238
+ // that historically synthesized it. Cwd is the universal final
239
+ // fallback.
240
+ const projectDir = process.env.PI_CONFIG_DIR || process.env.PI_PROJECT_DIR || process.cwd();
202
241
  const db = getOrCreateDB();
203
242
  // ── 1. session_start — Initialize session ──────────────
204
243
  pi.on("session_start", (_event, ctx) => {
@@ -534,6 +573,18 @@ export default function piExtension(pi) {
534
573
  // Best-effort: a missing bundle or a spawn failure must NOT prevent
535
574
  // the rest of the extension (session capture, hooks, slash commands)
536
575
  // from initializing. We log to stderr and continue.
576
+ // Short-circuit guard (#534): skip the MCP bridge bootstrap for
577
+ // `pi --help` / `pi --version` / `pi help` and similar. Pi prints and
578
+ // exits within milliseconds, but the bridge child would otherwise live
579
+ // long enough to be reparented to PID 1, half-close stdin, and pin a CPU
580
+ // core via the MCP SDK's stdio loop. We use process.argv directly so the
581
+ // guard works for any caller that boots Pi with a short-circuit token,
582
+ // regardless of how the runtime wires its CLI parser.
583
+ const piArgv = process.argv.slice(2);
584
+ if (isPiShortCircuitArgv(piArgv)) {
585
+ _mcpBridgeReady = Promise.resolve();
586
+ return;
587
+ }
537
588
  const serverBundle = resolve(pluginRoot, "server.bundle.mjs");
538
589
  if (existsSync(serverBundle)) {
539
590
  _mcpBridgeReady = bootstrapMCPTools(pi, serverBundle).then((handle) => {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * adapters/qwen-code/hooks — Qwen Code hook definitions.
3
+ *
4
+ * Qwen Code is a Gemini CLI fork (packages/core/src/tools/tool-names.ts —
5
+ * shares native names like `run_shell_command`, `read_file`). The hook wire
6
+ * protocol is JSON stdin / stdout, identical to Claude Code and Gemini CLI.
7
+ *
8
+ * Config: ~/.qwen/settings.json under "hooks" key.
9
+ */
10
+ /**
11
+ * Negative-lookahead matcher for external MCP tool namespaces on Qwen Code (#529).
12
+ *
13
+ * Qwen Code MCP wire shape: `mcp__<server>__<tool>` (shared with Gemini CLI
14
+ * upstream). Own context-mode MCP surfaces as both
15
+ * `mcp__plugin_context-mode_context-mode__ctx_*` (Claude shim path when users
16
+ * install via the Claude marketplace) and `mcp__context-mode__ctx_*` (Qwen
17
+ * canonical — see hooks/core/tool-naming.mjs). The negative lookahead
18
+ * `(?!.*context-mode)` excludes both variants from the external-MCP routing
19
+ * branch so context-mode's own tools (already wired by the explicit entries
20
+ * above this catch-all) are not double-routed.
21
+ *
22
+ * Without this matcher, large payloads from slack / telegram / gdrive / notion
23
+ * MCPs bypass the routing nudge and flood the model's context window —
24
+ * PostToolUse runs too late to keep the raw data out.
25
+ */
26
+ export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!.*context-mode)";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * adapters/qwen-code/hooks — Qwen Code hook definitions.
3
+ *
4
+ * Qwen Code is a Gemini CLI fork (packages/core/src/tools/tool-names.ts —
5
+ * shares native names like `run_shell_command`, `read_file`). The hook wire
6
+ * protocol is JSON stdin / stdout, identical to Claude Code and Gemini CLI.
7
+ *
8
+ * Config: ~/.qwen/settings.json under "hooks" key.
9
+ */
10
+ // ─────────────────────────────────────────────────────────
11
+ // External MCP routing matcher (#529)
12
+ // ─────────────────────────────────────────────────────────
13
+ /**
14
+ * Negative-lookahead matcher for external MCP tool namespaces on Qwen Code (#529).
15
+ *
16
+ * Qwen Code MCP wire shape: `mcp__<server>__<tool>` (shared with Gemini CLI
17
+ * upstream). Own context-mode MCP surfaces as both
18
+ * `mcp__plugin_context-mode_context-mode__ctx_*` (Claude shim path when users
19
+ * install via the Claude marketplace) and `mcp__context-mode__ctx_*` (Qwen
20
+ * canonical — see hooks/core/tool-naming.mjs). The negative lookahead
21
+ * `(?!.*context-mode)` excludes both variants from the external-MCP routing
22
+ * branch so context-mode's own tools (already wired by the explicit entries
23
+ * above this catch-all) are not double-routed.
24
+ *
25
+ * Without this matcher, large payloads from slack / telegram / gdrive / notion
26
+ * MCPs bypass the routing nudge and flood the model's context window —
27
+ * PostToolUse runs too late to keep the raw data out.
28
+ */
29
+ export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!.*context-mode)";
@@ -16,6 +16,7 @@ import { readFileSync, writeFileSync, existsSync, } from "node:fs";
16
16
  import { resolve, join } from "node:path";
17
17
  import { homedir } from "node:os";
18
18
  import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
19
+ import { EXTERNAL_MCP_MATCHER_PATTERN } from "./hooks.js";
19
20
  import { buildNodeCommand, } from "../types.js";
20
21
  // ─────────────────────────────────────────────────────────
21
22
  // Adapter implementation
@@ -55,6 +56,9 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
55
56
  "mcp__plugin_context-mode_context-mode__ctx_execute",
56
57
  "mcp__plugin_context-mode_context-mode__ctx_execute_file",
57
58
  "mcp__plugin_context-mode_context-mode__ctx_batch_execute",
59
+ // External MCP catch-all (#529). Negative-lookahead excludes context-mode's
60
+ // own server segments so the explicit entries above are not double-routed.
61
+ EXTERNAL_MCP_MATCHER_PATTERN,
58
62
  ].join("|");
59
63
  return {
60
64
  PreToolUse: [
@@ -247,6 +251,8 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
247
251
  "mcp__plugin_context-mode_context-mode__ctx_execute",
248
252
  "mcp__plugin_context-mode_context-mode__ctx_execute_file",
249
253
  "mcp__plugin_context-mode_context-mode__ctx_batch_execute",
254
+ // External MCP catch-all (#529) — keep in sync with generateHookConfig above.
255
+ EXTERNAL_MCP_MATCHER_PATTERN,
250
256
  ].join("|"),
251
257
  },
252
258
  {
package/build/cli.js CHANGED
@@ -25,7 +25,7 @@ import { resolveClaudeConfigDir } from "./util/claude-config.js";
25
25
  // v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
26
26
  // mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
27
27
  // @ts-expect-error — JS module, no TS declarations
28
- import { healPluginJsonMcpServers } from "../scripts/heal-installed-plugins.mjs";
28
+ import { healPluginJsonMcpServers, healMcpJsonArgs } from "../scripts/heal-installed-plugins.mjs";
29
29
  // Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
30
30
  // duplicated here so the cli bundle does not pull server.ts top-level boot side effects.
31
31
  // Keep in sync — pure data, no I/O.
@@ -125,7 +125,16 @@ if (args[0] === "doctor") {
125
125
  doctor().then((code) => process.exit(code));
126
126
  }
127
127
  else if (args[0] === "upgrade") {
128
- upgrade().catch((err) => {
128
+ // Issue #542 — accept --platform <id> from the ctx_upgrade MCP handler,
129
+ // which forwards the live MCP clientInfo's resolved PlatformId. The flag
130
+ // wins over upgrade()'s own detectPlatform() heuristic chain so an
131
+ // ambiguous config-dir collision (e.g. ~/.cursor + ~/.pi both present)
132
+ // can never misroute the upgrade.
133
+ const platformFlagIdx = args.indexOf("--platform");
134
+ const platformArg = platformFlagIdx >= 0 && args[platformFlagIdx + 1]
135
+ ? args[platformFlagIdx + 1]
136
+ : undefined;
137
+ upgrade(platformArg ? { platform: platformArg } : undefined).catch((err) => {
129
138
  const message = err instanceof Error ? err.message : String(err);
130
139
  p.log.error(color.red(message));
131
140
  process.exit(1);
@@ -601,11 +610,18 @@ async function insight(port) {
601
610
  /* -------------------------------------------------------
602
611
  * Upgrade — adapter-aware hook configuration
603
612
  * ------------------------------------------------------- */
604
- async function upgrade() {
613
+ async function upgrade(opts) {
605
614
  if (process.stdout.isTTY)
606
615
  console.clear();
607
- // Detect platform
608
- const detection = detectPlatform();
616
+ // Issue #542 — when the MCP ctx_upgrade handler threads through an
617
+ // explicit --platform <id> (resolved from live clientInfo), trust it
618
+ // over the local heuristic chain. detectPlatform() with no args cannot
619
+ // see the MCP handshake and falls through to the config-dir tier,
620
+ // which misdetects Pi/OMP installs as Cursor on systems where both
621
+ // ~/.cursor/ and ~/.pi/ exist.
622
+ const detection = opts?.platform
623
+ ? { platform: opts.platform, confidence: "high", reason: `--platform ${opts.platform} from ctx_upgrade handler` }
624
+ : detectPlatform();
609
625
  const adapter = await getAdapter(detection.platform);
610
626
  p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
611
627
  p.log.info(`Platform: ${color.cyan(adapter.name)}` +
@@ -804,6 +820,31 @@ async function upgrade() {
804
820
  const message = err instanceof Error ? err.message : String(err);
805
821
  throw new Error(`plugin.json drift check failed: ${message}`);
806
822
  }
823
+ // v1.0.122 — Issue #531 — Layer 6 heal: assert .mcp.json's
824
+ // mcpServers["context-mode"].args[0] is the literal ${CLAUDE_PLUGIN_ROOT}/start.mjs
825
+ // placeholder. Asymmetric-heal sibling of the plugin.json assertion above.
826
+ // cli.ts writes .mcp.json at ~line 829-845 with the placeholder, but never
827
+ // asserted the on-disk shape afterwards — if a future regression dropped
828
+ // the placeholder write or a parallel normalize baked in an absolute path,
829
+ // upgrade() would declare success on a poisoned tree. Belt-and-braces:
830
+ // first call cleans any drift; second call MUST return healed:[] or throw.
831
+ // Single source of truth shared with start.mjs HEAL block + postinstall.
832
+ try {
833
+ const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
834
+ const pluginKey = "context-mode@context-mode";
835
+ const firstPass = healMcpJsonArgs({ pluginRoot, pluginCacheRoot, pluginKey });
836
+ if (firstPass && firstPass.error) {
837
+ throw new Error(firstPass.error);
838
+ }
839
+ const secondPass = healMcpJsonArgs({ pluginRoot, pluginCacheRoot, pluginKey });
840
+ if (secondPass && Array.isArray(secondPass.healed) && secondPass.healed.length > 0) {
841
+ throw new Error(`.mcp.json drift: mcpServers.args still poisoned after first heal pass (healed=${secondPass.healed.join(",")})`);
842
+ }
843
+ }
844
+ catch (err) {
845
+ const message = err instanceof Error ? err.message : String(err);
846
+ throw new Error(`.mcp.json drift check failed: ${message}`);
847
+ }
807
848
  // v1.0.114 hotfix — marketplace post-pull assertion: clone (if
808
849
  // present) MUST be on newVersion. Mert's case showed marketplace
809
850
  // stuck at v1.0.89 — the sync block above swallowed that silently.
package/build/executor.js CHANGED
@@ -238,11 +238,11 @@ export class PolyglotExecutor {
238
238
  ? cmd.slice(1).map(a => a.replace(/\\/g, "/"))
239
239
  : cmd.slice(1);
240
240
  }
241
- const proc = spawn(spawnCmd, spawnArgs, {
241
+ // Common options shared by both spawn variants below.
242
+ const commonOpts = {
242
243
  cwd,
243
244
  stdio: ["ignore", "pipe", "pipe"],
244
245
  env: this.#buildSafeEnv(sandboxTmpDir),
245
- shell: needsShell,
246
246
  // On Unix, create a new process group so killTree can kill all children
247
247
  detached: !isWin,
248
248
  // Hide the spawned-process console window on Windows. Without this,
@@ -250,7 +250,22 @@ export class PolyglotExecutor {
250
250
  // leaving the MCP response empty and popping a Git Bash terminal over
251
251
  // the user's IDE. Issue #384.
252
252
  ...buildSpawnOptions(process.platform),
253
- });
253
+ };
254
+ // DEP0190 fix: when shell is true (Windows .cmd/.bat shims), pass a
255
+ // single command string instead of cmd + args array. Node.js warns
256
+ // that args are unsafely concatenated when shell:true is combined with
257
+ // the args-array form of spawn(). Colllapsing to a string avoids the
258
+ // warning while preserving the same shell behavior.
259
+ let proc;
260
+ if (needsShell) {
261
+ const fullCmd = [spawnCmd, ...spawnArgs]
262
+ .map(a => /\s/.test(a) ? JSON.stringify(a) : a)
263
+ .join(" ");
264
+ proc = spawn(fullCmd, [], { ...commonOpts, shell: true });
265
+ }
266
+ else {
267
+ proc = spawn(spawnCmd, spawnArgs, { ...commonOpts, shell: false });
268
+ }
254
269
  let timedOut = false;
255
270
  let resolved = false;
256
271
  // Issue #406 — if the caller didn't pass a timeout we don't fire one.
@@ -44,6 +44,21 @@ export interface IsParentAliveDeps {
44
44
  * {@link defaultIsParentAlive} (captured once at module load).
45
45
  */
46
46
  export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () => boolean;
47
+ /**
48
+ * Resolve the parent-liveness poll interval based on context (#534).
49
+ *
50
+ * When this process is the MCP bridge child spawned by the Pi adapter
51
+ * (`bootstrapMCPTools` in `src/adapters/pi/mcp-bridge.ts` sets
52
+ * `CONTEXT_MODE_BRIDGE_DEPTH=1` in the child env), we tighten the poll to
53
+ * 1 s. The Pi parent can disappear in under 50 ms (`pi --help` prints
54
+ * usage and returns), so the default 30 s window leaves a long-lived
55
+ * CPU-spinning orphan. For top-level MCP servers (depth 0 / absent) we
56
+ * keep the original 30 s cadence — the existing #311/#388 ppid + stdin
57
+ * recovery paths already cover Claude Code style hosts.
58
+ *
59
+ * Exported for unit-testing.
60
+ */
61
+ export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
47
62
  /**
48
63
  * Start the lifecycle guard. Returns a cleanup function.
49
64
  * Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
@@ -71,12 +71,35 @@ export function makeDefaultIsParentAlive(deps = {}) {
71
71
  };
72
72
  }
73
73
  const defaultIsParentAlive = makeDefaultIsParentAlive();
74
+ /**
75
+ * Resolve the parent-liveness poll interval based on context (#534).
76
+ *
77
+ * When this process is the MCP bridge child spawned by the Pi adapter
78
+ * (`bootstrapMCPTools` in `src/adapters/pi/mcp-bridge.ts` sets
79
+ * `CONTEXT_MODE_BRIDGE_DEPTH=1` in the child env), we tighten the poll to
80
+ * 1 s. The Pi parent can disappear in under 50 ms (`pi --help` prints
81
+ * usage and returns), so the default 30 s window leaves a long-lived
82
+ * CPU-spinning orphan. For top-level MCP servers (depth 0 / absent) we
83
+ * keep the original 30 s cadence — the existing #311/#388 ppid + stdin
84
+ * recovery paths already cover Claude Code style hosts.
85
+ *
86
+ * Exported for unit-testing.
87
+ */
88
+ export function lifecycleGuardIntervalForEnv(env = process.env) {
89
+ const raw = env.CONTEXT_MODE_BRIDGE_DEPTH;
90
+ if (raw === undefined)
91
+ return 30_000;
92
+ const depth = Number.parseInt(raw, 10);
93
+ if (!Number.isFinite(depth) || depth <= 0)
94
+ return 30_000;
95
+ return 1000;
96
+ }
74
97
  /**
75
98
  * Start the lifecycle guard. Returns a cleanup function.
76
99
  * Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
77
100
  */
78
101
  export function startLifecycleGuard(opts) {
79
- const interval = opts.checkIntervalMs ?? 30_000;
102
+ const interval = opts.checkIntervalMs ?? lifecycleGuardIntervalForEnv();
80
103
  const check = opts.isParentAlive ?? defaultIsParentAlive;
81
104
  let stopped = false;
82
105
  const shutdown = () => {
package/build/runtime.js CHANGED
@@ -60,11 +60,15 @@ function runnableExists(cmd) {
60
60
  // fallthrough can be slow). On POSIX, 1500ms is plenty for a real binary
61
61
  // and keeps cold detection of python3 → python → py under ~5s total (#454).
62
62
  try {
63
- execFileSync(cmd, ["--version"], {
64
- shell: isWindows,
65
- stdio: "pipe",
66
- timeout: isWindows ? 5000 : 1500,
67
- });
63
+ // DEP0190 fix: avoid args array with shell:true on Windows.
64
+ // Use execSync with a command string when shell is required;
65
+ // keep execFileSync (no shell) on POSIX.
66
+ if (isWindows) {
67
+ execSync(`"${cmd}" --version`, { stdio: "pipe", timeout: 5000 });
68
+ }
69
+ else {
70
+ execFileSync(cmd, ["--version"], { stdio: "pipe", timeout: 1500 });
71
+ }
68
72
  return true;
69
73
  }
70
74
  catch {
@@ -152,14 +156,31 @@ function resolveWindowsBash() {
152
156
  }
153
157
  function getVersion(cmd, args = ["--version"]) {
154
158
  try {
155
- return execFileSync(cmd, args, {
156
- encoding: "utf-8",
157
- shell: process.platform === "win32",
158
- stdio: ["pipe", "pipe", "pipe"],
159
- timeout: 5000,
160
- })
161
- .trim()
162
- .split(/\r?\n/)[0];
159
+ // DEP0190 fix: avoid args array with shell:true on Windows.
160
+ if (process.platform === "win32") {
161
+ // Hardening (PR #537 review): quote any cmd.exe metacharacter, not just
162
+ // whitespace. Current arg sources are internally controlled, but cheap
163
+ // defense-in-depth for future call sites.
164
+ const cmdStr = [cmd, ...args]
165
+ .map(a => /[\s"&|<>^()%!]/.test(a) ? JSON.stringify(a) : a)
166
+ .join(" ");
167
+ return execSync(cmdStr, {
168
+ encoding: "utf-8",
169
+ stdio: ["pipe", "pipe", "pipe"],
170
+ timeout: 5000,
171
+ })
172
+ .trim()
173
+ .split(/\r?\n/)[0];
174
+ }
175
+ else {
176
+ return execFileSync(cmd, args, {
177
+ encoding: "utf-8",
178
+ stdio: ["pipe", "pipe", "pipe"],
179
+ timeout: 5000,
180
+ })
181
+ .trim()
182
+ .split(/\r?\n/)[0];
183
+ }
163
184
  }
164
185
  catch {
165
186
  return "unknown";
package/build/server.js CHANGED
@@ -2745,12 +2745,27 @@ server.registerTool("ctx_upgrade", {
2745
2745
  }
2746
2746
  }
2747
2747
  catch { /* best effort — don't block upgrade */ }
2748
+ // Issue #542 — thread MCP clientInfo into the spawned upgrade
2749
+ // process. detectPlatform() runs IN-PROCESS here (no spawn boundary)
2750
+ // so clientInfo from the MCP handshake is the highest-confidence
2751
+ // signal available. We forward the resolved PlatformId as a
2752
+ // --platform flag (cross-shell safe on POSIX, Git Bash, PowerShell,
2753
+ // and cmd.exe — unlike env-var prefixes). If detection fails we
2754
+ // skip the flag and let upgrade()'s own detectPlatform() fall back.
2755
+ let platformFlag = "";
2756
+ try {
2757
+ const { detectPlatform } = await import("./adapters/detect.js");
2758
+ const clientInfo = server.server.getClientVersion();
2759
+ const signal = detectPlatform(clientInfo ?? undefined);
2760
+ platformFlag = ` --platform ${signal.platform}`;
2761
+ }
2762
+ catch { /* best effort — fall back to upgrade()'s own detect */ }
2748
2763
  let cmd;
2749
2764
  if (existsSync(bundlePath)) {
2750
- cmd = `${buildNodeCommand(bundlePath)} upgrade`;
2765
+ cmd = `${buildNodeCommand(bundlePath)} upgrade${platformFlag}`;
2751
2766
  }
2752
2767
  else if (existsSync(fallbackPath)) {
2753
- cmd = `${buildNodeCommand(fallbackPath)} upgrade`;
2768
+ cmd = `${buildNodeCommand(fallbackPath)} upgrade${platformFlag}`;
2754
2769
  }
2755
2770
  else {
2756
2771
  // Inline fallback: neither CLI file exists (e.g. marketplace installs).