context-mode 1.0.88 → 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 +27 -141
- 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 +113 -47
- 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/pi-extension.js +24 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +129 -21
- 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 +7 -0
- package/build/store.js +117 -18
- package/build/truncate.d.ts +6 -0
- package/build/truncate.js +51 -29
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +157 -136
- 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 +43 -20
- 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/session-snapshot.bundle.mjs +14 -14
- 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 +262 -32
- 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 +117 -99
- package/skills/ctx-insight/SKILL.md +1 -1
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([
|
|
@@ -502,7 +506,7 @@ server.registerTool("ctx_execute", {
|
|
|
502
506
|
.string()
|
|
503
507
|
.describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."),
|
|
504
508
|
timeout: z
|
|
505
|
-
.number()
|
|
509
|
+
.coerce.number()
|
|
506
510
|
.optional()
|
|
507
511
|
.default(30000)
|
|
508
512
|
.describe("Max execution time in ms"),
|
|
@@ -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()
|
|
@@ -798,7 +802,7 @@ server.registerTool("ctx_execute_file", {
|
|
|
798
802
|
.string()
|
|
799
803
|
.describe("Code to process FILE_CONTENT (file_content in Elixir). Print summary via console.log/print/echo/IO.puts."),
|
|
800
804
|
timeout: z
|
|
801
|
-
.number()
|
|
805
|
+
.coerce.number()
|
|
802
806
|
.optional()
|
|
803
807
|
.default(30000)
|
|
804
808
|
.describe("Max execution time in ms"),
|
|
@@ -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({
|
|
@@ -1399,7 +1412,7 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1399
1412
|
"Each returns top 5 matching sections with full content. " +
|
|
1400
1413
|
"This is your ONLY chance — put ALL your questions here. No follow-up calls needed.")),
|
|
1401
1414
|
timeout: z
|
|
1402
|
-
.number()
|
|
1415
|
+
.coerce.number()
|
|
1403
1416
|
.optional()
|
|
1404
1417
|
.default(60000)
|
|
1405
1418
|
.describe("Max execution time in ms (default: 60s)"),
|
|
@@ -1862,7 +1875,7 @@ server.registerTool("ctx_insight", {
|
|
|
1862
1875
|
"parallel work patterns, project focus, and actionable insights. " +
|
|
1863
1876
|
"First run installs dependencies (~30s). Subsequent runs open instantly.",
|
|
1864
1877
|
inputSchema: z.object({
|
|
1865
|
-
port: z.number().optional().describe("Port to serve on (default: 4747)"),
|
|
1878
|
+
port: z.coerce.number().optional().describe("Port to serve on (default: 4747)"),
|
|
1866
1879
|
}),
|
|
1867
1880
|
}, async ({ port: userPort }) => {
|
|
1868
1881
|
const port = userPort || 4747;
|
|
@@ -1895,11 +1908,26 @@ server.registerTool("ctx_insight", {
|
|
|
1895
1908
|
const hasNodeModules = existsSync(join(cacheDir, "node_modules"));
|
|
1896
1909
|
if (!hasNodeModules) {
|
|
1897
1910
|
steps.push("Installing dependencies (first run, ~30s)...");
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1911
|
+
try {
|
|
1912
|
+
execSync("npm install --production=false", {
|
|
1913
|
+
cwd: cacheDir,
|
|
1914
|
+
stdio: "pipe",
|
|
1915
|
+
timeout: 300000,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
// Clean up partial install so next run retries fresh
|
|
1920
|
+
try {
|
|
1921
|
+
rmSync(join(cacheDir, "node_modules"), { recursive: true, force: true });
|
|
1922
|
+
}
|
|
1923
|
+
catch { }
|
|
1924
|
+
throw new Error("npm install failed — please retry");
|
|
1925
|
+
}
|
|
1926
|
+
// Sentinel check: verify install completed (cold cache can timeout leaving partial node_modules)
|
|
1927
|
+
if (!existsSync(join(cacheDir, "node_modules", "vite")) || !existsSync(join(cacheDir, "node_modules", "better-sqlite3"))) {
|
|
1928
|
+
rmSync(join(cacheDir, "node_modules"), { recursive: true, force: true });
|
|
1929
|
+
throw new Error("npm install incomplete — please retry");
|
|
1930
|
+
}
|
|
1903
1931
|
steps.push("Dependencies installed.");
|
|
1904
1932
|
}
|
|
1905
1933
|
// Build
|
|
@@ -1907,20 +1935,93 @@ server.registerTool("ctx_insight", {
|
|
|
1907
1935
|
execSync("npx vite build", {
|
|
1908
1936
|
cwd: cacheDir,
|
|
1909
1937
|
stdio: "pipe",
|
|
1910
|
-
timeout:
|
|
1938
|
+
timeout: 60000,
|
|
1911
1939
|
});
|
|
1912
1940
|
steps.push("Build complete.");
|
|
1913
|
-
//
|
|
1941
|
+
// Pre-check: is port already in use? (prevents orphan zombie processes)
|
|
1942
|
+
try {
|
|
1943
|
+
const { request } = await import("node:http");
|
|
1944
|
+
await new Promise((resolve, reject) => {
|
|
1945
|
+
const req = request(`http://127.0.0.1:${port}/api/overview`, { timeout: 2000 }, (res) => {
|
|
1946
|
+
res.resume();
|
|
1947
|
+
resolve(); // port is responding = already running
|
|
1948
|
+
});
|
|
1949
|
+
req.on("error", () => reject()); // port free
|
|
1950
|
+
req.on("timeout", () => { req.destroy(); reject(); });
|
|
1951
|
+
req.end();
|
|
1952
|
+
});
|
|
1953
|
+
// If we get here, port is already responding
|
|
1954
|
+
steps.push("Dashboard already running.");
|
|
1955
|
+
// Open browser anyway
|
|
1956
|
+
const url = `http://localhost:${port}`;
|
|
1957
|
+
const platform = process.platform;
|
|
1958
|
+
try {
|
|
1959
|
+
if (platform === "darwin")
|
|
1960
|
+
execSync(`open "${url}"`, { stdio: "pipe" });
|
|
1961
|
+
else if (platform === "win32")
|
|
1962
|
+
execSync(`start "" "${url}"`, { stdio: "pipe" });
|
|
1963
|
+
else
|
|
1964
|
+
execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null`, { stdio: "pipe" });
|
|
1965
|
+
}
|
|
1966
|
+
catch { /* browser open is best-effort */ }
|
|
1967
|
+
return trackResponse("ctx_insight", {
|
|
1968
|
+
content: [{ type: "text", text: `Dashboard already running at http://localhost:${port}` }],
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
catch {
|
|
1972
|
+
// Port is free, proceed with spawn
|
|
1973
|
+
}
|
|
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.
|
|
1914
1985
|
const { spawn } = await import("node:child_process");
|
|
1915
1986
|
const child = spawn("node", [join(cacheDir, "server.mjs")], {
|
|
1916
1987
|
cwd: cacheDir,
|
|
1917
|
-
env: {
|
|
1988
|
+
env: {
|
|
1989
|
+
...process.env,
|
|
1990
|
+
PORT: String(port),
|
|
1991
|
+
INSIGHT_SESSION_DIR: getSessionDir(),
|
|
1992
|
+
INSIGHT_CONTENT_DIR: join(dirname(getSessionDir()), "content"),
|
|
1993
|
+
INSIGHT_PARENT_PID: String(process.pid),
|
|
1994
|
+
},
|
|
1918
1995
|
detached: true,
|
|
1919
1996
|
stdio: "ignore",
|
|
1920
1997
|
});
|
|
1998
|
+
child.on("error", () => { }); // prevent unhandled error crash
|
|
1921
1999
|
child.unref();
|
|
2000
|
+
_insightChild = child;
|
|
1922
2001
|
// Wait for server to be ready
|
|
1923
2002
|
await new Promise(r => setTimeout(r, 1500));
|
|
2003
|
+
// Verify server is actually running
|
|
2004
|
+
try {
|
|
2005
|
+
const { request } = await import("node:http");
|
|
2006
|
+
await new Promise((resolve, reject) => {
|
|
2007
|
+
const req = request(`http://127.0.0.1:${port}/api/overview`, { timeout: 3000 }, (res) => {
|
|
2008
|
+
resolve();
|
|
2009
|
+
res.resume();
|
|
2010
|
+
});
|
|
2011
|
+
req.on("error", reject);
|
|
2012
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
2013
|
+
req.end();
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
catch {
|
|
2017
|
+
// Server didn't start — likely port in use
|
|
2018
|
+
return trackResponse("ctx_insight", {
|
|
2019
|
+
content: [{
|
|
2020
|
+
type: "text",
|
|
2021
|
+
text: `Port ${port} appears to be in use. Either a previous dashboard is still running, or another service is using this port.\n\nTo fix:\n- Kill the existing process: ${process.platform === "win32" ? `netstat -ano | findstr :${port}` : `lsof -ti:${port} | xargs kill`}\n- Or use a different port: ctx_insight({ port: ${port + 1} })`,
|
|
2022
|
+
}],
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
1924
2025
|
// Open browser (cross-platform)
|
|
1925
2026
|
const url = `http://localhost:${port}`;
|
|
1926
2027
|
const platform = process.platform;
|
|
@@ -1937,7 +2038,7 @@ server.registerTool("ctx_insight", {
|
|
|
1937
2038
|
return trackResponse("ctx_insight", {
|
|
1938
2039
|
content: [{
|
|
1939
2040
|
type: "text",
|
|
1940
|
-
text: steps.map(s => `- ${s}`).join("\n") + `\n\nOpen: ${url}\nPID: ${child.pid} · Stop: kill ${child.pid}`,
|
|
2041
|
+
text: steps.map(s => `- ${s}`).join("\n") + `\n\nOpen: ${url}\nPID: ${child.pid} · Stop: ${process.platform === "win32" ? `taskkill /PID ${child.pid} /F` : `kill ${child.pid}`}`,
|
|
1941
2042
|
}],
|
|
1942
2043
|
});
|
|
1943
2044
|
}
|
|
@@ -1973,6 +2074,13 @@ async function main() {
|
|
|
1973
2074
|
unlinkSync(mcpSentinel);
|
|
1974
2075
|
}
|
|
1975
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
|
+
}
|
|
1976
2084
|
};
|
|
1977
2085
|
const gracefulShutdown = async () => {
|
|
1978
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
|
}
|
package/build/session/db.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { SQLiteBase } from "../db-base.js";
|
|
9
9
|
import type { SessionEvent } from "../types.js";
|
|
10
|
+
import type { ProjectAttribution } from "./project-attribution.js";
|
|
10
11
|
/**
|
|
11
12
|
* Returns the worktree suffix to append to session identifiers.
|
|
12
13
|
* Returns empty string when running in the main working tree.
|
|
@@ -24,6 +25,9 @@ export interface StoredEvent {
|
|
|
24
25
|
category: string;
|
|
25
26
|
priority: number;
|
|
26
27
|
data: string;
|
|
28
|
+
project_dir: string;
|
|
29
|
+
attribution_source: string;
|
|
30
|
+
attribution_confidence: number;
|
|
27
31
|
source_hook: string;
|
|
28
32
|
created_at: string;
|
|
29
33
|
data_hash: string;
|
|
@@ -71,7 +75,7 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
71
75
|
* Eviction: if session exceeds MAX_EVENTS_PER_SESSION, evicts the
|
|
72
76
|
* lowest-priority (then oldest) event.
|
|
73
77
|
*/
|
|
74
|
-
insertEvent(sessionId: string, event: SessionEvent, sourceHook?: string): void;
|
|
78
|
+
insertEvent(sessionId: string, event: SessionEvent, sourceHook?: string, attribution?: Partial<ProjectAttribution>): void;
|
|
75
79
|
/**
|
|
76
80
|
* Retrieve events for a session with optional filtering.
|
|
77
81
|
*/
|
|
@@ -84,8 +88,13 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
84
88
|
* Get the total event count for a session.
|
|
85
89
|
*/
|
|
86
90
|
getEventCount(sessionId: string): number;
|
|
91
|
+
/**
|
|
92
|
+
* Return the most recently attributed project dir for a session.
|
|
93
|
+
*/
|
|
94
|
+
getLatestAttributedProjectDir(sessionId: string): string | null;
|
|
87
95
|
/**
|
|
88
96
|
* Ensure a session metadata entry exists. Idempotent (INSERT OR IGNORE).
|
|
97
|
+
* `projectDir` is the session origin directory, not per-event attribution.
|
|
89
98
|
*/
|
|
90
99
|
ensureSession(sessionId: string, projectDir: string): void;
|
|
91
100
|
/**
|