context-mode 1.0.89 → 1.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +184 -60
  6. package/build/adapters/antigravity/index.d.ts +3 -5
  7. package/build/adapters/antigravity/index.js +7 -35
  8. package/build/adapters/base.d.ts +27 -0
  9. package/build/adapters/base.js +59 -0
  10. package/build/adapters/claude-code/index.d.ts +9 -25
  11. package/build/adapters/claude-code/index.js +12 -140
  12. package/build/adapters/claude-code-base.d.ts +49 -0
  13. package/build/adapters/claude-code-base.js +113 -0
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +21 -14
  16. package/build/adapters/codex/hooks.js +22 -15
  17. package/build/adapters/codex/index.d.ts +6 -10
  18. package/build/adapters/codex/index.js +13 -43
  19. package/build/adapters/copilot-base.d.ts +78 -0
  20. package/build/adapters/copilot-base.js +281 -0
  21. package/build/adapters/cursor/index.d.ts +3 -5
  22. package/build/adapters/cursor/index.js +6 -34
  23. package/build/adapters/detect.d.ts +7 -0
  24. package/build/adapters/detect.js +57 -56
  25. package/build/adapters/gemini-cli/index.d.ts +3 -5
  26. package/build/adapters/gemini-cli/index.js +7 -35
  27. package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
  28. package/build/adapters/jetbrains-copilot/config.js +8 -0
  29. package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
  30. package/build/adapters/jetbrains-copilot/hooks.js +82 -0
  31. package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
  32. package/build/adapters/jetbrains-copilot/index.js +119 -0
  33. package/build/adapters/kiro/hooks.d.ts +14 -0
  34. package/build/adapters/kiro/hooks.js +23 -0
  35. package/build/adapters/kiro/index.d.ts +3 -5
  36. package/build/adapters/kiro/index.js +10 -38
  37. package/build/adapters/openclaw/index.d.ts +3 -4
  38. package/build/adapters/openclaw/index.js +6 -22
  39. package/build/adapters/opencode/index.d.ts +2 -3
  40. package/build/adapters/opencode/index.js +5 -16
  41. package/build/adapters/qwen-code/index.d.ts +39 -0
  42. package/build/adapters/qwen-code/index.js +199 -0
  43. package/build/adapters/types.d.ts +1 -1
  44. package/build/adapters/vscode-copilot/index.d.ts +16 -46
  45. package/build/adapters/vscode-copilot/index.js +29 -320
  46. package/build/adapters/zed/index.d.ts +3 -5
  47. package/build/adapters/zed/index.js +7 -35
  48. package/build/cli.js +13 -0
  49. package/build/lifecycle.d.ts +23 -0
  50. package/build/lifecycle.js +54 -13
  51. package/build/opencode-plugin.d.ts +19 -7
  52. package/build/opencode-plugin.js +19 -7
  53. package/build/runtime.js +24 -9
  54. package/build/security.d.ts +17 -1
  55. package/build/security.js +40 -6
  56. package/build/server.js +53 -10
  57. package/build/session/analytics.d.ts +8 -7
  58. package/build/session/analytics.js +107 -76
  59. package/build/session/db.d.ts +10 -1
  60. package/build/session/db.js +67 -8
  61. package/build/session/extract.js +10 -2
  62. package/build/session/project-attribution.d.ts +73 -0
  63. package/build/session/project-attribution.js +231 -0
  64. package/build/store.d.ts +4 -0
  65. package/build/store.js +58 -9
  66. package/build/types.d.ts +8 -0
  67. package/cli.bundle.mjs +135 -121
  68. package/configs/antigravity/GEMINI.md +31 -36
  69. package/configs/claude-code/CLAUDE.md +31 -37
  70. package/configs/codex/AGENTS.md +35 -49
  71. package/configs/cursor/context-mode.mdc +24 -25
  72. package/configs/gemini-cli/GEMINI.md +30 -36
  73. package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
  74. package/configs/jetbrains-copilot/hooks.json +16 -0
  75. package/configs/jetbrains-copilot/mcp.json +8 -0
  76. package/configs/kilo/AGENTS.md +30 -36
  77. package/configs/kiro/KIRO.md +30 -36
  78. package/configs/kiro/agent.json +1 -1
  79. package/configs/openclaw/AGENTS.md +30 -36
  80. package/configs/opencode/AGENTS.md +30 -36
  81. package/configs/pi/AGENTS.md +31 -36
  82. package/configs/qwen-code/QWEN.md +63 -0
  83. package/configs/vscode-copilot/copilot-instructions.md +30 -36
  84. package/configs/zed/AGENTS.md +31 -36
  85. package/hooks/codex/posttooluse.mjs +7 -7
  86. package/hooks/codex/pretooluse.mjs +3 -3
  87. package/hooks/codex/sessionstart.mjs +2 -1
  88. package/hooks/core/formatters.mjs +24 -0
  89. package/hooks/core/routing.mjs +40 -15
  90. package/hooks/core/tool-naming.mjs +2 -0
  91. package/hooks/cursor/posttooluse.mjs +7 -7
  92. package/hooks/cursor/pretooluse.mjs +3 -3
  93. package/hooks/cursor/sessionstart.mjs +2 -1
  94. package/hooks/cursor/stop.mjs +2 -2
  95. package/hooks/ensure-deps.mjs +22 -10
  96. package/hooks/gemini-cli/aftertool.mjs +8 -8
  97. package/hooks/gemini-cli/beforetool.mjs +3 -2
  98. package/hooks/gemini-cli/precompress.mjs +2 -2
  99. package/hooks/gemini-cli/sessionstart.mjs +12 -4
  100. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  101. package/hooks/jetbrains-copilot/precompact.mjs +54 -0
  102. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  103. package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
  104. package/hooks/kiro/posttooluse.mjs +6 -7
  105. package/hooks/kiro/pretooluse.mjs +3 -2
  106. package/hooks/posttooluse.mjs +8 -8
  107. package/hooks/precompact.mjs +3 -4
  108. package/hooks/pretooluse.mjs +5 -4
  109. package/hooks/routing-block.mjs +35 -33
  110. package/hooks/session-attribution.bundle.mjs +1 -0
  111. package/hooks/session-db.bundle.mjs +27 -8
  112. package/hooks/session-extract.bundle.mjs +2 -1
  113. package/hooks/session-helpers.mjs +44 -3
  114. package/hooks/session-loaders.mjs +37 -0
  115. package/hooks/sessionstart.mjs +5 -5
  116. package/hooks/userpromptsubmit.mjs +26 -9
  117. package/hooks/vscode-copilot/posttooluse.mjs +8 -8
  118. package/hooks/vscode-copilot/precompact.mjs +2 -2
  119. package/hooks/vscode-copilot/pretooluse.mjs +3 -2
  120. package/hooks/vscode-copilot/sessionstart.mjs +2 -2
  121. package/insight/server.mjs +237 -25
  122. package/insight/src/lib/api.ts +2 -1
  123. package/insight/src/routes/index.tsx +16 -3
  124. package/insight/src/routes/search.tsx +1 -1
  125. package/openclaw.plugin.json +1 -1
  126. package/package.json +11 -2
  127. package/server.bundle.mjs +94 -80
  128. package/skills/ctx-insight/SKILL.md +1 -1
@@ -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
- * Loaded by OpenCode via: import("context-mode/plugin").ContextModePlugin(ctx)
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
- * OpenCode plugin factory. Called once when OpenCode loads the plugin.
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 const ContextModePlugin = async (ctx) => {
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, "opencode");
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
- // Bun installs to ~/.bun/bin which may not be in PATH in MCP server environments
18
- if (!isWindows) {
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 execSync(`${cmd} --version`, {
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)
@@ -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
- // Normalize backslashes to forward slashes for cross-platform matching
364
- const normalized = filePath.replace(/\\/g, "/");
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
- if (fileGlobToRegex(glob, caseInsensitive).test(normalized)) {
368
- return { denied: true, matchedPattern: glob };
398
+ const regex = fileGlobToRegex(glob, caseInsensitive);
399
+ for (const candidate of candidates) {
400
+ if (regex.test(candidate)) {
401
+ return { denied: true, matchedPattern: glob };
402
+ }
369
403
  }
370
404
  }
371
405
  }
package/build/server.js CHANGED
@@ -97,6 +97,9 @@ function maybeIndexSessionEvents(store) {
97
97
  // platform-specific paths. All session DB paths go through it — no
98
98
  // hardcoded configDir detection in tool handlers.
99
99
  let _detectedAdapter = null;
100
+ // Tracks the ctx_insight dashboard child so shutdown can terminate it.
101
+ // See ctx_insight handler + shutdown() in main().
102
+ let _insightChild = null;
100
103
  /**
101
104
  * Get the platform-specific sessions directory from the detected adapter.
102
105
  * Falls back to ~/.claude/context-mode/sessions/ before adapter detection.
@@ -228,10 +231,21 @@ function getUpgradeHint() {
228
231
  return "npm run build";
229
232
  return "npm update -g context-mode";
230
233
  }
234
+ function semverNewer(a, b) {
235
+ const pa = a.split(".").map(Number);
236
+ const pb = b.split(".").map(Number);
237
+ for (let i = 0; i < 3; i++) {
238
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
239
+ return true;
240
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
241
+ return false;
242
+ }
243
+ return false;
244
+ }
231
245
  function isOutdated() {
232
246
  if (!_latestVersion || _latestVersion === "unknown")
233
247
  return false;
234
- return _latestVersion !== VERSION;
248
+ return semverNewer(_latestVersion, VERSION);
235
249
  }
236
250
  function shouldShowVersionWarning() {
237
251
  if (!isOutdated())
@@ -325,8 +339,9 @@ function checkNonShellDenyPolicy(code, language, toolName) {
325
339
  */
326
340
  function checkFilePathDenyPolicy(filePath, toolName) {
327
341
  try {
328
- const denyGlobs = readToolDenyPatterns("Read", process.env.CLAUDE_PROJECT_DIR);
329
- const result = evaluateFilePath(filePath, denyGlobs);
342
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
343
+ const denyGlobs = readToolDenyPatterns("Read", projectDir);
344
+ const result = evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir);
330
345
  if (result.denied) {
331
346
  return trackResponse(toolName, {
332
347
  content: [{
@@ -481,7 +496,7 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
481
496
  // ─────────────────────────────────────────────────────────
482
497
  server.registerTool("ctx_execute", {
483
498
  title: "Execute Code",
484
- description: `MANDATORY: Use for any command where output exceeds 20 lines. Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess.${bunNote} Available: ${langList}.\n\nPREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.\n\nTHINK IN CODE: When you need to analyze, count, filter, compare, or process data — write code that does the work and console.log() only the answer. Do NOT read raw data into context to process mentally. Program the analysis, don't compute it in your reasoning. Write robust, pure JavaScript (no npm dependencies). Use only Node.js built-ins (fs, path, child_process). Always wrap in try/catch. Handle null/undefined. Works on both Node.js and Bun.`,
499
+ description: `MANDATORY: Use for any command where output exceeds 20 lines. Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess.${bunNote} Available: ${langList}.\n\nPREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.\n\nTHINK IN CODE: When you need to analyze, count, filter, compare, or process data — write code that does the work and console.log() only the answer. Do NOT read raw data into context to process mentally. Program the analysis, don't compute it in your reasoning. Write robust, pure JavaScript (no npm dependencies). Use only Node.js built-ins (fs, path, child_process). Always wrap in try/catch. Handle null/undefined. Works on both Node.js and Bun.\n\nWhen reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].`,
485
500
  inputSchema: z.object({
486
501
  language: z
487
502
  .enum([
@@ -774,7 +789,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
774
789
  // ─────────────────────────────────────────────────────────
775
790
  server.registerTool("ctx_execute_file", {
776
791
  title: "Execute File Processing",
777
- description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.\n\nTHINK IN CODE: Write code that processes FILE_CONTENT and console.log() only the answer. Don't read files into context to analyze mentally. Write robust, pure JavaScript — no npm deps, try/catch, null-safe. Node.js + Bun compatible.",
792
+ description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.\n\nTHINK IN CODE: Write code that processes FILE_CONTENT and console.log() only the answer. Don't read files into context to analyze mentally. Write robust, pure JavaScript — no npm deps, try/catch, null-safe. Node.js + Bun compatible.\n\nWhen reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
778
793
  inputSchema: z.object({
779
794
  path: z
780
795
  .string()
@@ -917,6 +932,7 @@ server.registerTool("ctx_index", {
917
932
  "- README files, migration guides, changelog entries\n" +
918
933
  "- Any content with code examples you may need to reference precisely\n\n" +
919
934
  "After indexing, use 'search' to retrieve specific sections on-demand.\n" +
935
+ "When `path` is provided, a content hash is stored for automatic stale detection in search results.\n" +
920
936
  "Do NOT use for: log files, test output, CSV, build output — use 'execute_file' for those.",
921
937
  inputSchema: z.object({
922
938
  content: z
@@ -1016,8 +1032,10 @@ function coerceCommandsArray(val) {
1016
1032
  server.registerTool("ctx_search", {
1017
1033
  title: "Search Indexed Content",
1018
1034
  description: "Search indexed content. Requires prior indexing via ctx_batch_execute, ctx_index, or ctx_fetch_and_index. " +
1019
- "Pass ALL search questions as queries array in ONE call.\n\n" +
1020
- "TIPS: 2-4 specific terms per query. Use 'source' to scope results.",
1035
+ "Pass ALL search questions as queries array in ONE call. " +
1036
+ "File-backed sources are auto-refreshed when the source file changes.\n\n" +
1037
+ "TIPS: 2-4 specific terms per query. Use 'source' to scope results.\n\n" +
1038
+ "When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
1021
1039
  inputSchema: z.object({
1022
1040
  queries: z.preprocess(coerceJsonArray, z
1023
1041
  .array(z.string())
@@ -1121,6 +1139,10 @@ server.registerTool("ctx_search", {
1121
1139
  totalSize += formatted.length;
1122
1140
  }
1123
1141
  let output = sections.join("\n\n---\n\n");
1142
+ // Report auto-refreshed stale sources
1143
+ if (store.lastRefreshCount > 0) {
1144
+ output = `> Auto-refreshed ${store.lastRefreshCount} stale source${store.lastRefreshCount > 1 ? "s" : ""} (file changed since indexing).\n\n` + output;
1145
+ }
1124
1146
  // Add throttle warning after threshold
1125
1147
  if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
1126
1148
  output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
@@ -1230,7 +1252,8 @@ server.registerTool("ctx_fetch_and_index", {
1230
1252
  description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
1231
1253
  "and returns a ~3KB preview. Full content stays in sandbox — use search() for deeper lookups.\n\n" +
1232
1254
  "Better than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.\n\n" +
1233
- "Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.",
1255
+ "Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.\n\n" +
1256
+ "When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
1234
1257
  inputSchema: z.object({
1235
1258
  url: z.string().describe("The URL to fetch and index"),
1236
1259
  source: z
@@ -1379,7 +1402,8 @@ server.registerTool("ctx_batch_execute", {
1379
1402
  "THIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\n" +
1380
1403
  "One batch_execute call replaces 30+ execute calls + 10+ search calls.\n" +
1381
1404
  "Provide all commands to run and all queries to search — everything happens in one round trip.\n\n" +
1382
- "THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work.",
1405
+ "THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work.\n\n" +
1406
+ "When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
1383
1407
  inputSchema: z.object({
1384
1408
  commands: z.preprocess(coerceCommandsArray, z
1385
1409
  .array(z.object({
@@ -1958,7 +1982,17 @@ server.registerTool("ctx_insight", {
1958
1982
  catch {
1959
1983
  // Port is free, proceed with spawn
1960
1984
  }
1961
- // Start server in background
1985
+ // Kill any previous insight child this MCP spawned (e.g. re-invocation).
1986
+ if (_insightChild && _insightChild.pid && !_insightChild.killed) {
1987
+ try {
1988
+ _insightChild.kill("SIGTERM");
1989
+ }
1990
+ catch { /* best effort */ }
1991
+ }
1992
+ // Start server in background. `detached: true` keeps MCP stdio free, but
1993
+ // we track the handle and kill it in shutdown() so the dashboard does
1994
+ // not orphan when Claude closes. The child also watches INSIGHT_PARENT_PID
1995
+ // as a fallback for SIGKILL/crash paths.
1962
1996
  const { spawn } = await import("node:child_process");
1963
1997
  const child = spawn("node", [join(cacheDir, "server.mjs")], {
1964
1998
  cwd: cacheDir,
@@ -1967,12 +2001,14 @@ server.registerTool("ctx_insight", {
1967
2001
  PORT: String(port),
1968
2002
  INSIGHT_SESSION_DIR: getSessionDir(),
1969
2003
  INSIGHT_CONTENT_DIR: join(dirname(getSessionDir()), "content"),
2004
+ INSIGHT_PARENT_PID: String(process.pid),
1970
2005
  },
1971
2006
  detached: true,
1972
2007
  stdio: "ignore",
1973
2008
  });
1974
2009
  child.on("error", () => { }); // prevent unhandled error crash
1975
2010
  child.unref();
2011
+ _insightChild = child;
1976
2012
  // Wait for server to be ready
1977
2013
  await new Promise(r => setTimeout(r, 1500));
1978
2014
  // Verify server is actually running
@@ -2049,6 +2085,13 @@ async function main() {
2049
2085
  unlinkSync(mcpSentinel);
2050
2086
  }
2051
2087
  catch { /* best effort */ }
2088
+ // Stop ctx_insight dashboard so it does not outlive Claude.
2089
+ if (_insightChild && _insightChild.pid && !_insightChild.killed) {
2090
+ try {
2091
+ _insightChild.kill("SIGTERM");
2092
+ }
2093
+ catch { /* best effort */ }
2094
+ }
2052
2095
  };
2053
2096
  const gracefulShutdown = async () => {
2054
2097
  shutdown();
@@ -149,13 +149,14 @@ export declare class AnalyticsEngine {
149
149
  queryAll(runtimeStats: RuntimeStats): FullReport;
150
150
  }
151
151
  /**
152
- * Render a FullReport as a before/after comparison developers instantly understand.
152
+ * Render a FullReport as a visual savings dashboard designed for screenshotting.
153
153
  *
154
- * Design rules:
155
- * - If no savings, show "fresh session" format (no fake percentages)
156
- * - Active session shows BEFORE vs AFTER -- what would have flooded your conversation vs what actually did
157
- * - Per-tool table only if 2+ different tools were called
158
- * - Time gained is the hero metric
159
- * - Under 15 lines for typical sessions
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;