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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/index.ts +3 -2
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +152 -34
- package/bin/statusline.mjs +144 -127
- package/build/adapters/base.d.ts +8 -5
- package/build/adapters/base.js +8 -18
- package/build/adapters/claude-code/index.d.ts +24 -3
- package/build/adapters/claude-code/index.js +44 -11
- package/build/adapters/codex/hooks.d.ts +10 -5
- package/build/adapters/codex/hooks.js +10 -5
- package/build/adapters/codex/index.d.ts +17 -5
- package/build/adapters/codex/index.js +337 -37
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/cursor/index.d.ts +6 -0
- package/build/adapters/cursor/index.js +83 -2
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +29 -6
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +75 -0
- package/build/adapters/omp/plugin.js +220 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +198 -0
- package/build/adapters/openclaw/plugin.d.ts +130 -0
- package/build/adapters/openclaw/plugin.js +629 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/plugin.d.ts +145 -0
- package/build/adapters/opencode/plugin.js +457 -0
- package/build/adapters/pi/extension.d.ts +26 -0
- package/build/adapters/pi/extension.js +552 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +113 -0
- package/build/adapters/pi/mcp-bridge.js +251 -0
- package/build/adapters/types.d.ts +11 -6
- package/build/cli.js +186 -170
- package/build/db-base.d.ts +15 -2
- package/build/db-base.js +50 -5
- package/build/executor.d.ts +2 -0
- package/build/executor.js +15 -2
- package/build/opencode-plugin.js +1 -1
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.js +64 -5
- package/build/search/auto-memory.js +6 -4
- package/build/security.js +30 -10
- package/build/server.d.ts +23 -1
- package/build/server.js +652 -174
- package/build/session/analytics.d.ts +404 -1
- package/build/session/analytics.js +1347 -42
- package/build/session/db.d.ts +114 -5
- package/build/session/db.js +275 -27
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +1 -0
- package/build/session/extract.js +79 -12
- package/build/session/purge.d.ts +111 -0
- package/build/session/purge.js +138 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +69 -6
- package/build/util/claude-config.d.ts +26 -0
- package/build/util/claude-config.js +91 -0
- package/build/util/hook-config.d.ts +4 -0
- package/build/util/hook-config.js +39 -0
- package/cli.bundle.mjs +411 -208
- package/configs/antigravity/GEMINI.md +0 -3
- package/configs/claude-code/CLAUDE.md +1 -4
- package/configs/codex/AGENTS.md +1 -4
- package/configs/codex/config.toml +3 -0
- package/configs/codex/hooks.json +8 -0
- package/configs/cursor/context-mode.mdc +0 -3
- package/configs/gemini-cli/GEMINI.md +0 -3
- package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
- package/configs/kilo/AGENTS.md +0 -3
- package/configs/kiro/KIRO.md +0 -3
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +0 -3
- package/configs/opencode/AGENTS.md +0 -3
- package/configs/pi/AGENTS.md +0 -3
- package/configs/qwen-code/QWEN.md +1 -4
- package/configs/vscode-copilot/copilot-instructions.md +0 -3
- package/configs/zed/AGENTS.md +0 -3
- package/hooks/codex/posttooluse.mjs +9 -2
- package/hooks/codex/precompact.mjs +69 -0
- package/hooks/codex/sessionstart.mjs +13 -9
- package/hooks/codex/stop.mjs +1 -2
- package/hooks/codex/userpromptsubmit.mjs +1 -2
- package/hooks/core/routing.mjs +237 -18
- package/hooks/cursor/afteragentresponse.mjs +1 -1
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +1 -1
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +1 -1
- package/hooks/ensure-deps.mjs +12 -13
- package/hooks/gemini-cli/aftertool.mjs +1 -1
- package/hooks/gemini-cli/beforeagent.mjs +1 -1
- package/hooks/gemini-cli/precompress.mjs +3 -2
- package/hooks/gemini-cli/sessionstart.mjs +9 -9
- package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
- package/hooks/jetbrains-copilot/precompact.mjs +3 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
- package/hooks/kiro/agentspawn.mjs +5 -5
- package/hooks/kiro/posttooluse.mjs +2 -2
- package/hooks/kiro/userpromptsubmit.mjs +1 -1
- package/hooks/posttooluse.mjs +45 -0
- package/hooks/precompact.mjs +17 -0
- package/hooks/pretooluse.mjs +23 -0
- package/hooks/routing-block.mjs +0 -12
- package/hooks/run-hook.mjs +16 -3
- package/hooks/session-db.bundle.mjs +27 -18
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +101 -64
- package/hooks/sessionstart.mjs +51 -2
- package/hooks/vscode-copilot/posttooluse.mjs +1 -1
- package/hooks/vscode-copilot/precompact.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +14 -8
- package/server.bundle.mjs +349 -147
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/context-mode-ops/SKILL.md +0 -299
- package/skills/context-mode-ops/agent-teams.md +0 -198
- package/skills/context-mode-ops/communication.md +0 -224
- package/skills/context-mode-ops/marketing.md +0 -124
- package/skills/context-mode-ops/release.md +0 -214
- package/skills/context-mode-ops/review-pr.md +0 -269
- package/skills/context-mode-ops/tdd.md +0 -329
- package/skills/context-mode-ops/triage-issue.md +0 -266
- package/skills/context-mode-ops/validation.md +0 -307
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- package/skills/tdd/tests.md +0 -61
package/hooks/core/routing.mjs
CHANGED
|
@@ -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
|
-
|
|
108
|
+
rmSyncRobust(guidanceDirFor());
|
|
91
109
|
if (sessionId) {
|
|
92
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -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)`);
|
package/hooks/cursor/stop.mjs
CHANGED
package/hooks/ensure-deps.mjs
CHANGED
|
@@ -141,19 +141,18 @@ export function ensureNativeCompat(pluginRoot) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
if (skipProbe) {
|
|
144
|
-
// On modern Node
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|