context-mode 1.0.39 → 1.0.41

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.39"
9
+ "version": "1.0.41"
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.39",
16
+ "version": "1.0.41",
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.39",
3
+ "version": "1.0.41",
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.39",
6
+ "version": "1.0.41",
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.39",
3
+ "version": "1.0.41",
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",
@@ -10,7 +10,7 @@
10
10
  * - Claude Code: CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
11
11
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
12
12
  * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
13
- * - OpenClaw: OPENCLAW_HOME, OPENCLAW_PROJECT_DIR | ~/.openclaw/
13
+ * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
14
14
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
15
15
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
16
16
  * - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
@@ -10,7 +10,7 @@
10
10
  * - Claude Code: CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
11
11
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
12
12
  * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
13
- * - OpenClaw: OPENCLAW_HOME, OPENCLAW_PROJECT_DIR | ~/.openclaw/
13
+ * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
14
14
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
15
15
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
16
16
  * - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
@@ -67,11 +67,11 @@ export function detectPlatform(clientInfo) {
67
67
  reason: "GEMINI_PROJECT_DIR or GEMINI_CLI env var set",
68
68
  };
69
69
  }
70
- if (process.env.OPENCLAW_HOME || process.env.OPENCLAW_PROJECT_DIR) {
70
+ if (process.env.OPENCLAW_HOME || process.env.OPENCLAW_CLI) {
71
71
  return {
72
72
  platform: "openclaw",
73
73
  confidence: "high",
74
- reason: "OPENCLAW_HOME or OPENCLAW_PROJECT_DIR env var set",
74
+ reason: "OPENCLAW_HOME or OPENCLAW_CLI env var set",
75
75
  };
76
76
  }
77
77
  if (process.env.OPENCODE || process.env.OPENCODE_PID) {
@@ -11,7 +11,7 @@
11
11
  * - Arg modification: mutate event.params in tool_call:before
12
12
  * - Blocking: return { block: true, blockReason } from tool_call:before
13
13
  * - Session ID: event context (no specific env var)
14
- * - Project dir: process.env.OPENCLAW_PROJECT_DIR or process.cwd()
14
+ * - Project dir: process.cwd()
15
15
  * - Config: openclaw.json plugins.entries, ~/.openclaw/extensions/
16
16
  * - Session dir: ~/.openclaw/context-mode/sessions/
17
17
  */
@@ -11,7 +11,7 @@
11
11
  * - Arg modification: mutate event.params in tool_call:before
12
12
  * - Blocking: return { block: true, blockReason } from tool_call:before
13
13
  * - Session ID: event context (no specific env var)
14
- * - Project dir: process.env.OPENCLAW_PROJECT_DIR or process.cwd()
14
+ * - Project dir: process.cwd()
15
15
  * - Config: openclaw.json plugins.entries, ~/.openclaw/extensions/
16
16
  * - Session dir: ~/.openclaw/context-mode/sessions/
17
17
  */
@@ -45,7 +45,7 @@ export class OpenClawAdapter {
45
45
  toolName: input.toolName ?? input.tool_name ?? "",
46
46
  toolInput: input.params ?? input.tool_input ?? {},
47
47
  sessionId: this.extractSessionId(input),
48
- projectDir: process.env.OPENCLAW_PROJECT_DIR || process.cwd(),
48
+ projectDir: process.cwd(),
49
49
  raw,
50
50
  };
51
51
  }
@@ -57,7 +57,7 @@ export class OpenClawAdapter {
57
57
  toolOutput: input.output ?? input.tool_output,
58
58
  isError: input.isError ?? input.is_error,
59
59
  sessionId: this.extractSessionId(input),
60
- projectDir: process.env.OPENCLAW_PROJECT_DIR || process.cwd(),
60
+ projectDir: process.cwd(),
61
61
  raw,
62
62
  };
63
63
  }
@@ -65,7 +65,7 @@ export class OpenClawAdapter {
65
65
  const input = raw;
66
66
  return {
67
67
  sessionId: this.extractSessionId(input),
68
- projectDir: process.env.OPENCLAW_PROJECT_DIR || process.cwd(),
68
+ projectDir: process.cwd(),
69
69
  raw,
70
70
  };
71
71
  }
@@ -89,7 +89,7 @@ export class OpenClawAdapter {
89
89
  return {
90
90
  sessionId: this.extractSessionId(input),
91
91
  source,
92
- projectDir: process.env.OPENCLAW_PROJECT_DIR || process.cwd(),
92
+ projectDir: process.cwd(),
93
93
  raw,
94
94
  };
95
95
  }
@@ -42,8 +42,8 @@ export class OpenCodeAdapter {
42
42
  parsePreToolUseInput(raw) {
43
43
  const input = raw;
44
44
  return {
45
- toolName: input.tool_name ?? "",
46
- toolInput: input.tool_input ?? {},
45
+ toolName: input.tool ?? "",
46
+ toolInput: input.args ?? {},
47
47
  sessionId: this.extractSessionId(input),
48
48
  projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
49
49
  raw,
@@ -52,10 +52,10 @@ export class OpenCodeAdapter {
52
52
  parsePostToolUseInput(raw) {
53
53
  const input = raw;
54
54
  return {
55
- toolName: input.tool_name ?? "",
56
- toolInput: input.tool_input ?? {},
57
- toolOutput: input.tool_output,
58
- isError: input.is_error,
55
+ toolName: input.tool ?? "",
56
+ toolInput: input.args ?? {},
57
+ toolOutput: input.output,
58
+ isError: undefined, // OpenCode doesn't provide isError
59
59
  sessionId: this.extractSessionId(input),
60
60
  projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
61
61
  raw,
package/build/cli.js CHANGED
@@ -367,12 +367,12 @@ async function upgrade() {
367
367
  execSync("npm install --no-audit --no-fund", {
368
368
  cwd: srcDir,
369
369
  stdio: "pipe",
370
- timeout: 60000,
370
+ timeout: 120000,
371
371
  });
372
372
  execSync("npm run build", {
373
373
  cwd: srcDir,
374
374
  stdio: "pipe",
375
- timeout: 30000,
375
+ timeout: 60000,
376
376
  });
377
377
  s.stop("Built successfully");
378
378
  // Step 3: Update in-place
@@ -98,7 +98,7 @@ export default {
98
98
  register(api) {
99
99
  // Resolve build dir from compiled JS location
100
100
  const buildDir = dirname(fileURLToPath(import.meta.url));
101
- const projectDir = process.env.OPENCLAW_PROJECT_DIR || process.cwd();
101
+ const projectDir = process.cwd();
102
102
  const pluginRoot = resolve(buildDir, "..");
103
103
  // Structured logger — wraps api.logger, falls back to no-op.
104
104
  // info/error always emit; debug only when api.logger.debug is present
@@ -17,21 +17,45 @@
17
17
  interface PluginContext {
18
18
  directory: string;
19
19
  }
20
- /** Shape of the input object OpenCode passes to hook functions. */
21
- interface ToolHookInput {
22
- tool_name?: string;
23
- tool_input?: Record<string, unknown>;
24
- tool_output?: string;
25
- is_error?: boolean;
26
- sessionID?: string;
20
+ /** OpenCode tool.execute.before first parameter */
21
+ interface BeforeHookInput {
22
+ tool: string;
23
+ sessionID: string;
24
+ callID: string;
25
+ }
26
+ /** OpenCode tool.execute.before — second parameter */
27
+ interface BeforeHookOutput {
28
+ args: any;
29
+ }
30
+ /** OpenCode tool.execute.after — first parameter */
31
+ interface AfterHookInput {
32
+ tool: string;
33
+ sessionID: string;
34
+ callID: string;
35
+ args: any;
36
+ }
37
+ /** OpenCode tool.execute.after — second parameter */
38
+ interface AfterHookOutput {
39
+ title: string;
40
+ output: string;
41
+ metadata: any;
42
+ }
43
+ /** OpenCode experimental.session.compacting — first parameter */
44
+ interface CompactingHookInput {
45
+ sessionID: string;
46
+ }
47
+ /** OpenCode experimental.session.compacting — second parameter */
48
+ interface CompactingHookOutput {
49
+ context: string[];
50
+ prompt?: string;
27
51
  }
28
52
  /**
29
53
  * OpenCode plugin factory. Called once when OpenCode loads the plugin.
30
54
  * Returns an object mapping hook event names to async handler functions.
31
55
  */
32
56
  export declare const ContextModePlugin: (ctx: PluginContext) => Promise<{
33
- "tool.execute.before": (input: ToolHookInput) => Promise<void>;
34
- "tool.execute.after": (input: ToolHookInput) => Promise<void>;
35
- "experimental.session.compacting": () => Promise<string>;
57
+ "tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
58
+ "tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
59
+ "experimental.session.compacting": (input: CompactingHookInput, output: CompactingHookOutput) => Promise<string>;
36
60
  }>;
37
61
  export {};
@@ -63,9 +63,9 @@ export const ContextModePlugin = async (ctx) => {
63
63
  db.cleanupOldSessions(0);
64
64
  return {
65
65
  // ── PreToolUse: Routing enforcement ─────────────────
66
- "tool.execute.before": async (input) => {
67
- const toolName = input.tool_name ?? "";
68
- const toolInput = input.tool_input ?? {};
66
+ "tool.execute.before": async (input, output) => {
67
+ const toolName = input.tool ?? "";
68
+ const toolInput = output.args ?? {};
69
69
  let decision;
70
70
  try {
71
71
  decision = routing.routePreToolUse(toolName, toolInput, projectDir);
@@ -80,19 +80,19 @@ export const ContextModePlugin = async (ctx) => {
80
80
  throw new Error(decision.reason ?? "Blocked by context-mode");
81
81
  }
82
82
  if (decision.action === "modify" && decision.updatedInput) {
83
- // Mutate args in place — OpenCode reads the mutated input
84
- Object.assign(toolInput, decision.updatedInput);
83
+ // Mutate output.args — OpenCode reads the mutated output object
84
+ Object.assign(output.args, decision.updatedInput);
85
85
  }
86
86
  // "context" action → no-op (OpenCode doesn't support context injection)
87
87
  },
88
88
  // ── PostToolUse: Session event capture ──────────────
89
- "tool.execute.after": async (input) => {
89
+ "tool.execute.after": async (input, output) => {
90
90
  try {
91
91
  const hookInput = {
92
- tool_name: input.tool_name ?? "",
93
- tool_input: input.tool_input ?? {},
94
- tool_response: input.tool_output,
95
- tool_output: input.is_error ? { isError: true } : undefined,
92
+ tool_name: input.tool ?? "",
93
+ tool_input: input.args ?? {},
94
+ tool_response: output.output,
95
+ tool_output: undefined, // OpenCode doesn't provide isError
96
96
  };
97
97
  const events = extractEvents(hookInput);
98
98
  for (const event of events) {
@@ -105,7 +105,7 @@ export const ContextModePlugin = async (ctx) => {
105
105
  }
106
106
  },
107
107
  // ── PreCompact: Snapshot generation ─────────────────
108
- "experimental.session.compacting": async () => {
108
+ "experimental.session.compacting": async (input, output) => {
109
109
  try {
110
110
  const events = db.getEvents(sessionId);
111
111
  if (events.length === 0)
@@ -116,6 +116,8 @@ export const ContextModePlugin = async (ctx) => {
116
116
  });
117
117
  db.upsertResume(sessionId, snapshot, events.length);
118
118
  db.incrementCompactCount(sessionId);
119
+ // Mutate output.context to inject the snapshot
120
+ output.context.push(snapshot);
119
121
  return snapshot;
120
122
  }
121
123
  catch {
package/build/server.js CHANGED
@@ -80,7 +80,7 @@ function maybeIndexSessionEvents(store) {
80
80
  function getStorePath() {
81
81
  const projectDir = process.env.CLAUDE_PROJECT_DIR
82
82
  || process.env.GEMINI_PROJECT_DIR
83
- || process.env.OPENCLAW_PROJECT_DIR
83
+ || process.env.OPENCLAW_HOME
84
84
  || process.cwd();
85
85
  const normalized = projectDir.replace(/\\/g, "/");
86
86
  const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
@@ -1504,43 +1504,71 @@ server.registerTool("ctx_stats", {
1504
1504
  content: [{ type: "text", text }],
1505
1505
  });
1506
1506
  });
1507
- // ── ctx-doctor: diagnostics meta-tool ──────────────────────────────────────
1507
+ // ── ctx-doctor: diagnostics (server-side) ─────────────────────────────────
1508
1508
  server.registerTool("ctx_doctor", {
1509
1509
  title: "Run Diagnostics",
1510
- description: "Diagnose context-mode installation. Returns a shell command to execute. " +
1511
- "You MUST run the returned command using your shell tool (Bash, shell_execute, " +
1512
- "run_in_terminal, etc.) and display the output as a markdown checklist.",
1510
+ description: "Diagnose context-mode installation. Runs all checks server-side and " +
1511
+ "returns results as a markdown checklist. No CLI execution needed.",
1513
1512
  inputSchema: z.object({}),
1514
1513
  }, async () => {
1515
- const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
1516
- const bundlePath = resolve(pluginRoot, "cli.bundle.mjs");
1517
- const fallbackPath = resolve(pluginRoot, "build", "cli.js");
1518
- const cliPath = existsSync(bundlePath) ? bundlePath : fallbackPath;
1519
- const cmd = `node "${cliPath}" doctor`;
1520
- const text = [
1521
- "## ctx-doctor",
1522
- "",
1523
- "Run this command using your shell execution tool:",
1524
- "",
1525
- "```",
1526
- cmd,
1527
- "```",
1528
- "",
1529
- "After the command completes, display results as a markdown checklist:",
1530
- "- `[x]` for PASS, `[ ]` for FAIL, `[-]` for WARN",
1531
- "- Example format:",
1532
- " ```",
1533
- " ## context-mode doctor",
1534
- " - [x] Runtimes: 6/10 (javascript, typescript, python, shell, ruby, perl)",
1535
- " - [x] Performance: FAST (Bun)",
1536
- " - [x] Server test: PASS",
1537
- " - [x] Hooks: PASS",
1538
- " - [x] FTS5: PASS",
1539
- " - [x] npm: v0.9.23",
1540
- " ```",
1541
- ].join("\n");
1514
+ const lines = ["## context-mode doctor", ""];
1515
+ // __pkg_dir is build/ for tsc, plugin root for bundle — resolve to plugin root
1516
+ const pluginRoot = existsSync(resolve(__pkg_dir, "package.json")) ? __pkg_dir : dirname(__pkg_dir);
1517
+ // Runtimes
1518
+ const total = 11;
1519
+ const pct = ((available.length / total) * 100).toFixed(0);
1520
+ lines.push(`- [x] Runtimes: ${available.length}/${total} (${pct}%) — ${available.join(", ")}`);
1521
+ // Performance
1522
+ if (hasBunRuntime()) {
1523
+ lines.push("- [x] Performance: FAST (Bun)");
1524
+ }
1525
+ else {
1526
+ lines.push("- [-] Performance: NORMAL — install Bun for 3-5x speed boost");
1527
+ }
1528
+ // Server test
1529
+ try {
1530
+ const testExecutor = new PolyglotExecutor({ runtimes });
1531
+ const result = await testExecutor.execute({ language: "javascript", code: 'console.log("ok");', timeout: 5000 });
1532
+ if (result.exitCode === 0 && result.stdout.trim() === "ok") {
1533
+ lines.push("- [x] Server test: PASS");
1534
+ }
1535
+ else {
1536
+ lines.push(`- [ ] Server test: FAIL — exit ${result.exitCode}`);
1537
+ }
1538
+ }
1539
+ catch (err) {
1540
+ lines.push(`- [ ] Server test: FAIL — ${err instanceof Error ? err.message : err}`);
1541
+ }
1542
+ // FTS5 / SQLite
1543
+ try {
1544
+ const Database = loadDatabase();
1545
+ const db = new Database(":memory:");
1546
+ db.exec("CREATE VIRTUAL TABLE fts_test USING fts5(content)");
1547
+ db.exec("INSERT INTO fts_test(content) VALUES ('hello world')");
1548
+ const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get();
1549
+ db.close();
1550
+ if (row && row.content === "hello world") {
1551
+ lines.push("- [x] FTS5 / SQLite: PASS — native module works");
1552
+ }
1553
+ else {
1554
+ lines.push("- [ ] FTS5 / SQLite: FAIL — unexpected result");
1555
+ }
1556
+ }
1557
+ catch (err) {
1558
+ lines.push(`- [ ] FTS5 / SQLite: FAIL — ${err instanceof Error ? err.message : err}`);
1559
+ }
1560
+ // Hook script
1561
+ const hookPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
1562
+ if (existsSync(hookPath)) {
1563
+ lines.push(`- [x] Hook script: PASS — ${hookPath}`);
1564
+ }
1565
+ else {
1566
+ lines.push(`- [ ] Hook script: FAIL — not found at ${hookPath}`);
1567
+ }
1568
+ // Version
1569
+ lines.push(`- [x] Version: v${VERSION}`);
1542
1570
  return trackResponse("ctx_doctor", {
1543
- content: [{ type: "text", text }],
1571
+ content: [{ type: "text", text: lines.join("\n") }],
1544
1572
  });
1545
1573
  });
1546
1574
  // ── ctx-upgrade: upgrade meta-tool ─────────────────────────────────────────
@@ -1552,11 +1580,58 @@ server.registerTool("ctx_upgrade", {
1552
1580
  "Tell the user to restart their session after upgrade.",
1553
1581
  inputSchema: z.object({}),
1554
1582
  }, async () => {
1555
- const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
1583
+ // __pkg_dir is build/ for tsc, plugin root for bundle — resolve to plugin root
1584
+ const pluginRoot = existsSync(resolve(__pkg_dir, "package.json")) ? __pkg_dir : dirname(__pkg_dir);
1556
1585
  const bundlePath = resolve(pluginRoot, "cli.bundle.mjs");
1557
1586
  const fallbackPath = resolve(pluginRoot, "build", "cli.js");
1558
- const cliPath = existsSync(bundlePath) ? bundlePath : fallbackPath;
1559
- const cmd = `node "${cliPath}" upgrade`;
1587
+ let cmd;
1588
+ if (existsSync(bundlePath)) {
1589
+ cmd = `node "${bundlePath}" upgrade`;
1590
+ }
1591
+ else if (existsSync(fallbackPath)) {
1592
+ cmd = `node "${fallbackPath}" upgrade`;
1593
+ }
1594
+ else {
1595
+ // Inline fallback: neither CLI file exists (e.g. marketplace installs).
1596
+ // Generate a self-contained node -e script that performs the upgrade.
1597
+ const repoUrl = "https://github.com/mksglu/context-mode.git";
1598
+ const copyDirs = ["build", "hooks", "skills", "scripts", ".claude-plugin"];
1599
+ const copyFiles = ["start.mjs", "server.bundle.mjs", "cli.bundle.mjs", "package.json"];
1600
+ // Write inline script to a temp .mjs file — avoids quote-escaping issues
1601
+ // across cmd.exe, PowerShell, and bash (node -e '...' breaks on Windows).
1602
+ const scriptLines = [
1603
+ `import{execSync}from"node:child_process";`,
1604
+ `import{cpSync,rmSync,existsSync,mkdtempSync}from"node:fs";`,
1605
+ `import{join}from"node:path";`,
1606
+ `import{tmpdir}from"node:os";`,
1607
+ `const P=${JSON.stringify(pluginRoot)};`,
1608
+ `const T=mkdtempSync(join(tmpdir(),"ctx-upgrade-"));`,
1609
+ `try{`,
1610
+ `console.log("- [x] Starting inline upgrade (no CLI found)");`,
1611
+ `execSync("git clone --depth 1 ${repoUrl} \\""+T+"\\"",{stdio:"inherit"});`,
1612
+ `console.log("- [x] Cloned latest source");`,
1613
+ `execSync("npm install",{cwd:T,stdio:"inherit"});`,
1614
+ `execSync("npm run build",{cwd:T,stdio:"inherit"});`,
1615
+ `console.log("- [x] Built from source");`,
1616
+ ...copyDirs.map((d) => `if(existsSync(join(T,${JSON.stringify(d)})))cpSync(join(T,${JSON.stringify(d)}),join(P,${JSON.stringify(d)}),{recursive:true,force:true});`),
1617
+ ...copyFiles.map((f) => `if(existsSync(join(T,${JSON.stringify(f)})))cpSync(join(T,${JSON.stringify(f)}),join(P,${JSON.stringify(f)}),{force:true});`),
1618
+ `console.log("- [x] Copied build artifacts");`,
1619
+ `execSync("npm install --production",{cwd:P,stdio:"inherit"});`,
1620
+ `console.log("- [x] Installed production dependencies");`,
1621
+ `console.log("## context-mode upgrade complete");`,
1622
+ `}catch(e){`,
1623
+ `console.error("- [ ] Upgrade failed:",e.message);`,
1624
+ `process.exit(1);`,
1625
+ `}finally{`,
1626
+ `try{rmSync(T,{recursive:true,force:true})}catch{}`,
1627
+ `}`,
1628
+ ].join("\n");
1629
+ // Server writes the temp script file — avoids shell quoting issues entirely
1630
+ const tmpScript = resolve(pluginRoot, ".ctx-upgrade-inline.mjs");
1631
+ const { writeFileSync: writeTmp } = await import("node:fs");
1632
+ writeTmp(tmpScript, scriptLines);
1633
+ cmd = `node "${tmpScript}"`;
1634
+ }
1560
1635
  const text = [
1561
1636
  "## ctx-upgrade",
1562
1637
  "",