context-mode 1.0.89 → 1.0.90
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 +41 -9
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +95 -75
- 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.
|
|
@@ -325,8 +328,9 @@ function checkNonShellDenyPolicy(code, language, toolName) {
|
|
|
325
328
|
*/
|
|
326
329
|
function checkFilePathDenyPolicy(filePath, toolName) {
|
|
327
330
|
try {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
331
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
332
|
+
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
333
|
+
const result = evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir);
|
|
330
334
|
if (result.denied) {
|
|
331
335
|
return trackResponse(toolName, {
|
|
332
336
|
content: [{
|
|
@@ -481,7 +485,7 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
|
|
|
481
485
|
// ─────────────────────────────────────────────────────────
|
|
482
486
|
server.registerTool("ctx_execute", {
|
|
483
487
|
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.`,
|
|
488
|
+
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
489
|
inputSchema: z.object({
|
|
486
490
|
language: z
|
|
487
491
|
.enum([
|
|
@@ -774,7 +778,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
774
778
|
// ─────────────────────────────────────────────────────────
|
|
775
779
|
server.registerTool("ctx_execute_file", {
|
|
776
780
|
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.",
|
|
781
|
+
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
782
|
inputSchema: z.object({
|
|
779
783
|
path: z
|
|
780
784
|
.string()
|
|
@@ -917,6 +921,7 @@ server.registerTool("ctx_index", {
|
|
|
917
921
|
"- README files, migration guides, changelog entries\n" +
|
|
918
922
|
"- Any content with code examples you may need to reference precisely\n\n" +
|
|
919
923
|
"After indexing, use 'search' to retrieve specific sections on-demand.\n" +
|
|
924
|
+
"When `path` is provided, a content hash is stored for automatic stale detection in search results.\n" +
|
|
920
925
|
"Do NOT use for: log files, test output, CSV, build output — use 'execute_file' for those.",
|
|
921
926
|
inputSchema: z.object({
|
|
922
927
|
content: z
|
|
@@ -1016,8 +1021,10 @@ function coerceCommandsArray(val) {
|
|
|
1016
1021
|
server.registerTool("ctx_search", {
|
|
1017
1022
|
title: "Search Indexed Content",
|
|
1018
1023
|
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
|
-
"
|
|
1024
|
+
"Pass ALL search questions as queries array in ONE call. " +
|
|
1025
|
+
"File-backed sources are auto-refreshed when the source file changes.\n\n" +
|
|
1026
|
+
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.\n\n" +
|
|
1027
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1021
1028
|
inputSchema: z.object({
|
|
1022
1029
|
queries: z.preprocess(coerceJsonArray, z
|
|
1023
1030
|
.array(z.string())
|
|
@@ -1121,6 +1128,10 @@ server.registerTool("ctx_search", {
|
|
|
1121
1128
|
totalSize += formatted.length;
|
|
1122
1129
|
}
|
|
1123
1130
|
let output = sections.join("\n\n---\n\n");
|
|
1131
|
+
// Report auto-refreshed stale sources
|
|
1132
|
+
if (store.lastRefreshCount > 0) {
|
|
1133
|
+
output = `> Auto-refreshed ${store.lastRefreshCount} stale source${store.lastRefreshCount > 1 ? "s" : ""} (file changed since indexing).\n\n` + output;
|
|
1134
|
+
}
|
|
1124
1135
|
// Add throttle warning after threshold
|
|
1125
1136
|
if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
|
|
1126
1137
|
output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
|
|
@@ -1230,7 +1241,8 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1230
1241
|
description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
|
|
1231
1242
|
"and returns a ~3KB preview. Full content stays in sandbox — use search() for deeper lookups.\n\n" +
|
|
1232
1243
|
"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
|
|
1244
|
+
"Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.\n\n" +
|
|
1245
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1234
1246
|
inputSchema: z.object({
|
|
1235
1247
|
url: z.string().describe("The URL to fetch and index"),
|
|
1236
1248
|
source: z
|
|
@@ -1379,7 +1391,8 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1379
1391
|
"THIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\n" +
|
|
1380
1392
|
"One batch_execute call replaces 30+ execute calls + 10+ search calls.\n" +
|
|
1381
1393
|
"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
|
|
1394
|
+
"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" +
|
|
1395
|
+
"When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
|
|
1383
1396
|
inputSchema: z.object({
|
|
1384
1397
|
commands: z.preprocess(coerceCommandsArray, z
|
|
1385
1398
|
.array(z.object({
|
|
@@ -1958,7 +1971,17 @@ server.registerTool("ctx_insight", {
|
|
|
1958
1971
|
catch {
|
|
1959
1972
|
// Port is free, proceed with spawn
|
|
1960
1973
|
}
|
|
1961
|
-
//
|
|
1974
|
+
// Kill any previous insight child this MCP spawned (e.g. re-invocation).
|
|
1975
|
+
if (_insightChild && _insightChild.pid && !_insightChild.killed) {
|
|
1976
|
+
try {
|
|
1977
|
+
_insightChild.kill("SIGTERM");
|
|
1978
|
+
}
|
|
1979
|
+
catch { /* best effort */ }
|
|
1980
|
+
}
|
|
1981
|
+
// Start server in background. `detached: true` keeps MCP stdio free, but
|
|
1982
|
+
// we track the handle and kill it in shutdown() so the dashboard does
|
|
1983
|
+
// not orphan when Claude closes. The child also watches INSIGHT_PARENT_PID
|
|
1984
|
+
// as a fallback for SIGKILL/crash paths.
|
|
1962
1985
|
const { spawn } = await import("node:child_process");
|
|
1963
1986
|
const child = spawn("node", [join(cacheDir, "server.mjs")], {
|
|
1964
1987
|
cwd: cacheDir,
|
|
@@ -1967,12 +1990,14 @@ server.registerTool("ctx_insight", {
|
|
|
1967
1990
|
PORT: String(port),
|
|
1968
1991
|
INSIGHT_SESSION_DIR: getSessionDir(),
|
|
1969
1992
|
INSIGHT_CONTENT_DIR: join(dirname(getSessionDir()), "content"),
|
|
1993
|
+
INSIGHT_PARENT_PID: String(process.pid),
|
|
1970
1994
|
},
|
|
1971
1995
|
detached: true,
|
|
1972
1996
|
stdio: "ignore",
|
|
1973
1997
|
});
|
|
1974
1998
|
child.on("error", () => { }); // prevent unhandled error crash
|
|
1975
1999
|
child.unref();
|
|
2000
|
+
_insightChild = child;
|
|
1976
2001
|
// Wait for server to be ready
|
|
1977
2002
|
await new Promise(r => setTimeout(r, 1500));
|
|
1978
2003
|
// Verify server is actually running
|
|
@@ -2049,6 +2074,13 @@ async function main() {
|
|
|
2049
2074
|
unlinkSync(mcpSentinel);
|
|
2050
2075
|
}
|
|
2051
2076
|
catch { /* best effort */ }
|
|
2077
|
+
// Stop ctx_insight dashboard so it does not outlive Claude.
|
|
2078
|
+
if (_insightChild && _insightChild.pid && !_insightChild.killed) {
|
|
2079
|
+
try {
|
|
2080
|
+
_insightChild.kill("SIGTERM");
|
|
2081
|
+
}
|
|
2082
|
+
catch { /* best effort */ }
|
|
2083
|
+
}
|
|
2052
2084
|
};
|
|
2053
2085
|
const gracefulShutdown = async () => {
|
|
2054
2086
|
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;
|
|
@@ -216,7 +216,7 @@ export class AnalyticsEngine {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
// ─────────────────────────────────────────────────────────
|
|
219
|
-
// formatReport — renders FullReport as
|
|
219
|
+
// formatReport — renders FullReport as sales-grade savings dashboard
|
|
220
220
|
// ─────────────────────────────────────────────────────────
|
|
221
221
|
/** Format bytes as human-readable KB or MB. */
|
|
222
222
|
function kb(b) {
|
|
@@ -224,7 +224,7 @@ function kb(b) {
|
|
|
224
224
|
return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
225
225
|
if (b >= 1024)
|
|
226
226
|
return `${(b / 1024).toFixed(1)} KB`;
|
|
227
|
-
return `${b} B`;
|
|
227
|
+
return `${Math.round(b)} B`;
|
|
228
228
|
}
|
|
229
229
|
/** Format session uptime as human-readable duration. */
|
|
230
230
|
function formatDuration(uptimeMin) {
|
|
@@ -237,28 +237,34 @@ function formatDuration(uptimeMin) {
|
|
|
237
237
|
const m = Math.round(min % 60);
|
|
238
238
|
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
239
239
|
}
|
|
240
|
+
/** Format large numbers with K/M suffixes */
|
|
241
|
+
function fmtNum(n) {
|
|
242
|
+
if (n >= 1_000_000)
|
|
243
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
244
|
+
if (n >= 1_000)
|
|
245
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
246
|
+
return String(n);
|
|
247
|
+
}
|
|
240
248
|
/**
|
|
241
|
-
* Build a
|
|
242
|
-
*
|
|
243
|
-
* The "without" bar is always full (40 chars).
|
|
244
|
-
* The "with" bar is proportional to the ratio of returned vs total.
|
|
249
|
+
* Build a proportional bar using █ chars, scaled to a fixed width.
|
|
250
|
+
* Returns e.g. "████████████████████████████████████████" for full width.
|
|
245
251
|
*/
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
return { withoutBar, withBar };
|
|
252
|
+
function dataBar(bytes, maxBytes, width = 40) {
|
|
253
|
+
if (maxBytes <= 0)
|
|
254
|
+
return "░".repeat(width);
|
|
255
|
+
const filled = Math.max(1, Math.round((bytes / maxBytes) * width));
|
|
256
|
+
return "█".repeat(Math.min(filled, width)) + "░".repeat(Math.max(0, width - filled));
|
|
252
257
|
}
|
|
253
258
|
/**
|
|
254
|
-
* Render a FullReport as a
|
|
259
|
+
* Render a FullReport as a visual savings dashboard designed for screenshotting.
|
|
255
260
|
*
|
|
256
|
-
* Design
|
|
257
|
-
* -
|
|
258
|
-
* -
|
|
259
|
-
* - Per-tool
|
|
260
|
-
* -
|
|
261
|
-
* -
|
|
261
|
+
* Design principles:
|
|
262
|
+
* - Before/After comparison bar is the HERO — one glance = "wow"
|
|
263
|
+
* - "tokens saved" is the number people share
|
|
264
|
+
* - Per-tool breakdown shows what each tool SAVED, sorted by impact
|
|
265
|
+
* - Session memory: one line, reframed as value
|
|
266
|
+
* - No: Pct column, category tables, tips, jargon
|
|
267
|
+
* - Under 22 lines for heavy sessions, under 10 for fresh
|
|
262
268
|
*/
|
|
263
269
|
export function formatReport(report, version, latestVersion) {
|
|
264
270
|
const lines = [];
|
|
@@ -267,86 +273,100 @@ export function formatReport(report, version, latestVersion) {
|
|
|
267
273
|
const totalKeptOut = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
|
|
268
274
|
const totalReturned = report.savings.total_bytes_returned;
|
|
269
275
|
const totalCalls = report.savings.total_calls;
|
|
270
|
-
|
|
276
|
+
const grandTotal = totalKeptOut + totalReturned;
|
|
277
|
+
const savingsPct = grandTotal > 0 ? (totalKeptOut / grandTotal) * 100 : 0;
|
|
278
|
+
const tokensSaved = Math.round(totalKeptOut / 4);
|
|
279
|
+
// ── Fresh session: no savings yet ──
|
|
271
280
|
if (totalKeptOut === 0) {
|
|
272
|
-
lines.push(`context-mode
|
|
281
|
+
lines.push(`context-mode ${duration} ${totalCalls} calls`);
|
|
273
282
|
lines.push("");
|
|
274
283
|
if (totalCalls === 0) {
|
|
275
|
-
lines.push("No tool calls yet.");
|
|
284
|
+
lines.push("No tool calls yet. Use batch_execute or execute to start saving tokens.");
|
|
276
285
|
}
|
|
277
286
|
else {
|
|
278
|
-
|
|
279
|
-
lines.push(`${callLabel} | ${kb(totalReturned)} in context | no savings yet`);
|
|
287
|
+
lines.push(`${kb(totalReturned)} entered context | 0 tokens saved`);
|
|
280
288
|
}
|
|
289
|
+
// Footer
|
|
281
290
|
lines.push("");
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
lines.push(version ? `v${version}` : "context-mode");
|
|
291
|
+
const versionStr = version ? `v${version}` : "context-mode";
|
|
292
|
+
lines.push(versionStr);
|
|
285
293
|
if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
|
|
286
|
-
lines.push(`Update available: v${version} -> v${latestVersion} |
|
|
294
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | ctx_upgrade`);
|
|
287
295
|
}
|
|
288
296
|
return lines.join("\n");
|
|
289
297
|
}
|
|
290
|
-
// ── Active session
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const minSaved = Math.round(totalKeptOut / 4 / 1000);
|
|
298
|
-
lines.push(`context-mode -- session (${duration})`);
|
|
298
|
+
// ── Active session: visual savings dashboard ──
|
|
299
|
+
// Line 1: Hero metric — the screenshottable number
|
|
300
|
+
lines.push(`${fmtNum(tokensSaved)} tokens saved · ${savingsPct.toFixed(1)}% reduction · ${duration}`);
|
|
301
|
+
lines.push("");
|
|
302
|
+
// Lines 2-3: Before/After comparison bars — the visual proof
|
|
303
|
+
lines.push(`Without context-mode |${dataBar(grandTotal, grandTotal)}| ${kb(grandTotal)}`);
|
|
304
|
+
lines.push(`With context-mode |${dataBar(totalReturned, grandTotal)}| ${kb(totalReturned)}`);
|
|
299
305
|
lines.push("");
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
lines.push(`Without context-mode: |${withoutBar}| ${kb(grandTotal)} in your conversation`);
|
|
303
|
-
lines.push(`With context-mode: |${withBar}| ${kb(totalReturned)} in your conversation`);
|
|
306
|
+
// Value statement — the line people share
|
|
307
|
+
lines.push(`${kb(totalKeptOut)} kept out of your conversation. Never entered context.`);
|
|
304
308
|
lines.push("");
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
? `+${Math.floor(minSaved / 60)}h ${minSaved % 60}m`
|
|
310
|
-
: `+${minSaved}m`;
|
|
311
|
-
lines.push(`${timeSaved} session time gained.`);
|
|
309
|
+
// Compact stats row
|
|
310
|
+
const statParts = [`${totalCalls} calls`];
|
|
311
|
+
if (report.cache && report.cache.hits > 0) {
|
|
312
|
+
statParts.push(`${report.cache.hits} cache hits (+${kb(report.cache.bytes_saved)})`);
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
+
lines.push(statParts.join(" · "));
|
|
315
|
+
// ── Per-tool breakdown (only if 2+ tools, sorted by saved) ──
|
|
314
316
|
const activatedTools = report.savings.by_tool.filter((t) => t.calls > 0);
|
|
315
317
|
if (activatedTools.length >= 2) {
|
|
316
318
|
lines.push("");
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
319
|
+
// Estimate per-tool saved using global savings ratio
|
|
320
|
+
const toolRows = activatedTools.map((t) => {
|
|
321
|
+
const returnedBytes = t.context_kb * 1024;
|
|
322
|
+
const estimatedTotal = savingsPct < 100
|
|
323
|
+
? returnedBytes / (1 - savingsPct / 100)
|
|
324
|
+
: returnedBytes;
|
|
325
|
+
const estimatedSaved = Math.max(0, estimatedTotal - returnedBytes);
|
|
326
|
+
return { ...t, returnedBytes, estimatedSaved };
|
|
327
|
+
}).sort((a, b) => b.estimatedSaved - a.estimatedSaved);
|
|
328
|
+
// Compact table: tool name, calls, saved
|
|
329
|
+
for (const t of toolRows) {
|
|
330
|
+
const name = t.tool.length > 22 ? t.tool.slice(0, 19) + "..." : t.tool;
|
|
331
|
+
lines.push(` ${name.padEnd(22)} ${String(t.calls).padStart(4)} calls ${kb(t.estimatedSaved).padStart(8)} saved`);
|
|
321
332
|
}
|
|
322
333
|
}
|
|
323
|
-
// ── Session
|
|
324
|
-
if (report.continuity.
|
|
325
|
-
lines.push("");
|
|
326
|
-
lines.push(`Session continuity: ${report.continuity.total_events} events preserved across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
|
|
334
|
+
// ── Session memory — business-friendly ──
|
|
335
|
+
if (report.continuity.total_events > 0) {
|
|
327
336
|
lines.push("");
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
337
|
+
const cats = report.continuity.by_category;
|
|
338
|
+
// Pick the top 3-4 most impactful categories for a human-readable summary
|
|
339
|
+
const highlights = [];
|
|
340
|
+
const fileCount = cats.find(c => c.category === "file")?.count;
|
|
341
|
+
const gitCount = cats.find(c => c.category === "git")?.count;
|
|
342
|
+
const promptCount = cats.find(c => c.category === "prompt")?.count;
|
|
343
|
+
const errorCount = cats.find(c => c.category === "error")?.count;
|
|
344
|
+
const taskCount = cats.find(c => c.category === "task")?.count;
|
|
345
|
+
if (fileCount)
|
|
346
|
+
highlights.push(`${fileCount} files`);
|
|
347
|
+
if (gitCount)
|
|
348
|
+
highlights.push(`${gitCount} git ops`);
|
|
349
|
+
if (promptCount)
|
|
350
|
+
highlights.push(`${promptCount} prompts`);
|
|
351
|
+
if (errorCount)
|
|
352
|
+
highlights.push(`${errorCount} errors`);
|
|
353
|
+
if (taskCount)
|
|
354
|
+
highlights.push(`${taskCount} tasks`);
|
|
355
|
+
const summary = highlights.length > 0 ? ` · ${highlights.join(", ")}` : "";
|
|
356
|
+
if (report.continuity.compact_count > 0) {
|
|
357
|
+
lines.push(`${fmtNum(report.continuity.total_events)} events remembered across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}${summary}`);
|
|
358
|
+
lines.push("Zero knowledge lost — picks up exactly where you left off.");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
lines.push(`${fmtNum(report.continuity.total_events)} events tracked${summary}`);
|
|
333
362
|
}
|
|
334
363
|
}
|
|
335
|
-
// ── Footer
|
|
336
|
-
const footerParts = [];
|
|
337
|
-
if (report.continuity.by_category.length === 0 && report.continuity.compact_count > 0) {
|
|
338
|
-
footerParts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
|
|
339
|
-
}
|
|
340
|
-
if (report.continuity.by_category.length === 0 && report.continuity.total_events > 0) {
|
|
341
|
-
footerParts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
|
|
342
|
-
}
|
|
343
|
-
const versionStr = version ? `v${version}` : "context-mode";
|
|
344
|
-
footerParts.push(versionStr);
|
|
364
|
+
// ── Footer ──
|
|
345
365
|
lines.push("");
|
|
346
|
-
|
|
347
|
-
|
|
366
|
+
const versionStr = version ? `v${version}` : "context-mode";
|
|
367
|
+
lines.push(versionStr);
|
|
348
368
|
if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
|
|
349
|
-
lines.push(`Update available: v${version} -> v${latestVersion} |
|
|
369
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | ctx_upgrade`);
|
|
350
370
|
}
|
|
351
371
|
return lines.join("\n");
|
|
352
372
|
}
|