context-mode 1.0.110 → 1.0.112

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 (151) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/opencode-plugin.js +1 -1
  47. package/build/runPool.d.ts +36 -0
  48. package/build/runPool.js +51 -0
  49. package/build/runtime.js +64 -5
  50. package/build/search/auto-memory.js +6 -4
  51. package/build/security.js +30 -10
  52. package/build/server.d.ts +23 -1
  53. package/build/server.js +652 -174
  54. package/build/session/analytics.d.ts +404 -1
  55. package/build/session/analytics.js +1347 -42
  56. package/build/session/db.d.ts +114 -5
  57. package/build/session/db.js +275 -27
  58. package/build/session/event-emit.d.ts +48 -0
  59. package/build/session/event-emit.js +101 -0
  60. package/build/session/extract.d.ts +1 -0
  61. package/build/session/extract.js +79 -12
  62. package/build/session/purge.d.ts +111 -0
  63. package/build/session/purge.js +138 -0
  64. package/build/store.d.ts +7 -0
  65. package/build/store.js +69 -6
  66. package/build/util/claude-config.d.ts +26 -0
  67. package/build/util/claude-config.js +91 -0
  68. package/build/util/hook-config.d.ts +4 -0
  69. package/build/util/hook-config.js +39 -0
  70. package/cli.bundle.mjs +411 -208
  71. package/configs/antigravity/GEMINI.md +0 -3
  72. package/configs/claude-code/CLAUDE.md +1 -4
  73. package/configs/codex/AGENTS.md +1 -4
  74. package/configs/codex/config.toml +3 -0
  75. package/configs/codex/hooks.json +8 -0
  76. package/configs/cursor/context-mode.mdc +0 -3
  77. package/configs/gemini-cli/GEMINI.md +0 -3
  78. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  79. package/configs/kilo/AGENTS.md +0 -3
  80. package/configs/kiro/KIRO.md +0 -3
  81. package/configs/omp/SYSTEM.md +85 -0
  82. package/configs/omp/mcp.json +7 -0
  83. package/configs/openclaw/AGENTS.md +0 -3
  84. package/configs/opencode/AGENTS.md +0 -3
  85. package/configs/pi/AGENTS.md +0 -3
  86. package/configs/qwen-code/QWEN.md +1 -4
  87. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  88. package/configs/zed/AGENTS.md +0 -3
  89. package/hooks/codex/posttooluse.mjs +9 -2
  90. package/hooks/codex/precompact.mjs +69 -0
  91. package/hooks/codex/sessionstart.mjs +13 -9
  92. package/hooks/codex/stop.mjs +1 -2
  93. package/hooks/codex/userpromptsubmit.mjs +1 -2
  94. package/hooks/core/routing.mjs +237 -18
  95. package/hooks/cursor/afteragentresponse.mjs +1 -1
  96. package/hooks/cursor/hooks.json +31 -0
  97. package/hooks/cursor/posttooluse.mjs +1 -1
  98. package/hooks/cursor/sessionstart.mjs +5 -5
  99. package/hooks/cursor/stop.mjs +1 -1
  100. package/hooks/ensure-deps.mjs +12 -13
  101. package/hooks/gemini-cli/aftertool.mjs +1 -1
  102. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  103. package/hooks/gemini-cli/precompress.mjs +3 -2
  104. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  105. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  106. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  107. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  108. package/hooks/kiro/agentspawn.mjs +5 -5
  109. package/hooks/kiro/posttooluse.mjs +2 -2
  110. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  111. package/hooks/posttooluse.mjs +45 -0
  112. package/hooks/precompact.mjs +17 -0
  113. package/hooks/pretooluse.mjs +23 -0
  114. package/hooks/routing-block.mjs +0 -12
  115. package/hooks/run-hook.mjs +16 -3
  116. package/hooks/session-db.bundle.mjs +27 -18
  117. package/hooks/session-extract.bundle.mjs +2 -2
  118. package/hooks/session-helpers.mjs +101 -64
  119. package/hooks/sessionstart.mjs +51 -2
  120. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  121. package/hooks/vscode-copilot/precompact.mjs +3 -2
  122. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  123. package/openclaw.plugin.json +1 -1
  124. package/package.json +14 -8
  125. package/server.bundle.mjs +349 -147
  126. package/skills/UPSTREAM-CREDITS.md +0 -51
  127. package/skills/context-mode-ops/SKILL.md +0 -299
  128. package/skills/context-mode-ops/agent-teams.md +0 -198
  129. package/skills/context-mode-ops/communication.md +0 -224
  130. package/skills/context-mode-ops/marketing.md +0 -124
  131. package/skills/context-mode-ops/release.md +0 -214
  132. package/skills/context-mode-ops/review-pr.md +0 -269
  133. package/skills/context-mode-ops/tdd.md +0 -329
  134. package/skills/context-mode-ops/triage-issue.md +0 -266
  135. package/skills/context-mode-ops/validation.md +0 -307
  136. package/skills/diagnose/SKILL.md +0 -122
  137. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  138. package/skills/grill-me/SKILL.md +0 -15
  139. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  140. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  141. package/skills/grill-with-docs/SKILL.md +0 -93
  142. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  143. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  144. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  145. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  146. package/skills/tdd/SKILL.md +0 -114
  147. package/skills/tdd/deep-modules.md +0 -33
  148. package/skills/tdd/interface-design.md +0 -31
  149. package/skills/tdd/mocking.md +0 -59
  150. package/skills/tdd/refactoring.md +0 -10
  151. package/skills/tdd/tests.md +0 -61
@@ -16,7 +16,7 @@ import {
16
16
  } from "../routing-block.mjs";
17
17
  import { createToolNamer } from "./tool-naming.mjs";
18
18
  import { isMCPReady } from "./mcp-ready.mjs";
19
- import { existsSync, mkdirSync, rmSync, openSync, closeSync, constants as fsConstants } from "node:fs";
19
+ import { existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, unlinkSync, openSync, closeSync, statSync, constants as fsConstants } from "node:fs";
20
20
 
21
21
  /**
22
22
  * Guard for actions that redirect to MCP tools (#230).
@@ -84,12 +84,30 @@ function guidanceOnce(type, content, sessionId) {
84
84
  return { action: "context", additionalContext: content };
85
85
  }
86
86
 
87
+ /**
88
+ * Robust recursive delete. On Windows, `fs.rmSync` on directories under a
89
+ * tmpdir whose path contains non-ASCII characters (e.g. a Chinese / Japanese /
90
+ * Korean username) silently no-ops without throwing — see #454. Fall back to a
91
+ * manual unlink + rmdir walk so the marker dir actually goes away.
92
+ */
93
+ function rmSyncRobust(dir) {
94
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
95
+ if (!existsSync(dir)) return;
96
+ // Manual fallback for Windows + non-ASCII tmpdir paths
97
+ try {
98
+ for (const name of readdirSync(dir)) {
99
+ try { unlinkSync(resolve(dir, name)); } catch {}
100
+ }
101
+ rmdirSync(dir);
102
+ } catch {}
103
+ }
104
+
87
105
  export function resetGuidanceThrottle(sessionId) {
88
106
  _guidanceShown.clear();
89
107
  // Clear ppid-based dir (legacy / fallback callers) and the sessionId dir if given
90
- try { rmSync(guidanceDirFor(), { recursive: true, force: true }); } catch {}
108
+ rmSyncRobust(guidanceDirFor());
91
109
  if (sessionId) {
92
- try { rmSync(guidanceDirFor(sessionId), { recursive: true, force: true }); } catch {}
110
+ rmSyncRobust(guidanceDirFor(sessionId));
93
111
  }
94
112
  }
95
113
 
@@ -112,15 +130,145 @@ function stripQuotedContent(cmd) {
112
130
  .replace(/"[^"]*"/g, '""'); // double-quoted strings
113
131
  }
114
132
 
133
+ /**
134
+ * Built-in allowlist of structurally-bounded Bash commands (#463).
135
+ *
136
+ * The PreToolUse Bash nudge ("May produce large output. Use ctx_…") is
137
+ * tuned for unbounded commands like `find /` or `cat large-file`. On
138
+ * commands whose stdout is structurally bounded (system probes, version
139
+ * checks, simple git read subcommands), the nudge is pure noise — a
140
+ * recurring ~85 tokens that trains the agent to ignore the warning.
141
+ *
142
+ * isStructurallyBounded() returns true ONLY when the command:
143
+ * 1. Has no shell control operators (pipe, redirect, command
144
+ * substitution, &&, ||, ;) — any of those can compose with an
145
+ * unbounded command and re-introduce flooding.
146
+ * 2. Matches one of the conservative patterns below.
147
+ *
148
+ * Unknown commands are treated as unbounded (false) — fail-safe default.
149
+ */
150
+ const SAFE_COMMAND_PATTERNS = [
151
+ // System probes (no stdout, or one short line)
152
+ // Defense-in-depth (#470): trailing wildcards use `[^\r\n]+` instead of
153
+ // `.+`. The primary gate is SHELL_CONTROL_OPERATORS, which already rejects
154
+ // `\n` / `\r`, but in JS regex `\s` matches LF/CR too — so a pattern like
155
+ // `\s+.+$` would silently span a newline if the operator gate ever
156
+ // regressed. Anchoring `.+` to a single line removes that latent footgun.
157
+ /^pwd$/,
158
+ /^whoami$/,
159
+ /^hostname(?:\s+-[a-zA-Z]+)?$/,
160
+ /^date(?:\s+[^\r\n]+)?$/,
161
+ /^echo\s/,
162
+ /^printf\s/,
163
+ /^which\s+\S+(?:\s+\S+)*$/,
164
+ /^type\s+\S+(?:\s+\S+)*$/,
165
+ /^command\s+-v\s+\S+(?:\s+\S+)*$/,
166
+ /^readlink(?:\s+[^\r\n]+)?$/,
167
+ /^basename(?:\s+[^\r\n]+)?$/,
168
+ /^dirname(?:\s+[^\r\n]+)?$/,
169
+ // Filesystem ops (silent on success, errors on stderr only).
170
+ // For cp / mv / rm we explicitly refuse `-v` / `--verbose`: verbose
171
+ // mode prints one line per file and can flood on big trees
172
+ // (recursive copy of /etc, mass rename, etc.). The "silent on
173
+ // success" invariant only holds without -v.
174
+ /^cd(?:\s+[^\r\n]+)?$/,
175
+ /^mkdir(?:\s+[^\r\n]+)?$/,
176
+ /^touch\s+[^\r\n]+$/,
177
+ /^mv(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
178
+ /^cp(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
179
+ /^rm(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
180
+ // ls — refuse recursive (-R / --recursive) to keep output bounded.
181
+ /^ls(?!\s+-[a-zA-Z]*R)(?!\s+--recursive)(?:\s+[^\r\n]+)?$/,
182
+ // git read-only / status subcommands
183
+ /^git\s+status(?:\s+[^\r\n]+)?$/,
184
+ /^git\s+rev-parse(?:\s+[^\r\n]+)?$/,
185
+ /^git\s+remote(?:\s+-v|\s+show\s+\S+)?$/,
186
+ /^git\s+branch(?:\s+[^\r\n]+)?$/,
187
+ /^git\s+config\s+--get(?:\s+[^\r\n]+)?$/,
188
+ /^git\s+diff\s+--stat(?:\s+[^\r\n]+)?$/,
189
+ /^git\s+diff\s+--name-only(?:\s+[^\r\n]+)?$/,
190
+ /^git\s+stash\s+list$/,
191
+ /^git\s+tag(?:\s+-l(?:\s+[^\r\n]+)?)?$/,
192
+ // git log only when explicitly bounded by -<N> with N up to two digits
193
+ /^git\s+log\s+-\d{1,2}(?:\s+[^\r\n]+)?$/,
194
+ // Version probes (--version anywhere, or `cmd -V`)
195
+ /(?:^|\s)--version(?:\s|$)/,
196
+ /^\S+\s+-V(?:\s|$)/,
197
+ ];
198
+
199
+ // Bash shell control operators that can compose a safe command with an
200
+ // unbounded sink. Any match disqualifies the command from the allowlist.
201
+ //
202
+ // Note `&` (single — background + sequence): listed BEFORE `&&` in the
203
+ // alternation so the regex engine doesn't accidentally short-match `&&`
204
+ // when `&` is itself a separator (`date & cat huge.log`). Without this,
205
+ // `^date(?:\s+.+)?$` would match the whole string and bypass the gate.
206
+ //
207
+ // `\n` / `\r` (newline injection — #470): bash treats LF as a statement
208
+ // separator equivalent to `;`. CRLF (Windows clipboard paste) and bare CR
209
+ // fall in the same defect class. Without these, `git status\nfind /`
210
+ // would short-match the single-line `^git\s+status` pattern and bypass
211
+ // the gate entirely.
212
+ const SHELL_CONTROL_OPERATORS = /[|`\n\r]|\$\(|>>|>|<(?!<)|&(?!&)|&&|\|\||;/;
213
+
214
+ /**
215
+ * @param {string} command Raw Bash command string from the hook payload.
216
+ * @returns {boolean} true when the command's output is bounded enough that
217
+ * the routing nudge would be noise. Conservative — unknown commands
218
+ * return false.
219
+ */
220
+ export function isStructurallyBounded(command) {
221
+ if (!command) return false;
222
+ const trimmed = command.trim();
223
+ if (SHELL_CONTROL_OPERATORS.test(trimmed)) return false;
224
+ return SAFE_COMMAND_PATTERNS.some(rx => rx.test(trimmed));
225
+ }
226
+
115
227
  // Try to import security module — may not exist
116
228
  let security = null;
229
+ let securityInitFailed = false;
117
230
 
231
+ /**
232
+ * @returns {boolean} true if security module loaded successfully.
233
+ *
234
+ * Loud fail: if `build/security.js` is missing or fails to import, log a
235
+ * clear stderr warning instead of swallowing the error silently. Without
236
+ * this, user-configured `permissions.deny` patterns (#466) become no-ops
237
+ * with no indication that policy enforcement is disabled — a fail-open
238
+ * security regression.
239
+ */
118
240
  export async function initSecurity(buildDir) {
119
241
  try {
242
+ const { existsSync } = await import("node:fs");
243
+ const { resolve } = await import("node:path");
120
244
  const { pathToFileURL } = await import("node:url");
121
- const secPath = (await import("node:path")).resolve(buildDir, "security.js");
245
+ const secPath = resolve(buildDir, "security.js");
246
+ if (!existsSync(secPath)) {
247
+ if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
248
+ process.stderr.write(
249
+ `[context-mode] WARNING: ${secPath} not found — security deny patterns will NOT be enforced. ` +
250
+ `Run \`npm run build\` to generate it. Set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1 to silence.\n`,
251
+ );
252
+ }
253
+ securityInitFailed = true;
254
+ return false;
255
+ }
122
256
  security = await import(pathToFileURL(secPath).href);
123
- } catch { /* not available */ }
257
+ return true;
258
+ } catch (err) {
259
+ if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
260
+ process.stderr.write(
261
+ `[context-mode] WARNING: failed to load security module — deny patterns NOT enforced: ${err?.message ?? err}\n`,
262
+ );
263
+ }
264
+ securityInitFailed = true;
265
+ return false;
266
+ }
267
+ }
268
+
269
+ /** @returns {boolean} true if a previous initSecurity() call failed to load the module. */
270
+ export function isSecurityInitFailed() {
271
+ return securityInitFailed;
124
272
  }
125
273
 
126
274
  /**
@@ -182,6 +330,21 @@ const TOOL_ALIASES = {
182
330
  "execute_bash": "Bash",
183
331
  };
184
332
 
333
+ function toolLeafName(toolName) {
334
+ const raw = String(toolName ?? "");
335
+ const withoutMcpPrefix = raw.startsWith("MCP:") ? raw.slice(4) : raw;
336
+ const parts = withoutMcpPrefix.split(/__|\//).filter(Boolean);
337
+ return parts.at(-1) ?? withoutMcpPrefix;
338
+ }
339
+
340
+ function matchesContextModeTool(toolName, ctxName, legacyName) {
341
+ const raw = String(toolName ?? "");
342
+ const leaf = toolLeafName(raw);
343
+ if (leaf === ctxName) return true;
344
+ if (raw.startsWith("MCP:") && leaf === legacyName) return true;
345
+ return raw.includes("context-mode") && leaf === legacyName;
346
+ }
347
+
185
348
  /**
186
349
  * Route a PreToolUse event. Returns normalized decision object or null for passthrough.
187
350
  *
@@ -194,6 +357,23 @@ const TOOL_ALIASES = {
194
357
  * invocations even when process.ppid shifts (Windows/Git Bash — see #298).
195
358
  */
196
359
  export function routePreToolUse(toolName, toolInput, projectDir, platform, sessionId) {
360
+ // ─── Opt-in fail-closed gate (#468 follow-up) ───
361
+ // Default behavior on security-module load failure is fail-OPEN (a stderr
362
+ // warning is emitted but routing continues). Security-conscious users can
363
+ // opt in to fail-CLOSED via CONTEXT_MODE_REQUIRE_SECURITY=1 — every PreToolUse
364
+ // event is denied with a clear reason until the security module loads cleanly.
365
+ // Universal gate (applies to all tools, not just Bash) since user `permissions.deny`
366
+ // patterns may target Read/Write paths that would otherwise leak before security loads.
367
+ if (process.env.CONTEXT_MODE_REQUIRE_SECURITY === "1" && securityInitFailed) {
368
+ return {
369
+ action: "deny",
370
+ reason:
371
+ "context-mode: security module unavailable and CONTEXT_MODE_REQUIRE_SECURITY=1 — fail-closed engaged. " +
372
+ "Run `npm run build` (or reinstall context-mode) to restore security enforcement. " +
373
+ "To bypass, unset or set CONTEXT_MODE_REQUIRE_SECURITY=0.",
374
+ };
375
+ }
376
+
197
377
  // Build platform-specific tool namer (defaults to claude-code for backward compat)
198
378
  const t = createToolNamer(platform || "claude-code");
199
379
 
@@ -276,6 +456,15 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
276
456
  updatedInput: {
277
457
  command: `echo "context-mode: curl/wget blocked. Think in Code — use ${t("ctx_execute")}(language, code) to write code that fetches, processes, and prints only the answer. Or use ${t("ctx_fetch_and_index")}(url, source) to fetch and index. Write pure JS with try/catch, no npm deps. Do NOT retry with curl/wget."`,
278
458
  },
459
+ // D2 PRD Phase 3.1: marker payload for PostToolUse byte accounting.
460
+ redirectMeta: {
461
+ tool: "Bash",
462
+ type: "bash-redirected",
463
+ // 8192 byte default — typical curl/wget HTTP body the agent would
464
+ // have spilled into the model's context window had we not blocked.
465
+ bytesAvoided: 8192,
466
+ commandSummary: command.slice(0, 200),
467
+ },
279
468
  });
280
469
  }
281
470
  // All segments safe → allow through
@@ -314,12 +503,41 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
314
503
  });
315
504
  }
316
505
 
506
+ // Skip the routing nudge for commands whose output is structurally
507
+ // bounded (#463) — pwd, whoami, git status, --version probes, etc.
508
+ // Conservative: any pipe/redirect/chain disqualifies, unknown commands
509
+ // still get the nudge.
510
+ if (isStructurallyBounded(command)) {
511
+ return null;
512
+ }
513
+
317
514
  // allow all other Bash commands, but inject routing nudge (once per session)
318
515
  return guidanceOnce("bash", bashGuidance, sessionId);
319
516
  }
320
517
 
321
- // ─── Read: nudge toward execute_file (once per session) ───
518
+ // ─── Read: nudge toward execute_file + large-file byte accounting ───
519
+ // D2 PRD Phase 4 (slices 4.4–4.6): when the file is large enough to flood
520
+ // context, attach `redirectMeta` so PostToolUse can emit a `read-redirected`
521
+ // event with the actual file size as bytes_avoided. Threshold = 50 000 bytes;
522
+ // smaller reads stay on the existing one-shot guidance nudge.
322
523
  if (canonical === "Read") {
524
+ const filePath = toolInput.file_path ?? toolInput.path ?? "";
525
+ if (filePath) {
526
+ try {
527
+ const st = statSync(filePath);
528
+ if (st.isFile() && st.size > 50_000) {
529
+ const decision = guidanceOnce("read", readGuidance, sessionId)
530
+ ?? { action: "context", additionalContext: readGuidance };
531
+ decision.redirectMeta = {
532
+ tool: "Read",
533
+ type: "read-redirected",
534
+ bytesAvoided: st.size,
535
+ commandSummary: String(filePath).slice(0, 200),
536
+ };
537
+ return decision;
538
+ }
539
+ } catch { /* file missing or unreadable — fall through to plain guidance */ }
540
+ }
323
541
  return guidanceOnce("read", readGuidance, sessionId);
324
542
  }
325
543
 
@@ -334,6 +552,15 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
334
552
  return mcpRedirect({
335
553
  action: "deny",
336
554
  reason: `context-mode: WebFetch blocked. Think in Code — use ${t("ctx_fetch_and_index")}(url: "${url}", source: "...") to fetch and index, then ${t("ctx_search")}(queries: [...]) to query. Or use ${t("ctx_execute")}(language, code) to fetch, process, and console.log() only what you need. Write pure JS, no npm deps. Do NOT use curl, wget, or WebFetch.`,
555
+ // D2 PRD Phase 4.1: marker payload for PostToolUse byte accounting.
556
+ redirectMeta: {
557
+ tool: "WebFetch",
558
+ type: "webfetch-redirected",
559
+ // 16384 = typical web page body bytes prevented from entering the
560
+ // model's context window.
561
+ bytesAvoided: 16384,
562
+ commandSummary: String(url).slice(0, 200),
563
+ },
337
564
  });
338
565
  }
339
566
 
@@ -356,12 +583,8 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
356
583
  }
357
584
 
358
585
  // ─── MCP execute: security check for shell commands ───
359
- // Match both __execute and __ctx_execute (prefixed tool names)
360
- // Cursor can also surface the tool as MCP:ctx_execute_file.
361
- if (
362
- (toolName.includes("context-mode") && /(?:__|\/)(ctx_)?execute$/.test(toolName)) ||
363
- /^MCP:(ctx_)?execute$/.test(toolName)
364
- ) {
586
+ // Match bare, generic MCP, and legacy context-mode execute tool names.
587
+ if (matchesContextModeTool(toolName, "ctx_execute", "execute")) {
365
588
  if (security && toolInput.language === "shell") {
366
589
  const code = toolInput.code ?? "";
367
590
  const policies = security.readBashPolicies(projectDir);
@@ -379,11 +602,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
379
602
  }
380
603
 
381
604
  // ─── MCP execute_file: check file path + code against deny patterns ───
382
- // Cursor can also surface the tool as MCP:ctx_execute_file.
383
- if (
384
- (toolName.includes("context-mode") && /(?:__|\/)(ctx_)?execute_file$/.test(toolName)) ||
385
- /^MCP:(ctx_)?execute_file$/.test(toolName)
386
- ) {
605
+ if (matchesContextModeTool(toolName, "ctx_execute_file", "execute_file")) {
387
606
  if (security) {
388
607
  // Check file path against Read deny patterns
389
608
  const filePath = toolInput.path ?? "";
@@ -413,7 +632,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
413
632
  }
414
633
 
415
634
  // ─── MCP batch_execute: check each command individually ───
416
- if (toolName.includes("context-mode") && /(?:__|\/)(ctx_)?batch_execute$/.test(toolName)) {
635
+ if (matchesContextModeTool(toolName, "ctx_batch_execute", "batch_execute")) {
417
636
  if (security) {
418
637
  const commands = toolInput.commands ?? [];
419
638
  const policies = security.readBashPolicies(projectDir);
@@ -51,7 +51,7 @@ try {
51
51
  : text;
52
52
 
53
53
  const { SessionDB } = await loadSessionDB();
54
- const dbPath = getSessionDBPath(OPTS);
54
+ const dbPath = getSessionDBPath(OPTS, projectDir);
55
55
  const db = new SessionDB({ dbPath });
56
56
  const sessionId = getSessionId(input, OPTS);
57
57
 
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "preToolUse": [
5
+ {
6
+ "command": "npx -y context-mode hook cursor pretooluse",
7
+ "matcher": "Shell|Read|Grep|WebFetch|mcp_web_fetch|mcp_fetch_tool|Task|MCP:ctx_execute|MCP:ctx_execute_file|MCP:ctx_batch_execute"
8
+ }
9
+ ],
10
+ "postToolUse": [
11
+ {
12
+ "command": "npx -y context-mode hook cursor posttooluse"
13
+ }
14
+ ],
15
+ "sessionStart": [
16
+ {
17
+ "command": "npx -y context-mode hook cursor sessionstart"
18
+ }
19
+ ],
20
+ "afterAgentResponse": [
21
+ {
22
+ "command": "npx -y context-mode hook cursor afteragentresponse"
23
+ }
24
+ ],
25
+ "stop": [
26
+ {
27
+ "command": "npx -y context-mode hook cursor stop"
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -41,7 +41,7 @@ try {
41
41
  const { resolveProjectAttributions } = await loadProjectAttribution();
42
42
  const { SessionDB } = await loadSessionDB();
43
43
 
44
- const dbPath = getSessionDBPath(OPTS);
44
+ const dbPath = getSessionDBPath(OPTS, projectDir);
45
45
  const db = new SessionDB({ dbPath });
46
46
  const sessionId = getSessionId(input, OPTS);
47
47
 
@@ -47,7 +47,7 @@ try {
47
47
 
48
48
  if (source === "compact" || source === "resume") {
49
49
  const { SessionDB } = await loadSessionDB();
50
- const dbPath = getSessionDBPath(OPTS);
50
+ const dbPath = getSessionDBPath(OPTS, projectDir);
51
51
  const db = new SessionDB({ dbPath });
52
52
 
53
53
  if (source === "compact") {
@@ -57,7 +57,7 @@ try {
57
57
  db.markResumeConsumed(sessionId);
58
58
  }
59
59
  } else {
60
- try { unlinkSync(getCleanupFlagPath(OPTS)); } catch { /* no flag */ }
60
+ try { unlinkSync(getCleanupFlagPath(OPTS, projectDir)); } catch { /* no flag */ }
61
61
  }
62
62
 
63
63
  // Filter events to the session being resumed/compacted. Falling back to
@@ -68,16 +68,16 @@ try {
68
68
  const sessionId = getSessionId(input, OPTS);
69
69
  const events = sessionId ? getSessionEvents(db, sessionId) : [];
70
70
  if (events.length > 0) {
71
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
71
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS, projectDir));
72
72
  additionalContext += buildSessionDirective(source, eventMeta, toolNamer);
73
73
  }
74
74
 
75
75
  db.close();
76
76
  } else if (source === "startup") {
77
77
  const { SessionDB } = await loadSessionDB();
78
- const dbPath = getSessionDBPath(OPTS);
78
+ const dbPath = getSessionDBPath(OPTS, projectDir);
79
79
  const db = new SessionDB({ dbPath });
80
- try { unlinkSync(getSessionEventsPath(OPTS)); } catch { /* no stale file */ }
80
+ try { unlinkSync(getSessionEventsPath(OPTS, projectDir)); } catch { /* no stale file */ }
81
81
 
82
82
  db.cleanupOldSessions(7);
83
83
  db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
@@ -28,7 +28,7 @@ try {
28
28
 
29
29
  const { SessionDB } = await loadSessionDB();
30
30
 
31
- const dbPath = getSessionDBPath(OPTS);
31
+ const dbPath = getSessionDBPath(OPTS, projectDir);
32
32
  const db = new SessionDB({ dbPath });
33
33
  const sessionId = getSessionId(input, OPTS);
34
34
 
@@ -141,19 +141,18 @@ export function ensureNativeCompat(pluginRoot) {
141
141
  }
142
142
 
143
143
  if (skipProbe) {
144
- // On modern Node: if binary exists, trust it; if missing, rebuild without probing
145
- if (!existsSync(binaryPath)) {
146
- execSync(`${process.platform === "win32" ? "npm.cmd" : "npm"} rebuild better-sqlite3 --ignore-scripts=false`, {
147
- cwd: pluginRoot,
148
- stdio: "pipe",
149
- timeout: 60000,
150
- shell: true,
151
- });
152
- codesignBinary(binaryPath);
153
- // Cache the rebuilt binary for this ABI
154
- if (existsSync(binaryPath)) {
155
- copyFileSync(binaryPath, abiCachePath);
156
- }
144
+ // On modern Node, the current ABI cache is the compatibility marker.
145
+ // Without it, rebuild even when the active binary exists: it may be stale
146
+ // from a previous Node ABI and cannot be probed safely here.
147
+ execSync(`${process.platform === "win32" ? "npm.cmd" : "npm"} rebuild better-sqlite3 --ignore-scripts=false`, {
148
+ cwd: pluginRoot,
149
+ stdio: "pipe",
150
+ timeout: 60000,
151
+ shell: true,
152
+ });
153
+ codesignBinary(binaryPath);
154
+ if (existsSync(binaryPath)) {
155
+ copyFileSync(binaryPath, abiCachePath);
157
156
  }
158
157
  return;
159
158
  }
@@ -33,7 +33,7 @@ try {
33
33
  const { resolveProjectAttributions } = await loadProjectAttribution();
34
34
  const { SessionDB } = await loadSessionDB();
35
35
 
36
- const dbPath = getSessionDBPath(OPTS);
36
+ const dbPath = getSessionDBPath(OPTS, projectDir);
37
37
  const db = new SessionDB({ dbPath });
38
38
  const sessionId = getSessionId(input, OPTS);
39
39
 
@@ -48,7 +48,7 @@ try {
48
48
  const { SessionDB } = await loadSessionDB();
49
49
  const { extractUserEvents } = await loadExtract();
50
50
  const { resolveProjectAttributions } = await loadProjectAttribution();
51
- const dbPath = getSessionDBPath(OPTS);
51
+ const dbPath = getSessionDBPath(OPTS, projectDir);
52
52
  const db = new SessionDB({ dbPath });
53
53
  const sessionId = getSessionId(input, OPTS);
54
54
 
@@ -9,7 +9,7 @@ import "../ensure-deps.mjs";
9
9
  * snapshot (<2KB XML), and stores it for injection after compress.
10
10
  */
11
11
 
12
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, GEMINI_OPTS } from "../session-helpers.mjs";
12
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, GEMINI_OPTS } from "../session-helpers.mjs";
13
13
  import { createSessionLoaders } from "../session-loaders.mjs";
14
14
  import { appendFileSync } from "node:fs";
15
15
  import { join, dirname } from "node:path";
@@ -24,11 +24,12 @@ const DEBUG_LOG = join(homedir(), ".gemini", "context-mode", "precompress-debug.
24
24
  try {
25
25
  const raw = await readStdin();
26
26
  const input = parseStdin(raw);
27
+ const projectDir = getInputProjectDir(input, OPTS);
27
28
 
28
29
  const { buildResumeSnapshot } = await loadSnapshot();
29
30
  const { SessionDB } = await loadSessionDB();
30
31
 
31
- const dbPath = getSessionDBPath(OPTS);
32
+ const dbPath = getSessionDBPath(OPTS, projectDir);
32
33
  const db = new SessionDB({ dbPath });
33
34
  const sessionId = getSessionId(input, OPTS);
34
35
 
@@ -19,7 +19,7 @@ const ROUTING_BLOCK = createRoutingBlock(toolNamer);
19
19
  import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "../session-directive.mjs";
20
20
  import {
21
21
  readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
22
- getProjectDir, GEMINI_OPTS,
22
+ getInputProjectDir, GEMINI_OPTS,
23
23
  } from "../session-helpers.mjs";
24
24
  import { createSessionLoaders } from "../session-loaders.mjs";
25
25
  import { join, dirname } from "node:path";
@@ -37,10 +37,11 @@ try {
37
37
  const raw = await readStdin();
38
38
  const input = parseStdin(raw);
39
39
  const source = input.source ?? "startup";
40
+ const projectDir = getInputProjectDir(input, OPTS);
40
41
 
41
42
  if (source === "compact") {
42
43
  const { SessionDB } = await loadSessionDB();
43
- const dbPath = getSessionDBPath(OPTS);
44
+ const dbPath = getSessionDBPath(OPTS, projectDir);
44
45
  const db = new SessionDB({ dbPath });
45
46
  const sessionId = getSessionId(input, OPTS);
46
47
  const resume = db.getResume(sessionId);
@@ -51,16 +52,16 @@ try {
51
52
 
52
53
  const events = getSessionEvents(db, sessionId);
53
54
  if (events.length > 0) {
54
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
55
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS, projectDir));
55
56
  additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
56
57
  }
57
58
 
58
59
  db.close();
59
60
  } else if (source === "resume") {
60
- try { unlinkSync(getCleanupFlagPath(OPTS)); } catch { /* no flag */ }
61
+ try { unlinkSync(getCleanupFlagPath(OPTS, projectDir)); } catch { /* no flag */ }
61
62
 
62
63
  const { SessionDB } = await loadSessionDB();
63
- const dbPath = getSessionDBPath(OPTS);
64
+ const dbPath = getSessionDBPath(OPTS, projectDir);
64
65
  const db = new SessionDB({ dbPath });
65
66
 
66
67
  // Filter events to the session being resumed. Falling back to
@@ -70,22 +71,21 @@ try {
70
71
  const sessionId = getSessionId(input, OPTS);
71
72
  const events = sessionId ? getSessionEvents(db, sessionId) : [];
72
73
  if (events.length > 0) {
73
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
74
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS, projectDir));
74
75
  additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
75
76
  }
76
77
 
77
78
  db.close();
78
79
  } else if (source === "startup") {
79
80
  const { SessionDB } = await loadSessionDB();
80
- const dbPath = getSessionDBPath(OPTS);
81
+ const dbPath = getSessionDBPath(OPTS, projectDir);
81
82
  const db = new SessionDB({ dbPath });
82
- try { unlinkSync(getSessionEventsPath(OPTS)); } catch { /* no stale file */ }
83
+ try { unlinkSync(getSessionEventsPath(OPTS, projectDir)); } catch { /* no stale file */ }
83
84
 
84
85
  db.cleanupOldSessions(7);
85
86
  db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
86
87
 
87
88
  const sessionId = getSessionId(input, OPTS);
88
- const projectDir = getProjectDir(OPTS);
89
89
  db.ensureSession(sessionId, projectDir);
90
90
 
91
91
  // Auto-write GEMINI.md on startup if missing or not merged yet
@@ -33,7 +33,7 @@ try {
33
33
  const { resolveProjectAttributions } = await loadProjectAttribution();
34
34
  const { SessionDB } = await loadSessionDB();
35
35
 
36
- const dbPath = getSessionDBPath(OPTS);
36
+ const dbPath = getSessionDBPath(OPTS, projectDir);
37
37
  const db = new SessionDB({ dbPath });
38
38
  const sessionId = getSessionId(input, OPTS);
39
39
 
@@ -10,7 +10,7 @@ import "../ensure-deps.mjs";
10
10
  */
11
11
 
12
12
  import { createSessionLoaders } from "../session-loaders.mjs";
13
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, JETBRAINS_OPTS } from "../session-helpers.mjs";
13
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, JETBRAINS_OPTS } from "../session-helpers.mjs";
14
14
  import { appendFileSync } from "node:fs";
15
15
  import { join, dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
@@ -24,11 +24,12 @@ const DEBUG_LOG = join(homedir(), ".config", "JetBrains", "context-mode", "preco
24
24
  try {
25
25
  const raw = await readStdin();
26
26
  const input = parseStdin(raw);
27
+ const projectDir = getInputProjectDir(input, OPTS);
27
28
 
28
29
  const { buildResumeSnapshot } = await loadSnapshot();
29
30
  const { SessionDB } = await loadSessionDB();
30
31
 
31
- const dbPath = getSessionDBPath(OPTS);
32
+ const dbPath = getSessionDBPath(OPTS, projectDir);
32
33
  const db = new SessionDB({ dbPath });
33
34
  const sessionId = getSessionId(input, OPTS);
34
35