context-mode 0.9.21 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/hooks/hooks.json +46 -4
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +377 -191
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +53 -0
- package/build/adapters/claude-code/hooks.js +88 -0
- package/build/adapters/claude-code/index.d.ts +50 -0
- package/build/adapters/claude-code/index.js +523 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +21 -0
- package/build/adapters/codex/hooks.js +27 -0
- package/build/adapters/codex/index.d.ts +44 -0
- package/build/adapters/codex/index.js +223 -0
- package/build/adapters/detect.d.ts +26 -0
- package/build/adapters/detect.js +131 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +44 -0
- package/build/adapters/gemini-cli/hooks.js +64 -0
- package/build/adapters/gemini-cli/index.d.ts +57 -0
- package/build/adapters/gemini-cli/index.js +468 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +52 -0
- package/build/adapters/opencode/index.js +386 -0
- package/build/adapters/types.d.ts +218 -0
- package/build/adapters/types.js +13 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
- package/build/adapters/vscode-copilot/hooks.js +76 -0
- package/build/adapters/vscode-copilot/index.d.ts +58 -0
- package/build/adapters/vscode-copilot/index.js +512 -0
- package/build/cli.d.ts +9 -6
- package/build/cli.js +133 -423
- package/build/db-base.d.ts +84 -0
- package/build/db-base.js +128 -0
- package/build/executor.d.ts +6 -7
- package/build/executor.js +111 -51
- package/build/opencode-plugin.d.ts +37 -0
- package/build/opencode-plugin.js +118 -0
- package/build/runtime.js +1 -1
- package/build/server.js +436 -117
- package/build/session/db.d.ts +110 -0
- package/build/session/db.js +285 -0
- package/build/session/extract.d.ts +51 -0
- package/build/session/extract.js +407 -0
- package/build/session/snapshot.d.ts +70 -0
- package/build/session/snapshot.js +309 -0
- package/build/store.d.ts +4 -22
- package/build/store.js +67 -55
- package/build/truncate.d.ts +59 -0
- package/build/truncate.js +157 -0
- package/build/types.d.ts +101 -0
- package/build/types.js +20 -0
- package/configs/claude-code/CLAUDE.md +62 -0
- package/configs/codex/AGENTS.md +58 -0
- package/configs/codex/config.toml +5 -0
- package/configs/gemini-cli/GEMINI.md +58 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +49 -0
- package/configs/opencode/AGENTS.md +58 -0
- package/configs/opencode/opencode.json +10 -0
- package/configs/vscode-copilot/copilot-instructions.md +58 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +8 -0
- package/hooks/core/formatters.mjs +86 -0
- package/hooks/core/routing.mjs +262 -0
- package/hooks/core/stdin.mjs +19 -0
- package/hooks/formatters/claude-code.mjs +57 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftertool.mjs +58 -0
- package/hooks/gemini-cli/beforetool.mjs +25 -0
- package/hooks/gemini-cli/precompress.mjs +51 -0
- package/hooks/gemini-cli/sessionstart.mjs +117 -0
- package/hooks/hooks.json +46 -4
- package/hooks/posttooluse.mjs +53 -0
- package/hooks/precompact.mjs +55 -0
- package/hooks/pretooluse.mjs +23 -266
- package/hooks/routing-block.mjs +19 -6
- package/hooks/session-directive.mjs +353 -0
- package/hooks/session-helpers.mjs +112 -0
- package/hooks/sessionstart.mjs +123 -16
- package/hooks/userpromptsubmit.mjs +58 -0
- package/hooks/vscode-copilot/posttooluse.mjs +58 -0
- package/hooks/vscode-copilot/precompact.mjs +51 -0
- package/hooks/vscode-copilot/pretooluse.mjs +25 -0
- package/hooks/vscode-copilot/sessionstart.mjs +115 -0
- package/package.json +20 -17
- package/skills/context-mode/SKILL.md +49 -49
- package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
- package/skills/{stats → ctx-stats}/SKILL.md +3 -3
- package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
- package/start.mjs +47 -0
- package/hooks/pretooluse.sh +0 -147
- package/server.bundle.mjs +0 -341
package/build/server.js
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, unlinkSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
10
|
import { z } from "zod";
|
|
6
11
|
import { PolyglotExecutor } from "./executor.js";
|
|
7
12
|
import { ContentStore, cleanupStaleDBs } from "./store.js";
|
|
8
13
|
import { readBashPolicies, evaluateCommandDenyOnly, extractShellCommands, readToolDenyPatterns, evaluateFilePath, } from "./security.js";
|
|
9
14
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
10
|
-
const VERSION = "0.
|
|
15
|
+
const VERSION = "1.0.0";
|
|
16
|
+
// Prevent silent server death from unhandled async errors
|
|
17
|
+
process.on("unhandledRejection", (err) => {
|
|
18
|
+
process.stderr.write(`[context-mode] unhandledRejection: ${err}\n`);
|
|
19
|
+
});
|
|
20
|
+
process.on("uncaughtException", (err) => {
|
|
21
|
+
process.stderr.write(`[context-mode] uncaughtException: ${err?.message ?? err}\n`);
|
|
22
|
+
});
|
|
11
23
|
const runtimes = detectRuntimes();
|
|
12
24
|
const available = getAvailableLanguages(runtimes);
|
|
13
25
|
const server = new McpServer({
|
|
@@ -20,9 +32,35 @@ const executor = new PolyglotExecutor({
|
|
|
20
32
|
});
|
|
21
33
|
// Lazy singleton — no DB overhead unless index/search is used
|
|
22
34
|
let _store = null;
|
|
35
|
+
/**
|
|
36
|
+
* Auto-index session events files written by SessionStart hook.
|
|
37
|
+
* Scans ~/.claude/context-mode/sessions/ for *-events.md files.
|
|
38
|
+
* CLAUDE_PROJECT_DIR is NOT available to MCP servers — only to hooks —
|
|
39
|
+
* so we glob-scan instead of computing a specific hash.
|
|
40
|
+
* Files are consumed (deleted) after indexing to prevent double-indexing.
|
|
41
|
+
* Called on every getStore() — readdirSync is sub-millisecond when no files match.
|
|
42
|
+
*/
|
|
43
|
+
function maybeIndexSessionEvents(store) {
|
|
44
|
+
try {
|
|
45
|
+
const sessionsDir = join(homedir(), ".claude", "context-mode", "sessions");
|
|
46
|
+
if (!existsSync(sessionsDir))
|
|
47
|
+
return;
|
|
48
|
+
const files = readdirSync(sessionsDir).filter(f => f.endsWith("-events.md"));
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const filePath = join(sessionsDir, file);
|
|
51
|
+
try {
|
|
52
|
+
store.index({ path: filePath, source: "session-events" });
|
|
53
|
+
unlinkSync(filePath);
|
|
54
|
+
}
|
|
55
|
+
catch { /* best-effort per file */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { /* best-effort — session continuity never blocks tools */ }
|
|
59
|
+
}
|
|
23
60
|
function getStore() {
|
|
24
61
|
if (!_store)
|
|
25
62
|
_store = new ContentStore();
|
|
63
|
+
maybeIndexSessionEvents(_store);
|
|
26
64
|
return _store;
|
|
27
65
|
}
|
|
28
66
|
// ─────────────────────────────────────────────────────────
|
|
@@ -232,7 +270,7 @@ export function extractSnippet(content, query, maxLen = 1500, highlighted) {
|
|
|
232
270
|
// ─────────────────────────────────────────────────────────
|
|
233
271
|
// Tool: execute
|
|
234
272
|
// ─────────────────────────────────────────────────────────
|
|
235
|
-
server.registerTool("
|
|
273
|
+
server.registerTool("ctx_execute", {
|
|
236
274
|
title: "Execute Code",
|
|
237
275
|
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.`,
|
|
238
276
|
inputSchema: z.object({
|
|
@@ -259,6 +297,11 @@ server.registerTool("execute", {
|
|
|
259
297
|
.optional()
|
|
260
298
|
.default(30000)
|
|
261
299
|
.describe("Max execution time in ms"),
|
|
300
|
+
background: z
|
|
301
|
+
.boolean()
|
|
302
|
+
.optional()
|
|
303
|
+
.default(false)
|
|
304
|
+
.describe("Keep process running after timeout (for servers/daemons). Returns partial output without killing the process. IMPORTANT: Do NOT add setTimeout/self-close timers in background scripts — the process must stay alive until the timeout detaches it. For server+fetch patterns, prefer putting both server and fetch in ONE ctx_execute call instead of using background."),
|
|
262
305
|
intent: z
|
|
263
306
|
.string()
|
|
264
307
|
.optional()
|
|
@@ -267,7 +310,7 @@ server.registerTool("execute", {
|
|
|
267
310
|
"Use search(queries: [...]) to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
|
|
268
311
|
"\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
|
|
269
312
|
}),
|
|
270
|
-
}, async ({ language, code, timeout, intent }) => {
|
|
313
|
+
}, async ({ language, code, timeout, background, intent }) => {
|
|
271
314
|
// Security: deny-only firewall
|
|
272
315
|
if (language === "shell") {
|
|
273
316
|
const denied = checkDenyPolicy(code, "execute");
|
|
@@ -280,22 +323,61 @@ server.registerTool("execute", {
|
|
|
280
323
|
return denied;
|
|
281
324
|
}
|
|
282
325
|
try {
|
|
283
|
-
// For JS/TS: wrap in async IIFE with fetch
|
|
326
|
+
// For JS/TS: wrap in async IIFE with fetch + http/https interceptors to track network bytes
|
|
284
327
|
let instrumentedCode = code;
|
|
285
328
|
if (language === "javascript" || language === "typescript") {
|
|
329
|
+
// Wrap user code in a closure that shadows CJS require with http/https interceptor.
|
|
330
|
+
// globalThis.require does NOT work because CJS require is module-scoped, not global.
|
|
331
|
+
// The closure approach (function(__cm_req){ var require=...; })(require) correctly
|
|
332
|
+
// shadows the CJS require for all code inside, including __cm_main().
|
|
286
333
|
instrumentedCode = `
|
|
287
|
-
let __cm_net=0;
|
|
334
|
+
let __cm_net=0;
|
|
335
|
+
// Report network bytes on process exit — works with both promise and callback patterns.
|
|
336
|
+
// process.on('exit') fires after all I/O completes, unlike .finally() which fires
|
|
337
|
+
// when __cm_main() resolves (immediately for callback-based http.get without await).
|
|
338
|
+
process.on('exit',()=>{if(__cm_net>0)try{process.stderr.write('__CM_NET__:'+__cm_net+'\\n')}catch{}});
|
|
339
|
+
;(function(__cm_req){
|
|
340
|
+
// Intercept globalThis.fetch
|
|
341
|
+
const __cm_f=globalThis.fetch;
|
|
288
342
|
globalThis.fetch=async(...a)=>{const r=await __cm_f(...a);
|
|
289
343
|
try{const cl=r.clone();const b=await cl.arrayBuffer();__cm_net+=b.byteLength}catch{}
|
|
290
344
|
return r};
|
|
345
|
+
// Shadow CJS require with http/https network tracking.
|
|
346
|
+
const __cm_hc=new Map();
|
|
347
|
+
const __cm_hm=new Set(['http','https','node:http','node:https']);
|
|
348
|
+
function __cm_wf(m,origFn){return function(...a){
|
|
349
|
+
const li=a.length-1;
|
|
350
|
+
if(li>=0&&typeof a[li]==='function'){const oc=a[li];a[li]=function(res){
|
|
351
|
+
res.on('data',function(c){__cm_net+=c.length});oc(res);};}
|
|
352
|
+
const req=origFn.apply(m,a);
|
|
353
|
+
const oOn=req.on.bind(req);
|
|
354
|
+
req.on=function(ev,cb,...r){
|
|
355
|
+
if(ev==='response'){return oOn(ev,function(res){
|
|
356
|
+
res.on('data',function(c){__cm_net+=c.length});cb(res);
|
|
357
|
+
},...r);}
|
|
358
|
+
return oOn(ev,cb,...r);
|
|
359
|
+
};
|
|
360
|
+
return req;
|
|
361
|
+
}}
|
|
362
|
+
var require=__cm_req?function(id){
|
|
363
|
+
const m=__cm_req(id);
|
|
364
|
+
if(!__cm_hm.has(id))return m;
|
|
365
|
+
const k=id.replace('node:','');
|
|
366
|
+
if(__cm_hc.has(k))return __cm_hc.get(k);
|
|
367
|
+
const w=Object.create(m);
|
|
368
|
+
if(typeof m.get==='function')w.get=__cm_wf(m,m.get);
|
|
369
|
+
if(typeof m.request==='function')w.request=__cm_wf(m,m.request);
|
|
370
|
+
__cm_hc.set(k,w);return w;
|
|
371
|
+
}:__cm_req;
|
|
372
|
+
if(__cm_req){if(__cm_req.resolve)require.resolve=__cm_req.resolve;
|
|
373
|
+
if(__cm_req.cache)require.cache=__cm_req.cache;}
|
|
291
374
|
async function __cm_main(){
|
|
292
375
|
${code}
|
|
293
376
|
}
|
|
294
|
-
__cm_main().catch(e=>{console.error(e);process.exitCode=1})
|
|
295
|
-
|
|
296
|
-
});`;
|
|
377
|
+
__cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nsetInterval(()=>{},2147483647);' : ''}
|
|
378
|
+
})(typeof require!=='undefined'?require:null);`;
|
|
297
379
|
}
|
|
298
|
-
const result = await executor.execute({ language, code: instrumentedCode, timeout });
|
|
380
|
+
const result = await executor.execute({ language, code: instrumentedCode, timeout, background });
|
|
299
381
|
// Parse sandbox network metrics from stderr
|
|
300
382
|
const netMatch = result.stderr?.match(/__CM_NET__:(\d+)/);
|
|
301
383
|
if (netMatch) {
|
|
@@ -304,11 +386,34 @@ if(__cm_net>0)process.stderr.write('__CM_NET__:'+__cm_net+'\\n');
|
|
|
304
386
|
result.stderr = result.stderr.replace(/\n?__CM_NET__:\d+\n?/g, "");
|
|
305
387
|
}
|
|
306
388
|
if (result.timedOut) {
|
|
307
|
-
|
|
389
|
+
const partialOutput = result.stdout?.trim();
|
|
390
|
+
if (result.backgrounded && partialOutput) {
|
|
391
|
+
// Background mode: process is still running, return partial output as success
|
|
392
|
+
return trackResponse("ctx_execute", {
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: `${partialOutput}\n\n_(process backgrounded after ${timeout}ms — still running)_`,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (partialOutput) {
|
|
402
|
+
// Timeout with partial output — return as success with note
|
|
403
|
+
return trackResponse("ctx_execute", {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: `${partialOutput}\n\n_(timed out after ${timeout}ms — partial output shown above)_`,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return trackResponse("ctx_execute", {
|
|
308
413
|
content: [
|
|
309
414
|
{
|
|
310
415
|
type: "text",
|
|
311
|
-
text: `Execution timed out after ${timeout}ms\n\
|
|
416
|
+
text: `Execution timed out after ${timeout}ms\n\nstderr:\n${result.stderr}`,
|
|
312
417
|
},
|
|
313
418
|
],
|
|
314
419
|
isError: true,
|
|
@@ -318,14 +423,14 @@ if(__cm_net>0)process.stderr.write('__CM_NET__:'+__cm_net+'\\n');
|
|
|
318
423
|
const output = `Exit code: ${result.exitCode}\n\nstdout:\n${result.stdout}\n\nstderr:\n${result.stderr}`;
|
|
319
424
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
320
425
|
trackIndexed(Buffer.byteLength(output));
|
|
321
|
-
return trackResponse("
|
|
426
|
+
return trackResponse("ctx_execute", {
|
|
322
427
|
content: [
|
|
323
428
|
{ type: "text", text: intentSearch(output, intent, `execute:${language}:error`) },
|
|
324
429
|
],
|
|
325
430
|
isError: true,
|
|
326
431
|
});
|
|
327
432
|
}
|
|
328
|
-
return trackResponse("
|
|
433
|
+
return trackResponse("ctx_execute", {
|
|
329
434
|
content: [
|
|
330
435
|
{ type: "text", text: output },
|
|
331
436
|
],
|
|
@@ -336,13 +441,13 @@ if(__cm_net>0)process.stderr.write('__CM_NET__:'+__cm_net+'\\n');
|
|
|
336
441
|
// Intent-driven search: if intent provided and output is large enough
|
|
337
442
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
338
443
|
trackIndexed(Buffer.byteLength(stdout));
|
|
339
|
-
return trackResponse("
|
|
444
|
+
return trackResponse("ctx_execute", {
|
|
340
445
|
content: [
|
|
341
446
|
{ type: "text", text: intentSearch(stdout, intent, `execute:${language}`) },
|
|
342
447
|
],
|
|
343
448
|
});
|
|
344
449
|
}
|
|
345
|
-
return trackResponse("
|
|
450
|
+
return trackResponse("ctx_execute", {
|
|
346
451
|
content: [
|
|
347
452
|
{ type: "text", text: stdout },
|
|
348
453
|
],
|
|
@@ -350,7 +455,7 @@ if(__cm_net>0)process.stderr.write('__CM_NET__:'+__cm_net+'\\n');
|
|
|
350
455
|
}
|
|
351
456
|
catch (err) {
|
|
352
457
|
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
-
return trackResponse("
|
|
458
|
+
return trackResponse("ctx_execute", {
|
|
354
459
|
content: [
|
|
355
460
|
{ type: "text", text: `Runtime error: ${message}` },
|
|
356
461
|
],
|
|
@@ -422,7 +527,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
422
527
|
// ─────────────────────────────────────────────────────────
|
|
423
528
|
// Tool: execute_file
|
|
424
529
|
// ─────────────────────────────────────────────────────────
|
|
425
|
-
server.registerTool("
|
|
530
|
+
server.registerTool("ctx_execute_file", {
|
|
426
531
|
title: "Execute File Processing",
|
|
427
532
|
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.",
|
|
428
533
|
inputSchema: z.object({
|
|
@@ -482,7 +587,7 @@ server.registerTool("execute_file", {
|
|
|
482
587
|
timeout,
|
|
483
588
|
});
|
|
484
589
|
if (result.timedOut) {
|
|
485
|
-
return trackResponse("
|
|
590
|
+
return trackResponse("ctx_execute_file", {
|
|
486
591
|
content: [
|
|
487
592
|
{
|
|
488
593
|
type: "text",
|
|
@@ -496,14 +601,14 @@ server.registerTool("execute_file", {
|
|
|
496
601
|
const output = `Error processing ${path} (exit ${result.exitCode}):\n${result.stderr || result.stdout}`;
|
|
497
602
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
498
603
|
trackIndexed(Buffer.byteLength(output));
|
|
499
|
-
return trackResponse("
|
|
604
|
+
return trackResponse("ctx_execute_file", {
|
|
500
605
|
content: [
|
|
501
606
|
{ type: "text", text: intentSearch(output, intent, `file:${path}:error`) },
|
|
502
607
|
],
|
|
503
608
|
isError: true,
|
|
504
609
|
});
|
|
505
610
|
}
|
|
506
|
-
return trackResponse("
|
|
611
|
+
return trackResponse("ctx_execute_file", {
|
|
507
612
|
content: [
|
|
508
613
|
{ type: "text", text: output },
|
|
509
614
|
],
|
|
@@ -513,13 +618,13 @@ server.registerTool("execute_file", {
|
|
|
513
618
|
const stdout = result.stdout || "(no output)";
|
|
514
619
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
515
620
|
trackIndexed(Buffer.byteLength(stdout));
|
|
516
|
-
return trackResponse("
|
|
621
|
+
return trackResponse("ctx_execute_file", {
|
|
517
622
|
content: [
|
|
518
623
|
{ type: "text", text: intentSearch(stdout, intent, `file:${path}`) },
|
|
519
624
|
],
|
|
520
625
|
});
|
|
521
626
|
}
|
|
522
|
-
return trackResponse("
|
|
627
|
+
return trackResponse("ctx_execute_file", {
|
|
523
628
|
content: [
|
|
524
629
|
{ type: "text", text: stdout },
|
|
525
630
|
],
|
|
@@ -527,7 +632,7 @@ server.registerTool("execute_file", {
|
|
|
527
632
|
}
|
|
528
633
|
catch (err) {
|
|
529
634
|
const message = err instanceof Error ? err.message : String(err);
|
|
530
|
-
return trackResponse("
|
|
635
|
+
return trackResponse("ctx_execute_file", {
|
|
531
636
|
content: [
|
|
532
637
|
{ type: "text", text: `Runtime error: ${message}` },
|
|
533
638
|
],
|
|
@@ -538,7 +643,7 @@ server.registerTool("execute_file", {
|
|
|
538
643
|
// ─────────────────────────────────────────────────────────
|
|
539
644
|
// Tool: index
|
|
540
645
|
// ─────────────────────────────────────────────────────────
|
|
541
|
-
server.registerTool("
|
|
646
|
+
server.registerTool("ctx_index", {
|
|
542
647
|
title: "Index Content",
|
|
543
648
|
description: "Index documentation or knowledge content into a searchable BM25 knowledge base. " +
|
|
544
649
|
"Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. " +
|
|
@@ -568,7 +673,7 @@ server.registerTool("index", {
|
|
|
568
673
|
}),
|
|
569
674
|
}, async ({ content, path, source }) => {
|
|
570
675
|
if (!content && !path) {
|
|
571
|
-
return trackResponse("
|
|
676
|
+
return trackResponse("ctx_index", {
|
|
572
677
|
content: [
|
|
573
678
|
{
|
|
574
679
|
type: "text",
|
|
@@ -591,7 +696,7 @@ server.registerTool("index", {
|
|
|
591
696
|
}
|
|
592
697
|
const store = getStore();
|
|
593
698
|
const result = store.index({ content, path, source });
|
|
594
|
-
return trackResponse("
|
|
699
|
+
return trackResponse("ctx_index", {
|
|
595
700
|
content: [
|
|
596
701
|
{
|
|
597
702
|
type: "text",
|
|
@@ -602,7 +707,7 @@ server.registerTool("index", {
|
|
|
602
707
|
}
|
|
603
708
|
catch (err) {
|
|
604
709
|
const message = err instanceof Error ? err.message : String(err);
|
|
605
|
-
return trackResponse("
|
|
710
|
+
return trackResponse("ctx_index", {
|
|
606
711
|
content: [
|
|
607
712
|
{ type: "text", text: `Index error: ${message}` },
|
|
608
713
|
],
|
|
@@ -619,7 +724,7 @@ let searchWindowStart = Date.now();
|
|
|
619
724
|
const SEARCH_WINDOW_MS = 60_000;
|
|
620
725
|
const SEARCH_MAX_RESULTS_AFTER = 3; // after 3 calls: 1 result per query
|
|
621
726
|
const SEARCH_BLOCK_AFTER = 8; // after 8 calls: refuse, demand batching
|
|
622
|
-
server.registerTool("
|
|
727
|
+
server.registerTool("ctx_search", {
|
|
623
728
|
title: "Search Indexed Content",
|
|
624
729
|
description: "Search indexed content. Pass ALL search questions as queries array in ONE call.\n\n" +
|
|
625
730
|
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.",
|
|
@@ -651,7 +756,7 @@ server.registerTool("search", {
|
|
|
651
756
|
queryList.push(raw.query);
|
|
652
757
|
}
|
|
653
758
|
if (queryList.length === 0) {
|
|
654
|
-
return trackResponse("
|
|
759
|
+
return trackResponse("ctx_search", {
|
|
655
760
|
content: [{ type: "text", text: "Error: provide query or queries." }],
|
|
656
761
|
isError: true,
|
|
657
762
|
});
|
|
@@ -666,7 +771,7 @@ server.registerTool("search", {
|
|
|
666
771
|
searchCallCount++;
|
|
667
772
|
// After SEARCH_BLOCK_AFTER calls: refuse
|
|
668
773
|
if (searchCallCount > SEARCH_BLOCK_AFTER) {
|
|
669
|
-
return trackResponse("
|
|
774
|
+
return trackResponse("ctx_search", {
|
|
670
775
|
content: [{
|
|
671
776
|
type: "text",
|
|
672
777
|
text: `BLOCKED: ${searchCallCount} search calls in ${Math.round((now - searchWindowStart) / 1000)}s. ` +
|
|
@@ -716,17 +821,17 @@ server.registerTool("search", {
|
|
|
716
821
|
const sourceList = sources.length > 0
|
|
717
822
|
? `\nIndexed sources: ${sources.map((s) => `"${s.label}" (${s.chunkCount} sections)`).join(", ")}`
|
|
718
823
|
: "";
|
|
719
|
-
return trackResponse("
|
|
824
|
+
return trackResponse("ctx_search", {
|
|
720
825
|
content: [{ type: "text", text: `No results found.${sourceList}` }],
|
|
721
826
|
});
|
|
722
827
|
}
|
|
723
|
-
return trackResponse("
|
|
828
|
+
return trackResponse("ctx_search", {
|
|
724
829
|
content: [{ type: "text", text: output }],
|
|
725
830
|
});
|
|
726
831
|
}
|
|
727
832
|
catch (err) {
|
|
728
833
|
const message = err instanceof Error ? err.message : String(err);
|
|
729
|
-
return trackResponse("
|
|
834
|
+
return trackResponse("ctx_search", {
|
|
730
835
|
content: [{ type: "text", text: `Search error: ${message}` }],
|
|
731
836
|
isError: true,
|
|
732
837
|
});
|
|
@@ -757,13 +862,23 @@ function resolveGfmPluginPath() {
|
|
|
757
862
|
// Subprocess code that fetches a URL, detects Content-Type, and outputs a
|
|
758
863
|
// __CM_CT__:<type> marker on the first line so the handler can route to the
|
|
759
864
|
// appropriate indexing strategy. HTML is converted to markdown via Turndown.
|
|
760
|
-
function buildFetchCode(url) {
|
|
865
|
+
function buildFetchCode(url, outputPath) {
|
|
761
866
|
const turndownPath = JSON.stringify(resolveTurndownPath());
|
|
762
867
|
const gfmPath = JSON.stringify(resolveGfmPluginPath());
|
|
868
|
+
const escapedOutputPath = JSON.stringify(outputPath);
|
|
763
869
|
return `
|
|
764
870
|
const TurndownService = require(${turndownPath});
|
|
765
871
|
const { gfm } = require(${gfmPath});
|
|
872
|
+
const fs = require('fs');
|
|
766
873
|
const url = ${JSON.stringify(url)};
|
|
874
|
+
const outputPath = ${escapedOutputPath};
|
|
875
|
+
|
|
876
|
+
function emit(ct, content) {
|
|
877
|
+
// Write content to file to bypass executor stdout truncation (100KB limit).
|
|
878
|
+
// Only the content-type marker goes to stdout.
|
|
879
|
+
fs.writeFileSync(outputPath, content);
|
|
880
|
+
console.log('__CM_CT__:' + ct);
|
|
881
|
+
}
|
|
767
882
|
|
|
768
883
|
async function main() {
|
|
769
884
|
const resp = await fetch(url);
|
|
@@ -775,12 +890,9 @@ async function main() {
|
|
|
775
890
|
const text = await resp.text();
|
|
776
891
|
try {
|
|
777
892
|
const pretty = JSON.stringify(JSON.parse(text), null, 2);
|
|
778
|
-
|
|
779
|
-
console.log(pretty);
|
|
893
|
+
emit('json', pretty);
|
|
780
894
|
} catch {
|
|
781
|
-
|
|
782
|
-
console.log('__CM_CT__:text');
|
|
783
|
-
console.log(text);
|
|
895
|
+
emit('text', text);
|
|
784
896
|
}
|
|
785
897
|
return;
|
|
786
898
|
}
|
|
@@ -791,20 +903,18 @@ async function main() {
|
|
|
791
903
|
const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
792
904
|
td.use(gfm);
|
|
793
905
|
td.remove(['script', 'style', 'nav', 'header', 'footer', 'noscript']);
|
|
794
|
-
|
|
795
|
-
console.log(td.turndown(html));
|
|
906
|
+
emit('html', td.turndown(html));
|
|
796
907
|
return;
|
|
797
908
|
}
|
|
798
909
|
|
|
799
910
|
// --- Everything else: plain text, CSV, XML, etc. ---
|
|
800
911
|
const text = await resp.text();
|
|
801
|
-
|
|
802
|
-
console.log(text);
|
|
912
|
+
emit('text', text);
|
|
803
913
|
}
|
|
804
914
|
main();
|
|
805
915
|
`;
|
|
806
916
|
}
|
|
807
|
-
server.registerTool("
|
|
917
|
+
server.registerTool("ctx_fetch_and_index", {
|
|
808
918
|
title: "Fetch & Index URL",
|
|
809
919
|
description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
|
|
810
920
|
"and returns a ~3KB preview. Full content stays in sandbox — use search() for deeper lookups.\n\n" +
|
|
@@ -818,16 +928,18 @@ server.registerTool("fetch_and_index", {
|
|
|
818
928
|
.describe("Label for the indexed content (e.g., 'React useEffect docs', 'Supabase Auth API')"),
|
|
819
929
|
}),
|
|
820
930
|
}, async ({ url, source }) => {
|
|
931
|
+
// Generate a unique temp file path for the subprocess to write fetched content.
|
|
932
|
+
// This bypasses the executor's 100KB stdout truncation — content goes file→handler directly.
|
|
933
|
+
const outputPath = join(tmpdir(), `ctx-fetch-${Date.now()}-${Math.random().toString(36).slice(2)}.dat`);
|
|
821
934
|
try {
|
|
822
|
-
|
|
823
|
-
const fetchCode = buildFetchCode(url);
|
|
935
|
+
const fetchCode = buildFetchCode(url, outputPath);
|
|
824
936
|
const result = await executor.execute({
|
|
825
937
|
language: "javascript",
|
|
826
938
|
code: fetchCode,
|
|
827
939
|
timeout: 30_000,
|
|
828
940
|
});
|
|
829
941
|
if (result.exitCode !== 0) {
|
|
830
|
-
return trackResponse("
|
|
942
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
831
943
|
content: [
|
|
832
944
|
{
|
|
833
945
|
type: "text",
|
|
@@ -837,15 +949,27 @@ server.registerTool("fetch_and_index", {
|
|
|
837
949
|
isError: true,
|
|
838
950
|
});
|
|
839
951
|
}
|
|
840
|
-
// Parse content-type marker from
|
|
952
|
+
// Parse content-type marker from stdout (content is in the temp file)
|
|
841
953
|
const store = getStore();
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
954
|
+
const header = (result.stdout || "").trim();
|
|
955
|
+
// Read full content from temp file (bypasses smartTruncate)
|
|
956
|
+
let markdown;
|
|
957
|
+
try {
|
|
958
|
+
markdown = readFileSync(outputPath, "utf-8").trim();
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
962
|
+
content: [
|
|
963
|
+
{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: `Fetched ${url} but could not read subprocess output`,
|
|
966
|
+
},
|
|
967
|
+
],
|
|
968
|
+
isError: true,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
847
971
|
if (markdown.length === 0) {
|
|
848
|
-
return trackResponse("
|
|
972
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
849
973
|
content: [
|
|
850
974
|
{
|
|
851
975
|
type: "text",
|
|
@@ -882,24 +1006,31 @@ server.registerTool("fetch_and_index", {
|
|
|
882
1006
|
"",
|
|
883
1007
|
preview,
|
|
884
1008
|
].join("\n");
|
|
885
|
-
return trackResponse("
|
|
1009
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
886
1010
|
content: [{ type: "text", text }],
|
|
887
1011
|
});
|
|
888
1012
|
}
|
|
889
1013
|
catch (err) {
|
|
890
1014
|
const message = err instanceof Error ? err.message : String(err);
|
|
891
|
-
return trackResponse("
|
|
1015
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
892
1016
|
content: [
|
|
893
1017
|
{ type: "text", text: `Fetch error: ${message}` },
|
|
894
1018
|
],
|
|
895
1019
|
isError: true,
|
|
896
1020
|
});
|
|
897
1021
|
}
|
|
1022
|
+
finally {
|
|
1023
|
+
// Clean up temp file
|
|
1024
|
+
try {
|
|
1025
|
+
rmSync(outputPath);
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* already gone */ }
|
|
1028
|
+
}
|
|
898
1029
|
});
|
|
899
1030
|
// ─────────────────────────────────────────────────────────
|
|
900
1031
|
// Tool: batch_execute
|
|
901
1032
|
// ─────────────────────────────────────────────────────────
|
|
902
|
-
server.registerTool("
|
|
1033
|
+
server.registerTool("ctx_batch_execute", {
|
|
903
1034
|
title: "Batch Execute & Search",
|
|
904
1035
|
description: "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. " +
|
|
905
1036
|
"Returns search results directly — no follow-up calls needed.\n\n" +
|
|
@@ -938,32 +1069,52 @@ server.registerTool("batch_execute", {
|
|
|
938
1069
|
return denied;
|
|
939
1070
|
}
|
|
940
1071
|
try {
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1072
|
+
// Execute each command individually so every command gets its own
|
|
1073
|
+
// smartTruncate budget (~100KB). Previously, all commands were
|
|
1074
|
+
// concatenated into a single script where smartTruncate (60% head +
|
|
1075
|
+
// 40% tail) could silently drop middle commands. (Issue #61)
|
|
1076
|
+
const perCommandOutputs = [];
|
|
1077
|
+
const startTime = Date.now();
|
|
1078
|
+
let timedOut = false;
|
|
1079
|
+
for (const cmd of commands) {
|
|
1080
|
+
const elapsed = Date.now() - startTime;
|
|
1081
|
+
const remaining = timeout - elapsed;
|
|
1082
|
+
if (remaining <= 0) {
|
|
1083
|
+
perCommandOutputs.push(`# ${cmd.label}\n\n(skipped — batch timeout exceeded)\n`);
|
|
1084
|
+
timedOut = true;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const result = await executor.execute({
|
|
1088
|
+
language: "shell",
|
|
1089
|
+
code: `${cmd.command} 2>&1`,
|
|
1090
|
+
timeout: remaining,
|
|
1091
|
+
});
|
|
1092
|
+
const output = result.stdout || "(no output)";
|
|
1093
|
+
perCommandOutputs.push(`# ${cmd.label}\n\n${output}\n`);
|
|
1094
|
+
if (result.timedOut) {
|
|
1095
|
+
timedOut = true;
|
|
1096
|
+
// Mark remaining commands as skipped
|
|
1097
|
+
const idx = commands.indexOf(cmd);
|
|
1098
|
+
for (let i = idx + 1; i < commands.length; i++) {
|
|
1099
|
+
perCommandOutputs.push(`# ${commands[i].label}\n\n(skipped — batch timeout exceeded)\n`);
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const stdout = perCommandOutputs.join("\n");
|
|
1105
|
+
const totalBytes = Buffer.byteLength(stdout);
|
|
1106
|
+
const totalLines = stdout.split("\n").length;
|
|
1107
|
+
if (timedOut && perCommandOutputs.length === 0) {
|
|
1108
|
+
return trackResponse("ctx_batch_execute", {
|
|
955
1109
|
content: [
|
|
956
1110
|
{
|
|
957
1111
|
type: "text",
|
|
958
|
-
text: `Batch timed out after ${timeout}ms.
|
|
1112
|
+
text: `Batch timed out after ${timeout}ms. No output captured.`,
|
|
959
1113
|
},
|
|
960
1114
|
],
|
|
961
1115
|
isError: true,
|
|
962
1116
|
});
|
|
963
1117
|
}
|
|
964
|
-
const stdout = result.stdout || "(no output)";
|
|
965
|
-
const totalBytes = Buffer.byteLength(stdout);
|
|
966
|
-
const totalLines = stdout.split("\n").length;
|
|
967
1118
|
// Track indexed bytes (raw data that stays in sandbox)
|
|
968
1119
|
trackIndexed(totalBytes);
|
|
969
1120
|
// Index into knowledge base — markdown heading chunking splits by # labels
|
|
@@ -994,16 +1145,23 @@ server.registerTool("batch_execute", {
|
|
|
994
1145
|
}
|
|
995
1146
|
// Tier 1: scoped search with fallback (porter → trigram → fuzzy)
|
|
996
1147
|
let results = store.searchWithFallback(query, 3, source);
|
|
997
|
-
|
|
1148
|
+
let crossSource = false;
|
|
1149
|
+
// Tier 2: global fallback (no source filter) — warn about cross-source (Issue #61)
|
|
998
1150
|
if (results.length === 0) {
|
|
999
1151
|
results = store.searchWithFallback(query, 3);
|
|
1152
|
+
crossSource = results.length > 0;
|
|
1000
1153
|
}
|
|
1001
1154
|
queryResults.push(`## ${query}`);
|
|
1155
|
+
if (crossSource) {
|
|
1156
|
+
queryResults.push(`> **Note:** No results in current batch output. Showing results from previously indexed content.`);
|
|
1157
|
+
}
|
|
1002
1158
|
queryResults.push("");
|
|
1003
1159
|
if (results.length > 0) {
|
|
1004
1160
|
for (const r of results) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1161
|
+
// Use larger snippet (3KB) for batch_execute to reduce tiny-fragment issue (Issue #61)
|
|
1162
|
+
const snippet = extractSnippet(r.content, query, 3000, r.highlighted);
|
|
1163
|
+
const sourceTag = crossSource ? ` _(source: ${r.source})_` : "";
|
|
1164
|
+
queryResults.push(`### ${r.title}${sourceTag}`);
|
|
1007
1165
|
queryResults.push(snippet);
|
|
1008
1166
|
queryResults.push("");
|
|
1009
1167
|
outputSize += snippet.length + r.title.length;
|
|
@@ -1029,13 +1187,13 @@ server.registerTool("batch_execute", {
|
|
|
1029
1187
|
? `\nSearchable terms for follow-up: ${distinctiveTerms.join(", ")}`
|
|
1030
1188
|
: "",
|
|
1031
1189
|
].join("\n");
|
|
1032
|
-
return trackResponse("
|
|
1190
|
+
return trackResponse("ctx_batch_execute", {
|
|
1033
1191
|
content: [{ type: "text", text: output }],
|
|
1034
1192
|
});
|
|
1035
1193
|
}
|
|
1036
1194
|
catch (err) {
|
|
1037
1195
|
const message = err instanceof Error ? err.message : String(err);
|
|
1038
|
-
return trackResponse("
|
|
1196
|
+
return trackResponse("ctx_batch_execute", {
|
|
1039
1197
|
content: [
|
|
1040
1198
|
{
|
|
1041
1199
|
type: "text",
|
|
@@ -1049,7 +1207,7 @@ server.registerTool("batch_execute", {
|
|
|
1049
1207
|
// ─────────────────────────────────────────────────────────
|
|
1050
1208
|
// Tool: stats
|
|
1051
1209
|
// ─────────────────────────────────────────────────────────
|
|
1052
|
-
server.registerTool("
|
|
1210
|
+
server.registerTool("ctx_stats", {
|
|
1053
1211
|
title: "Session Statistics",
|
|
1054
1212
|
description: "Returns context consumption statistics for the current session. " +
|
|
1055
1213
|
"Shows total bytes returned to context, breakdown by tool, call counts, " +
|
|
@@ -1072,48 +1230,208 @@ server.registerTool("stats", {
|
|
|
1072
1230
|
return `${(b / 1024 / 1024).toFixed(1)}MB`;
|
|
1073
1231
|
return `${(b / 1024).toFixed(1)}KB`;
|
|
1074
1232
|
};
|
|
1075
|
-
// ──
|
|
1233
|
+
// ── Header ──
|
|
1076
1234
|
const lines = [
|
|
1077
|
-
`## context-mode
|
|
1078
|
-
"",
|
|
1079
|
-
`| Metric | Value |`,
|
|
1080
|
-
`|--------|------:|`,
|
|
1081
|
-
`| Session | ${uptimeMin} min |`,
|
|
1082
|
-
`| Tool calls | ${totalCalls} |`,
|
|
1083
|
-
`| Total data processed | **${kb(totalProcessed)}** |`,
|
|
1084
|
-
`| Kept in sandbox | **${kb(keptOut)}** |`,
|
|
1085
|
-
`| Entered context | ${kb(totalBytesReturned)} |`,
|
|
1086
|
-
`| Tokens consumed | ~${Math.round(totalBytesReturned / 4).toLocaleString()} |`,
|
|
1087
|
-
`| **Context savings** | **${savingsRatio.toFixed(1)}x (${reductionPct}% reduction)** |`,
|
|
1235
|
+
`## context-mode — Session Report (${uptimeMin} min)`,
|
|
1088
1236
|
];
|
|
1089
|
-
// ──
|
|
1090
|
-
|
|
1091
|
-
...Object.keys(sessionStats.calls),
|
|
1092
|
-
...Object.keys(sessionStats.bytesReturned),
|
|
1093
|
-
]);
|
|
1094
|
-
if (toolNames.size > 0) {
|
|
1095
|
-
lines.push("", `| Tool | Calls | Context | Tokens |`, `|------|------:|--------:|-------:|`);
|
|
1096
|
-
for (const tool of Array.from(toolNames).sort()) {
|
|
1097
|
-
const calls = sessionStats.calls[tool] || 0;
|
|
1098
|
-
const bytes = sessionStats.bytesReturned[tool] || 0;
|
|
1099
|
-
const tokens = Math.round(bytes / 4);
|
|
1100
|
-
lines.push(`| ${tool} | ${calls} | ${kb(bytes)} | ~${tokens.toLocaleString()} |`);
|
|
1101
|
-
}
|
|
1102
|
-
lines.push(`| **Total** | **${totalCalls}** | **${kb(totalBytesReturned)}** | **~${Math.round(totalBytesReturned / 4).toLocaleString()}** |`);
|
|
1103
|
-
}
|
|
1104
|
-
// ── DevRel summary ──
|
|
1105
|
-
const tokensSaved = Math.round(keptOut / 4);
|
|
1237
|
+
// ── Feature 1: Context Window Protection ──
|
|
1238
|
+
lines.push("", `### Context Window Protection`, "");
|
|
1106
1239
|
if (totalCalls === 0) {
|
|
1107
|
-
lines.push(
|
|
1108
|
-
}
|
|
1109
|
-
else if (keptOut === 0) {
|
|
1110
|
-
lines.push("", `> context-mode handled **${totalCalls}** tool calls. All outputs were compact enough to enter context directly. Process larger data or batch multiple commands for bigger savings.`);
|
|
1240
|
+
lines.push(`No context-mode tool calls yet. Use \`batch_execute\`, \`execute\`, or \`fetch_and_index\` to keep raw output out of your context window.`);
|
|
1111
1241
|
}
|
|
1112
1242
|
else {
|
|
1113
|
-
lines.push(
|
|
1243
|
+
lines.push(`| Metric | Value |`, `|--------|------:|`, `| Total data processed | **${kb(totalProcessed)}** |`, `| Kept in sandbox (never entered context) | **${kb(keptOut)}** |`, `| Entered context | ${kb(totalBytesReturned)} |`, `| Estimated tokens saved | ~${Math.round(keptOut / 4).toLocaleString()} |`, `| **Context savings** | **${savingsRatio.toFixed(1)}x (${reductionPct}% reduction)** |`);
|
|
1244
|
+
// Per-tool breakdown
|
|
1245
|
+
const toolNames = new Set([
|
|
1246
|
+
...Object.keys(sessionStats.calls),
|
|
1247
|
+
...Object.keys(sessionStats.bytesReturned),
|
|
1248
|
+
]);
|
|
1249
|
+
if (toolNames.size > 0) {
|
|
1250
|
+
lines.push("", `| Tool | Calls | Context | Tokens |`, `|------|------:|--------:|-------:|`);
|
|
1251
|
+
for (const tool of Array.from(toolNames).sort()) {
|
|
1252
|
+
const calls = sessionStats.calls[tool] || 0;
|
|
1253
|
+
const bytes = sessionStats.bytesReturned[tool] || 0;
|
|
1254
|
+
const tokens = Math.round(bytes / 4);
|
|
1255
|
+
lines.push(`| ${tool} | ${calls} | ${kb(bytes)} | ~${tokens.toLocaleString()} |`);
|
|
1256
|
+
}
|
|
1257
|
+
lines.push(`| **Total** | **${totalCalls}** | **${kb(totalBytesReturned)}** | **~${Math.round(totalBytesReturned / 4).toLocaleString()}** |`);
|
|
1258
|
+
}
|
|
1259
|
+
if (keptOut > 0) {
|
|
1260
|
+
lines.push("", `Without context-mode, **${kb(totalProcessed)}** of raw output would flood your context window. Instead, **${reductionPct}%** stayed in sandbox.`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// ── Session Continuity ──
|
|
1264
|
+
try {
|
|
1265
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1266
|
+
const dbHash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
|
|
1267
|
+
const sessionDbPath = join(homedir(), ".claude", "context-mode", "sessions", `${dbHash}.db`);
|
|
1268
|
+
if (existsSync(sessionDbPath)) {
|
|
1269
|
+
const require = createRequire(import.meta.url);
|
|
1270
|
+
const Database = require("better-sqlite3");
|
|
1271
|
+
const sdb = new Database(sessionDbPath, { readonly: true });
|
|
1272
|
+
const eventTotal = sdb.prepare("SELECT COUNT(*) as cnt FROM session_events").get();
|
|
1273
|
+
const byCategory = sdb.prepare("SELECT category, COUNT(*) as cnt FROM session_events GROUP BY category ORDER BY cnt DESC").all();
|
|
1274
|
+
const meta = sdb.prepare("SELECT compact_count FROM session_meta ORDER BY started_at DESC LIMIT 1").get();
|
|
1275
|
+
const resume = sdb.prepare("SELECT event_count, consumed FROM session_resume ORDER BY created_at DESC LIMIT 1").get();
|
|
1276
|
+
if (eventTotal.cnt > 0) {
|
|
1277
|
+
const compacts = meta?.compact_count ?? 0;
|
|
1278
|
+
// Query actual data per category for preview
|
|
1279
|
+
const previewRows = sdb.prepare(`SELECT category, type, data FROM session_events ORDER BY id DESC`).all();
|
|
1280
|
+
// Build previews: unique values per category
|
|
1281
|
+
const previews = new Map();
|
|
1282
|
+
for (const row of previewRows) {
|
|
1283
|
+
if (!previews.has(row.category))
|
|
1284
|
+
previews.set(row.category, new Set());
|
|
1285
|
+
const set = previews.get(row.category);
|
|
1286
|
+
if (set.size < 5) {
|
|
1287
|
+
let display = row.data;
|
|
1288
|
+
if (row.category === "file") {
|
|
1289
|
+
display = row.data.split("/").pop() || row.data;
|
|
1290
|
+
}
|
|
1291
|
+
else if (row.category === "prompt") {
|
|
1292
|
+
display = display.length > 50 ? display.slice(0, 47) + "..." : display;
|
|
1293
|
+
}
|
|
1294
|
+
if (display.length > 40)
|
|
1295
|
+
display = display.slice(0, 37) + "...";
|
|
1296
|
+
set.add(display);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const categoryLabels = {
|
|
1300
|
+
file: "Files tracked",
|
|
1301
|
+
rule: "Project rules (CLAUDE.md)",
|
|
1302
|
+
prompt: "Your requests saved",
|
|
1303
|
+
mcp: "Plugin tools used",
|
|
1304
|
+
git: "Git operations",
|
|
1305
|
+
env: "Environment setup",
|
|
1306
|
+
error: "Errors caught",
|
|
1307
|
+
task: "Tasks in progress",
|
|
1308
|
+
decision: "Your decisions",
|
|
1309
|
+
cwd: "Working directory",
|
|
1310
|
+
skill: "Skills used",
|
|
1311
|
+
subagent: "Delegated work",
|
|
1312
|
+
intent: "Session mode",
|
|
1313
|
+
data: "Data references",
|
|
1314
|
+
role: "Behavioral directives",
|
|
1315
|
+
};
|
|
1316
|
+
const categoryHints = {
|
|
1317
|
+
file: "Restored after compact — no need to re-read",
|
|
1318
|
+
rule: "Your project instructions survive context resets",
|
|
1319
|
+
prompt: "Continues exactly where you left off",
|
|
1320
|
+
decision: "Applied automatically — won't ask again",
|
|
1321
|
+
task: "Picks up from where it stopped",
|
|
1322
|
+
error: "Tracked and monitored across compacts",
|
|
1323
|
+
git: "Branch, commit, and repo state preserved",
|
|
1324
|
+
env: "Runtime config carried forward",
|
|
1325
|
+
mcp: "Tool usage patterns remembered",
|
|
1326
|
+
subagent: "Delegation history preserved",
|
|
1327
|
+
skill: "Skill invocations tracked",
|
|
1328
|
+
};
|
|
1329
|
+
lines.push("", "### Session Continuity", "", "| What's preserved | Count | I remember... | Why it matters |", "|------------------|------:|---------------|----------------|");
|
|
1330
|
+
for (const row of byCategory) {
|
|
1331
|
+
const label = categoryLabels[row.category] || row.category;
|
|
1332
|
+
const preview = previews.get(row.category);
|
|
1333
|
+
const previewStr = preview ? Array.from(preview).join(", ") : "";
|
|
1334
|
+
const hint = categoryHints[row.category] || "Survives context resets";
|
|
1335
|
+
lines.push(`| ${label} | ${row.cnt} | ${previewStr} | ${hint} |`);
|
|
1336
|
+
}
|
|
1337
|
+
lines.push(`| **Total** | **${eventTotal.cnt}** | | **Zero knowledge lost on compact** |`);
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
if (compacts > 0) {
|
|
1340
|
+
lines.push(`Context has been compacted **${compacts} time(s)** — session knowledge was preserved each time.`);
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
lines.push(`When your context compacts, all of this will restore Claude's awareness — no starting from scratch.`);
|
|
1344
|
+
}
|
|
1345
|
+
if (resume && !resume.consumed) {
|
|
1346
|
+
lines.push(`Resume snapshot ready (${resume.event_count} events) for the next compaction.`);
|
|
1347
|
+
}
|
|
1348
|
+
lines.push("");
|
|
1349
|
+
lines.push(`> **Note:** Previous session data is loaded when you start a new session. Without \`--continue\`, old session history is cleaned up to keep the database lean.`);
|
|
1350
|
+
}
|
|
1351
|
+
sdb.close();
|
|
1352
|
+
}
|
|
1114
1353
|
}
|
|
1354
|
+
catch {
|
|
1355
|
+
// Session DB not available or incompatible — skip silently
|
|
1356
|
+
}
|
|
1357
|
+
// No separate DevRel summary — integrated into feature sections above
|
|
1358
|
+
// Formatting directive — ensures all LLMs display the report verbatim
|
|
1359
|
+
lines.push("", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
|
|
1115
1360
|
const text = lines.join("\n");
|
|
1116
|
-
return trackResponse("
|
|
1361
|
+
return trackResponse("ctx_stats", {
|
|
1362
|
+
content: [{ type: "text", text }],
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
// ── ctx-doctor: diagnostics meta-tool ──────────────────────────────────────
|
|
1366
|
+
server.registerTool("ctx_doctor", {
|
|
1367
|
+
title: "Run Diagnostics",
|
|
1368
|
+
description: "Diagnose context-mode installation. Returns a shell command to execute. " +
|
|
1369
|
+
"You MUST run the returned command using your shell tool (Bash, shell_execute, " +
|
|
1370
|
+
"run_in_terminal, etc.) and display the output as a markdown checklist.",
|
|
1371
|
+
inputSchema: z.object({}),
|
|
1372
|
+
}, async () => {
|
|
1373
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
1374
|
+
const cmd = `node "${pluginRoot}/build/cli.js" doctor`;
|
|
1375
|
+
const text = [
|
|
1376
|
+
"## ctx-doctor",
|
|
1377
|
+
"",
|
|
1378
|
+
"Run this command using your shell execution tool:",
|
|
1379
|
+
"",
|
|
1380
|
+
"```",
|
|
1381
|
+
cmd,
|
|
1382
|
+
"```",
|
|
1383
|
+
"",
|
|
1384
|
+
"After the command completes, display results as a markdown checklist:",
|
|
1385
|
+
"- `[x]` for PASS, `[ ]` for FAIL, `[-]` for WARN",
|
|
1386
|
+
"- Example format:",
|
|
1387
|
+
" ```",
|
|
1388
|
+
" ## context-mode doctor",
|
|
1389
|
+
" - [x] Runtimes: 6/10 (javascript, typescript, python, shell, ruby, perl)",
|
|
1390
|
+
" - [x] Performance: FAST (Bun)",
|
|
1391
|
+
" - [x] Server test: PASS",
|
|
1392
|
+
" - [x] Hooks: PASS",
|
|
1393
|
+
" - [x] FTS5: PASS",
|
|
1394
|
+
" - [x] npm: v0.9.23",
|
|
1395
|
+
" ```",
|
|
1396
|
+
].join("\n");
|
|
1397
|
+
return trackResponse("ctx_doctor", {
|
|
1398
|
+
content: [{ type: "text", text }],
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1401
|
+
// ── ctx-upgrade: upgrade meta-tool ─────────────────────────────────────────
|
|
1402
|
+
server.registerTool("ctx_upgrade", {
|
|
1403
|
+
title: "Upgrade Plugin",
|
|
1404
|
+
description: "Upgrade context-mode to the latest version. Returns a shell command to execute. " +
|
|
1405
|
+
"You MUST run the returned command using your shell tool (Bash, shell_execute, " +
|
|
1406
|
+
"run_in_terminal, etc.) and display the output as a checklist. " +
|
|
1407
|
+
"Tell the user to restart their session after upgrade.",
|
|
1408
|
+
inputSchema: z.object({}),
|
|
1409
|
+
}, async () => {
|
|
1410
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
1411
|
+
const cmd = `node "${pluginRoot}/build/cli.js" upgrade`;
|
|
1412
|
+
const text = [
|
|
1413
|
+
"## ctx-upgrade",
|
|
1414
|
+
"",
|
|
1415
|
+
"Run this command using your shell execution tool:",
|
|
1416
|
+
"",
|
|
1417
|
+
"```",
|
|
1418
|
+
cmd,
|
|
1419
|
+
"```",
|
|
1420
|
+
"",
|
|
1421
|
+
"After the command completes, display results as a markdown checklist:",
|
|
1422
|
+
"- `[x]` for success, `[ ]` for failure",
|
|
1423
|
+
"- Example format:",
|
|
1424
|
+
" ```",
|
|
1425
|
+
" ## context-mode upgrade",
|
|
1426
|
+
" - [x] Pulled latest from GitHub",
|
|
1427
|
+
" - [x] Built and installed v0.9.24",
|
|
1428
|
+
" - [x] npm global updated",
|
|
1429
|
+
" - [x] Hooks configured",
|
|
1430
|
+
" - [x] Doctor: all checks PASS",
|
|
1431
|
+
" ```",
|
|
1432
|
+
"- Tell the user to restart their session to pick up the new version.",
|
|
1433
|
+
].join("\n");
|
|
1434
|
+
return trackResponse("ctx_upgrade", {
|
|
1117
1435
|
content: [{ type: "text", text }],
|
|
1118
1436
|
});
|
|
1119
1437
|
});
|
|
@@ -1126,8 +1444,9 @@ async function main() {
|
|
|
1126
1444
|
if (cleaned > 0) {
|
|
1127
1445
|
console.error(`Cleaned up ${cleaned} stale DB file(s) from previous sessions`);
|
|
1128
1446
|
}
|
|
1129
|
-
// Clean up own DB on shutdown
|
|
1447
|
+
// Clean up own DB + backgrounded processes on shutdown
|
|
1130
1448
|
const shutdown = () => {
|
|
1449
|
+
executor.cleanupBackgrounded();
|
|
1131
1450
|
if (_store)
|
|
1132
1451
|
_store.cleanup();
|
|
1133
1452
|
};
|