context-mode 0.9.21 → 1.0.0

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 (102) hide show
  1. package/.claude-plugin/hooks/hooks.json +46 -4
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +4 -4
  4. package/README.md +377 -191
  5. package/build/adapters/claude-code/config.d.ts +8 -0
  6. package/build/adapters/claude-code/config.js +8 -0
  7. package/build/adapters/claude-code/hooks.d.ts +53 -0
  8. package/build/adapters/claude-code/hooks.js +88 -0
  9. package/build/adapters/claude-code/index.d.ts +50 -0
  10. package/build/adapters/claude-code/index.js +523 -0
  11. package/build/adapters/codex/config.d.ts +8 -0
  12. package/build/adapters/codex/config.js +8 -0
  13. package/build/adapters/codex/hooks.d.ts +21 -0
  14. package/build/adapters/codex/hooks.js +27 -0
  15. package/build/adapters/codex/index.d.ts +44 -0
  16. package/build/adapters/codex/index.js +223 -0
  17. package/build/adapters/detect.d.ts +26 -0
  18. package/build/adapters/detect.js +131 -0
  19. package/build/adapters/gemini-cli/config.d.ts +8 -0
  20. package/build/adapters/gemini-cli/config.js +8 -0
  21. package/build/adapters/gemini-cli/hooks.d.ts +44 -0
  22. package/build/adapters/gemini-cli/hooks.js +64 -0
  23. package/build/adapters/gemini-cli/index.d.ts +57 -0
  24. package/build/adapters/gemini-cli/index.js +468 -0
  25. package/build/adapters/opencode/config.d.ts +8 -0
  26. package/build/adapters/opencode/config.js +8 -0
  27. package/build/adapters/opencode/hooks.d.ts +38 -0
  28. package/build/adapters/opencode/hooks.js +50 -0
  29. package/build/adapters/opencode/index.d.ts +52 -0
  30. package/build/adapters/opencode/index.js +386 -0
  31. package/build/adapters/types.d.ts +218 -0
  32. package/build/adapters/types.js +13 -0
  33. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  34. package/build/adapters/vscode-copilot/config.js +8 -0
  35. package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
  36. package/build/adapters/vscode-copilot/hooks.js +76 -0
  37. package/build/adapters/vscode-copilot/index.d.ts +58 -0
  38. package/build/adapters/vscode-copilot/index.js +512 -0
  39. package/build/cli.d.ts +9 -6
  40. package/build/cli.js +133 -423
  41. package/build/db-base.d.ts +84 -0
  42. package/build/db-base.js +128 -0
  43. package/build/executor.d.ts +6 -7
  44. package/build/executor.js +111 -51
  45. package/build/opencode-plugin.d.ts +37 -0
  46. package/build/opencode-plugin.js +118 -0
  47. package/build/runtime.js +1 -1
  48. package/build/server.js +436 -117
  49. package/build/session/db.d.ts +110 -0
  50. package/build/session/db.js +285 -0
  51. package/build/session/extract.d.ts +51 -0
  52. package/build/session/extract.js +407 -0
  53. package/build/session/snapshot.d.ts +70 -0
  54. package/build/session/snapshot.js +309 -0
  55. package/build/store.d.ts +4 -22
  56. package/build/store.js +67 -55
  57. package/build/truncate.d.ts +59 -0
  58. package/build/truncate.js +157 -0
  59. package/build/types.d.ts +101 -0
  60. package/build/types.js +20 -0
  61. package/configs/claude-code/CLAUDE.md +62 -0
  62. package/configs/codex/AGENTS.md +58 -0
  63. package/configs/codex/config.toml +5 -0
  64. package/configs/gemini-cli/GEMINI.md +58 -0
  65. package/configs/gemini-cli/mcp.json +7 -0
  66. package/configs/gemini-cli/settings.json +49 -0
  67. package/configs/opencode/AGENTS.md +58 -0
  68. package/configs/opencode/opencode.json +10 -0
  69. package/configs/vscode-copilot/copilot-instructions.md +58 -0
  70. package/configs/vscode-copilot/hooks.json +16 -0
  71. package/configs/vscode-copilot/mcp.json +8 -0
  72. package/hooks/core/formatters.mjs +86 -0
  73. package/hooks/core/routing.mjs +262 -0
  74. package/hooks/core/stdin.mjs +19 -0
  75. package/hooks/formatters/claude-code.mjs +57 -0
  76. package/hooks/formatters/gemini-cli.mjs +55 -0
  77. package/hooks/formatters/vscode-copilot.mjs +55 -0
  78. package/hooks/gemini-cli/aftertool.mjs +58 -0
  79. package/hooks/gemini-cli/beforetool.mjs +25 -0
  80. package/hooks/gemini-cli/precompress.mjs +51 -0
  81. package/hooks/gemini-cli/sessionstart.mjs +117 -0
  82. package/hooks/hooks.json +46 -4
  83. package/hooks/posttooluse.mjs +53 -0
  84. package/hooks/precompact.mjs +55 -0
  85. package/hooks/pretooluse.mjs +23 -266
  86. package/hooks/routing-block.mjs +19 -6
  87. package/hooks/session-directive.mjs +353 -0
  88. package/hooks/session-helpers.mjs +112 -0
  89. package/hooks/sessionstart.mjs +123 -16
  90. package/hooks/userpromptsubmit.mjs +58 -0
  91. package/hooks/vscode-copilot/posttooluse.mjs +58 -0
  92. package/hooks/vscode-copilot/precompact.mjs +51 -0
  93. package/hooks/vscode-copilot/pretooluse.mjs +25 -0
  94. package/hooks/vscode-copilot/sessionstart.mjs +115 -0
  95. package/package.json +20 -17
  96. package/skills/context-mode/SKILL.md +49 -49
  97. package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
  98. package/skills/{stats → ctx-stats}/SKILL.md +3 -3
  99. package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
  100. package/start.mjs +47 -0
  101. package/hooks/pretooluse.sh +0 -147
  102. package/server.bundle.mjs +0 -341
@@ -1,30 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Unified PreToolUse hook for context-mode
3
+ * Unified PreToolUse hook for context-mode (Claude Code)
4
4
  * Redirects data-fetching tools to context-mode MCP tools
5
5
  *
6
6
  * Cross-platform (Windows/macOS/Linux) — no bash/jq dependency.
7
7
  *
8
- * Routing is structured as a pure function that returns a response object
9
- * (or null for passthrough). This avoids process.exit() which drops piped
10
- * stdout on Windows before the buffer is flushed.
8
+ * Routing is delegated to core/routing.mjs (shared across platforms).
9
+ * This file retains the Claude Code-specific self-heal block and
10
+ * uses core/formatters.mjs for Claude Code output format.
11
11
  */
12
12
 
13
- import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, readdirSync, statSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, readdirSync } from "node:fs";
14
14
  import { resolve, dirname, basename } from "node:path";
15
- import { fileURLToPath, pathToFileURL } from "node:url";
15
+ import { fileURLToPath } from "node:url";
16
16
  import { homedir, tmpdir } from "node:os";
17
- import { ROUTING_BLOCK, READ_GUIDANCE, GREP_GUIDANCE } from "./routing-block.mjs";
18
-
19
- // ─── Security module: graceful import from compiled build ───
20
- let security = null;
21
- try {
22
- const __hookDir = dirname(fileURLToPath(import.meta.url));
23
- const secPath = resolve(__hookDir, "..", "build", "security.js");
24
- security = await import(pathToFileURL(secPath).href);
25
- } catch {
26
- // Build not available — skip security checks, rely on existing routing
27
- }
17
+ import { readStdin } from "./core/stdin.mjs";
18
+ import { routePreToolUse, initSecurity } from "./core/routing.mjs";
19
+ import { formatDecision } from "./core/formatters.mjs";
28
20
 
29
21
  // ─── Manual recursive copy (avoids cpSync libuv crash on non-ASCII paths, Windows + Node 24) ───
30
22
  function copyDirSync(src, dest) {
@@ -92,7 +84,7 @@ try {
92
84
  writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
93
85
  }
94
86
 
95
- // 3. Update hook path in settings.json
87
+ // 3. Update hook path + matcher in settings.json
96
88
  const settingsPath = resolve(homedir(), ".claude", "settings.json");
97
89
  try {
98
90
  const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
@@ -100,6 +92,11 @@ try {
100
92
  if (Array.isArray(hooks)) {
101
93
  let changed = false;
102
94
  for (const entry of hooks) {
95
+ // Fix deprecated Task-only matcher → Agent|Task
96
+ if (entry.matcher && entry.matcher.includes("Task") && !entry.matcher.includes("Agent")) {
97
+ entry.matcher = entry.matcher.replace("Task", "Agent|Task");
98
+ changed = true;
99
+ }
103
100
  for (const h of (entry.hooks || [])) {
104
101
  if (h.command?.includes("pretooluse.mjs") && !h.command.includes(targetDir)) {
105
102
  h.command = "node " + resolve(targetDir, "hooks", "pretooluse.mjs");
@@ -125,259 +122,19 @@ try {
125
122
  }
126
123
  } catch { /* best effort — don't block hook */ }
127
124
 
128
- // Event-based flowing mode avoids two platform bugs:
129
- // - `for await (process.stdin)` hangs on macOS when piped via spawnSync
130
- // - `readFileSync(0)` throws EOF/EISDIR on Windows, EAGAIN on Linux
131
- const raw = await new Promise((resolve, reject) => {
132
- let data = "";
133
- process.stdin.setEncoding("utf-8");
134
- process.stdin.on("data", (chunk) => { data += chunk; });
135
- process.stdin.on("end", () => resolve(data));
136
- process.stdin.on("error", reject);
137
- process.stdin.resume();
138
- });
125
+ // ─── Init security from compiled build ───
126
+ const __hookDir = dirname(fileURLToPath(import.meta.url));
127
+ await initSecurity(resolve(__hookDir, "..", "build"));
139
128
 
129
+ // ─── Read stdin ───
130
+ const raw = await readStdin();
140
131
  const input = JSON.parse(raw);
141
132
  const tool = input.tool_name ?? "";
142
133
  const toolInput = input.tool_input ?? {};
143
134
 
144
- // ─── Route tool to appropriate response ───
145
- // Returns a response object, or null for passthrough.
146
- function route() {
147
- // ─── Bash: Stage 1 security check, then Stage 2 routing ───
148
- if (tool === "Bash") {
149
- const command = toolInput.command ?? "";
150
-
151
- // Stage 1: Security check against user's deny/allow patterns.
152
- // Only act when an explicit pattern matched. When no pattern matches,
153
- // evaluateCommand returns { decision: "ask" } with no matchedPattern —
154
- // in that case fall through so other hooks and Claude Code's native engine can decide.
155
- if (security) {
156
- const policies = security.readBashPolicies(process.env.CLAUDE_PROJECT_DIR);
157
- if (policies.length > 0) {
158
- const result = security.evaluateCommand(command, policies);
159
- if (result.decision === "deny") {
160
- return {
161
- hookSpecificOutput: {
162
- hookEventName: "PreToolUse",
163
- permissionDecision: "deny",
164
- reason: `Blocked by security policy: matches deny pattern ${result.matchedPattern}`,
165
- },
166
- };
167
- }
168
- if (result.decision === "ask" && result.matchedPattern) {
169
- return {
170
- hookSpecificOutput: {
171
- hookEventName: "PreToolUse",
172
- permissionDecision: "ask",
173
- },
174
- };
175
- }
176
- // "allow" or no match → fall through to Stage 2
177
- }
178
- }
179
-
180
- // Stage 2: Context-mode routing (existing behavior)
181
-
182
- // curl/wget → replace with echo redirect
183
- if (/(^|\s|&&|\||\;)(curl|wget)\s/i.test(command)) {
184
- return {
185
- hookSpecificOutput: {
186
- hookEventName: "PreToolUse",
187
- updatedInput: {
188
- command: 'echo "context-mode: curl/wget blocked. You MUST use mcp__context-mode__fetch_and_index(url, source) to fetch URLs, or mcp__context-mode__execute(language, code) to run HTTP calls in sandbox. Do NOT retry with curl/wget."',
189
- },
190
- },
191
- };
192
- }
193
-
194
- // inline fetch (node -e, python -c, etc.) → replace with echo redirect
195
- if (
196
- /fetch\s*\(\s*['"](https?:\/\/|http)/i.test(command) ||
197
- /requests\.(get|post|put)\s*\(/i.test(command) ||
198
- /http\.(get|request)\s*\(/i.test(command)
199
- ) {
200
- return {
201
- hookSpecificOutput: {
202
- hookEventName: "PreToolUse",
203
- updatedInput: {
204
- command: 'echo "context-mode: Inline HTTP blocked. Use mcp__context-mode__execute(language, code) to run HTTP calls in sandbox, or mcp__context-mode__fetch_and_index(url, source) for web pages. Do NOT retry with Bash."',
205
- },
206
- },
207
- };
208
- }
209
-
210
- // allow all other Bash commands
211
- return null;
212
- }
213
-
214
- // ─── Read: nudge toward execute_file ───
215
- if (tool === "Read") {
216
- return {
217
- hookSpecificOutput: {
218
- hookEventName: "PreToolUse",
219
- additionalContext: READ_GUIDANCE,
220
- },
221
- };
222
- }
223
-
224
- // ─── Grep: nudge toward execute ───
225
- if (tool === "Grep") {
226
- return {
227
- hookSpecificOutput: {
228
- hookEventName: "PreToolUse",
229
- additionalContext: GREP_GUIDANCE,
230
- },
231
- };
232
- }
233
-
234
- // ─── WebFetch: deny + redirect to sandbox ───
235
- if (tool === "WebFetch") {
236
- const url = toolInput.url ?? "";
237
- return {
238
- hookSpecificOutput: {
239
- hookEventName: "PreToolUse",
240
- permissionDecision: "deny",
241
- reason: `context-mode: WebFetch blocked. Use mcp__context-mode__fetch_and_index(url: "${url}", source: "...") to fetch this URL in sandbox. Then use mcp__context-mode__search(queries: [...]) to query results. Do NOT use curl/wget — they are also blocked.`,
242
- },
243
- };
244
- }
245
-
246
- // ─── Task: inject context-mode routing into subagent prompts ───
247
- if (tool === "Task") {
248
- const subagentType = toolInput.subagent_type ?? "";
249
- const prompt = toolInput.prompt ?? "";
250
-
251
- const updatedInput =
252
- subagentType === "Bash"
253
- ? { ...toolInput, prompt: prompt + ROUTING_BLOCK, subagent_type: "general-purpose" }
254
- : { ...toolInput, prompt: prompt + ROUTING_BLOCK };
255
-
256
- return {
257
- hookSpecificOutput: {
258
- hookEventName: "PreToolUse",
259
- updatedInput,
260
- },
261
- };
262
- }
263
-
264
- // ─── MCP execute: security check for shell commands ───
265
- if (tool.includes("context-mode") && tool.endsWith("__execute")) {
266
- if (security && toolInput.language === "shell") {
267
- const code = toolInput.code ?? "";
268
- const policies = security.readBashPolicies(process.env.CLAUDE_PROJECT_DIR);
269
- if (policies.length > 0) {
270
- const result = security.evaluateCommand(code, policies);
271
- if (result.decision === "deny") {
272
- return {
273
- hookSpecificOutput: {
274
- hookEventName: "PreToolUse",
275
- permissionDecision: "deny",
276
- reason: `Blocked by security policy: shell code matches deny pattern ${result.matchedPattern}`,
277
- },
278
- };
279
- }
280
- if (result.decision === "ask" && result.matchedPattern) {
281
- return {
282
- hookSpecificOutput: {
283
- hookEventName: "PreToolUse",
284
- permissionDecision: "ask",
285
- },
286
- };
287
- }
288
- }
289
- }
290
- return null;
291
- }
292
-
293
- // ─── MCP execute_file: check file path + code against deny patterns ───
294
- if (tool.includes("context-mode") && tool.endsWith("__execute_file")) {
295
- if (security) {
296
- // Check file path against Read deny patterns
297
- const filePath = toolInput.path ?? "";
298
- const denyGlobs = security.readToolDenyPatterns("Read", process.env.CLAUDE_PROJECT_DIR);
299
- const evalResult = security.evaluateFilePath(filePath, denyGlobs);
300
- if (evalResult.denied) {
301
- return {
302
- hookSpecificOutput: {
303
- hookEventName: "PreToolUse",
304
- permissionDecision: "deny",
305
- reason: `Blocked by security policy: file path matches Read deny pattern ${evalResult.matchedPattern}`,
306
- },
307
- };
308
- }
309
-
310
- // Check code parameter against Bash deny patterns (same as execute)
311
- const lang = toolInput.language ?? "";
312
- const code = toolInput.code ?? "";
313
- if (lang === "shell") {
314
- const policies = security.readBashPolicies(process.env.CLAUDE_PROJECT_DIR);
315
- if (policies.length > 0) {
316
- const result = security.evaluateCommand(code, policies);
317
- if (result.decision === "deny") {
318
- return {
319
- hookSpecificOutput: {
320
- hookEventName: "PreToolUse",
321
- permissionDecision: "deny",
322
- reason: `Blocked by security policy: shell code matches deny pattern ${result.matchedPattern}`,
323
- },
324
- };
325
- }
326
- if (result.decision === "ask" && result.matchedPattern) {
327
- return {
328
- hookSpecificOutput: {
329
- hookEventName: "PreToolUse",
330
- permissionDecision: "ask",
331
- },
332
- };
333
- }
334
- }
335
- }
336
- }
337
- return null;
338
- }
339
-
340
- // ─── MCP batch_execute: check each command individually ───
341
- if (tool.includes("context-mode") && tool.endsWith("__batch_execute")) {
342
- if (security) {
343
- const commands = toolInput.commands ?? [];
344
- const policies = security.readBashPolicies(process.env.CLAUDE_PROJECT_DIR);
345
- if (policies.length > 0) {
346
- for (const entry of commands) {
347
- const cmd = entry.command ?? "";
348
- const result = security.evaluateCommand(cmd, policies);
349
- if (result.decision === "deny") {
350
- return {
351
- hookSpecificOutput: {
352
- hookEventName: "PreToolUse",
353
- permissionDecision: "deny",
354
- reason: `Blocked by security policy: batch command "${entry.label ?? cmd}" matches deny pattern ${result.matchedPattern}`,
355
- },
356
- };
357
- }
358
- if (result.decision === "ask" && result.matchedPattern) {
359
- return {
360
- hookSpecificOutput: {
361
- hookEventName: "PreToolUse",
362
- permissionDecision: "ask",
363
- },
364
- };
365
- }
366
- }
367
- }
368
- }
369
- return null;
370
- }
371
-
372
- // Unknown tool — pass through
373
- return null;
374
- }
375
-
376
- // ─── Output response ───
377
- // Write to stdout and let Node.js exit naturally. This guarantees stdout
378
- // is fully flushed on all platforms. On Windows, process.exit() can drop
379
- // piped stdout before the buffer is written — so we never call it.
380
- const response = route();
135
+ // ─── Route and format response ───
136
+ const decision = routePreToolUse(tool, toolInput, process.env.CLAUDE_PROJECT_DIR);
137
+ const response = formatDecision("claude-code", decision);
381
138
  if (response !== null) {
382
139
  process.stdout.write(JSON.stringify(response) + "\n");
383
140
  }
@@ -10,19 +10,19 @@ export const ROUTING_BLOCK = `
10
10
  </priority_instructions>
11
11
 
12
12
  <tool_selection_hierarchy>
13
- 1. GATHER: mcp__context-mode__batch_execute(commands, queries)
13
+ 1. GATHER: mcp__plugin_context-mode_context-mode__ctx_batch_execute(commands, queries)
14
14
  - Primary tool for research. Runs all commands, auto-indexes, and searches.
15
15
  - ONE call replaces many individual steps.
16
- 2. FOLLOW-UP: mcp__context-mode__search(queries: ["q1", "q2", ...])
16
+ 2. FOLLOW-UP: mcp__plugin_context-mode_context-mode__ctx_search(queries: ["q1", "q2", ...])
17
17
  - Use for all follow-up questions. ONE call, many queries.
18
- 3. PROCESSING: mcp__context-mode__execute(language, code) | mcp__context-mode__execute_file(path, language, code)
18
+ 3. PROCESSING: mcp__plugin_context-mode_context-mode__ctx_execute(language, code) | mcp__plugin_context-mode_context-mode__ctx_execute_file(path, language, code)
19
19
  - Use for API calls, log analysis, and data processing.
20
20
  </tool_selection_hierarchy>
21
21
 
22
22
  <forbidden_actions>
23
23
  - DO NOT use Bash for commands producing >20 lines of output.
24
24
  - DO NOT use Read for analysis (use execute_file). Read IS correct for files you intend to Edit.
25
- - DO NOT use WebFetch (use mcp__context-mode__fetch_and_index instead).
25
+ - DO NOT use WebFetch (use mcp__plugin_context-mode_context-mode__ctx_fetch_and_index instead).
26
26
  - Bash is ONLY for git/mkdir/rm/mv/navigation.
27
27
  </forbidden_actions>
28
28
 
@@ -40,8 +40,21 @@ export const ROUTING_BLOCK = `
40
40
  - Key findings
41
41
  </response_format>
42
42
  </output_constraints>
43
+
44
+ <ctx_commands>
45
+ When the user says "ctx stats", "ctx-stats", "/ctx-stats", or asks about context savings:
46
+ → Call the stats MCP tool and display the full output verbatim.
47
+
48
+ When the user says "ctx doctor", "ctx-doctor", "/ctx-doctor", or asks to diagnose context-mode:
49
+ → Call the doctor MCP tool, execute the returned shell command, display results as a checklist.
50
+
51
+ When the user says "ctx upgrade", "ctx-upgrade", "/ctx-upgrade", or asks to update context-mode:
52
+ → Call the upgrade MCP tool, execute the returned shell command, display results as a checklist.
53
+ </ctx_commands>
43
54
  </context_window_protection>`;
44
55
 
45
- export const READ_GUIDANCE = '<context_guidance>\n <tip>\n If you are reading this file to Edit it, Read is the correct tool — Edit needs file content in context.\n If you are reading to analyze or explore, use mcp__context-mode__execute_file(path, language, code) instead — only your printed summary will enter the context.\n </tip>\n</context_guidance>';
56
+ export const READ_GUIDANCE = '<context_guidance>\n <tip>\n If you are reading this file to Edit it, Read is the correct tool — Edit needs file content in context.\n If you are reading to analyze or explore, use mcp__plugin_context-mode_context-mode__ctx_execute_file(path, language, code) instead — only your printed summary will enter the context.\n </tip>\n</context_guidance>';
57
+
58
+ export const GREP_GUIDANCE = '<context_guidance>\n <tip>\n This operation may flood your context window. To stay efficient:\n - Use mcp__plugin_context-mode_context-mode__ctx_execute(language: "shell", code: "...") to run searches in the sandbox.\n - Only your final printed summary will enter the context.\n </tip>\n</context_guidance>';
46
59
 
47
- export const GREP_GUIDANCE = '<context_guidance>\n <tip>\n This operation may flood your context window. To stay efficient:\n - Use mcp__context-mode__execute(language: "shell", code: "...") to run searches in the sandbox.\n - Only your final printed summary will enter the context.\n </tip>\n</context_guidance>';
60
+ export const BASH_GUIDANCE = '<context_guidance>\n <tip>\n This Bash command may produce large output. To stay efficient:\n - Use mcp__plugin_context-mode_context-mode__ctx_batch_execute(commands, queries) for multiple commands\n - Use mcp__plugin_context-mode_context-mode__ctx_execute(language: "shell", code: "...") to run in sandbox\n - Only your final printed summary will enter the context.\n - Bash is best for: git, mkdir, rm, mv, navigation, and short-output commands only.\n </tip>\n</context_guidance>';