context-mode 1.0.89 → 1.0.91
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/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +184 -60
- package/build/adapters/antigravity/index.d.ts +3 -5
- package/build/adapters/antigravity/index.js +7 -35
- package/build/adapters/base.d.ts +27 -0
- package/build/adapters/base.js +59 -0
- package/build/adapters/claude-code/index.d.ts +9 -25
- package/build/adapters/claude-code/index.js +12 -140
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +21 -14
- package/build/adapters/codex/hooks.js +22 -15
- package/build/adapters/codex/index.d.ts +6 -10
- package/build/adapters/codex/index.js +13 -43
- package/build/adapters/copilot-base.d.ts +78 -0
- package/build/adapters/copilot-base.js +281 -0
- package/build/adapters/cursor/index.d.ts +3 -5
- package/build/adapters/cursor/index.js +6 -34
- package/build/adapters/detect.d.ts +7 -0
- package/build/adapters/detect.js +57 -56
- package/build/adapters/gemini-cli/index.d.ts +3 -5
- package/build/adapters/gemini-cli/index.js +7 -35
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
- package/build/adapters/jetbrains-copilot/hooks.js +82 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
- package/build/adapters/jetbrains-copilot/index.js +119 -0
- package/build/adapters/kiro/hooks.d.ts +14 -0
- package/build/adapters/kiro/hooks.js +23 -0
- package/build/adapters/kiro/index.d.ts +3 -5
- package/build/adapters/kiro/index.js +10 -38
- package/build/adapters/openclaw/index.d.ts +3 -4
- package/build/adapters/openclaw/index.js +6 -22
- package/build/adapters/opencode/index.d.ts +2 -3
- package/build/adapters/opencode/index.js +5 -16
- package/build/adapters/qwen-code/index.d.ts +39 -0
- package/build/adapters/qwen-code/index.js +199 -0
- package/build/adapters/types.d.ts +1 -1
- package/build/adapters/vscode-copilot/index.d.ts +16 -46
- package/build/adapters/vscode-copilot/index.js +29 -320
- package/build/adapters/zed/index.d.ts +3 -5
- package/build/adapters/zed/index.js +7 -35
- package/build/cli.js +13 -0
- package/build/lifecycle.d.ts +23 -0
- package/build/lifecycle.js +54 -13
- package/build/opencode-plugin.d.ts +19 -7
- package/build/opencode-plugin.js +19 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +53 -10
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +107 -76
- package/build/session/db.d.ts +10 -1
- package/build/session/db.js +67 -8
- package/build/session/extract.js +10 -2
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +231 -0
- package/build/store.d.ts +4 -0
- package/build/store.js +58 -9
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +135 -121
- package/configs/antigravity/GEMINI.md +31 -36
- package/configs/claude-code/CLAUDE.md +31 -37
- package/configs/codex/AGENTS.md +35 -49
- package/configs/cursor/context-mode.mdc +24 -25
- package/configs/gemini-cli/GEMINI.md +30 -36
- package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +8 -0
- package/configs/kilo/AGENTS.md +30 -36
- package/configs/kiro/KIRO.md +30 -36
- package/configs/kiro/agent.json +1 -1
- package/configs/openclaw/AGENTS.md +30 -36
- package/configs/opencode/AGENTS.md +30 -36
- package/configs/pi/AGENTS.md +31 -36
- package/configs/qwen-code/QWEN.md +63 -0
- package/configs/vscode-copilot/copilot-instructions.md +30 -36
- package/configs/zed/AGENTS.md +31 -36
- package/hooks/codex/posttooluse.mjs +7 -7
- package/hooks/codex/pretooluse.mjs +3 -3
- package/hooks/codex/sessionstart.mjs +2 -1
- package/hooks/core/formatters.mjs +24 -0
- package/hooks/core/routing.mjs +40 -15
- package/hooks/core/tool-naming.mjs +2 -0
- package/hooks/cursor/posttooluse.mjs +7 -7
- package/hooks/cursor/pretooluse.mjs +3 -3
- package/hooks/cursor/sessionstart.mjs +2 -1
- package/hooks/cursor/stop.mjs +2 -2
- package/hooks/ensure-deps.mjs +22 -10
- package/hooks/gemini-cli/aftertool.mjs +8 -8
- package/hooks/gemini-cli/beforetool.mjs +3 -2
- package/hooks/gemini-cli/precompress.mjs +2 -2
- package/hooks/gemini-cli/sessionstart.mjs +12 -4
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +54 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
- package/hooks/kiro/posttooluse.mjs +6 -7
- package/hooks/kiro/pretooluse.mjs +3 -2
- package/hooks/posttooluse.mjs +8 -8
- package/hooks/precompact.mjs +3 -4
- package/hooks/pretooluse.mjs +5 -4
- package/hooks/routing-block.mjs +35 -33
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +27 -8
- package/hooks/session-extract.bundle.mjs +2 -1
- package/hooks/session-helpers.mjs +44 -3
- package/hooks/session-loaders.mjs +37 -0
- package/hooks/sessionstart.mjs +5 -5
- package/hooks/userpromptsubmit.mjs +26 -9
- package/hooks/vscode-copilot/posttooluse.mjs +8 -8
- package/hooks/vscode-copilot/precompact.mjs +2 -2
- package/hooks/vscode-copilot/pretooluse.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +2 -2
- package/insight/server.mjs +237 -25
- package/insight/src/lib/api.ts +2 -1
- package/insight/src/routes/index.tsx +16 -3
- package/insight/src/routes/search.tsx +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -2
- package/server.bundle.mjs +94 -80
- package/skills/ctx-insight/SKILL.md +1 -1
package/build/opencode-plugin.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode TypeScript plugin entry point for context-mode.
|
|
2
|
+
* OpenCode / KiloCode TypeScript plugin entry point for context-mode.
|
|
3
3
|
*
|
|
4
4
|
* Provides three hooks:
|
|
5
5
|
* - tool.execute.before — Routing enforcement (deny/modify/passthrough)
|
|
6
6
|
* - tool.execute.after — Session event capture
|
|
7
7
|
* - experimental.session.compacting — Compaction snapshot generation
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* KiloCode loads this via: import("context-mode") → expects default export
|
|
10
|
+
* with shape { server: (input) => Promise<Hooks> } (PluginModule).
|
|
11
|
+
*
|
|
12
|
+
* OpenCode loads this via: import("context-mode/plugin") → also supports
|
|
13
|
+
* the named export ContextModePlugin for backward compat.
|
|
10
14
|
*
|
|
11
15
|
* Constraints:
|
|
12
16
|
* - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
|
|
@@ -27,10 +31,13 @@ function getPlatform() {
|
|
|
27
31
|
}
|
|
28
32
|
// ── Plugin Factory ────────────────────────────────────────
|
|
29
33
|
/**
|
|
30
|
-
*
|
|
34
|
+
* Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
|
|
31
35
|
* Returns an object mapping hook event names to async handler functions.
|
|
32
|
-
|
|
33
|
-
export
|
|
36
|
+
*
|
|
37
|
+
* KiloCode expects: export default { server: (input) => Promise<Hooks> }
|
|
38
|
+
* OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
|
|
39
|
+
*/
|
|
40
|
+
async function createContextModePlugin(ctx) {
|
|
34
41
|
// Resolve build dir from compiled JS location
|
|
35
42
|
const adapter = new OpenCodeAdapter(getPlatform());
|
|
36
43
|
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -52,7 +59,7 @@ export const ContextModePlugin = async (ctx) => {
|
|
|
52
59
|
const toolInput = output.args ?? {};
|
|
53
60
|
let decision;
|
|
54
61
|
try {
|
|
55
|
-
decision = routing.routePreToolUse(toolName, toolInput, projectDir,
|
|
62
|
+
decision = routing.routePreToolUse(toolName, toolInput, projectDir, getPlatform());
|
|
56
63
|
}
|
|
57
64
|
catch {
|
|
58
65
|
return; // Routing failure → allow passthrough
|
|
@@ -109,4 +116,9 @@ export const ContextModePlugin = async (ctx) => {
|
|
|
109
116
|
}
|
|
110
117
|
},
|
|
111
118
|
};
|
|
112
|
-
}
|
|
119
|
+
}
|
|
120
|
+
// ── Exports ──────────────────────────────────────────────
|
|
121
|
+
// KiloCode PluginModule: default export with { server } shape
|
|
122
|
+
// OpenCode compat: named export for direct import("context-mode/plugin")
|
|
123
|
+
export default { server: createContextModePlugin };
|
|
124
|
+
export { createContextModePlugin as ContextModePlugin };
|
package/build/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
const isWindows = process.platform === "win32";
|
|
4
4
|
function commandExists(cmd) {
|
|
@@ -14,10 +14,8 @@ function commandExists(cmd) {
|
|
|
14
14
|
function bunExists() {
|
|
15
15
|
if (commandExists("bun"))
|
|
16
16
|
return true;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
20
|
-
if (home && existsSync(`${home}/.bun/bin/bun`))
|
|
17
|
+
for (const p of bunFallbackPaths()) {
|
|
18
|
+
if (existsSync(p))
|
|
21
19
|
return true;
|
|
22
20
|
}
|
|
23
21
|
return false;
|
|
@@ -25,8 +23,24 @@ function bunExists() {
|
|
|
25
23
|
function bunCommand() {
|
|
26
24
|
if (commandExists("bun"))
|
|
27
25
|
return "bun";
|
|
26
|
+
for (const p of bunFallbackPaths()) {
|
|
27
|
+
if (existsSync(p))
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
28
30
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
29
|
-
return `${home}/.bun/bin/bun`;
|
|
31
|
+
return isWindows ? `${home}\\.bun\\bin\\bun.exe` : `${home}/.bun/bin/bun`;
|
|
32
|
+
}
|
|
33
|
+
/** Fallback paths where Bun may be installed but not on PATH. */
|
|
34
|
+
function bunFallbackPaths() {
|
|
35
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
36
|
+
if (isWindows) {
|
|
37
|
+
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
38
|
+
return [
|
|
39
|
+
...(home ? [`${home}\\.bun\\bin\\bun.exe`] : []),
|
|
40
|
+
...(localAppData ? [`${localAppData}\\bun\\bin\\bun.exe`] : []),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
return home ? [`${home}/.bun/bin/bun`] : [];
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
46
|
* On Windows, resolve the first non-WSL bash in PATH.
|
|
@@ -61,10 +75,11 @@ function resolveWindowsBash() {
|
|
|
61
75
|
return null;
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
|
-
function getVersion(cmd) {
|
|
78
|
+
function getVersion(cmd, args = ["--version"]) {
|
|
65
79
|
try {
|
|
66
|
-
return
|
|
80
|
+
return execFileSync(cmd, args, {
|
|
67
81
|
encoding: "utf-8",
|
|
82
|
+
shell: process.platform === "win32",
|
|
68
83
|
stdio: ["pipe", "pipe", "pipe"],
|
|
69
84
|
timeout: 5000,
|
|
70
85
|
})
|
|
@@ -132,7 +147,7 @@ export function getRuntimeSummary(runtimes) {
|
|
|
132
147
|
if (runtimes.ruby)
|
|
133
148
|
lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
|
|
134
149
|
if (runtimes.go)
|
|
135
|
-
lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go)})`);
|
|
150
|
+
lines.push(` Go: ${runtimes.go} (${getVersion(runtimes.go, ["version"])})`);
|
|
136
151
|
if (runtimes.rust)
|
|
137
152
|
lines.push(` Rust: ${runtimes.rust} (${getVersion(runtimes.rust)})`);
|
|
138
153
|
if (runtimes.php)
|
package/build/security.d.ts
CHANGED
|
@@ -104,8 +104,24 @@ export declare function evaluateCommandDenyOnly(command: string, policies: Secur
|
|
|
104
104
|
*
|
|
105
105
|
* Normalizes backslashes to forward slashes before matching so that
|
|
106
106
|
* Windows paths work with Unix-style glob patterns.
|
|
107
|
+
*
|
|
108
|
+
* When `projectRoot` is supplied, the path is also matched in its
|
|
109
|
+
* fully-resolved absolute form **and** — when the file exists — in
|
|
110
|
+
* its canonical form (`fs.realpathSync`). This prevents two classes
|
|
111
|
+
* of bypass:
|
|
112
|
+
*
|
|
113
|
+
* 1. `..` traversal: a relative path like `../../.ssh/id_rsa` no
|
|
114
|
+
* longer evades absolute-path deny rules.
|
|
115
|
+
* 2. Symlink escape: a project-local path whose realpath points
|
|
116
|
+
* outside the project (e.g. `safe.log -> ~/.ssh/id_rsa`) no
|
|
117
|
+
* longer evades absolute-path deny rules.
|
|
118
|
+
*
|
|
119
|
+
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
120
|
+
* or the syscall fails for any reason, the lexical resolved form is
|
|
121
|
+
* still checked. This keeps the function usable for paths that will
|
|
122
|
+
* be created during execution.
|
|
107
123
|
*/
|
|
108
|
-
export declare function evaluateFilePath(filePath: string, denyGlobs: string[][], caseInsensitive?: boolean): {
|
|
124
|
+
export declare function evaluateFilePath(filePath: string, denyGlobs: string[][], caseInsensitive?: boolean, projectRoot?: string): {
|
|
109
125
|
denied: boolean;
|
|
110
126
|
matchedPattern?: string;
|
|
111
127
|
};
|
package/build/security.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
// ==============================================================================
|
|
@@ -358,14 +358,48 @@ export function evaluateCommandDenyOnly(command, policies, caseInsensitive = pro
|
|
|
358
358
|
*
|
|
359
359
|
* Normalizes backslashes to forward slashes before matching so that
|
|
360
360
|
* Windows paths work with Unix-style glob patterns.
|
|
361
|
+
*
|
|
362
|
+
* When `projectRoot` is supplied, the path is also matched in its
|
|
363
|
+
* fully-resolved absolute form **and** — when the file exists — in
|
|
364
|
+
* its canonical form (`fs.realpathSync`). This prevents two classes
|
|
365
|
+
* of bypass:
|
|
366
|
+
*
|
|
367
|
+
* 1. `..` traversal: a relative path like `../../.ssh/id_rsa` no
|
|
368
|
+
* longer evades absolute-path deny rules.
|
|
369
|
+
* 2. Symlink escape: a project-local path whose realpath points
|
|
370
|
+
* outside the project (e.g. `safe.log -> ~/.ssh/id_rsa`) no
|
|
371
|
+
* longer evades absolute-path deny rules.
|
|
372
|
+
*
|
|
373
|
+
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
374
|
+
* or the syscall fails for any reason, the lexical resolved form is
|
|
375
|
+
* still checked. This keeps the function usable for paths that will
|
|
376
|
+
* be created during execution.
|
|
361
377
|
*/
|
|
362
|
-
export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.platform === "win32") {
|
|
363
|
-
|
|
364
|
-
|
|
378
|
+
export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.platform === "win32", projectRoot) {
|
|
379
|
+
const toForward = (path) => path.replace(/\\/g, "/");
|
|
380
|
+
// Match against the raw input, the lexically-resolved absolute path,
|
|
381
|
+
// and the canonical (symlink-resolved) path when the file exists.
|
|
382
|
+
// Deduplicated so absolute inputs and paths that don't cross symlinks
|
|
383
|
+
// don't pay the matching cost multiple times.
|
|
384
|
+
const candidates = new Set();
|
|
385
|
+
candidates.add(toForward(filePath));
|
|
386
|
+
if (projectRoot) {
|
|
387
|
+
const lexical = resolve(projectRoot, filePath);
|
|
388
|
+
candidates.add(toForward(lexical));
|
|
389
|
+
try {
|
|
390
|
+
candidates.add(toForward(realpathSync(lexical)));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// File does not exist yet, or realpath failed — rely on lexical form.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
365
396
|
for (const globs of denyGlobs) {
|
|
366
397
|
for (const glob of globs) {
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
const regex = fileGlobToRegex(glob, caseInsensitive);
|
|
399
|
+
for (const candidate of candidates) {
|
|
400
|
+
if (regex.test(candidate)) {
|
|
401
|
+
return { denied: true, matchedPattern: glob };
|
|
402
|
+
}
|
|
369
403
|
}
|
|
370
404
|
}
|
|
371
405
|
}
|
package/build/server.js
CHANGED
|
@@ -97,6 +97,9 @@ function maybeIndexSessionEvents(store) {
|
|
|
97
97
|
// platform-specific paths. All session DB paths go through it — no
|
|
98
98
|
// hardcoded configDir detection in tool handlers.
|
|
99
99
|
let _detectedAdapter = null;
|
|
100
|
+
// Tracks the ctx_insight dashboard child so shutdown can terminate it.
|
|
101
|
+
// See ctx_insight handler + shutdown() in main().
|
|
102
|
+
let _insightChild = null;
|
|
100
103
|
/**
|
|
101
104
|
* Get the platform-specific sessions directory from the detected adapter.
|
|
102
105
|
* Falls back to ~/.claude/context-mode/sessions/ before adapter detection.
|
|
@@ -228,10 +231,21 @@ function getUpgradeHint() {
|
|
|
228
231
|
return "npm run build";
|
|
229
232
|
return "npm update -g context-mode";
|
|
230
233
|
}
|
|
234
|
+
function semverNewer(a, b) {
|
|
235
|
+
const pa = a.split(".").map(Number);
|
|
236
|
+
const pb = b.split(".").map(Number);
|
|
237
|
+
for (let i = 0; i < 3; i++) {
|
|
238
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
239
|
+
return true;
|
|
240
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
231
245
|
function isOutdated() {
|
|
232
246
|
if (!_latestVersion || _latestVersion === "unknown")
|
|
233
247
|
return false;
|
|
234
|
-
return _latestVersion
|
|
248
|
+
return semverNewer(_latestVersion, VERSION);
|
|
235
249
|
}
|
|
236
250
|
function shouldShowVersionWarning() {
|
|
237
251
|
if (!isOutdated())
|
|
@@ -325,8 +339,9 @@ function checkNonShellDenyPolicy(code, language, toolName) {
|
|
|
325
339
|
*/
|
|
326
340
|
function checkFilePathDenyPolicy(filePath, toolName) {
|
|
327
341
|
try {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
342
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
343
|
+
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
344
|
+
const result = evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir);
|
|
330
345
|
if (result.denied) {
|
|
331
346
|
return trackResponse(toolName, {
|
|
332
347
|
content: [{
|
|
@@ -481,7 +496,7 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
|
|
|
481
496
|
// ─────────────────────────────────────────────────────────
|
|
482
497
|
server.registerTool("ctx_execute", {
|
|
483
498
|
title: "Execute Code",
|
|
484
|
-
description: `MANDATORY: Use for any command where output exceeds 20 lines. Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess.${bunNote} Available: ${langList}.\n\nPREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.\n\nTHINK IN CODE: When you need to analyze, count, filter, compare, or process data — write code that does the work and console.log() only the answer. Do NOT read raw data into context to process mentally. Program the analysis, don't compute it in your reasoning. Write robust, pure JavaScript (no npm dependencies). Use only Node.js built-ins (fs, path, child_process). Always wrap in try/catch. Handle null/undefined. Works on both Node.js and Bun.`,
|
|
499
|
+
description: `MANDATORY: Use for any command where output exceeds 20 lines. Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess.${bunNote} Available: ${langList}.\n\nPREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.\n\nTHINK IN CODE: When you need to analyze, count, filter, compare, or process data — write code that does the work and console.log() only the answer. Do NOT read raw data into context to process mentally. Program the analysis, don't compute it in your reasoning. Write robust, pure JavaScript (no npm dependencies). Use only Node.js built-ins (fs, path, child_process). Always wrap in try/catch. Handle null/undefined. Works on both Node.js and Bun.\n\nWhen reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].`,
|
|
485
500
|
inputSchema: z.object({
|
|
486
501
|
language: z
|
|
487
502
|
.enum([
|
|
@@ -774,7 +789,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
774
789
|
// ─────────────────────────────────────────────────────────
|
|
775
790
|
server.registerTool("ctx_execute_file", {
|
|
776
791
|
title: "Execute File Processing",
|
|
777
|
-
description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.\n\nTHINK IN CODE: Write code that processes FILE_CONTENT and console.log() only the answer. Don't read files into context to analyze mentally. Write robust, pure JavaScript — no npm deps, try/catch, null-safe. Node.js + Bun compatible.",
|
|
792
|
+
description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.\n\nTHINK IN CODE: Write code that processes FILE_CONTENT and console.log() only the answer. Don't read files into context to analyze mentally. Write robust, pure JavaScript — no npm deps, try/catch, null-safe. Node.js + Bun compatible.\n\nWhen reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
778
793
|
inputSchema: z.object({
|
|
779
794
|
path: z
|
|
780
795
|
.string()
|
|
@@ -917,6 +932,7 @@ server.registerTool("ctx_index", {
|
|
|
917
932
|
"- README files, migration guides, changelog entries\n" +
|
|
918
933
|
"- Any content with code examples you may need to reference precisely\n\n" +
|
|
919
934
|
"After indexing, use 'search' to retrieve specific sections on-demand.\n" +
|
|
935
|
+
"When `path` is provided, a content hash is stored for automatic stale detection in search results.\n" +
|
|
920
936
|
"Do NOT use for: log files, test output, CSV, build output — use 'execute_file' for those.",
|
|
921
937
|
inputSchema: z.object({
|
|
922
938
|
content: z
|
|
@@ -1016,8 +1032,10 @@ function coerceCommandsArray(val) {
|
|
|
1016
1032
|
server.registerTool("ctx_search", {
|
|
1017
1033
|
title: "Search Indexed Content",
|
|
1018
1034
|
description: "Search indexed content. Requires prior indexing via ctx_batch_execute, ctx_index, or ctx_fetch_and_index. " +
|
|
1019
|
-
"Pass ALL search questions as queries array in ONE call
|
|
1020
|
-
"
|
|
1035
|
+
"Pass ALL search questions as queries array in ONE call. " +
|
|
1036
|
+
"File-backed sources are auto-refreshed when the source file changes.\n\n" +
|
|
1037
|
+
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.\n\n" +
|
|
1038
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1021
1039
|
inputSchema: z.object({
|
|
1022
1040
|
queries: z.preprocess(coerceJsonArray, z
|
|
1023
1041
|
.array(z.string())
|
|
@@ -1121,6 +1139,10 @@ server.registerTool("ctx_search", {
|
|
|
1121
1139
|
totalSize += formatted.length;
|
|
1122
1140
|
}
|
|
1123
1141
|
let output = sections.join("\n\n---\n\n");
|
|
1142
|
+
// Report auto-refreshed stale sources
|
|
1143
|
+
if (store.lastRefreshCount > 0) {
|
|
1144
|
+
output = `> Auto-refreshed ${store.lastRefreshCount} stale source${store.lastRefreshCount > 1 ? "s" : ""} (file changed since indexing).\n\n` + output;
|
|
1145
|
+
}
|
|
1124
1146
|
// Add throttle warning after threshold
|
|
1125
1147
|
if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
|
|
1126
1148
|
output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
|
|
@@ -1230,7 +1252,8 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1230
1252
|
description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
|
|
1231
1253
|
"and returns a ~3KB preview. Full content stays in sandbox — use search() for deeper lookups.\n\n" +
|
|
1232
1254
|
"Better than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.\n\n" +
|
|
1233
|
-
"Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly
|
|
1255
|
+
"Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.\n\n" +
|
|
1256
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1234
1257
|
inputSchema: z.object({
|
|
1235
1258
|
url: z.string().describe("The URL to fetch and index"),
|
|
1236
1259
|
source: z
|
|
@@ -1379,7 +1402,8 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1379
1402
|
"THIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\n" +
|
|
1380
1403
|
"One batch_execute call replaces 30+ execute calls + 10+ search calls.\n" +
|
|
1381
1404
|
"Provide all commands to run and all queries to search — everything happens in one round trip.\n\n" +
|
|
1382
|
-
"THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work
|
|
1405
|
+
"THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work.\n\n" +
|
|
1406
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1383
1407
|
inputSchema: z.object({
|
|
1384
1408
|
commands: z.preprocess(coerceCommandsArray, z
|
|
1385
1409
|
.array(z.object({
|
|
@@ -1958,7 +1982,17 @@ server.registerTool("ctx_insight", {
|
|
|
1958
1982
|
catch {
|
|
1959
1983
|
// Port is free, proceed with spawn
|
|
1960
1984
|
}
|
|
1961
|
-
//
|
|
1985
|
+
// Kill any previous insight child this MCP spawned (e.g. re-invocation).
|
|
1986
|
+
if (_insightChild && _insightChild.pid && !_insightChild.killed) {
|
|
1987
|
+
try {
|
|
1988
|
+
_insightChild.kill("SIGTERM");
|
|
1989
|
+
}
|
|
1990
|
+
catch { /* best effort */ }
|
|
1991
|
+
}
|
|
1992
|
+
// Start server in background. `detached: true` keeps MCP stdio free, but
|
|
1993
|
+
// we track the handle and kill it in shutdown() so the dashboard does
|
|
1994
|
+
// not orphan when Claude closes. The child also watches INSIGHT_PARENT_PID
|
|
1995
|
+
// as a fallback for SIGKILL/crash paths.
|
|
1962
1996
|
const { spawn } = await import("node:child_process");
|
|
1963
1997
|
const child = spawn("node", [join(cacheDir, "server.mjs")], {
|
|
1964
1998
|
cwd: cacheDir,
|
|
@@ -1967,12 +2001,14 @@ server.registerTool("ctx_insight", {
|
|
|
1967
2001
|
PORT: String(port),
|
|
1968
2002
|
INSIGHT_SESSION_DIR: getSessionDir(),
|
|
1969
2003
|
INSIGHT_CONTENT_DIR: join(dirname(getSessionDir()), "content"),
|
|
2004
|
+
INSIGHT_PARENT_PID: String(process.pid),
|
|
1970
2005
|
},
|
|
1971
2006
|
detached: true,
|
|
1972
2007
|
stdio: "ignore",
|
|
1973
2008
|
});
|
|
1974
2009
|
child.on("error", () => { }); // prevent unhandled error crash
|
|
1975
2010
|
child.unref();
|
|
2011
|
+
_insightChild = child;
|
|
1976
2012
|
// Wait for server to be ready
|
|
1977
2013
|
await new Promise(r => setTimeout(r, 1500));
|
|
1978
2014
|
// Verify server is actually running
|
|
@@ -2049,6 +2085,13 @@ async function main() {
|
|
|
2049
2085
|
unlinkSync(mcpSentinel);
|
|
2050
2086
|
}
|
|
2051
2087
|
catch { /* best effort */ }
|
|
2088
|
+
// Stop ctx_insight dashboard so it does not outlive Claude.
|
|
2089
|
+
if (_insightChild && _insightChild.pid && !_insightChild.killed) {
|
|
2090
|
+
try {
|
|
2091
|
+
_insightChild.kill("SIGTERM");
|
|
2092
|
+
}
|
|
2093
|
+
catch { /* best effort */ }
|
|
2094
|
+
}
|
|
2052
2095
|
};
|
|
2053
2096
|
const gracefulShutdown = async () => {
|
|
2054
2097
|
shutdown();
|
|
@@ -149,13 +149,14 @@ export declare class AnalyticsEngine {
|
|
|
149
149
|
queryAll(runtimeStats: RuntimeStats): FullReport;
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
|
-
* Render a FullReport as a
|
|
152
|
+
* Render a FullReport as a visual savings dashboard designed for screenshotting.
|
|
153
153
|
*
|
|
154
|
-
* Design
|
|
155
|
-
* -
|
|
156
|
-
* -
|
|
157
|
-
* - Per-tool
|
|
158
|
-
* -
|
|
159
|
-
* -
|
|
154
|
+
* Design principles:
|
|
155
|
+
* - Before/After comparison bar is the HERO — one glance = "wow"
|
|
156
|
+
* - "tokens saved" is the number people share
|
|
157
|
+
* - Per-tool breakdown shows what each tool SAVED, sorted by impact
|
|
158
|
+
* - Session memory: one line, reframed as value
|
|
159
|
+
* - No: Pct column, category tables, tips, jargon
|
|
160
|
+
* - Under 22 lines for heavy sessions, under 10 for fresh
|
|
160
161
|
*/
|
|
161
162
|
export declare function formatReport(report: FullReport, version?: string, latestVersion?: string | null): string;
|