context-mode 0.8.1 → 0.9.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.
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
10
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
11
11
  }
12
12
  ]
13
13
  },
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
19
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
20
20
  }
21
21
  ]
22
22
  },
@@ -25,7 +25,7 @@
25
25
  "hooks": [
26
26
  {
27
27
  "type": "command",
28
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
28
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
29
29
  }
30
30
  ]
31
31
  },
@@ -34,7 +34,7 @@
34
34
  "hooks": [
35
35
  {
36
36
  "type": "command",
37
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
37
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
38
38
  }
39
39
  ]
40
40
  },
@@ -43,7 +43,7 @@
43
43
  "hooks": [
44
44
  {
45
45
  "type": "command",
46
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
46
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
47
47
  }
48
48
  ]
49
49
  }
@@ -13,7 +13,7 @@
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "0.8.1",
16
+ "version": "0.9.0",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -21,8 +21,8 @@
21
21
  ],
22
22
  "mcpServers": {
23
23
  "context-mode": {
24
- "command": "sh",
25
- "args": ["${CLAUDE_PLUGIN_ROOT}/start.sh"]
24
+ "command": "node",
25
+ "args": ["${CLAUDE_PLUGIN_ROOT}/start.mjs"]
26
26
  }
27
27
  },
28
28
  "skills": "./skills/"
package/.mcp.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "context-mode": {
4
- "command": "sh",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/start.sh"]
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/start.mjs"]
6
6
  }
7
7
  }
8
8
  }
package/README.md CHANGED
@@ -205,6 +205,10 @@ npm run test:all # full suite
205
205
  <img src="https://contrib.rocks/image?repo=mksglu/claude-context-mode&columns=8&anon=1" />
206
206
  </a>
207
207
 
208
+ ### Special Thanks
209
+
210
+ <a href="https://github.com/mksglu/claude-context-mode/issues/15"><img src="https://github.com/vaban-ru.png" width="32" /></a>
211
+
208
212
  ## License
209
213
 
210
214
  MIT
package/build/cli.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import * as p from "@clack/prompts";
13
13
  import color from "picocolors";
14
14
  import { execSync } from "node:child_process";
15
- import { readFileSync, writeFileSync, copyFileSync, chmodSync, accessSync, readdirSync, constants } from "node:fs";
15
+ import { readFileSync, writeFileSync, copyFileSync, cpSync, chmodSync, accessSync, readdirSync, rmSync, constants } from "node:fs";
16
16
  import { resolve, dirname } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { homedir } from "node:os";
@@ -22,7 +22,7 @@ if (args[0] === "setup") {
22
22
  setup();
23
23
  }
24
24
  else if (args[0] === "doctor") {
25
- doctor();
25
+ doctor().then((code) => process.exit(code));
26
26
  }
27
27
  else if (args[0] === "upgrade") {
28
28
  upgrade();
@@ -52,7 +52,7 @@ function readSettings() {
52
52
  }
53
53
  }
54
54
  function getHookScriptPath() {
55
- return resolve(getPluginRoot(), "hooks", "pretooluse.sh");
55
+ return resolve(getPluginRoot(), "hooks", "pretooluse.mjs");
56
56
  }
57
57
  function getLocalVersion() {
58
58
  try {
@@ -126,6 +126,7 @@ async function doctor() {
126
126
  if (process.stdout.isTTY)
127
127
  console.clear();
128
128
  p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
129
+ let criticalFails = 0;
129
130
  const s = p.spinner();
130
131
  s.start("Running diagnostics");
131
132
  const runtimes = detectRuntimes();
@@ -145,8 +146,16 @@ async function doctor() {
145
146
  // Language coverage
146
147
  const total = 11;
147
148
  const pct = ((available.length / total) * 100).toFixed(0);
148
- p.log.info(`Language coverage: ${available.length}/${total} (${pct}%)` +
149
- color.dim(` — ${available.join(", ")}`));
149
+ if (available.length < 2) {
150
+ criticalFails++;
151
+ p.log.error(color.red(`Language coverage: ${available.length}/${total} (${pct}%)`) +
152
+ " — too few runtimes detected" +
153
+ color.dim(` — ${available.join(", ") || "none"}`));
154
+ }
155
+ else {
156
+ p.log.info(`Language coverage: ${available.length}/${total} (${pct}%)` +
157
+ color.dim(` — ${available.join(", ")}`));
158
+ }
150
159
  // Server test
151
160
  p.log.step("Testing server initialization...");
152
161
  try {
@@ -161,10 +170,12 @@ async function doctor() {
161
170
  p.log.success(color.green("Server test: PASS"));
162
171
  }
163
172
  else {
173
+ criticalFails++;
164
174
  p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}`);
165
175
  }
166
176
  }
167
177
  catch (err) {
178
+ criticalFails++;
168
179
  const message = err instanceof Error ? err.message : String(err);
169
180
  p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
170
181
  }
@@ -176,13 +187,13 @@ async function doctor() {
176
187
  const hooks = settings.hooks;
177
188
  const preToolUse = hooks?.PreToolUse;
178
189
  if (preToolUse && preToolUse.length > 0) {
179
- const hasCorrectHook = preToolUse.some((entry) => entry.hooks?.some((h) => h.command?.includes("pretooluse.sh")));
190
+ const hasCorrectHook = preToolUse.some((entry) => entry.hooks?.some((h) => h.command?.includes("pretooluse.mjs")));
180
191
  if (hasCorrectHook) {
181
192
  p.log.success(color.green("Hooks installed: PASS") + " — PreToolUse hook configured");
182
193
  }
183
194
  else {
184
195
  p.log.error(color.red("Hooks installed: FAIL") +
185
- " — PreToolUse exists but does not point to pretooluse.sh" +
196
+ " — PreToolUse exists but does not point to pretooluse.mjs" +
186
197
  color.dim("\n Run: npx context-mode upgrade"));
187
198
  }
188
199
  }
@@ -245,10 +256,12 @@ async function doctor() {
245
256
  p.log.success(color.green("FTS5 / better-sqlite3: PASS") + " — native module works");
246
257
  }
247
258
  else {
259
+ criticalFails++;
248
260
  p.log.error(color.red("FTS5 / better-sqlite3: FAIL") + " — query returned unexpected result");
249
261
  }
250
262
  }
251
263
  catch (err) {
264
+ criticalFails++;
252
265
  const message = err instanceof Error ? err.message : String(err);
253
266
  p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
254
267
  ` — ${message}` +
@@ -292,9 +305,14 @@ async function doctor() {
292
305
  color.dim(" — could not verify against npm registry"));
293
306
  }
294
307
  // Summary
308
+ if (criticalFails > 0) {
309
+ p.outro(color.red(`Diagnostics failed — ${criticalFails} critical issue(s) found`));
310
+ return 1;
311
+ }
295
312
  p.outro(available.length >= 4
296
313
  ? color.green("Diagnostics complete!")
297
314
  : color.yellow("Some checks need attention — see above for details"));
315
+ return 0;
298
316
  }
299
317
  /* -------------------------------------------------------
300
318
  * Upgrade
@@ -327,12 +345,12 @@ async function upgrade() {
327
345
  }
328
346
  // Step 2: Install dependencies + build
329
347
  s.start("Installing dependencies & building");
330
- execSync("npm install --no-audit --no-fund 2>/dev/null", {
348
+ execSync("npm install --no-audit --no-fund", {
331
349
  cwd: srcDir,
332
350
  stdio: "pipe",
333
351
  timeout: 60000,
334
352
  });
335
- execSync("npm run build 2>/dev/null", {
353
+ execSync("npm run build", {
336
354
  cwd: srcDir,
337
355
  stdio: "pipe",
338
356
  timeout: 30000,
@@ -342,19 +360,19 @@ async function upgrade() {
342
360
  s.start("Installing files");
343
361
  const items = [
344
362
  "build", "hooks", "skills", ".claude-plugin",
345
- "start.sh", "server.bundle.mjs", "package.json", ".mcp.json",
363
+ "start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
346
364
  ];
347
365
  for (const item of items) {
348
366
  try {
349
- execSync(`rm -rf "${pluginRoot}/${item}"`, { stdio: "pipe" });
350
- execSync(`cp -r "${srcDir}/${item}" "${pluginRoot}/"`, { stdio: "pipe" });
367
+ rmSync(resolve(pluginRoot, item), { recursive: true, force: true });
368
+ cpSync(resolve(srcDir, item), resolve(pluginRoot, item), { recursive: true });
351
369
  }
352
370
  catch { /* some files may not exist */ }
353
371
  }
354
372
  s.stop(color.green("Files installed"));
355
373
  // Install production deps in plugin root
356
374
  s.start("Installing production dependencies");
357
- execSync("npm install --production --no-audit --no-fund 2>/dev/null", {
375
+ execSync("npm install --production --no-audit --no-fund", {
358
376
  cwd: pluginRoot,
359
377
  stdio: "pipe",
360
378
  timeout: 60000,
@@ -367,8 +385,9 @@ async function upgrade() {
367
385
  const newCacheDir = cacheMatch[1] + newVersion;
368
386
  s.start(`Migrating cache: ${oldDirVersion} → ${newVersion}`);
369
387
  try {
370
- execSync(`rm -rf "${newCacheDir}"`, { stdio: "pipe" });
371
- execSync(`mv "${pluginRoot}" "${newCacheDir}"`, { stdio: "pipe" });
388
+ rmSync(newCacheDir, { recursive: true, force: true });
389
+ cpSync(pluginRoot, newCacheDir, { recursive: true });
390
+ rmSync(pluginRoot, { recursive: true, force: true });
372
391
  pluginRoot = newCacheDir;
373
392
  s.stop(color.green(`Cache directory: ${newVersion}`));
374
393
  changes.push(`Migrated cache: ${oldDirVersion} → ${newVersion}`);
@@ -392,7 +411,7 @@ async function upgrade() {
392
411
  p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
393
412
  }
394
413
  // Cleanup
395
- execSync(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
414
+ rmSync(tmpDir, { recursive: true, force: true });
396
415
  changes.push(newVersion !== localVersion
397
416
  ? `Updated v${localVersion} → v${newVersion}`
398
417
  : `Reinstalled v${localVersion} from GitHub`);
@@ -406,7 +425,7 @@ async function upgrade() {
406
425
  p.log.info(color.dim("Continuing with hooks/settings fix..."));
407
426
  // Cleanup on failure
408
427
  try {
409
- execSync(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
428
+ rmSync(tmpDir, { recursive: true, force: true });
410
429
  }
411
430
  catch { /* ignore */ }
412
431
  }
@@ -425,14 +444,14 @@ async function upgrade() {
425
444
  }
426
445
  // Step 4: Fix hooks
427
446
  p.log.step("Configuring PreToolUse hooks...");
428
- const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.sh");
447
+ const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
429
448
  const settings = readSettings() ?? {};
430
449
  const desiredHookEntry = {
431
450
  matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch|Task",
432
451
  hooks: [
433
452
  {
434
453
  type: "command",
435
- command: "bash " + hookScriptPath,
454
+ command: "node " + hookScriptPath,
436
455
  },
437
456
  ],
438
457
  };
@@ -441,7 +460,7 @@ async function upgrade() {
441
460
  if (existingPreToolUse && Array.isArray(existingPreToolUse)) {
442
461
  const existingIdx = existingPreToolUse.findIndex((entry) => {
443
462
  const entryHooks = entry.hooks;
444
- return entryHooks?.some((h) => h.command?.includes("pretooluse.sh"));
463
+ return entryHooks?.some((h) => h.command?.includes("pretooluse.mjs"));
445
464
  });
446
465
  if (existingIdx >= 0) {
447
466
  existingPreToolUse[existingIdx] = desiredHookEntry;
@@ -478,7 +497,7 @@ async function upgrade() {
478
497
  accessSync(hookScriptPath, constants.R_OK);
479
498
  chmodSync(hookScriptPath, 0o755);
480
499
  p.log.success(color.green("Permissions set") + color.dim(" — chmod +x " + hookScriptPath));
481
- changes.push("Set pretooluse.sh as executable");
500
+ changes.push("Set pretooluse.mjs as executable");
482
501
  }
483
502
  catch {
484
503
  p.log.error(color.red("Hook script not found") +
@@ -494,7 +513,10 @@ async function upgrade() {
494
513
  // Step 7: Run doctor
495
514
  p.log.step("Running doctor to verify...");
496
515
  console.log();
497
- await doctor();
516
+ const doctorCode = await doctor();
517
+ if (doctorCode !== 0) {
518
+ process.exit(doctorCode);
519
+ }
498
520
  }
499
521
  /* -------------------------------------------------------
500
522
  * Setup
package/build/executor.js CHANGED
@@ -4,6 +4,19 @@ import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { detectRuntimes, buildCommand, } from "./runtime.js";
7
+ const isWin = process.platform === "win32";
8
+ /** Kill process tree — on Windows, proc.kill() only kills the shell, not children. */
9
+ function killTree(proc) {
10
+ if (isWin && proc.pid) {
11
+ try {
12
+ execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: "pipe" });
13
+ }
14
+ catch { /* already dead */ }
15
+ }
16
+ else {
17
+ proc.kill("SIGKILL");
18
+ }
19
+ }
7
20
  export class PolyglotExecutor {
8
21
  #maxOutputBytes;
9
22
  #hardCapBytes;
@@ -77,13 +90,15 @@ export class PolyglotExecutor {
77
90
  return fp;
78
91
  }
79
92
  async #compileAndRun(srcPath, cwd, timeout) {
80
- const binPath = srcPath.replace(/\.rs$/, "");
93
+ const binSuffix = isWin ? ".exe" : "";
94
+ const binPath = srcPath.replace(/\.rs$/, "") + binSuffix;
81
95
  // Compile
82
96
  try {
83
- execSync(`rustc ${srcPath} -o ${binPath} 2>&1`, {
97
+ execSync(`rustc ${srcPath} -o ${binPath}`, {
84
98
  cwd,
85
99
  timeout: Math.min(timeout, 30_000),
86
100
  encoding: "utf-8",
101
+ stdio: ["pipe", "pipe", "pipe"],
87
102
  });
88
103
  }
89
104
  catch (err) {
@@ -137,15 +152,19 @@ export class PolyglotExecutor {
137
152
  }
138
153
  async #spawn(cmd, cwd, timeout) {
139
154
  return new Promise((res) => {
155
+ // Only .cmd/.bat shims need shell on Windows; real executables don't.
156
+ // Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
157
+ const needsShell = isWin && ["tsx", "ts-node", "elixir"].includes(cmd[0]);
140
158
  const proc = spawn(cmd[0], cmd.slice(1), {
141
159
  cwd,
142
160
  stdio: ["ignore", "pipe", "pipe"],
143
161
  env: this.#buildSafeEnv(cwd),
162
+ shell: needsShell,
144
163
  });
145
164
  let timedOut = false;
146
165
  const timer = setTimeout(() => {
147
166
  timedOut = true;
148
- proc.kill("SIGKILL");
167
+ killTree(proc);
149
168
  }, timeout);
150
169
  // Stream-level byte cap: kill the process once combined stdout+stderr
151
170
  // exceeds hardCapBytes. Without this, a command like `yes` or
@@ -162,7 +181,7 @@ export class PolyglotExecutor {
162
181
  }
163
182
  else if (!capExceeded) {
164
183
  capExceeded = true;
165
- proc.kill("SIGKILL");
184
+ killTree(proc);
166
185
  }
167
186
  });
168
187
  proc.stderr.on("data", (chunk) => {
@@ -172,7 +191,7 @@ export class PolyglotExecutor {
172
191
  }
173
192
  else if (!capExceeded) {
174
193
  capExceeded = true;
175
- proc.kill("SIGKILL");
194
+ killTree(proc);
176
195
  }
177
196
  });
178
197
  proc.on("close", (exitCode) => {
@@ -239,7 +258,7 @@ export class PolyglotExecutor {
239
258
  "XDG_DATA_HOME",
240
259
  ];
241
260
  const env = {
242
- PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
261
+ PATH: process.env.PATH ?? (isWin ? "" : "/usr/local/bin:/usr/bin:/bin"),
243
262
  HOME: realHome,
244
263
  TMPDIR: tmpDir,
245
264
  LANG: "en_US.UTF-8",
@@ -247,6 +266,18 @@ export class PolyglotExecutor {
247
266
  PYTHONUNBUFFERED: "1",
248
267
  NO_COLOR: "1",
249
268
  };
269
+ // Windows-critical env vars
270
+ if (isWin) {
271
+ const winVars = [
272
+ "SYSTEMROOT", "SystemRoot", "COMSPEC", "PATHEXT",
273
+ "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP",
274
+ "GOROOT", "GOPATH",
275
+ ];
276
+ for (const key of winVars) {
277
+ if (process.env[key])
278
+ env[key] = process.env[key];
279
+ }
280
+ }
250
281
  for (const key of passthrough) {
251
282
  if (process.env[key]) {
252
283
  env[key] = process.env[key];
package/build/runtime.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { execSync } from "node:child_process";
2
+ const isWindows = process.platform === "win32";
2
3
  function commandExists(cmd) {
3
4
  try {
4
- execSync(`command -v ${cmd} 2>/dev/null`, { stdio: "pipe" });
5
+ const check = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
6
+ execSync(check, { stdio: "pipe" });
5
7
  return true;
6
8
  }
7
9
  catch {
@@ -10,8 +12,9 @@ function commandExists(cmd) {
10
12
  }
11
13
  function getVersion(cmd) {
12
14
  try {
13
- return execSync(`${cmd} --version 2>/dev/null`, {
15
+ return execSync(`${cmd} --version`, {
14
16
  encoding: "utf-8",
17
+ stdio: ["pipe", "pipe", "pipe"],
15
18
  timeout: 5000,
16
19
  })
17
20
  .trim()
package/hooks/hooks.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
10
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
11
11
  }
12
12
  ]
13
13
  },
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
19
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
20
20
  }
21
21
  ]
22
22
  },
@@ -25,7 +25,7 @@
25
25
  "hooks": [
26
26
  {
27
27
  "type": "command",
28
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
28
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
29
29
  }
30
30
  ]
31
31
  },
@@ -34,7 +34,7 @@
34
34
  "hooks": [
35
35
  {
36
36
  "type": "command",
37
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
37
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
38
38
  }
39
39
  ]
40
40
  },
@@ -43,7 +43,7 @@
43
43
  "hooks": [
44
44
  {
45
45
  "type": "command",
46
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.sh"
46
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
47
47
  }
48
48
  ]
49
49
  }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unified PreToolUse hook for context-mode
4
+ * Redirects data-fetching tools to context-mode MCP tools
5
+ *
6
+ * Cross-platform (Windows/macOS/Linux) — no bash/jq dependency.
7
+ */
8
+
9
+ let raw = "";
10
+ process.stdin.setEncoding("utf-8");
11
+ for await (const chunk of process.stdin) raw += chunk;
12
+
13
+ const input = JSON.parse(raw);
14
+ const tool = input.tool_name ?? "";
15
+ const toolInput = input.tool_input ?? {};
16
+
17
+ // ─── Bash: redirect data-fetching commands via updatedInput ───
18
+ if (tool === "Bash") {
19
+ const command = toolInput.command ?? "";
20
+
21
+ // curl/wget → replace with echo redirect
22
+ if (/(^|\s|&&|\||\;)(curl|wget)\s/i.test(command)) {
23
+ console.log(JSON.stringify({
24
+ hookSpecificOutput: {
25
+ hookEventName: "PreToolUse",
26
+ updatedInput: {
27
+ 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."',
28
+ },
29
+ },
30
+ }));
31
+ process.exit(0);
32
+ }
33
+
34
+ // inline fetch (node -e, python -c, etc.) → replace with echo redirect
35
+ if (
36
+ /fetch\s*\(\s*['"](https?:\/\/|http)/i.test(command) ||
37
+ /requests\.(get|post|put)\s*\(/i.test(command) ||
38
+ /http\.(get|request)\s*\(/i.test(command)
39
+ ) {
40
+ console.log(JSON.stringify({
41
+ hookSpecificOutput: {
42
+ hookEventName: "PreToolUse",
43
+ updatedInput: {
44
+ 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."',
45
+ },
46
+ },
47
+ }));
48
+ process.exit(0);
49
+ }
50
+
51
+ // allow all other Bash commands
52
+ process.exit(0);
53
+ }
54
+
55
+ // ─── Read: nudge toward execute_file ───
56
+ if (tool === "Read") {
57
+ console.log(JSON.stringify({
58
+ hookSpecificOutput: {
59
+ hookEventName: "PreToolUse",
60
+ additionalContext:
61
+ "CONTEXT TIP: If this file is large (>50 lines), prefer mcp__context-mode__execute_file(path, language, code) — processes in sandbox, only stdout enters context.",
62
+ },
63
+ }));
64
+ process.exit(0);
65
+ }
66
+
67
+ // ─── Grep: nudge toward execute ───
68
+ if (tool === "Grep") {
69
+ console.log(JSON.stringify({
70
+ hookSpecificOutput: {
71
+ hookEventName: "PreToolUse",
72
+ additionalContext:
73
+ 'CONTEXT TIP: If results may be large, prefer mcp__context-mode__execute(language: "shell", code: "grep ...") — runs in sandbox, only stdout enters context.',
74
+ },
75
+ }));
76
+ process.exit(0);
77
+ }
78
+
79
+ // ─── Glob: passthrough ───
80
+ if (tool === "Glob") {
81
+ process.exit(0);
82
+ }
83
+
84
+ // ─── WebFetch: deny + redirect to sandbox ───
85
+ if (tool === "WebFetch") {
86
+ const url = toolInput.url ?? "";
87
+ console.log(JSON.stringify({
88
+ hookSpecificOutput: {
89
+ hookEventName: "PreToolUse",
90
+ permissionDecision: "deny",
91
+ 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.`,
92
+ },
93
+ }));
94
+ process.exit(0);
95
+ }
96
+
97
+ // ─── WebSearch: passthrough ───
98
+ if (tool === "WebSearch") {
99
+ process.exit(0);
100
+ }
101
+
102
+ // ─── Task: inject context-mode routing into subagent prompts ───
103
+ if (tool === "Task") {
104
+ const subagentType = toolInput.subagent_type ?? "";
105
+ const prompt = toolInput.prompt ?? "";
106
+
107
+ const ROUTING_BLOCK = `
108
+
109
+ ---
110
+ CONTEXT WINDOW PROTECTION — USE CONTEXT-MODE MCP TOOLS
111
+
112
+ Raw Bash/Read/WebFetch output floods your context. You have context-mode tools that keep data in sandbox.
113
+
114
+ STEP 1 — GATHER: mcp__context-mode__batch_execute(commands, queries)
115
+ commands: [{label: "Name", command: "shell cmd"}, ...]
116
+ queries: ["query1", "query2", ...] — put 5-8 queries covering everything you need.
117
+ Runs all commands, indexes output, returns search results. ONE call, no follow-ups.
118
+
119
+ STEP 2 — FOLLOW-UP: mcp__context-mode__search(queries: ["q1", "q2", "q3", ...])
120
+ Pass ALL follow-up questions as queries array. ONE call, not separate calls.
121
+
122
+ OTHER: execute(language, code) | execute_file(path, language, code) | fetch_and_index(url) + search
123
+
124
+ FORBIDDEN: Bash for output, Read for files, WebFetch. Bash is ONLY for git/mkdir/rm/mv.
125
+
126
+ OUTPUT FORMAT — KEEP YOUR FINAL RESPONSE UNDER 500 WORDS:
127
+ The parent agent context window is precious. Your full response gets injected into it.
128
+
129
+ 1. ARTIFACTS (PRDs, configs, code files) → Write to FILES, never return as inline text.
130
+ Return only: file path + 1-line description.
131
+ 2. DETAILED FINDINGS → Index into knowledge base:
132
+ mcp__context-mode__index(content: "...", source: "descriptive-label")
133
+ The parent agent shares the SAME knowledge base and can search() your indexed content.
134
+ 3. YOUR RESPONSE must be a concise summary:
135
+ - What you did (2-3 bullets)
136
+ - File paths created/modified (if any)
137
+ - Source labels you indexed (so parent can search)
138
+ - Key findings in bullet points
139
+ Do NOT return raw data, full file contents, or lengthy explanations.
140
+ ---`;
141
+
142
+ const updatedInput =
143
+ subagentType === "Bash"
144
+ ? { ...toolInput, prompt: prompt + ROUTING_BLOCK, subagent_type: "general-purpose" }
145
+ : { ...toolInput, prompt: prompt + ROUTING_BLOCK };
146
+
147
+ console.log(JSON.stringify({
148
+ hookSpecificOutput: {
149
+ hookEventName: "PreToolUse",
150
+ updatedInput,
151
+ },
152
+ }));
153
+ process.exit(0);
154
+ }
155
+
156
+ // Unknown tool — pass through
157
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -32,7 +32,7 @@
32
32
  "skills",
33
33
  ".claude-plugin",
34
34
  ".mcp.json",
35
- "start.sh",
35
+ "start.mjs",
36
36
  "README.md",
37
37
  "LICENSE"
38
38
  ],
package/server.bundle.mjs CHANGED
@@ -166,7 +166,7 @@ ${n}`}}};import{createRequire as xT}from"node:module";import{readFileSync as $T,
166
166
  `).trim();u.length!==0&&(r.push({title:this.#o(o,i),content:u,hasCode:s.some(l=>/^`{3,}/.test(l))}),s=[])},c=0;for(;c<n.length;){let u=n[c];if(/^[-_*]{3,}\s*$/.test(u)){a(),c++;continue}let l=u.match(/^(#{1,4})\s+(.+)$/);if(l){a();let m=l[1].length,f=l[2].trim();for(;o.length>0&&o[o.length-1].level>=m;)o.pop();o.push({level:m,text:f}),i=f,s.push(u),c++;continue}let d=u.match(/^(`{3,})(.*)?$/);if(d){let m=d[1],f=[u];for(c++;c<n.length;){if(f.push(n[c]),n[c].startsWith(m)&&n[c].trim()===m){c++;break}c++}s.push(...f);continue}s.push(u),c++}return a(),r}#i(e,r){let n=e.split(/\n\s*\n/);if(n.length>=3&&n.length<=200&&n.every(c=>Buffer.byteLength(c)<5e3))return n.map((c,u)=>{let l=c.trim();return{title:l.split(`
167
167
  `)[0].slice(0,80)||`Section ${u+1}`,content:l}}).filter(c=>c.content.length>0);let o=e.split(`
168
168
  `);if(o.length<=r)return[{title:"Output",content:e}];let s=[],a=Math.max(r-2,1);for(let c=0;c<o.length;c+=a){let u=o.slice(c,c+r);if(u.length===0)break;let l=c+1,d=Math.min(c+u.length,o.length),m=u[0]?.trim().slice(0,80);s.push({title:m||`Lines ${l}-${d}`,content:u.join(`
169
- `)})}return s}#o(e,r){return e.length===0?r||"Untitled":e.map(n=>n.text).join(" > ")}};var d_="0.8.0",nd=pi(),PT=t_(nd),er=new ui({name:"context-mode",version:d_}),yi=new mi({runtimes:nd,projectRoot:process.env.CLAUDE_PROJECT_DIR}),xo=null;function $o(){return xo||(xo=new hi),xo}var je={calls:{},bytesReturned:{},bytesIndexed:0,bytesSandboxed:0,sessionStart:Date.now()};function ne(t,e){let r=e.content.reduce((n,o)=>n+Buffer.byteLength(o.text),0);return je.calls[t]=(je.calls[t]||0)+1,je.bytesReturned[t]=(je.bytesReturned[t]||0)+r,e}function Qt(t){je.bytesIndexed+=t}var RT=PT.join(", "),IT=ed()?" (Bun detected \u2014 JS/TS runs 3-5x faster)":"";function p_(t,e,r=1500){if(t.length<=r)return t;let n=e.toLowerCase().split(/\s+/).filter(l=>l.length>2),o=t.toLowerCase(),s=[];for(let l of n){let d=o.indexOf(l);for(;d!==-1;)s.push(d),d=o.indexOf(l,d+1)}if(s.length===0)return t.slice(0,r)+`
169
+ `)})}return s}#o(e,r){return e.length===0?r||"Untitled":e.map(n=>n.text).join(" > ")}};var d_="0.8.1",nd=pi(),PT=t_(nd),er=new ui({name:"context-mode",version:d_}),yi=new mi({runtimes:nd,projectRoot:process.env.CLAUDE_PROJECT_DIR}),xo=null;function $o(){return xo||(xo=new hi),xo}var je={calls:{},bytesReturned:{},bytesIndexed:0,bytesSandboxed:0,sessionStart:Date.now()};function ne(t,e){let r=e.content.reduce((n,o)=>n+Buffer.byteLength(o.text),0);return je.calls[t]=(je.calls[t]||0)+1,je.bytesReturned[t]=(je.bytesReturned[t]||0)+r,e}function Qt(t){je.bytesIndexed+=t}var RT=PT.join(", "),IT=ed()?" (Bun detected \u2014 JS/TS runs 3-5x faster)":"";function p_(t,e,r=1500){if(t.length<=r)return t;let n=e.toLowerCase().split(/\s+/).filter(l=>l.length>2),o=t.toLowerCase(),s=[];for(let l of n){let d=o.indexOf(l);for(;d!==-1;)s.push(d),d=o.indexOf(l,d+1)}if(s.length===0)return t.slice(0,r)+`
170
170
  \u2026`;s.sort((l,d)=>l-d);let i=300,a=[];for(let l of s){let d=Math.max(0,l-i),m=Math.min(t.length,l+i);a.length>0&&d<=a[a.length-1][1]?a[a.length-1][1]=m:a.push([d,m])}let c=[],u=0;for(let[l,d]of a){if(u>=r)break;let m=t.slice(l,Math.min(d,l+(r-u)));c.push((l>0?"\u2026":"")+m+(d<t.length?"\u2026":"")),u+=m.length}return c.join(`
171
171
 
172
172
  `)}er.registerTool("execute",{title:"Execute Code",description:`Execute code in a sandboxed subprocess. Only stdout enters context \u2014 raw data stays in the subprocess. Use instead of bash/cat when output would exceed 20 lines.${IT} Available: ${RT}.
package/start.mjs ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ process.chdir(__dirname);
9
+
10
+ if (!process.env.CLAUDE_PROJECT_DIR) {
11
+ process.env.CLAUDE_PROJECT_DIR = process.cwd();
12
+ }
13
+
14
+ // Ensure native module is available
15
+ if (!existsSync(resolve(__dirname, "node_modules", "better-sqlite3"))) {
16
+ try {
17
+ execSync("npm install better-sqlite3 --no-package-lock --no-save --silent", {
18
+ cwd: __dirname,
19
+ stdio: "pipe",
20
+ timeout: 60000,
21
+ });
22
+ } catch { /* best effort */ }
23
+ }
24
+
25
+ // Bundle exists (CI-built) — start instantly
26
+ if (existsSync(resolve(__dirname, "server.bundle.mjs"))) {
27
+ await import("./server.bundle.mjs");
28
+ } else {
29
+ // Dev or npm install — full build
30
+ if (!existsSync(resolve(__dirname, "node_modules"))) {
31
+ try {
32
+ execSync("npm install --silent", { cwd: __dirname, stdio: "pipe", timeout: 60000 });
33
+ } catch { /* best effort */ }
34
+ }
35
+ if (!existsSync(resolve(__dirname, "build", "server.js"))) {
36
+ try {
37
+ execSync("npx tsc --silent", { cwd: __dirname, stdio: "pipe", timeout: 30000 });
38
+ } catch { /* best effort */ }
39
+ }
40
+ await import("./build/server.js");
41
+ }
package/start.sh DELETED
@@ -1,15 +0,0 @@
1
- #!/bin/sh
2
- CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
3
- DIR="$(cd "$(dirname "$0")" && pwd)"
4
- cd "$DIR"
5
-
6
- # Bundle exists (CI-built) — start instantly, install native module in background
7
- if [ -f server.bundle.mjs ]; then
8
- [ -d node_modules/better-sqlite3 ] || npm install better-sqlite3 --no-package-lock --no-save --silent 2>/dev/null &
9
- CLAUDE_PROJECT_DIR="$CLAUDE_PROJECT_DIR" exec node server.bundle.mjs
10
- fi
11
-
12
- # Fallback: no bundle (dev or npm install) — full build
13
- [ -d node_modules ] || npm install --silent 2>/dev/null
14
- [ -f build/server.js ] || npx tsc --silent 2>/dev/null
15
- CLAUDE_PROJECT_DIR="$CLAUDE_PROJECT_DIR" exec node build/server.js