context-mode 0.8.1 → 0.9.1
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/hooks/hooks.json +5 -5
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -3
- package/.mcp.json +2 -2
- package/README.md +4 -0
- package/build/cli.js +59 -22
- package/build/executor.js +37 -6
- package/build/runtime.js +5 -2
- package/hooks/hooks.json +5 -5
- package/hooks/pretooluse.mjs +157 -0
- package/package.json +2 -2
- package/server.bundle.mjs +44 -44
- package/start.mjs +41 -0
- package/start.sh +0 -15
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
16
|
+
"version": "0.9.1",
|
|
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.
|
|
3
|
+
"version": "0.9.1",
|
|
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": "
|
|
25
|
-
"args": ["${CLAUDE_PLUGIN_ROOT}/start.
|
|
24
|
+
"command": "node",
|
|
25
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/start.mjs"]
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"skills": "./skills/"
|
package/.mcp.json
CHANGED
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.
|
|
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
|
-
|
|
149
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
363
|
+
"start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
|
|
346
364
|
];
|
|
347
365
|
for (const item of items) {
|
|
348
366
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
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}`);
|
|
@@ -376,6 +395,21 @@ async function upgrade() {
|
|
|
376
395
|
catch {
|
|
377
396
|
s.stop(color.yellow("Cache migration skipped — using existing directory"));
|
|
378
397
|
}
|
|
398
|
+
// Update installed_plugins.json so Claude Code loads from new path
|
|
399
|
+
const installedPluginsPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
400
|
+
try {
|
|
401
|
+
const raw = readFileSync(installedPluginsPath, "utf-8");
|
|
402
|
+
const updated = raw
|
|
403
|
+
.replace(new RegExp(oldDirVersion.replace(/\./g, "\\."), "g"), newVersion);
|
|
404
|
+
if (updated !== raw) {
|
|
405
|
+
writeFileSync(installedPluginsPath, updated, "utf-8");
|
|
406
|
+
p.log.success(color.green("Plugin registry updated") + color.dim(` — installed_plugins.json`));
|
|
407
|
+
changes.push("Updated plugin registry path");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
p.log.warn(color.yellow("Could not update installed_plugins.json — marketplace may show old version"));
|
|
412
|
+
}
|
|
379
413
|
}
|
|
380
414
|
// Update global npm package from same GitHub source
|
|
381
415
|
s.start("Updating npm global package");
|
|
@@ -392,7 +426,7 @@ async function upgrade() {
|
|
|
392
426
|
p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
|
|
393
427
|
}
|
|
394
428
|
// Cleanup
|
|
395
|
-
|
|
429
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
396
430
|
changes.push(newVersion !== localVersion
|
|
397
431
|
? `Updated v${localVersion} → v${newVersion}`
|
|
398
432
|
: `Reinstalled v${localVersion} from GitHub`);
|
|
@@ -406,7 +440,7 @@ async function upgrade() {
|
|
|
406
440
|
p.log.info(color.dim("Continuing with hooks/settings fix..."));
|
|
407
441
|
// Cleanup on failure
|
|
408
442
|
try {
|
|
409
|
-
|
|
443
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
410
444
|
}
|
|
411
445
|
catch { /* ignore */ }
|
|
412
446
|
}
|
|
@@ -425,14 +459,14 @@ async function upgrade() {
|
|
|
425
459
|
}
|
|
426
460
|
// Step 4: Fix hooks
|
|
427
461
|
p.log.step("Configuring PreToolUse hooks...");
|
|
428
|
-
const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.
|
|
462
|
+
const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
|
|
429
463
|
const settings = readSettings() ?? {};
|
|
430
464
|
const desiredHookEntry = {
|
|
431
465
|
matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch|Task",
|
|
432
466
|
hooks: [
|
|
433
467
|
{
|
|
434
468
|
type: "command",
|
|
435
|
-
command: "
|
|
469
|
+
command: "node " + hookScriptPath,
|
|
436
470
|
},
|
|
437
471
|
],
|
|
438
472
|
};
|
|
@@ -441,7 +475,7 @@ async function upgrade() {
|
|
|
441
475
|
if (existingPreToolUse && Array.isArray(existingPreToolUse)) {
|
|
442
476
|
const existingIdx = existingPreToolUse.findIndex((entry) => {
|
|
443
477
|
const entryHooks = entry.hooks;
|
|
444
|
-
return entryHooks?.some((h) => h.command?.includes("pretooluse.
|
|
478
|
+
return entryHooks?.some((h) => h.command?.includes("pretooluse.mjs"));
|
|
445
479
|
});
|
|
446
480
|
if (existingIdx >= 0) {
|
|
447
481
|
existingPreToolUse[existingIdx] = desiredHookEntry;
|
|
@@ -478,7 +512,7 @@ async function upgrade() {
|
|
|
478
512
|
accessSync(hookScriptPath, constants.R_OK);
|
|
479
513
|
chmodSync(hookScriptPath, 0o755);
|
|
480
514
|
p.log.success(color.green("Permissions set") + color.dim(" — chmod +x " + hookScriptPath));
|
|
481
|
-
changes.push("Set pretooluse.
|
|
515
|
+
changes.push("Set pretooluse.mjs as executable");
|
|
482
516
|
}
|
|
483
517
|
catch {
|
|
484
518
|
p.log.error(color.red("Hook script not found") +
|
|
@@ -494,7 +528,10 @@ async function upgrade() {
|
|
|
494
528
|
// Step 7: Run doctor
|
|
495
529
|
p.log.step("Running doctor to verify...");
|
|
496
530
|
console.log();
|
|
497
|
-
await doctor();
|
|
531
|
+
const doctorCode = await doctor();
|
|
532
|
+
if (doctorCode !== 0) {
|
|
533
|
+
process.exit(doctorCode);
|
|
534
|
+
}
|
|
498
535
|
}
|
|
499
536
|
/* -------------------------------------------------------
|
|
500
537
|
* 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
|
|
93
|
+
const binSuffix = isWin ? ".exe" : "";
|
|
94
|
+
const binPath = srcPath.replace(/\.rs$/, "") + binSuffix;
|
|
81
95
|
// Compile
|
|
82
96
|
try {
|
|
83
|
-
execSync(`rustc ${srcPath} -o ${binPath}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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);
|