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,86 @@
1
+ export function createRoutingBlock(t, options = {}) {
2
+ const { includeCommands = true } = options;
3
+ return `
4
+ <context_window_protection>
5
+ <priority_instructions>
6
+ Raw tool output floods context window. MUST use context-mode MCP tools. Keep raw data in sandbox.
7
+ </priority_instructions>
8
+
9
+ <tool_selection_hierarchy>
10
+ 0. MEMORY: ${t("ctx_search")}(sort: "timeline")
11
+ - After resume, check prior context before asking user.
12
+ 1. GATHER: ${t("ctx_batch_execute")}(commands, queries)
13
+ - Primary research tool. Runs commands, auto-indexes, searches. ONE call replaces many steps.
14
+ - Each command: {label: "section header", command: "shell command"}
15
+ - label becomes FTS5 chunk title — descriptive labels improve search.
16
+ 2. FOLLOW-UP: ${t("ctx_search")}(queries: ["q1", "q2", ...])
17
+ - All follow-up questions. ONE call, many queries (default relevance mode).
18
+ 3. PROCESSING: ${t("ctx_execute")}(language, code) | ${t("ctx_execute_file")}(path, language, code)
19
+ - API calls, log analysis, data processing.
20
+ </tool_selection_hierarchy>
21
+
22
+ <forbidden_actions>
23
+ - NO Bash for commands producing >20 lines output.
24
+ - NO Read for analysis — use execute_file. Read IS correct for files you intend to Edit.
25
+ - NO WebFetch — use ${t("ctx_fetch_and_index")}.
26
+ - Bash ONLY for git/mkdir/rm/mv/navigation.
27
+ - NO ${t("ctx_execute")} or ${t("ctx_execute_file")} for file creation/modification.
28
+ ctx_execute is for analysis, processing, computation only.
29
+ </forbidden_actions>
30
+
31
+ <file_writing_policy>
32
+ ALWAYS use native Write/Edit tools for file creation/modification.
33
+ NEVER use ${t("ctx_execute")}, ${t("ctx_execute_file")}, or Bash to write files.
34
+ Applies to all file types: code, configs, plans, specs, YAML, JSON, markdown.
35
+ </file_writing_policy>
36
+
37
+ <output_constraints>
38
+ <communication_style>
39
+ Terse like caveman. Technical substance exact. Only fluff die.
40
+ Use fragments when clear. Short synonyms (fix not "implement a solution for").
41
+ Technical terms exact. Code blocks unchanged.
42
+ Auto-expand for: security warnings, irreversible actions, user confusion.
43
+ </communication_style>
44
+ <artifact_policy>
45
+ Write artifacts (code, configs, PRDs) to FILES. NEVER inline.
46
+ Return only: file path + 1-line description.
47
+ </artifact_policy>
48
+ <response_format>
49
+ Concise summary:
50
+ - Actions taken (2-3 bullets)
51
+ - File paths created/modified
52
+ - Key findings
53
+ </response_format>
54
+ </output_constraints>
55
+ <session_continuity>
56
+ Skills, roles, and decisions set during this session remain active until the user revokes them.
57
+ Do not drop behavioral directives as context grows.
58
+ </session_continuity>
59
+ ${includeCommands ? `
60
+ <ctx_commands>
61
+ "ctx stats" | "ctx-stats" | "/ctx-stats" | context savings question
62
+ → Call stats MCP tool, display full output verbatim.
63
+
64
+ "ctx doctor" | "ctx-doctor" | "/ctx-doctor" | diagnose context-mode
65
+ → Call doctor MCP tool, run returned shell command, display as checklist.
66
+
67
+ "ctx upgrade" | "ctx-upgrade" | "/ctx-upgrade" | update context-mode
68
+ → Call upgrade MCP tool, run returned shell command, display as checklist.
69
+
70
+ "ctx purge" | "ctx-purge" | "/ctx-purge" | wipe/reset knowledge base
71
+ → Call purge MCP tool with confirm: true. Warn: irreversible.
72
+
73
+ After /clear or /compact: knowledge base preserved. Tell user: "context-mode knowledge base preserved. Use \`ctx purge\` to start fresh."
74
+ </ctx_commands>
75
+ ` : ''}
76
+ </context_window_protection>`;
77
+ }
78
+ export function createReadGuidance(t) {
79
+ return '<context_guidance>\n <tip>\n Reading to Edit? Read is correct — Edit needs content in context.\n Reading to analyze/explore? Use ' + t("ctx_execute_file") + '(path, language, code) — only printed summary enters context.\n </tip>\n</context_guidance>';
80
+ }
81
+ export function createGrepGuidance(t) {
82
+ return '<context_guidance>\n <tip>\n May flood context. Use ' + t("ctx_execute") + '(language: "shell", code: "...") to run searches in sandbox. Only printed summary enters context.\n </tip>\n</context_guidance>';
83
+ }
84
+ export function createBashGuidance(t) {
85
+ return '<context_guidance>\n <tip>\n May produce large output. Use ' + t("ctx_batch_execute") + '(commands, queries) for multiple commands, ' + t("ctx_execute") + '(language: "shell", code: "...") for single. Only printed summary enters context. Bash only for: git, mkdir, rm, mv, navigation.\n </tip>\n</context_guidance>';
86
+ }
@@ -65,42 +65,6 @@ export interface HookRuntime {
65
65
  * mask the mock and yield the host's real bun/node detection result.
66
66
  */
67
67
  export declare function resetHookRuntimeCache(): void;
68
- /**
69
- * Resolve the JS runtime to use for spawning hook scripts (issue #738).
70
- *
71
- * Returns Bun when:
72
- * - a bun binary is located via {@link bunCommand} (already handles the
73
- * Windows .cmd shim trap from #506 + absolute path fallbacks), AND
74
- * - `bun --version` exits 0 within the probe timeout, AND
75
- * - the reported semver major is ≥1.
76
- *
77
- * Returns Node (`process.execPath`) on every other path — missing bun,
78
- * version probe failure, version <1, malformed version banner. Silent
79
- * fallback: never throws, never logs to stderr (a noisy log would clutter
80
- * the same MCP boot output that #719 tightened up).
81
- *
82
- * Result is cached at module load so the cost is amortised across every
83
- * hook command emission for the lifetime of the process. The cache also
84
- * keeps the behaviour deterministic — if the user `brew uninstall bun`
85
- * mid-session, the cached resolution stays valid for that session and the
86
- * next MCP boot re-detects.
87
- *
88
- * Why bun ≥1.0 instead of "any bun":
89
- * - Bun 0.x had multiple ESM/module-resolution regressions that broke
90
- * dynamic `import()` inside hooks (and our hooks do ~7 dynamic imports
91
- * in `pretooluse.mjs`).
92
- * - 1.0 ships stable npm-compat that our better-sqlite3-adjacent code
93
- * relies on indirectly (hooks share `ensure-deps.mjs` which is
94
- * bun-safe past 1.0 but not 0.x).
95
- *
96
- * NOT used by:
97
- * - `buildNodeCommand` — kept on `process.execPath` for openclaw doctor /
98
- * upgrade hints which must invoke the better-sqlite3-loading CLI on
99
- * Node (#543: bun cannot dlopen better-sqlite3's prebuilt .node).
100
- * - `ensure-deps.mjs` — separate path, must stay on Node for the same
101
- * reason.
102
- * - `ctx_upgrade` — separate path, must stay on Node for the same reason.
103
- */
104
68
  export declare function resolveHookRuntime(): HookRuntime;
105
69
  export declare function getRuntimeSummary(runtimes: RuntimeMap): string;
106
70
  export declare function getAvailableLanguages(runtimes: RuntimeMap): Language[];
package/build/runtime.js CHANGED
@@ -27,6 +27,10 @@ function isWindowsWslBash(shellPath) {
27
27
  return /\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(lower) ||
28
28
  /\\microsoft\\windowsapps\\bash\.exe$/.test(lower);
29
29
  }
30
+ function isWindowsSystemCmd(shellPath) {
31
+ const lower = shellPath.toLowerCase().replace(/\//g, "\\");
32
+ return /\\windows\\(?:system32|sysnative)\\cmd\.exe$/.test(lower);
33
+ }
30
34
  const isWindows = process.platform === "win32";
31
35
  function commandExists(cmd) {
32
36
  try {
@@ -130,38 +134,69 @@ function bunFallbackPaths() {
130
134
  }
131
135
  return home ? [`${home}/.bun/bin/bun`] : [];
132
136
  }
137
+ /** Well-known Git-for-Windows bash.exe locations (MSYS bash that performs
138
+ * Windows→POSIX path conversion for native git — #826). */
139
+ const KNOWN_GIT_BASH_PATHS = [
140
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
141
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
142
+ ];
133
143
  /**
134
- * On Windows, resolve the first non-WSL bash in PATH.
135
- * WSL bash (C:\Windows\System32\bash.exe) cannot handle Windows paths,
136
- * so we skip it and prefer Git Bash or MSYS2 bash instead.
144
+ * On Windows, resolve the first non-WSL bash that is actually available.
145
+ *
146
+ * Availability is gated by `where bash` (#796): bash must be discoverable on
147
+ * PATH for us to claim it. WSL bash (C:\Windows\System32\bash.exe) cannot
148
+ * handle Windows paths, so we skip it and prefer Git Bash / MSYS2 bash.
149
+ *
150
+ * Routing the gate through `where bash` — rather than probing the known Git
151
+ * Bash paths with existsSync first — is deliberate: when bash is genuinely
152
+ * unavailable, the caller must fall through to pwsh (PR intent). Probing the
153
+ * filesystem first re-detected a real Git Bash on the runner even though the
154
+ * scenario was "bash unavailable", so pwsh was never reached.
155
+ *
156
+ * #826 is preserved: when `where bash` surfaces a Git Bash candidate we
157
+ * canonicalize it to the absolute Git\usr\bin\bash.exe path (so native git
158
+ * keeps MSYS path conversion) by preferring a matching known path that exists.
137
159
  */
138
160
  function resolveWindowsBash() {
139
- // First, try well-known Git Bash locations directly (works even when
140
- // Git\usr\bin is not on PATH, which is common in MCP server environments
141
- // that only inherit Git\cmd from the system PATH).
142
- const knownPaths = [
143
- "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
144
- "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
145
- ];
146
- for (const p of knownPaths) {
147
- if (existsSync(p))
148
- return p;
149
- }
150
- // Fallback: scan PATH via `where bash`, skipping WSL and WindowsApps entries.
161
+ let candidates;
151
162
  try {
152
163
  const result = execSync("where bash", { encoding: "utf-8", stdio: "pipe" });
153
- const candidates = result.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
154
- for (const p of candidates) {
155
- const lower = p.toLowerCase();
156
- if (lower.includes("system32") || lower.includes("windowsapps"))
157
- continue;
158
- return p;
159
- }
160
- return null;
164
+ candidates = result.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean);
161
165
  }
162
166
  catch {
167
+ // bash not on PATH → genuinely unavailable. Fall through to pwsh/etc.
163
168
  return null;
164
169
  }
170
+ for (const p of candidates) {
171
+ const lower = p.toLowerCase();
172
+ if (lower.includes("system32") || lower.includes("windowsapps"))
173
+ continue;
174
+ // Prefer the canonical Git\usr\bin\bash.exe so native git retains MSYS
175
+ // path conversion. `where bash` on a Git-for-Windows install may surface
176
+ // the Git\cmd\bash shim or usr\bin path; upgrade to a known absolute path
177
+ // when one exists on disk.
178
+ for (const known of KNOWN_GIT_BASH_PATHS) {
179
+ if (existsSync(known))
180
+ return known;
181
+ }
182
+ return p;
183
+ }
184
+ return null;
185
+ }
186
+ function resolveWindowsShell(windowsBash = resolveWindowsBash()) {
187
+ // Prefer Git Bash (#826) so native git keeps its MSYS path conversion.
188
+ // The caller passes the already-resolved windowsBash to avoid probing the
189
+ // filesystem twice (it also feeds the cmd.exe shellOverride guard above).
190
+ // Fall back through POSIX sh, then PowerShell Core (pwsh) for proper UTF-8
191
+ // handling, then Windows PowerShell, then cmd.exe as the last resort.
192
+ return windowsBash
193
+ ?? (commandExists("sh")
194
+ ? "sh"
195
+ : commandExists("pwsh")
196
+ ? "pwsh"
197
+ : commandExists("powershell")
198
+ ? "powershell"
199
+ : "cmd.exe");
165
200
  }
166
201
  function getVersion(cmd, args = ["--version"]) {
167
202
  try {
@@ -231,7 +266,17 @@ export function resolveJavascriptRuntime(bun, deps = {}) {
231
266
  if (JS_RUNTIMES.has(base)) {
232
267
  // Real JS runtime (node, bun, deno) — preserves #190 snap-Node fix
233
268
  // because the snap wrapper's binary is literally named `node`.
234
- return execPath;
269
+ //
270
+ // Issue #800 — liveness guard: on Homebrew, process.execPath points into
271
+ // the versioned Cellar (/opt/homebrew/Cellar/node/26.0.0/bin/node).
272
+ // `brew upgrade` + `brew cleanup` deletes the old Cellar, so the path
273
+ // dangles for the life of the already-running MCP server. If the path
274
+ // doesn't exist on disk, skip it and fall through to PATH node.
275
+ if (existsSync(execPath)) {
276
+ return execPath;
277
+ }
278
+ // Stale execPath (deleted Cellar, corrupted install, uninstall while
279
+ // process alive). Fall through to PATH resolution below.
235
280
  }
236
281
  // Host binary (opencode/kilo/etc.) — fall back to node on PATH.
237
282
  if (cmdExists("node"))
@@ -252,10 +297,15 @@ export function detectRuntimes() {
252
297
  // could redirect the executor to /usr/bin/python or any arbitrary binary.
253
298
  const userShell = process.env.SHELL;
254
299
  const isWin = process.platform === "win32";
300
+ const windowsBash = isWin ? resolveWindowsBash() : null;
255
301
  const shellOverride = userShell &&
256
302
  existsSync(userShell) &&
257
303
  isAllowlistedShell(userShell) &&
258
- !(isWin && isWindowsWslBash(userShell))
304
+ !(isWin && isWindowsWslBash(userShell)) &&
305
+ // Windows OpenSSH can inject the system cmd.exe as ambient SHELL. When
306
+ // Git Bash is installed, treating that as an explicit override breaks the
307
+ // POSIX shell executor path restored by #36/#384/#791.
308
+ !(isWin && windowsBash && isWindowsSystemCmd(userShell))
259
309
  ? userShell
260
310
  : null;
261
311
  return {
@@ -275,7 +325,7 @@ export function detectRuntimes() {
275
325
  ? "py"
276
326
  : null,
277
327
  shell: shellOverride ?? (isWin
278
- ? (resolveWindowsBash() ?? (commandExists("sh") ? "sh" : commandExists("powershell") ? "powershell" : "cmd.exe"))
328
+ ? resolveWindowsShell(windowsBash)
279
329
  : commandExists("bash") ? "bash" : "sh"),
280
330
  ruby: commandExists("ruby") ? "ruby" : null,
281
331
  go: commandExists("go") ? "go" : null,
@@ -360,10 +410,40 @@ function bunVersionAtLeast1(versionOutput) {
360
410
  * reason.
361
411
  * - `ctx_upgrade` — separate path, must stay on Node for the same reason.
362
412
  */
413
+ /**
414
+ * Liveness-guarded Node path for the hook-runtime fallback (issue #841).
415
+ *
416
+ * `process.execPath` is pinned into every baked hook command because PATH
417
+ * resolution is unreliable for hooks (#190 snap-Node re-invokes the wrapper;
418
+ * #369 Windows Git Bash / MSYS can't resolve a bare `node`). But under a
419
+ * version manager (mise / asdf / nvm) execPath is a *version-pinned* absolute
420
+ * path — e.g. `~/.local/share/mise/installs/node/20.1.0/bin/node`. A routine
421
+ * `mise upgrade node` installs the next patch and DELETES the 20.1.0 dir, so
422
+ * the cached path dangles and every hook spawn fails with ENOENT — silently
423
+ * killing context-mode for that user.
424
+ *
425
+ * Same liveness-guard shape as the #800/#803 fix in
426
+ * {@link resolveJavascriptRuntime}: use the pinned execPath IFF it still
427
+ * exists on disk (preserving the #190/#369 reasons it was pinned), otherwise
428
+ * re-resolve a working `node` from PATH. The version manager's shim dir is on
429
+ * PATH and always points at the current patch, so bare `node` heals the host
430
+ * without a re-install. Falls back to the (stale) execPath only when no PATH
431
+ * node is reachable either — a strictly-better last resort than a dangling
432
+ * versioned path, and the doctor/upgrade flows surface the actionable error.
433
+ */
434
+ function liveNodeRuntime() {
435
+ if (existsSync(process.execPath)) {
436
+ return { path: process.execPath, isBun: false };
437
+ }
438
+ if (commandExists("node")) {
439
+ return { path: "node", isBun: false };
440
+ }
441
+ return { path: process.execPath, isBun: false };
442
+ }
363
443
  export function resolveHookRuntime() {
364
444
  if (_hookRuntimeCache)
365
445
  return _hookRuntimeCache;
366
- const nodeFallback = { path: process.execPath, isBun: false };
446
+ const nodeFallback = liveNodeRuntime();
367
447
  try {
368
448
  if (!bunExists()) {
369
449
  _hookRuntimeCache = nodeFallback;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ctx_search flood-guard — per-agent-context progressive throttle.
3
+ *
4
+ * Background (#79 / #155 / #697): ctx_search carries a progressive throttle
5
+ * so a single actor cannot spam dozens of individual searches and flood the
6
+ * context window instead of batching via ctx_batch_execute. The original
7
+ * implementation kept ONE module-global counter on the MCP server process.
8
+ *
9
+ * Issue #769: a parallel multi-agent fan-out (Claude Code Task/Workflow)
10
+ * runs N subagents concurrently against the SAME per-session MCP server
11
+ * process. With a single global counter their independent calls are summed
12
+ * into one budget, so legitimate fan-out ("10 agents x 2 calls") trips the
13
+ * guard that was only ever meant to catch ONE actor spamming. The budget is
14
+ * tool-availability state that is logically per-agent-context, so the counter
15
+ * must be keyed per agent-context — NOT removed. Single-actor flood
16
+ * protection is preserved exactly; only the bucketing changes.
17
+ *
18
+ * This module is pure and transport-free so the policy is unit-testable
19
+ * without spinning up the MCP server. `src/server.ts` owns the singleton and
20
+ * supplies the per-call agent key (the session/agent id from
21
+ * currentAttribution()).
22
+ */
23
+ export interface FloodGuardConfig {
24
+ /** Rolling window length in ms. After this elapses a key's counter resets. */
25
+ windowMs: number;
26
+ /** After this many calls in the window, results taper to 1 per query. */
27
+ softCapAfter: number;
28
+ /** After this many calls in the window, the call is hard-blocked. */
29
+ blockAfter: number;
30
+ }
31
+ export interface FloodDecision {
32
+ /** This key's call count within the current rolling window (1-based). */
33
+ count: number;
34
+ /** Window start timestamp (ms) for this key — used for the "in Ns" message. */
35
+ windowStart: number;
36
+ /** True once count exceeds blockAfter — caller must refuse the search. */
37
+ blocked: boolean;
38
+ /** True once count exceeds softCapAfter — caller trims to 1 result/query. */
39
+ softCapped: boolean;
40
+ }
41
+ /**
42
+ * A rolling-window call counter bucketed per agent-context key. Each key gets
43
+ * an independent window + counter, so concurrent subagents do not consume one
44
+ * another's budget while a single greedy actor is still throttled and blocked
45
+ * exactly as before.
46
+ */
47
+ export declare class FloodGuard {
48
+ #private;
49
+ constructor(cfg: FloodGuardConfig, maxKeys?: number);
50
+ /**
51
+ * Record one ctx_search call for `key` at time `now` (ms) and return the
52
+ * throttle decision. Pure aside from the internal per-key counter state.
53
+ */
54
+ record(key: string, now?: number): FloodDecision;
55
+ /** Test/diagnostics helper — number of distinct keys currently tracked. */
56
+ size(): number;
57
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ctx_search flood-guard — per-agent-context progressive throttle.
3
+ *
4
+ * Background (#79 / #155 / #697): ctx_search carries a progressive throttle
5
+ * so a single actor cannot spam dozens of individual searches and flood the
6
+ * context window instead of batching via ctx_batch_execute. The original
7
+ * implementation kept ONE module-global counter on the MCP server process.
8
+ *
9
+ * Issue #769: a parallel multi-agent fan-out (Claude Code Task/Workflow)
10
+ * runs N subagents concurrently against the SAME per-session MCP server
11
+ * process. With a single global counter their independent calls are summed
12
+ * into one budget, so legitimate fan-out ("10 agents x 2 calls") trips the
13
+ * guard that was only ever meant to catch ONE actor spamming. The budget is
14
+ * tool-availability state that is logically per-agent-context, so the counter
15
+ * must be keyed per agent-context — NOT removed. Single-actor flood
16
+ * protection is preserved exactly; only the bucketing changes.
17
+ *
18
+ * This module is pure and transport-free so the policy is unit-testable
19
+ * without spinning up the MCP server. `src/server.ts` owns the singleton and
20
+ * supplies the per-call agent key (the session/agent id from
21
+ * currentAttribution()).
22
+ */
23
+ /**
24
+ * A rolling-window call counter bucketed per agent-context key. Each key gets
25
+ * an independent window + counter, so concurrent subagents do not consume one
26
+ * another's budget while a single greedy actor is still throttled and blocked
27
+ * exactly as before.
28
+ */
29
+ export class FloodGuard {
30
+ #cfg;
31
+ #buckets = new Map();
32
+ /**
33
+ * Hard ceiling on tracked keys — a defensive bound so a pathological host
34
+ * that mints unbounded distinct agent ids cannot grow the map without limit.
35
+ * When exceeded, the oldest-window bucket is evicted (its actor simply gets
36
+ * a fresh window on its next call — fail-open, never a false block).
37
+ */
38
+ #maxKeys;
39
+ constructor(cfg, maxKeys = 4096) {
40
+ this.#cfg = cfg;
41
+ this.#maxKeys = Math.max(1, maxKeys);
42
+ }
43
+ /**
44
+ * Record one ctx_search call for `key` at time `now` (ms) and return the
45
+ * throttle decision. Pure aside from the internal per-key counter state.
46
+ */
47
+ record(key, now = Date.now()) {
48
+ let bucket = this.#buckets.get(key);
49
+ if (!bucket || now - bucket.windowStart > this.#cfg.windowMs) {
50
+ bucket = { count: 0, windowStart: now };
51
+ this.#buckets.set(key, bucket);
52
+ this.#evictIfNeeded();
53
+ }
54
+ bucket.count++;
55
+ return {
56
+ count: bucket.count,
57
+ windowStart: bucket.windowStart,
58
+ blocked: bucket.count > this.#cfg.blockAfter,
59
+ softCapped: bucket.count > this.#cfg.softCapAfter,
60
+ };
61
+ }
62
+ /** Test/diagnostics helper — number of distinct keys currently tracked. */
63
+ size() {
64
+ return this.#buckets.size;
65
+ }
66
+ #evictIfNeeded() {
67
+ if (this.#buckets.size <= this.#maxKeys)
68
+ return;
69
+ let oldestKey;
70
+ let oldestStart = Infinity;
71
+ for (const [k, b] of this.#buckets) {
72
+ if (b.windowStart < oldestStart) {
73
+ oldestStart = b.windowStart;
74
+ oldestKey = k;
75
+ }
76
+ }
77
+ if (oldestKey !== undefined)
78
+ this.#buckets.delete(oldestKey);
79
+ }
80
+ }
@@ -41,14 +41,19 @@ export declare function fileGlobToRegex(glob: string, caseInsensitive?: boolean)
41
41
  */
42
42
  export declare function matchesAnyPattern(command: string, patterns: string[], caseInsensitive?: boolean): string | null;
43
43
  /**
44
- * Split a shell command on chain operators (&&, ||, ;, |) while
45
- * respecting single/double quotes and backticks.
44
+ * Split a shell command on chain operators (&&, ||, ;, |, \n, \r, &) while
45
+ * respecting single/double quotes, backticks, subshells, and escape backslashes.
46
46
  *
47
47
  * "echo hello && sudo rm -rf /" → ["echo hello", "sudo rm -rf /"]
48
48
  *
49
49
  * This prevents bypassing deny patterns by prepending innocent commands.
50
50
  */
51
51
  export declare function splitChainedCommands(command: string): string[];
52
+ /**
53
+ * Recursively extract all nested subshell commands from `$()` and `` `...` ``.
54
+ * Handles escaping and quote contexts to ensure correct command boundary detection.
55
+ */
56
+ export declare function extractSubshellCommands(command: string): string[];
52
57
  /**
53
58
  * Read Bash permission policies from up to 3 settings files.
54
59
  *
@@ -93,7 +98,7 @@ export declare function evaluateCommand(command: string, policies: SecurityPolic
93
98
  * The server has no UI for "ask" prompts, so allow/ask patterns are
94
99
  * irrelevant. Returns "deny" if any deny pattern matches, otherwise "allow".
95
100
  *
96
- * Also splits chained commands to prevent bypass.
101
+ * Also splits chained commands and nested subshells to prevent bypass.
97
102
  */
98
103
  export declare function evaluateCommandDenyOnly(command: string, policies: SecurityPolicy[], caseInsensitive?: boolean): {
99
104
  decision: "deny" | "allow";