context-mode 1.0.161 → 1.0.163

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 (153) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +142 -28
  7. package/bin/statusline.mjs +24 -4
  8. package/build/adapters/antigravity/index.d.ts +1 -1
  9. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  10. package/build/adapters/antigravity-cli/index.js +341 -0
  11. package/build/adapters/claude-code/hooks.d.ts +1 -0
  12. package/build/adapters/claude-code/hooks.js +3 -0
  13. package/build/adapters/claude-code/index.js +24 -5
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +5 -1
  16. package/build/adapters/codex/hooks.js +5 -1
  17. package/build/adapters/codex/index.d.ts +9 -1
  18. package/build/adapters/codex/index.js +87 -5
  19. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  20. package/build/adapters/copilot-cli/hooks.js +64 -0
  21. package/build/adapters/copilot-cli/index.d.ts +48 -0
  22. package/build/adapters/copilot-cli/index.js +341 -0
  23. package/build/adapters/detect.d.ts +1 -1
  24. package/build/adapters/detect.js +71 -3
  25. package/build/adapters/openclaw/mcp-tools.js +1 -1
  26. package/build/adapters/opencode/index.js +31 -17
  27. package/build/adapters/opencode/zod3tov4.js +27 -6
  28. package/build/adapters/pi/extension.d.ts +2 -12
  29. package/build/adapters/pi/extension.js +114 -96
  30. package/build/adapters/types.d.ts +5 -4
  31. package/build/adapters/types.js +4 -3
  32. package/build/cache-heal.d.ts +48 -0
  33. package/build/cache-heal.js +150 -0
  34. package/build/cli.js +37 -97
  35. package/build/executor.d.ts +25 -0
  36. package/build/executor.js +143 -22
  37. package/build/opencode-plugin.js +5 -2
  38. package/build/routing-block.d.ts +8 -0
  39. package/build/routing-block.js +86 -0
  40. package/build/runtime.d.ts +0 -36
  41. package/build/runtime.js +107 -27
  42. package/build/search/flood-guard.d.ts +57 -0
  43. package/build/search/flood-guard.js +80 -0
  44. package/build/security.d.ts +8 -3
  45. package/build/security.js +155 -29
  46. package/build/server.d.ts +14 -0
  47. package/build/server.js +368 -350
  48. package/build/session/analytics.d.ts +8 -8
  49. package/build/session/analytics.js +18 -13
  50. package/build/session/db.d.ts +1 -0
  51. package/build/session/db.js +37 -4
  52. package/build/session/extract.d.ts +46 -0
  53. package/build/session/extract.js +764 -13
  54. package/build/session/project-attribution.js +14 -0
  55. package/build/store.d.ts +1 -1
  56. package/build/store.js +139 -25
  57. package/build/tool-naming.d.ts +4 -0
  58. package/build/tool-naming.js +24 -0
  59. package/build/util/jsonc.d.ts +14 -0
  60. package/build/util/jsonc.js +104 -0
  61. package/cli.bundle.mjs +260 -254
  62. package/configs/antigravity/GEMINI.md +2 -2
  63. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  64. package/configs/antigravity-cli/hooks.json +37 -0
  65. package/configs/antigravity-cli/mcp_config.json +10 -0
  66. package/configs/antigravity-cli/plugin.json +14 -0
  67. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  68. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  69. package/configs/claude-code/CLAUDE.md +2 -2
  70. package/configs/codex/AGENTS.md +2 -2
  71. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  72. package/configs/copilot-cli/.mcp.json +12 -0
  73. package/configs/copilot-cli/README.md +47 -0
  74. package/configs/copilot-cli/hooks.json +41 -0
  75. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  76. package/configs/gemini-cli/GEMINI.md +2 -2
  77. package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
  78. package/configs/kilo/AGENTS.md +2 -2
  79. package/configs/kiro/KIRO.md +2 -2
  80. package/configs/omp/SYSTEM.md +2 -2
  81. package/configs/openclaw/AGENTS.md +2 -2
  82. package/configs/opencode/AGENTS.md +2 -2
  83. package/configs/qwen-code/QWEN.md +2 -2
  84. package/configs/vscode-copilot/copilot-instructions.md +2 -2
  85. package/configs/zed/AGENTS.md +2 -2
  86. package/hooks/antigravity-cli/payload.mjs +98 -0
  87. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  88. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  89. package/hooks/antigravity-cli/stop.mjs +58 -0
  90. package/hooks/codex/pretooluse.mjs +14 -4
  91. package/hooks/codex/stop.mjs +12 -4
  92. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  93. package/hooks/copilot-cli/precompact.mjs +66 -0
  94. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  95. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  96. package/hooks/copilot-cli/stop.mjs +59 -0
  97. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  98. package/hooks/core/codex-caps.mjs +112 -0
  99. package/hooks/core/formatters.mjs +158 -7
  100. package/hooks/core/mcp-ready.mjs +37 -8
  101. package/hooks/core/routing.mjs +94 -8
  102. package/hooks/core/tool-naming.mjs +3 -0
  103. package/hooks/hooks.json +12 -1
  104. package/hooks/pretooluse.mjs +6 -2
  105. package/hooks/routing-block.mjs +2 -2
  106. package/hooks/security.bundle.mjs +2 -1
  107. package/hooks/session-db.bundle.mjs +11 -7
  108. package/hooks/session-directive.mjs +88 -20
  109. package/hooks/session-extract.bundle.mjs +2 -2
  110. package/hooks/session-helpers.mjs +21 -0
  111. package/hooks/session-loaders.mjs +8 -5
  112. package/hooks/sessionstart.mjs +53 -7
  113. package/hooks/stop.mjs +49 -0
  114. package/hooks/userpromptsubmit.mjs +9 -2
  115. package/openclaw.plugin.json +1 -1
  116. package/package.json +4 -10
  117. package/scripts/install-antigravity-cli-plugin.mjs +141 -0
  118. package/server.bundle.mjs +214 -205
  119. package/skills/ctx-insight/SKILL.md +12 -17
  120. package/build/util/db-lock.d.ts +0 -65
  121. package/build/util/db-lock.js +0 -166
  122. package/insight/index.html +0 -13
  123. package/insight/package.json +0 -55
  124. package/insight/server.mjs +0 -1265
  125. package/insight/src/components/analytics.tsx +0 -112
  126. package/insight/src/components/ui/badge.tsx +0 -52
  127. package/insight/src/components/ui/button.tsx +0 -58
  128. package/insight/src/components/ui/card.tsx +0 -103
  129. package/insight/src/components/ui/chart.tsx +0 -371
  130. package/insight/src/components/ui/collapsible.tsx +0 -19
  131. package/insight/src/components/ui/input.tsx +0 -20
  132. package/insight/src/components/ui/progress.tsx +0 -83
  133. package/insight/src/components/ui/scroll-area.tsx +0 -55
  134. package/insight/src/components/ui/separator.tsx +0 -23
  135. package/insight/src/components/ui/table.tsx +0 -114
  136. package/insight/src/components/ui/tabs.tsx +0 -82
  137. package/insight/src/components/ui/tooltip.tsx +0 -64
  138. package/insight/src/lib/api.ts +0 -144
  139. package/insight/src/lib/utils.ts +0 -6
  140. package/insight/src/main.tsx +0 -22
  141. package/insight/src/routeTree.gen.ts +0 -189
  142. package/insight/src/router.tsx +0 -19
  143. package/insight/src/routes/__root.tsx +0 -55
  144. package/insight/src/routes/enterprise.tsx +0 -316
  145. package/insight/src/routes/index.tsx +0 -1482
  146. package/insight/src/routes/knowledge.tsx +0 -221
  147. package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
  148. package/insight/src/routes/search.tsx +0 -97
  149. package/insight/src/routes/sessions.tsx +0 -179
  150. package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
  151. package/insight/src/styles.css +0 -104
  152. package/insight/tsconfig.json +0 -29
  153. package/insight/vite.config.ts +0 -19
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Plugin cache self-heal — fixes broken CLAUDE_PLUGIN_ROOT references.
3
+ *
4
+ * Claude Code's plugin auto-update can leave installed_plugins.json pointing
5
+ * to a non-existent directory (anthropics/claude-code#46915). This module
6
+ * detects and repairs the mismatch by creating symlinks.
7
+ *
8
+ * 4-layer defense:
9
+ * 1. start.mjs startup — reverse heal (registry → symlink to us)
10
+ * 2. server.ts first tool call — mid-session heal
11
+ * 3. postinstall.mjs — backward symlink on new install
12
+ * 4. global hook auto-deploy — survives total plugin cache breakage
13
+ */
14
+ import { existsSync, readFileSync, symlinkSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { resolve, dirname } from "node:path";
16
+ import { homedir } from "node:os";
17
+ /**
18
+ * Core heal: if installed_plugins.json points to a non-existent directory,
19
+ * create a symlink from that path to our actual directory.
20
+ *
21
+ * @param currentDir - The directory we're actually running from
22
+ * @param installedPluginsPath - Path to installed_plugins.json (injectable for testing)
23
+ */
24
+ export function healRegistryMismatch(currentDir, installedPluginsPath) {
25
+ const ipPath = installedPluginsPath ?? resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
26
+ if (!existsSync(ipPath))
27
+ return { healed: false, action: "none" };
28
+ if (!existsSync(currentDir))
29
+ return { healed: false, action: "none" };
30
+ let ip;
31
+ try {
32
+ ip = JSON.parse(readFileSync(ipPath, "utf-8"));
33
+ }
34
+ catch {
35
+ return { healed: false, action: "none" };
36
+ }
37
+ for (const [key, entries] of Object.entries(ip.plugins ?? {})) {
38
+ if (!key.toLowerCase().includes("context-mode"))
39
+ continue;
40
+ for (const entry of entries) {
41
+ const registryPath = entry.installPath;
42
+ if (!registryPath)
43
+ continue;
44
+ // Registry path exists — no healing needed
45
+ if (existsSync(registryPath))
46
+ continue;
47
+ // Registry path doesn't exist — create symlink to our directory
48
+ try {
49
+ const parent = dirname(registryPath);
50
+ if (!existsSync(parent))
51
+ mkdirSync(parent, { recursive: true });
52
+ if (process.platform === "win32") {
53
+ // Windows: use junction (no admin required)
54
+ symlinkSync(currentDir, registryPath, "junction");
55
+ }
56
+ else {
57
+ symlinkSync(currentDir, registryPath);
58
+ }
59
+ return { healed: true, action: "symlink", from: registryPath, to: currentDir };
60
+ }
61
+ catch {
62
+ return { healed: false, action: "none" };
63
+ }
64
+ }
65
+ }
66
+ return { healed: false, action: "none" };
67
+ }
68
+ /**
69
+ * Deploy a global SessionStart hook that heals plugin cache mismatches.
70
+ * This hook lives outside the plugin directory, so it survives cache breakage.
71
+ *
72
+ * Written to ~/.claude/hooks/context-mode-cache-heal.sh
73
+ */
74
+ export function deployGlobalHealHook() {
75
+ const hooksDir = resolve(homedir(), ".claude", "hooks");
76
+ const hookPath = resolve(hooksDir, "context-mode-cache-heal.sh");
77
+ // Already deployed
78
+ if (existsSync(hookPath))
79
+ return { healed: false, action: "none" };
80
+ try {
81
+ if (!existsSync(hooksDir))
82
+ mkdirSync(hooksDir, { recursive: true });
83
+ const script = `#!/usr/bin/env bash
84
+ # context-mode plugin cache self-heal — auto-deployed by context-mode MCP server
85
+ # Fixes anthropics/claude-code#46915: auto-update breaks CLAUDE_PLUGIN_ROOT
86
+ # This hook runs at SessionStart (global, not plugin-level) so it works even
87
+ # when the plugin cache is broken.
88
+
89
+ set -euo pipefail
90
+
91
+ PLUGINS_FILE="$HOME/.claude/plugins/installed_plugins.json"
92
+ [[ -f "$PLUGINS_FILE" ]] || exit 0
93
+
94
+ # Find context-mode entries and heal missing directories
95
+ node -e '
96
+ const fs = require("fs");
97
+ const path = require("path");
98
+ try {
99
+ const ip = JSON.parse(fs.readFileSync(process.argv[1], "utf-8"));
100
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
101
+ if (!key.toLowerCase().includes("context-mode")) continue;
102
+ for (const entry of entries) {
103
+ const p = entry.installPath;
104
+ if (!p || fs.existsSync(p)) continue;
105
+ const parent = path.dirname(p);
106
+ if (!fs.existsSync(parent)) continue;
107
+ const dirs = fs.readdirSync(parent).filter(d => /^\\d+\\.\\d+/.test(d) && fs.statSync(path.join(parent, d)).isDirectory());
108
+ if (dirs.length === 0) continue;
109
+ dirs.sort((a, b) => {
110
+ const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
111
+ for (let i = 0; i < 3; i++) { if ((pa[i]||0) !== (pb[i]||0)) return (pa[i]||0) - (pb[i]||0); }
112
+ return 0;
113
+ });
114
+ const target = path.join(parent, dirs[dirs.length - 1]);
115
+ try { fs.symlinkSync(target, p); } catch {}
116
+ }
117
+ }
118
+ } catch {}
119
+ ' "$PLUGINS_FILE" 2>/dev/null || true
120
+ `;
121
+ writeFileSync(hookPath, script, { mode: 0o755 });
122
+ return { healed: true, action: "global-hook", from: hookPath };
123
+ }
124
+ catch {
125
+ return { healed: false, action: "none" };
126
+ }
127
+ }
128
+ /**
129
+ * Backward symlink: during postinstall, if the registry points to a
130
+ * non-existent OLD path, create a symlink from old → new (our directory).
131
+ * Same as healRegistryMismatch but called from postinstall context.
132
+ */
133
+ export { healRegistryMismatch as healBackwardCompat };
134
+ /** One-shot flag for mid-session heal in server.ts */
135
+ let _midSessionHealed = false;
136
+ /**
137
+ * Mid-session heal — call on first MCP tool invocation.
138
+ * Checks if registry path differs from our running directory.
139
+ * Creates symlink if needed. Runs only once per process.
140
+ */
141
+ export function healMidSession(currentDir) {
142
+ if (_midSessionHealed)
143
+ return { healed: false, action: "none" };
144
+ _midSessionHealed = true;
145
+ return healRegistryMismatch(currentDir);
146
+ }
147
+ /** Reset mid-session flag (for testing only) */
148
+ export function _resetMidSession() {
149
+ _midSessionHealed = false;
150
+ }
package/build/cli.js CHANGED
@@ -62,6 +62,7 @@ const HOOK_MAP = {
62
62
  precompact: "hooks/precompact.mjs",
63
63
  sessionstart: "hooks/sessionstart.mjs",
64
64
  userpromptsubmit: "hooks/userpromptsubmit.mjs",
65
+ stop: "hooks/stop.mjs",
65
66
  },
66
67
  "gemini-cli": {
67
68
  beforeagent: "hooks/gemini-cli/beforeagent.mjs",
@@ -101,6 +102,22 @@ const HOOK_MAP = {
101
102
  precompact: "hooks/jetbrains-copilot/precompact.mjs",
102
103
  sessionstart: "hooks/jetbrains-copilot/sessionstart.mjs",
103
104
  },
105
+ "copilot-cli": {
106
+ pretooluse: "hooks/copilot-cli/pretooluse.mjs",
107
+ posttooluse: "hooks/copilot-cli/posttooluse.mjs",
108
+ precompact: "hooks/copilot-cli/precompact.mjs",
109
+ sessionstart: "hooks/copilot-cli/sessionstart.mjs",
110
+ userpromptsubmit: "hooks/copilot-cli/userpromptsubmit.mjs",
111
+ stop: "hooks/copilot-cli/stop.mjs",
112
+ },
113
+ // Antigravity CLI (`agy`) — bounded PreToolUse enforcement plus capture-only
114
+ // PostToolUse/Stop hooks. Configured via an installed agy plugin's
115
+ // hooks/hooks.json or ~/.gemini/config/hooks.json.
116
+ "antigravity-cli": {
117
+ pretooluse: "hooks/antigravity-cli/pretooluse.mjs",
118
+ posttooluse: "hooks/antigravity-cli/posttooluse.mjs",
119
+ stop: "hooks/antigravity-cli/stop.mjs",
120
+ },
104
121
  "kimi": {
105
122
  pretooluse: "hooks/kimi/pretooluse.mjs",
106
123
  posttooluse: "hooks/kimi/posttooluse.mjs",
@@ -132,7 +149,16 @@ async function hookDispatch(platform, event) {
132
149
  }
133
150
  const scriptPath = HOOK_MAP[platform]?.[event];
134
151
  if (!scriptPath) {
135
- process.exit(1);
152
+ // Fail OPEN. context-mode has no hook for this platform/event — most often
153
+ // because a newer adapter's hook command (`context-mode hook copilot-cli …`)
154
+ // is running against an OLDER global binary that predates that adapter
155
+ // (version skew). Exit 0 (no decision) so the host ALLOWS the tool. Exiting
156
+ // non-zero here makes some hosts treat it as a hook ERROR and DENY the tool:
157
+ // verified against GitHub Copilot CLI 1.0.59, where an exit-1 + empty-stdout
158
+ // PreToolUse hook blocks EVERY tool ("Denied by preToolUse hook (hook
159
+ // errored)") — bricking the agent during a skew instead of just disabling
160
+ // context-mode's instrumentation.
161
+ process.exit(0);
136
162
  }
137
163
  const pluginRoot = getPluginRoot();
138
164
  await import(pathToFileURL(join(pluginRoot, scriptPath)).href);
@@ -205,7 +231,7 @@ else if (args[0] === "hook") {
205
231
  hookDispatch(args[1], args[2]);
206
232
  }
207
233
  else if (args[0] === "insight") {
208
- insight(args[1] ? Number(args[1]) : 4747);
234
+ insight();
209
235
  }
210
236
  else if (args[0] === "statusline") {
211
237
  // Status line implementation lives in bin/statusline.mjs to keep it
@@ -1030,102 +1056,16 @@ async function doctor() {
1030
1056
  return 0;
1031
1057
  }
1032
1058
  /* -------------------------------------------------------
1033
- * Insight — analytics dashboard
1059
+ * Insight — hosted analytics dashboard
1034
1060
  * ------------------------------------------------------- */
1035
- async function insight(port) {
1036
- try {
1037
- const { execSync, spawn } = await import("node:child_process");
1038
- const { statSync, mkdirSync, cpSync } = await import("node:fs");
1039
- const insightSource = resolve(getPluginRoot(), "insight");
1040
- // Detect platform + adapter for correct session/content paths
1041
- const detection = detectPlatform();
1042
- const adapter = await getAdapter(detection.platform);
1043
- const sessDir = ensureWritableStorageDir(resolveSessionStorageDir(() => adapter.getSessionDir()));
1044
- const contentDir = ensureWritableStorageDir(resolveContentStorageDir(() => sessDir));
1045
- const cacheDir = join(dirname(sessDir), "insight-cache");
1046
- if (!existsSync(join(insightSource, "server.mjs"))) {
1047
- console.error("Error: Insight source not found. Try upgrading context-mode.");
1048
- process.exit(1);
1049
- }
1050
- mkdirSync(cacheDir, { recursive: true });
1051
- // Copy source if newer
1052
- const srcMtime = statSync(join(insightSource, "server.mjs")).mtimeMs;
1053
- const cacheMtime = existsSync(join(cacheDir, "server.mjs"))
1054
- ? statSync(join(cacheDir, "server.mjs")).mtimeMs : 0;
1055
- if (srcMtime > cacheMtime) {
1056
- console.log("Copying Insight source...");
1057
- cpSync(insightSource, cacheDir, { recursive: true, force: true });
1058
- }
1059
- // Install deps
1060
- if (!existsSync(join(cacheDir, "node_modules"))) {
1061
- console.log("Installing dependencies (first run)...");
1062
- try {
1063
- npmExec("npm install --production=false", { cwd: cacheDir, stdio: "inherit", timeout: 300000 });
1064
- }
1065
- catch {
1066
- // Clean up partial install so next run retries fresh
1067
- try {
1068
- rmSync(join(cacheDir, "node_modules"), { recursive: true, force: true });
1069
- }
1070
- catch { }
1071
- throw new Error("npm install failed — please retry");
1072
- }
1073
- // Sentinel check: verify install completed (cold cache can timeout leaving partial node_modules)
1074
- if (!existsSync(join(cacheDir, "node_modules", "vite")) || !existsSync(join(cacheDir, "node_modules", "better-sqlite3"))) {
1075
- rmSync(join(cacheDir, "node_modules"), { recursive: true, force: true });
1076
- throw new Error("npm install incomplete — please retry");
1077
- }
1078
- }
1079
- // Build
1080
- console.log("Building dashboard...");
1081
- execSync("npx vite build", { cwd: cacheDir, stdio: "pipe", timeout: 60000 });
1082
- // Start server
1083
- const url = `http://localhost:${port}`;
1084
- console.log(`\n context-mode Insight\n ${url}\n`);
1085
- const child = spawn("node", [join(cacheDir, "server.mjs")], {
1086
- cwd: cacheDir,
1087
- env: {
1088
- ...process.env,
1089
- PORT: String(port),
1090
- INSIGHT_SESSION_DIR: sessDir,
1091
- INSIGHT_CONTENT_DIR: contentDir,
1092
- },
1093
- stdio: "inherit",
1094
- });
1095
- child.on("error", () => { }); // prevent unhandled error crash
1096
- // Wait for server to be ready, then verify it started
1097
- await new Promise(r => setTimeout(r, 1500));
1098
- try {
1099
- const { request } = await import("node:http");
1100
- await new Promise((resolve, reject) => {
1101
- const req = request(`http://127.0.0.1:${port}/api/overview`, { timeout: 3000 }, (res) => {
1102
- resolve();
1103
- res.resume();
1104
- });
1105
- req.on("error", reject);
1106
- req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
1107
- req.end();
1108
- });
1109
- }
1110
- catch {
1111
- console.error(`\nError: Port ${port} appears to be in use. Either a previous dashboard is still running, or another service is using this port.`);
1112
- console.error(`\nTo fix:`);
1113
- console.error(` Kill the existing process: ${process.platform === "win32" ? `netstat -ano | findstr :${port}` : `lsof -ti:${port} | xargs kill`}`);
1114
- console.error(` Or use a different port: context-mode insight ${port + 1}`);
1115
- child.kill();
1116
- process.exit(1);
1117
- }
1118
- // Open browser — execFile with arg array, no shell interpolation.
1119
- openInBrowser(url);
1120
- // Keep alive until Ctrl+C
1121
- process.on("SIGINT", () => { child.kill(); process.exit(0); });
1122
- process.on("SIGTERM", () => { child.kill(); process.exit(0); });
1123
- }
1124
- catch (err) {
1125
- const msg = err instanceof Error ? err.message : String(err);
1126
- console.error(`\nInsight error: ${msg}`);
1127
- process.exit(1);
1128
- }
1061
+ // Insight pivoted from a locally-built dashboard to the hosted product at
1062
+ // context-mode.com/insight (the landing page is the single source of truth).
1063
+ // The command now just opens that URL in the default browser.
1064
+ async function insight() {
1065
+ const url = "https://context-mode.com/insight";
1066
+ console.log(`\n context-mode Insight\n ${url}\n`);
1067
+ // Open browser — execFile with arg array, no shell interpolation.
1068
+ openInBrowser(url);
1129
1069
  }
1130
1070
  /* -------------------------------------------------------
1131
1071
  * Upgrade — adapter-aware hook configuration
@@ -13,6 +13,31 @@ export declare function buildSpawnOptions(platform: NodeJS.Platform): {
13
13
  };
14
14
  /** Pure helper — exported for unit testing. Restores parent PATH after shell startup. */
15
15
  export declare function buildShellScriptContent(code: string, inheritedPath: string | undefined, platform: NodeJS.Platform): string;
16
+ export declare function buildPowerShellScriptContent(code: string): string;
17
+ /**
18
+ * Pure helper — exported for unit testing. Issue #782.
19
+ *
20
+ * On Windows, the sandbox shell runtime is Git Bash. A bare `mvn` invocation
21
+ * runs Maven's POSIX shell script, which on the `mingw=true` branch (uname →
22
+ * MINGW64_NT-*) fails to convert `CLASSWORLDS_JAR` from a POSIX path
23
+ * (`/c/tools/maven/boot/plexus-classworlds-*.jar`) to a Windows path. Native
24
+ * `java.exe` then can't resolve the bootstrap jar → ClassNotFoundException for
25
+ * `org.codehaus.plexus.classworlds.launcher.Launcher`.
26
+ *
27
+ * The third-way fix (issue Option C): rewrite the bare `mvn` token to `mvn.cmd`,
28
+ * the native Windows launcher that uses Windows-native paths and bypasses the
29
+ * broken mingw shell branch entirely. This does NOT touch the global MSYS
30
+ * path-conversion env (MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL), which #826/#791
31
+ * deliberately leave unset so native git.exe launched from bash keeps its
32
+ * /tmp→C:\ argument conversion. Re-enabling global suppression would re-break
33
+ * native git; rewriting only the mvn token keeps both correct.
34
+ *
35
+ * Only a `mvn` that starts a command (start of string, or after a shell
36
+ * separator `&& | ; ( newline`) is rewritten. `mvnw`, `mvnd`, `mymvn`,
37
+ * paths like `./mvnw`, and an already-`mvn.cmd` token are left untouched
38
+ * (the token must be exactly `mvn` followed by whitespace or end-of-string).
39
+ */
40
+ export declare function rewriteWindowsBuildTools(code: string, platform: NodeJS.Platform): string;
16
41
  interface ExecuteOptions {
17
42
  language: Language;
18
43
  code: string;
package/build/executor.js CHANGED
@@ -55,6 +55,21 @@ export function buildShellScriptContent(code, inheritedPath, platform) {
55
55
  return code;
56
56
  return `export PATH=${quoteForPosixShell(inheritedPath)}\n${code}`;
57
57
  }
58
+ function isPowerShell(shellPath) {
59
+ const shellName = shellPath?.toLowerCase() ?? "";
60
+ return shellName.includes("powershell") || shellName.includes("pwsh");
61
+ }
62
+ export function buildPowerShellScriptContent(code) {
63
+ // Prefix a UTF-8 BOM so Windows PowerShell 5.1 reliably detects the script
64
+ // file as UTF-8 (without it, 5.1 falls back to the ANSI code page and
65
+ // mangles non-ASCII characters in the script body).
66
+ return [
67
+ "\uFEFF[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()",
68
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()",
69
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new()",
70
+ code,
71
+ ].join("\n");
72
+ }
58
73
  /**
59
74
  * Resolve the real OS temp directory, bypassing any TMPDIR env override.
60
75
  * os.tmpdir() reads TMPDIR from the environment, which some shells/tools
@@ -72,6 +87,86 @@ const OS_TMPDIR = (() => {
72
87
  catch { /* fall through */ }
73
88
  return "/tmp";
74
89
  })();
90
+ /**
91
+ * Pure helper — exported for unit testing. Issue #782.
92
+ *
93
+ * On Windows, the sandbox shell runtime is Git Bash. A bare `mvn` invocation
94
+ * runs Maven's POSIX shell script, which on the `mingw=true` branch (uname →
95
+ * MINGW64_NT-*) fails to convert `CLASSWORLDS_JAR` from a POSIX path
96
+ * (`/c/tools/maven/boot/plexus-classworlds-*.jar`) to a Windows path. Native
97
+ * `java.exe` then can't resolve the bootstrap jar → ClassNotFoundException for
98
+ * `org.codehaus.plexus.classworlds.launcher.Launcher`.
99
+ *
100
+ * The third-way fix (issue Option C): rewrite the bare `mvn` token to `mvn.cmd`,
101
+ * the native Windows launcher that uses Windows-native paths and bypasses the
102
+ * broken mingw shell branch entirely. This does NOT touch the global MSYS
103
+ * path-conversion env (MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL), which #826/#791
104
+ * deliberately leave unset so native git.exe launched from bash keeps its
105
+ * /tmp→C:\ argument conversion. Re-enabling global suppression would re-break
106
+ * native git; rewriting only the mvn token keeps both correct.
107
+ *
108
+ * Only a `mvn` that starts a command (start of string, or after a shell
109
+ * separator `&& | ; ( newline`) is rewritten. `mvnw`, `mvnd`, `mymvn`,
110
+ * paths like `./mvnw`, and an already-`mvn.cmd` token are left untouched
111
+ * (the token must be exactly `mvn` followed by whitespace or end-of-string).
112
+ */
113
+ export function rewriteWindowsBuildTools(code, platform) {
114
+ if (platform !== "win32")
115
+ return code;
116
+ // Rewrite a bare `mvn` command token to `mvn.cmd` (Maven's native Windows launcher).
117
+ // Algorithmic (no regex): only at a command-start position (string start or right
118
+ // after a shell separator ; & | ( newline, skipping leading spaces/tabs) and only
119
+ // when the token is exactly `mvn` followed by whitespace or end — leaves
120
+ // mvnw / mvnd / ./mvnw / already-mvn.cmd untouched.
121
+ const SEP = new Set([";", "&", "|", "(", "\n"]);
122
+ let out = "";
123
+ let atStart = true;
124
+ let i = 0;
125
+ while (i < code.length) {
126
+ const ch = code[i];
127
+ if (atStart && (ch === " " || ch === "\t")) {
128
+ out += ch;
129
+ i++;
130
+ continue;
131
+ }
132
+ if (atStart && code.startsWith("mvn", i)) {
133
+ const after = code[i + 3];
134
+ if (after === undefined || after === " " || after === "\t" || after === "\n") {
135
+ out += "mvn.cmd";
136
+ i += 3;
137
+ atStart = false;
138
+ continue;
139
+ }
140
+ }
141
+ out += ch;
142
+ atStart = SEP.has(ch);
143
+ i++;
144
+ }
145
+ return out;
146
+ }
147
+ /**
148
+ * Remove a sandbox temp dir, retrying on Windows. Issue #788.
149
+ *
150
+ * On Windows, a child process that opened SQLite databases inside the sandbox
151
+ * can leave `*-wal` / `*-shm` files with handles that linger briefly after the
152
+ * process exits. A single `rmSync` then throws EBUSY/EPERM/ENOTEMPTY and the
153
+ * old silent `catch {}` swallowed it, leaking `.ctx-mode-*` directories under
154
+ * `%TEMP%`. Node's `rmSync({ maxRetries, retryDelay })` is purpose-built for
155
+ * exactly this Windows-handle race, so let it back off and retry.
156
+ */
157
+ function cleanupTmpDir(tmpDir) {
158
+ try {
159
+ rmSync(tmpDir, {
160
+ recursive: true,
161
+ force: true,
162
+ maxRetries: isWin ? 8 : 2,
163
+ retryDelay: 100,
164
+ });
165
+ }
166
+ catch {
167
+ /* best-effort — OS will reclaim %TEMP% eventually */
168
+ }
169
+ }
75
170
  /** Kill process tree — on Windows uses taskkill /T; on Unix kills the process group. */
76
171
  function killTree(proc) {
77
172
  if (isWin && proc.pid) {
@@ -142,29 +237,27 @@ export class PolyglotExecutor {
142
237
  if (cmd[0] === "__rust_compile_run__") {
143
238
  return await this.#compileAndRun(filePath, tmpDir, timeout);
144
239
  }
145
- // Shell commands run in the project directory so git, relative paths,
146
- // and other project-aware tools work naturally. Non-shell languages
147
- // run in the temp directory where their script file is written.
148
- // Issue #45 `cwdOverride` lets per-call sites (Codex MCP handlers)
149
- // pin shell cwd without mutating process-wide state.
150
- const cwd = language === "shell"
151
- ? (cwdOverride ?? this.#projectRoot)
152
- : tmpDir;
240
+ // Every language runs in the project directory so git, relative paths,
241
+ // and other project-aware tools resolve naturally. The script FILE lives
242
+ // in the sandbox tmpDir and is passed to the runtime by absolute path
243
+ // (see buildCommand), so cwd is free to be the project root.
244
+ //
245
+ // Issue #788 previously only `shell` used the project root; non-shell
246
+ // runtimes (python/js/ts/…) used tmpDir, so repo-relative checks like
247
+ // `pathlib.Path("package.json").exists()` silently failed depending on
248
+ // the chosen language. Unifying cwd removes that surprise.
249
+ // Issue #45 — `cwdOverride` lets per-call sites (Codex MCP handlers) pin
250
+ // cwd without mutating process-wide state.
251
+ const cwd = cwdOverride ?? this.#projectRoot;
153
252
  const result = await this.#spawn(cmd, cwd, tmpDir, timeout, background);
154
253
  // Skip tmpDir cleanup if process was backgrounded — it may still need files
155
254
  if (!result.backgrounded) {
156
- try {
157
- rmSync(tmpDir, { recursive: true, force: true });
158
- }
159
- catch { /* ignore */ }
255
+ cleanupTmpDir(tmpDir);
160
256
  }
161
257
  return result;
162
258
  }
163
259
  catch (err) {
164
- try {
165
- rmSync(tmpDir, { recursive: true, force: true });
166
- }
167
- catch { /* ignore */ }
260
+ cleanupTmpDir(tmpDir);
168
261
  throw err;
169
262
  }
170
263
  }
@@ -190,7 +283,15 @@ export class PolyglotExecutor {
190
283
  }
191
284
  const fp = join(tmpDir, buildScriptFilename(language, process.platform, language === "shell" ? this.#runtimes.shell : null));
192
285
  if (language === "shell") {
193
- writeFileSync(fp, buildShellScriptContent(code, process.env.PATH, process.platform), { encoding: "utf-8", mode: 0o700 });
286
+ const shellPath = this.#runtimes.shell;
287
+ // #782 — on Windows Git Bash, rewrite bare `mvn` → `mvn.cmd` so Maven
288
+ // uses its native Windows launcher (correct path handling) instead of
289
+ // the broken mingw shell branch. No-op on non-Windows.
290
+ const rewritten = rewriteWindowsBuildTools(code, process.platform);
291
+ const shellCode = isWin && isPowerShell(shellPath)
292
+ ? buildPowerShellScriptContent(rewritten)
293
+ : rewritten;
294
+ writeFileSync(fp, buildShellScriptContent(shellCode, process.env.PATH, process.platform), { encoding: "utf-8", mode: 0o700 });
194
295
  }
195
296
  else {
196
297
  writeFileSync(fp, code, "utf-8");
@@ -289,8 +390,20 @@ export class PolyglotExecutor {
289
390
  if (proc.pid)
290
391
  this.#backgroundedPids.add(proc.pid);
291
392
  proc.unref();
292
- proc.stdout.destroy();
293
- proc.stderr.destroy();
393
+ // Do NOT destroy stdout/stderr — closing the read end of the pipe
394
+ // sends SIGPIPE to the child on its next write, killing it.
395
+ // Instead, replace the data listeners with no-op drains that
396
+ // consume the stream without accumulating buffers. This keeps
397
+ // the pipe open and prevents the child from blocking on a full
398
+ // pipe buffer.
399
+ if (proc.stdout) {
400
+ proc.stdout.removeAllListeners("data");
401
+ proc.stdout.on("data", () => { });
402
+ }
403
+ if (proc.stderr) {
404
+ proc.stderr.removeAllListeners("data");
405
+ proc.stderr.on("data", () => { });
406
+ }
294
407
  const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
295
408
  const rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
296
409
  res({
@@ -493,10 +606,18 @@ export class PolyglotExecutor {
493
606
  if (!env["PATH"]) {
494
607
  env["PATH"] = isWin ? "" : "/usr/local/bin:/usr/bin:/bin";
495
608
  }
496
- // Windows-critical env vars and path fixes
609
+ // Windows-critical PATH fixes.
497
610
  if (isWin) {
498
- env["MSYS_NO_PATHCONV"] = "1";
499
- env["MSYS2_ARG_CONV_EXCL"] = "*";
611
+ // Do not carry global MSYS path-conversion blockers into Git Bash.
612
+ // Native Windows tools launched from bash (notably git.exe) need MSYS
613
+ // to convert /tmp-style arguments to Windows paths so sibling tools see
614
+ // the same filesystem location (#791).
615
+ for (const key of Object.keys(env)) {
616
+ const upper = key.toUpperCase();
617
+ if (upper === "MSYS_NO_PATHCONV" || upper === "MSYS2_ARG_CONV_EXCL") {
618
+ delete env[key];
619
+ }
620
+ }
500
621
  const gitUsrBin = "C:\\Program Files\\Git\\usr\\bin";
501
622
  const gitBin = "C:\\Program Files\\Git\\bin";
502
623
  if (!env["PATH"].includes(gitUsrBin)) {
@@ -179,7 +179,7 @@ async function createContextModePlugin(ctx) {
179
179
  const toolInput = output.args ?? {};
180
180
  let decision;
181
181
  try {
182
- decision = routing.routePreToolUse(toolName, toolInput, projectDir, getPlatform());
182
+ decision = routing.routePreToolUse(toolName, toolInput, projectDir, platform);
183
183
  }
184
184
  catch {
185
185
  return; // Routing failure → allow passthrough
@@ -194,7 +194,10 @@ async function createContextModePlugin(ctx) {
194
194
  // Mutate output.args — OpenCode reads the mutated output object
195
195
  Object.assign(output.args, decision.updatedInput);
196
196
  }
197
- // "context" action no-op (OpenCode doesn't support context injection)
197
+ if (decision.action === "context" && decision.additionalContext) {
198
+ // Mutate output.args — OpenCode reads the mutated output object
199
+ output.args.additionalContext = decision.additionalContext;
200
+ }
198
201
  },
199
202
  // ── PostToolUse: Session event capture ──────────────
200
203
  "tool.execute.after": async (input, output) => {
@@ -0,0 +1,8 @@
1
+ import type { ToolNamer } from "./tool-naming.js";
2
+ export interface RoutingBlockOptions {
3
+ includeCommands?: boolean;
4
+ }
5
+ export declare function createRoutingBlock(t: ToolNamer, options?: RoutingBlockOptions): string;
6
+ export declare function createReadGuidance(t: ToolNamer): string;
7
+ export declare function createGrepGuidance(t: ToolNamer): string;
8
+ export declare function createBashGuidance(t: ToolNamer): string;