token-pilot 0.19.2 → 0.23.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.
Files changed (96) hide show
  1. package/.claude-plugin/hooks/hooks.json +30 -0
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +194 -313
  5. package/dist/agents/tp-audit-scanner.md +49 -0
  6. package/dist/agents/tp-commit-writer.md +41 -0
  7. package/dist/agents/tp-dead-code-finder.md +43 -0
  8. package/dist/agents/tp-debugger.md +45 -0
  9. package/dist/agents/tp-history-explorer.md +43 -0
  10. package/dist/agents/tp-impact-analyzer.md +44 -0
  11. package/dist/agents/tp-migration-scout.md +43 -0
  12. package/dist/agents/tp-onboard.md +40 -0
  13. package/dist/agents/tp-pr-reviewer.md +41 -0
  14. package/dist/agents/tp-refactor-planner.md +42 -0
  15. package/dist/agents/tp-run.md +48 -0
  16. package/dist/agents/tp-session-restorer.md +47 -0
  17. package/dist/agents/tp-test-triage.md +40 -0
  18. package/dist/agents/tp-test-writer.md +46 -0
  19. package/dist/cli/agent-frontmatter.d.ts +48 -0
  20. package/dist/cli/agent-frontmatter.js +189 -0
  21. package/dist/cli/bless-agents.d.ts +65 -0
  22. package/dist/cli/bless-agents.js +307 -0
  23. package/dist/cli/claudeignore.d.ts +33 -0
  24. package/dist/cli/claudeignore.js +88 -0
  25. package/dist/cli/claudemd-hygiene.d.ts +26 -0
  26. package/dist/cli/claudemd-hygiene.js +43 -0
  27. package/dist/cli/doctor-drift.d.ts +31 -0
  28. package/dist/cli/doctor-drift.js +130 -0
  29. package/dist/cli/doctor-env-check.d.ts +25 -0
  30. package/dist/cli/doctor-env-check.js +91 -0
  31. package/dist/cli/install-agents.d.ts +108 -0
  32. package/dist/cli/install-agents.js +402 -0
  33. package/dist/cli/save-doc.d.ts +42 -0
  34. package/dist/cli/save-doc.js +145 -0
  35. package/dist/cli/scan-agents.d.ts +46 -0
  36. package/dist/cli/scan-agents.js +227 -0
  37. package/dist/cli/stats.d.ts +36 -0
  38. package/dist/cli/stats.js +131 -0
  39. package/dist/cli/typo-guard.d.ts +27 -0
  40. package/dist/cli/typo-guard.js +119 -0
  41. package/dist/cli/unbless-agents.d.ts +33 -0
  42. package/dist/cli/unbless-agents.js +85 -0
  43. package/dist/cli/uninstall-agents.d.ts +36 -0
  44. package/dist/cli/uninstall-agents.js +117 -0
  45. package/dist/config/defaults.d.ts +1 -1
  46. package/dist/config/defaults.js +14 -8
  47. package/dist/config/loader.d.ts +1 -1
  48. package/dist/config/loader.js +105 -11
  49. package/dist/core/context-registry.d.ts +16 -1
  50. package/dist/core/context-registry.js +60 -28
  51. package/dist/core/event-log.d.ts +79 -0
  52. package/dist/core/event-log.js +190 -0
  53. package/dist/core/session-registry.d.ts +43 -0
  54. package/dist/core/session-registry.js +113 -0
  55. package/dist/core/session-savings.d.ts +19 -0
  56. package/dist/core/session-savings.js +60 -0
  57. package/dist/handlers/session-budget.d.ts +32 -0
  58. package/dist/handlers/session-budget.js +61 -0
  59. package/dist/handlers/session-snapshot-persist.d.ts +22 -0
  60. package/dist/handlers/session-snapshot-persist.js +76 -0
  61. package/dist/hooks/adaptive-threshold.d.ts +27 -0
  62. package/dist/hooks/adaptive-threshold.js +46 -0
  63. package/dist/hooks/format-deny-message.d.ts +21 -0
  64. package/dist/hooks/format-deny-message.js +147 -0
  65. package/dist/hooks/installer.js +130 -31
  66. package/dist/hooks/path-safety.d.ts +16 -0
  67. package/dist/hooks/path-safety.js +34 -0
  68. package/dist/hooks/post-bash.d.ts +46 -0
  69. package/dist/hooks/post-bash.js +77 -0
  70. package/dist/hooks/post-task.d.ts +67 -0
  71. package/dist/hooks/post-task.js +136 -0
  72. package/dist/hooks/session-start.d.ts +45 -0
  73. package/dist/hooks/session-start.js +179 -0
  74. package/dist/hooks/summary-ast-index.d.ts +28 -0
  75. package/dist/hooks/summary-ast-index.js +122 -0
  76. package/dist/hooks/summary-head-tail.d.ts +15 -0
  77. package/dist/hooks/summary-head-tail.js +78 -0
  78. package/dist/hooks/summary-pipeline.d.ts +35 -0
  79. package/dist/hooks/summary-pipeline.js +63 -0
  80. package/dist/hooks/summary-regex.d.ts +14 -0
  81. package/dist/hooks/summary-regex.js +130 -0
  82. package/dist/hooks/summary-types.d.ts +29 -0
  83. package/dist/hooks/summary-types.js +9 -0
  84. package/dist/index.d.ts +15 -3
  85. package/dist/index.js +538 -149
  86. package/dist/integration/context-mode-detector.d.ts +7 -1
  87. package/dist/integration/context-mode-detector.js +51 -15
  88. package/dist/server/tool-definitions.d.ts +149 -0
  89. package/dist/server/tool-definitions.js +424 -202
  90. package/dist/server.d.ts +1 -1
  91. package/dist/server.js +456 -179
  92. package/dist/templates/agent-builder.d.ts +49 -0
  93. package/dist/templates/agent-builder.js +104 -0
  94. package/dist/types.d.ts +38 -4
  95. package/package.json +4 -2
  96. package/skills/stats/SKILL.md +13 -2
package/dist/server.js CHANGED
@@ -1,49 +1,52 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
- import { AstIndexClient } from './ast-index/client.js';
4
- import { FileCache } from './core/file-cache.js';
5
- import { ContextRegistry } from './core/context-registry.js';
6
- import { SymbolResolver } from './core/symbol-resolver.js';
7
- import { SessionAnalytics } from './core/session-analytics.js';
8
- import { classifyIntent } from './core/intent-classifier.js';
9
- import { buildDecisionTrace } from './core/decision-trace.js';
10
- import { SessionCache } from './core/session-cache.js';
11
- import { loadConfig } from './config/loader.js';
12
- import { readFileSync } from 'node:fs';
13
- import { dirname, resolve } from 'node:path';
14
- import { execFile } from 'node:child_process';
15
- import { isDangerousRoot } from './core/validation.js';
16
- import { promisify } from 'node:util';
17
- import { GitWatcher } from './git/watcher.js';
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { AstIndexClient } from "./ast-index/client.js";
4
+ import { FileCache } from "./core/file-cache.js";
5
+ import { ContextRegistry } from "./core/context-registry.js";
6
+ import { SessionRegistryManager } from "./core/session-registry.js";
7
+ import { SymbolResolver } from "./core/symbol-resolver.js";
8
+ import { SessionAnalytics, } from "./core/session-analytics.js";
9
+ import { classifyIntent } from "./core/intent-classifier.js";
10
+ import { buildDecisionTrace } from "./core/decision-trace.js";
11
+ import { SessionCache } from "./core/session-cache.js";
12
+ import { loadConfig } from "./config/loader.js";
13
+ import { readFileSync } from "node:fs";
14
+ import { dirname, resolve } from "node:path";
15
+ import { execFile } from "node:child_process";
16
+ import { isDangerousRoot } from "./core/validation.js";
17
+ import { promisify } from "node:util";
18
+ import { GitWatcher } from "./git/watcher.js";
18
19
  const execFilePromise = promisify(execFile);
19
- import { FileWatcher } from './git/file-watcher.js';
20
- import { handleSmartRead } from './handlers/smart-read.js';
21
- import { handleReadSymbol } from './handlers/read-symbol.js';
22
- import { handleReadSymbols } from './handlers/read-symbols.js';
23
- import { handleReadRange } from './handlers/read-range.js';
24
- import { handleReadDiff } from './handlers/read-diff.js';
25
- import { handleFindUsages } from './handlers/find-usages.js';
26
- import { handleSmartReadMany } from './handlers/smart-read-many.js';
27
- import { handleProjectOverview } from './handlers/project-overview.js';
28
- import { handleNonCodeRead, isNonCodeStructured } from './handlers/non-code.js';
29
- import { handleFindUnused } from './handlers/find-unused.js';
30
- import { handleReadForEdit } from './handlers/read-for-edit.js';
31
- import { handleRelatedFiles } from './handlers/related-files.js';
32
- import { handleOutline } from './handlers/outline.js';
33
- import { handleCodeAudit } from './handlers/code-audit.js';
34
- import { handleModuleInfo } from './handlers/module-info.js';
35
- import { handleSmartDiff } from './handlers/smart-diff.js';
36
- import { handleExploreArea } from './handlers/explore-area.js';
37
- import { handleSmartLog } from './handlers/smart-log.js';
38
- import { handleTestSummary } from './handlers/test-summary.js';
39
- import { handleSessionSnapshot } from './handlers/session-snapshot.js';
40
- import { handleReadSection } from './handlers/read-section.js';
41
- import { detectContextMode } from './integration/context-mode-detector.js';
42
- import { estimateTokens } from './core/token-estimator.js';
43
- import { checkPolicy, isFullReadTool } from './core/policy-engine.js';
44
- import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS } from './server/tool-definitions.js';
45
- import { createTokenEstimates } from './server/token-estimates.js';
46
- import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from './core/validation.js';
20
+ import { FileWatcher } from "./git/file-watcher.js";
21
+ import { handleSmartRead } from "./handlers/smart-read.js";
22
+ import { handleReadSymbol } from "./handlers/read-symbol.js";
23
+ import { handleReadSymbols } from "./handlers/read-symbols.js";
24
+ import { handleReadRange } from "./handlers/read-range.js";
25
+ import { handleReadDiff } from "./handlers/read-diff.js";
26
+ import { handleFindUsages } from "./handlers/find-usages.js";
27
+ import { handleSmartReadMany } from "./handlers/smart-read-many.js";
28
+ import { handleProjectOverview } from "./handlers/project-overview.js";
29
+ import { handleNonCodeRead, isNonCodeStructured } from "./handlers/non-code.js";
30
+ import { handleFindUnused } from "./handlers/find-unused.js";
31
+ import { handleReadForEdit } from "./handlers/read-for-edit.js";
32
+ import { handleRelatedFiles } from "./handlers/related-files.js";
33
+ import { handleOutline } from "./handlers/outline.js";
34
+ import { handleCodeAudit } from "./handlers/code-audit.js";
35
+ import { handleModuleInfo } from "./handlers/module-info.js";
36
+ import { handleSmartDiff } from "./handlers/smart-diff.js";
37
+ import { handleExploreArea } from "./handlers/explore-area.js";
38
+ import { handleSmartLog } from "./handlers/smart-log.js";
39
+ import { handleTestSummary } from "./handlers/test-summary.js";
40
+ import { handleSessionSnapshot } from "./handlers/session-snapshot.js";
41
+ import { persistSnapshot } from "./handlers/session-snapshot-persist.js";
42
+ import { handleSessionBudget } from "./handlers/session-budget.js";
43
+ import { handleReadSection } from "./handlers/read-section.js";
44
+ import { detectContextMode } from "./integration/context-mode-detector.js";
45
+ import { estimateTokens } from "./core/token-estimator.js";
46
+ import { checkPolicy, isFullReadTool } from "./core/policy-engine.js";
47
+ import { MCP_INSTRUCTIONS, TOOL_DEFINITIONS, } from "./server/tool-definitions.js";
48
+ import { createTokenEstimates } from "./server/token-estimates.js";
49
+ import { validateSmartReadArgs, validateReadSymbolArgs, validateReadSymbolsArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, validateReadSectionArgs, } from "./core/validation.js";
47
50
  export async function createServer(projectRoot, options) {
48
51
  const config = await loadConfig(projectRoot);
49
52
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
@@ -52,7 +55,38 @@ export async function createServer(projectRoot, options) {
52
55
  });
53
56
  const fileCache = new FileCache(config.cache.maxSizeMB, config.smartRead.smallFileThreshold);
54
57
  const contextRegistry = new ContextRegistry();
58
+ const sessionRegistries = new SessionRegistryManager(projectRoot);
59
+ // Flush persisted session registries on shutdown (best-effort; every hot
60
+ // tool-call path also flushes immediately, so this is only for registries
61
+ // whose last access never got a post-call flush). `beforeExit` doesn't
62
+ // fire on signal-based termination (SIGINT / SIGTERM), so we hook those
63
+ // too — Node runs every signal listener before the default action, giving
64
+ // flushAll a fair chance to complete. `process.exit()` bypasses listeners
65
+ // entirely; callers that care about durability should not use it.
66
+ const shutdownFlush = () => {
67
+ void sessionRegistries.flushAll();
68
+ };
69
+ process.once("beforeExit", shutdownFlush);
70
+ process.once("SIGINT", shutdownFlush);
71
+ process.once("SIGTERM", shutdownFlush);
55
72
  const symbolResolver = new SymbolResolver(astIndex);
73
+ /**
74
+ * TP-69m — pick the right ContextRegistry for this tool call.
75
+ * - force:true → empty registry (agent wants to bypass dedup)
76
+ * - session_id present → per-session, disk-backed registry
77
+ * - neither → process-default (legacy behaviour for callers that
78
+ * don't yet know their session_id)
79
+ */
80
+ function pickRegistry(rawArgs) {
81
+ const a = (rawArgs ?? {});
82
+ const force = a.force === true;
83
+ const sessionId = typeof a.session_id === "string" ? a.session_id : "";
84
+ if (force)
85
+ return { reg: new ContextRegistry(), sessionId, force: true };
86
+ if (sessionId)
87
+ return { reg: sessionRegistries.getFor(sessionId), sessionId, force };
88
+ return { reg: contextRegistry, sessionId, force };
89
+ }
56
90
  // Try to init ast-index (non-fatal if not available)
57
91
  const needsAutoDetect = !!options?.skipAstIndex;
58
92
  try {
@@ -61,7 +95,7 @@ export async function createServer(projectRoot, options) {
61
95
  // Dangerous root (/, home dir) — don't build index yet
62
96
  // Will auto-detect real project root from first file path
63
97
  astIndex.disableIndex();
64
- console.error('[token-pilot] ast-index: waiting for first file path to auto-detect project root');
98
+ console.error("[token-pilot] ast-index: waiting for first file path to auto-detect project root");
65
99
  }
66
100
  else if (config.astIndex.buildOnStart) {
67
101
  await astIndex.ensureIndex();
@@ -96,10 +130,10 @@ export async function createServer(projectRoot, options) {
96
130
  if (caps?.roots) {
97
131
  const { roots } = await server.listRoots();
98
132
  for (const root of roots) {
99
- if (root.uri.startsWith('file://')) {
133
+ if (root.uri.startsWith("file://")) {
100
134
  const rootPath = decodeURIComponent(new URL(root.uri).pathname);
101
135
  if (rootPath && !isDangerousRoot(rootPath)) {
102
- await applyDetectedRoot(rootPath, 'MCP roots');
136
+ await applyDetectedRoot(rootPath, "MCP roots");
103
137
  return;
104
138
  }
105
139
  }
@@ -113,7 +147,7 @@ export async function createServer(projectRoot, options) {
113
147
  if (filePath) {
114
148
  const dir = dirname(filePath);
115
149
  try {
116
- const { stdout } = await execFilePromise('git', ['rev-parse', '--show-toplevel'], {
150
+ const { stdout } = await execFilePromise("git", ["rev-parse", "--show-toplevel"], {
117
151
  cwd: dir,
118
152
  timeout: 3000,
119
153
  });
@@ -133,16 +167,16 @@ export async function createServer(projectRoot, options) {
133
167
  */
134
168
  function extractFilePath(toolArgs) {
135
169
  const path = toolArgs?.path;
136
- if (path && typeof path === 'string' && path.startsWith('/'))
170
+ if (path && typeof path === "string" && path.startsWith("/"))
137
171
  return path;
138
172
  const paths = toolArgs?.paths;
139
- if (paths?.[0] && typeof paths[0] === 'string' && paths[0].startsWith('/'))
173
+ if (paths?.[0] && typeof paths[0] === "string" && paths[0].startsWith("/"))
140
174
  return paths[0];
141
175
  const file = toolArgs?.file;
142
- if (file && typeof file === 'string' && file.startsWith('/'))
176
+ if (file && typeof file === "string" && file.startsWith("/"))
143
177
  return file;
144
178
  const mod = toolArgs?.module;
145
- if (mod && typeof mod === 'string' && mod.startsWith('/'))
179
+ if (mod && typeof mod === "string" && mod.startsWith("/"))
146
180
  return mod;
147
181
  return undefined;
148
182
  }
@@ -160,7 +194,7 @@ export async function createServer(projectRoot, options) {
160
194
  const readForEditCalled = new Set();
161
195
  // Detect context-mode companion
162
196
  const cmEnabled = config.contextMode.enabled;
163
- const contextModeStatus = await detectContextMode(projectRoot, cmEnabled === 'auto' ? undefined : cmEnabled);
197
+ const contextModeStatus = await detectContextMode(projectRoot, cmEnabled === "auto" ? undefined : cmEnabled);
164
198
  if (contextModeStatus.detected) {
165
199
  console.error(`[token-pilot] context-mode detected (source: ${contextModeStatus.source})`);
166
200
  }
@@ -193,14 +227,16 @@ export async function createServer(projectRoot, options) {
193
227
  });
194
228
  }
195
229
  // Read version from package.json
196
- let pkgVersion = '0.1.1';
230
+ let pkgVersion = "0.1.1";
197
231
  try {
198
- const pkgPath = new URL('../package.json', import.meta.url).pathname;
199
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
232
+ const pkgPath = new URL("../package.json", import.meta.url).pathname;
233
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
200
234
  pkgVersion = pkg.version;
201
235
  }
202
- catch { /* fallback to hardcoded */ }
203
- const server = new Server({ name: 'token-pilot', version: pkgVersion }, {
236
+ catch {
237
+ /* fallback to hardcoded */
238
+ }
239
+ const server = new Server({ name: "token-pilot", version: pkgVersion }, {
204
240
  capabilities: { tools: {} },
205
241
  instructions: MCP_INSTRUCTIONS,
206
242
  });
@@ -232,7 +268,7 @@ export async function createServer(projectRoot, options) {
232
268
  if (isFullReadTool(rest.tool)) {
233
269
  fullFileReadsCount++;
234
270
  }
235
- if (rest.tool === 'read_for_edit' && call.path) {
271
+ if (rest.tool === "read_for_edit" && call.path) {
236
272
  readForEditCalled.add(call.path);
237
273
  }
238
274
  // Policy check
@@ -251,30 +287,32 @@ export async function createServer(projectRoot, options) {
251
287
  // Auto-detect project root on first tool call (when startup root was /)
252
288
  // Tries: MCP roots → git detect from file path in args
253
289
  if (needsAutoDetect && !autoDetectDone) {
254
- const detectedPath = extractFilePath((args ?? {}));
290
+ const detectedPath = extractFilePath(args ?? {});
255
291
  await tryAutoDetectRoot(detectedPath);
256
292
  }
257
293
  try {
258
294
  switch (name) {
259
- case 'smart_read': {
295
+ case "smart_read": {
260
296
  const validArgs = validateSmartReadArgs(args);
297
+ const picked = pickRegistry(args);
261
298
  // Try non-code handler for JSON/YAML/MD etc.
262
299
  if (isNonCodeStructured(validArgs.path)) {
263
- const nonCodeResult = await handleNonCodeRead(validArgs.path, projectRoot, contextRegistry, {
300
+ const nonCodeResult = await handleNonCodeRead(validArgs.path, projectRoot, picked.reg, {
264
301
  contextModeStatus,
265
302
  largeNonCodeThreshold: config.contextMode.largeNonCodeThreshold,
266
303
  adviseDelegation: config.contextMode.adviseDelegation,
267
304
  });
268
305
  if (nonCodeResult) {
269
- const text = nonCodeResult.content[0]?.text ?? '';
306
+ const text = nonCodeResult.content[0]?.text ?? "";
270
307
  recordWithTrace({
271
- tool: 'smart_read',
308
+ tool: "smart_read",
272
309
  path: validArgs.path,
273
310
  tokensReturned: estimateTokens(text),
274
- tokensWouldBe: await fullFileTokens(validArgs.path) || estimateTokens(text),
311
+ tokensWouldBe: (await fullFileTokens(validArgs.path)) ||
312
+ estimateTokens(text),
275
313
  timestamp: Date.now(),
276
- delegatedToContextMode: text.includes('ADVISORY:') && text.includes('context-mode'),
277
- savingsCategory: 'compression',
314
+ delegatedToContextMode: text.includes("ADVISORY:") && text.includes("context-mode"),
315
+ savingsCategory: "compression",
278
316
  absPath: resolve(projectRoot, validArgs.path),
279
317
  args: validArgs,
280
318
  });
@@ -282,78 +320,151 @@ export async function createServer(projectRoot, options) {
282
320
  }
283
321
  }
284
322
  // Dedup is handled inside handleSmartRead (step 5)
285
- const result = await handleSmartRead(validArgs, projectRoot, astIndex, fileCache, contextRegistry, config);
286
- const text = result.content[0]?.text ?? '';
323
+ const result = await handleSmartRead(validArgs, projectRoot, astIndex, fileCache, picked.reg, config);
324
+ if (picked.sessionId && !picked.force) {
325
+ void sessionRegistries.flush(picked.sessionId);
326
+ }
327
+ const text = result.content[0]?.text ?? "";
287
328
  const fullTokensSR = await fullFileTokens(validArgs.path);
288
- const policyAdv = recordWithTrace({ tool: 'smart_read', path: validArgs.path, tokensReturned: estimateTokens(text), tokensWouldBe: fullTokensSR || estimateTokens(text), timestamp: Date.now(), savingsCategory: detectSavingsCategory(text), absPath: resolve(projectRoot, validArgs.path), args: validArgs });
329
+ const policyAdv = recordWithTrace({
330
+ tool: "smart_read",
331
+ path: validArgs.path,
332
+ tokensReturned: estimateTokens(text),
333
+ tokensWouldBe: fullTokensSR || estimateTokens(text),
334
+ timestamp: Date.now(),
335
+ savingsCategory: detectSavingsCategory(text),
336
+ absPath: resolve(projectRoot, validArgs.path),
337
+ args: validArgs,
338
+ });
289
339
  if (policyAdv)
290
- result.content[0] = { type: 'text', text: text + policyAdv };
340
+ result.content[0] = { type: "text", text: text + policyAdv };
291
341
  return result;
292
342
  }
293
- case 'read_symbol': {
343
+ case "read_symbol": {
294
344
  const symArgs = validateReadSymbolArgs(args);
345
+ const pickedSym = pickRegistry(args);
295
346
  // Dedup is handled inside handleReadSymbol
296
- const symResult = await handleReadSymbol(symArgs, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, config.smartRead.advisoryReminders);
297
- const symText = symResult.content[0]?.text ?? '';
347
+ const symResult = await handleReadSymbol(symArgs, projectRoot, symbolResolver, fileCache, pickedSym.reg, astIndex, config.smartRead.advisoryReminders);
348
+ if (pickedSym.sessionId && !pickedSym.force) {
349
+ void sessionRegistries.flush(pickedSym.sessionId);
350
+ }
351
+ const symText = symResult.content[0]?.text ?? "";
298
352
  const symTokens = estimateTokens(symText);
299
353
  const fullTokensSym = await fullFileTokens(symArgs.path);
300
- recordWithTrace({ tool: 'read_symbol', path: symArgs.path, tokensReturned: symTokens, tokensWouldBe: fullTokensSym || symTokens, timestamp: Date.now(), savingsCategory: detectSavingsCategory(symText), absPath: resolve(projectRoot, symArgs.path), args: symArgs });
354
+ recordWithTrace({
355
+ tool: "read_symbol",
356
+ path: symArgs.path,
357
+ tokensReturned: symTokens,
358
+ tokensWouldBe: fullTokensSym || symTokens,
359
+ timestamp: Date.now(),
360
+ savingsCategory: detectSavingsCategory(symText),
361
+ absPath: resolve(projectRoot, symArgs.path),
362
+ args: symArgs,
363
+ });
301
364
  return symResult;
302
365
  }
303
- case 'read_symbols': {
366
+ case "read_symbols": {
304
367
  const rsArgs = validateReadSymbolsArgs(args);
305
368
  const rsResult = await handleReadSymbols(rsArgs, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, config.smartRead.advisoryReminders);
306
- const rsText = rsResult.content[0]?.text ?? '';
369
+ const rsText = rsResult.content[0]?.text ?? "";
307
370
  const rsTokens = estimateTokens(rsText);
308
371
  const fullTokensRs = await fullFileTokens(rsArgs.path);
309
- recordWithTrace({ tool: 'read_symbols', path: rsArgs.path, tokensReturned: rsTokens, tokensWouldBe: fullTokensRs || rsTokens, timestamp: Date.now(), savingsCategory: 'compression', absPath: resolve(projectRoot, rsArgs.path), args: rsArgs });
372
+ recordWithTrace({
373
+ tool: "read_symbols",
374
+ path: rsArgs.path,
375
+ tokensReturned: rsTokens,
376
+ tokensWouldBe: fullTokensRs || rsTokens,
377
+ timestamp: Date.now(),
378
+ savingsCategory: "compression",
379
+ absPath: resolve(projectRoot, rsArgs.path),
380
+ args: rsArgs,
381
+ });
310
382
  return rsResult;
311
383
  }
312
- case 'read_range': {
384
+ case "read_range": {
313
385
  const rangeArgs = validateReadRangeArgs(args);
314
- const rangeResult = await handleReadRange(rangeArgs, projectRoot, fileCache, contextRegistry, config.smartRead.advisoryReminders);
315
- const rangeText = rangeResult.content[0]?.text ?? '';
386
+ const pickedRange = pickRegistry(args);
387
+ const rangeResult = await handleReadRange(rangeArgs, projectRoot, fileCache, pickedRange.reg, config.smartRead.advisoryReminders);
388
+ if (pickedRange.sessionId && !pickedRange.force) {
389
+ void sessionRegistries.flush(pickedRange.sessionId);
390
+ }
391
+ const rangeText = rangeResult.content[0]?.text ?? "";
316
392
  const rangeTokens = estimateTokens(rangeText);
317
393
  const fullTokensRange = await fullFileTokens(rangeArgs.path);
318
- recordWithTrace({ tool: 'read_range', path: rangeArgs.path, tokensReturned: rangeTokens, tokensWouldBe: fullTokensRange || rangeTokens, timestamp: Date.now(), savingsCategory: detectSavingsCategory(rangeText), absPath: resolve(projectRoot, rangeArgs.path), args: rangeArgs });
394
+ recordWithTrace({
395
+ tool: "read_range",
396
+ path: rangeArgs.path,
397
+ tokensReturned: rangeTokens,
398
+ tokensWouldBe: fullTokensRange || rangeTokens,
399
+ timestamp: Date.now(),
400
+ savingsCategory: detectSavingsCategory(rangeText),
401
+ absPath: resolve(projectRoot, rangeArgs.path),
402
+ args: rangeArgs,
403
+ });
319
404
  return rangeResult;
320
405
  }
321
- case 'read_section': {
406
+ case "read_section": {
322
407
  const secArgs = validateReadSectionArgs(args);
323
408
  const secResult = await handleReadSection(secArgs, projectRoot, contextRegistry);
324
- const secText = secResult.content[0]?.text ?? '';
409
+ const secText = secResult.content[0]?.text ?? "";
325
410
  const secTokens = estimateTokens(secText);
326
411
  const fullTokensSec = await fullFileTokens(secArgs.path);
327
412
  recordWithTrace({
328
- tool: 'read_section', path: secArgs.path,
329
- tokensReturned: secTokens, tokensWouldBe: fullTokensSec || secTokens,
330
- timestamp: Date.now(), savingsCategory: 'compression',
331
- absPath: resolve(projectRoot, secArgs.path), args: secArgs,
413
+ tool: "read_section",
414
+ path: secArgs.path,
415
+ tokensReturned: secTokens,
416
+ tokensWouldBe: fullTokensSec || secTokens,
417
+ timestamp: Date.now(),
418
+ savingsCategory: "compression",
419
+ absPath: resolve(projectRoot, secArgs.path),
420
+ args: secArgs,
332
421
  });
333
422
  return secResult;
334
423
  }
335
- case 'read_diff': {
424
+ case "read_diff": {
336
425
  const diffArgs = validateReadDiffArgs(args);
337
426
  const diffResult = await handleReadDiff(diffArgs, projectRoot, fileCache, contextRegistry);
338
- const diffText = diffResult.content[0]?.text ?? '';
427
+ const diffText = diffResult.content[0]?.text ?? "";
339
428
  const diffTokens = estimateTokens(diffText);
340
429
  const fullTokensDiff = await fullFileTokens(diffArgs.path);
341
- recordWithTrace({ tool: 'read_diff', path: diffArgs.path, tokensReturned: diffTokens, tokensWouldBe: fullTokensDiff || diffTokens, timestamp: Date.now(), savingsCategory: 'compression', absPath: resolve(projectRoot, diffArgs.path), args: diffArgs });
430
+ recordWithTrace({
431
+ tool: "read_diff",
432
+ path: diffArgs.path,
433
+ tokensReturned: diffTokens,
434
+ tokensWouldBe: fullTokensDiff || diffTokens,
435
+ timestamp: Date.now(),
436
+ savingsCategory: "compression",
437
+ absPath: resolve(projectRoot, diffArgs.path),
438
+ args: diffArgs,
439
+ });
342
440
  return diffResult;
343
441
  }
344
- case 'read_for_edit': {
442
+ case "read_for_edit": {
345
443
  const editArgs = validateReadForEditArgs(args);
346
444
  const editResult = await handleReadForEdit(editArgs, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, { actionableHints: config.display.actionableHints });
347
- const editText = editResult.content[0]?.text ?? '';
445
+ const editText = editResult.content[0]?.text ?? "";
348
446
  const editTokens = estimateTokens(editText);
349
447
  const fullTokensEdit = await fullFileTokens(editArgs.path);
350
- recordWithTrace({ tool: 'read_for_edit', path: editArgs.path, tokensReturned: editTokens, tokensWouldBe: fullTokensEdit || editTokens, timestamp: Date.now(), savingsCategory: 'compression', absPath: resolve(projectRoot, editArgs.path), args: editArgs });
448
+ recordWithTrace({
449
+ tool: "read_for_edit",
450
+ path: editArgs.path,
451
+ tokensReturned: editTokens,
452
+ tokensWouldBe: fullTokensEdit || editTokens,
453
+ timestamp: Date.now(),
454
+ savingsCategory: "compression",
455
+ absPath: resolve(projectRoot, editArgs.path),
456
+ args: editArgs,
457
+ });
351
458
  return editResult;
352
459
  }
353
- case 'smart_read_many': {
460
+ case "smart_read_many": {
354
461
  const manyArgs = validateSmartReadManyArgs(args);
355
- const manyResult = await handleSmartReadMany(manyArgs, projectRoot, astIndex, fileCache, contextRegistry, config);
356
- const manyText = manyResult.content[0]?.text ?? '';
462
+ const pickedMany = pickRegistry(args);
463
+ const manyResult = await handleSmartReadMany(manyArgs, projectRoot, astIndex, fileCache, pickedMany.reg, config);
464
+ if (pickedMany.sessionId && !pickedMany.force) {
465
+ void sessionRegistries.flush(pickedMany.sessionId);
466
+ }
467
+ const manyText = manyResult.content[0]?.text ?? "";
357
468
  const manyTokens = estimateTokens(manyText);
358
469
  const uniqueManyPaths = Array.from(new Set(manyArgs.paths));
359
470
  let fullTokensMany = 0;
@@ -361,241 +472,407 @@ export async function createServer(projectRoot, options) {
361
472
  fullTokensMany += await fullFileTokens(p);
362
473
  }
363
474
  recordWithTrace({
364
- tool: 'smart_read_many',
365
- path: uniqueManyPaths.join(', '),
475
+ tool: "smart_read_many",
476
+ path: uniqueManyPaths.join(", "),
366
477
  tokensReturned: manyTokens,
367
478
  tokensWouldBe: fullTokensMany || manyTokens,
368
479
  timestamp: Date.now(),
369
- savingsCategory: 'compression',
480
+ savingsCategory: "compression",
370
481
  args: manyArgs,
371
482
  });
372
483
  return manyResult;
373
484
  }
374
- case 'find_usages': {
485
+ case "find_usages": {
375
486
  const usagesArgs = validateFindUsagesArgs(args);
376
- const cachedUsages = sessionCache?.get('find_usages', usagesArgs);
487
+ const cachedUsages = sessionCache?.get("find_usages", usagesArgs);
377
488
  if (cachedUsages) {
378
- recordWithTrace({ tool: 'find_usages', path: usagesArgs.symbol, tokensReturned: cachedUsages.tokenEstimate, tokensWouldBe: cachedUsages.tokensWouldBe ?? cachedUsages.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: usagesArgs });
489
+ recordWithTrace({
490
+ tool: "find_usages",
491
+ path: usagesArgs.symbol,
492
+ tokensReturned: cachedUsages.tokenEstimate,
493
+ tokensWouldBe: cachedUsages.tokensWouldBe ?? cachedUsages.tokenEstimate,
494
+ timestamp: Date.now(),
495
+ sessionCacheHit: true,
496
+ savingsCategory: "cache",
497
+ args: usagesArgs,
498
+ });
379
499
  return cachedUsages.result;
380
500
  }
381
501
  const usagesResult = await handleFindUsages(usagesArgs, astIndex, projectRoot);
382
- const usagesText = usagesResult.content[0]?.text ?? '';
502
+ const usagesText = usagesResult.content[0]?.text ?? "";
383
503
  const usagesTokens = estimateTokens(usagesText);
384
504
  const usagesWouldBe = await estimateFindUsagesWorkflowTokens(usagesResult.meta.files);
385
- sessionCache?.set('find_usages', usagesArgs, usagesResult, {
386
- files: usagesResult.meta.files.map(f => resolve(projectRoot, f)),
505
+ sessionCache?.set("find_usages", usagesArgs, usagesResult, {
506
+ files: usagesResult.meta.files.map((f) => resolve(projectRoot, f)),
387
507
  dependsOnAst: true,
388
508
  }, usagesTokens, usagesWouldBe || usagesTokens);
389
509
  recordWithTrace({
390
- tool: 'find_usages',
510
+ tool: "find_usages",
391
511
  path: usagesArgs.symbol,
392
512
  tokensReturned: usagesTokens,
393
513
  tokensWouldBe: usagesWouldBe || usagesTokens,
394
514
  timestamp: Date.now(),
395
- savingsCategory: 'compression',
515
+ savingsCategory: "compression",
396
516
  args: usagesArgs,
397
517
  });
398
518
  return usagesResult;
399
519
  }
400
- case 'project_overview': {
520
+ case "project_overview": {
401
521
  const overviewArgs = validateProjectOverviewArgs(args);
402
- const cachedOverview = sessionCache?.get('project_overview', overviewArgs);
522
+ const cachedOverview = sessionCache?.get("project_overview", overviewArgs);
403
523
  if (cachedOverview) {
404
- recordWithTrace({ tool: 'project_overview', path: projectRoot, tokensReturned: cachedOverview.tokenEstimate, tokensWouldBe: cachedOverview.tokensWouldBe ?? cachedOverview.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: overviewArgs });
524
+ recordWithTrace({
525
+ tool: "project_overview",
526
+ path: projectRoot,
527
+ tokensReturned: cachedOverview.tokenEstimate,
528
+ tokensWouldBe: cachedOverview.tokensWouldBe ?? cachedOverview.tokenEstimate,
529
+ timestamp: Date.now(),
530
+ sessionCacheHit: true,
531
+ savingsCategory: "cache",
532
+ args: overviewArgs,
533
+ });
405
534
  return cachedOverview.result;
406
535
  }
407
536
  const overviewResult = await handleProjectOverview(overviewArgs, projectRoot, astIndex, pkgVersion);
408
- const overviewText = overviewResult.content[0]?.text ?? '';
409
- overviewResult.content[0] = { type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${overviewText}` };
537
+ const overviewText = overviewResult.content[0]?.text ?? "";
538
+ overviewResult.content[0] = {
539
+ type: "text",
540
+ text: `TOKEN PILOT v${pkgVersion}\n\n${overviewText}`,
541
+ };
410
542
  const ovTokens = estimateTokens(overviewResult.content[0].text);
411
- const overviewWouldBe = await estimateProjectOverviewWorkflowTokens(overviewArgs.include ?? ['stack', 'ci', 'quality', 'architecture']);
412
- sessionCache?.set('project_overview', overviewArgs, overviewResult, {
543
+ const overviewWouldBe = await estimateProjectOverviewWorkflowTokens(overviewArgs.include ?? ["stack", "ci", "quality", "architecture"]);
544
+ sessionCache?.set("project_overview", overviewArgs, overviewResult, {
413
545
  dependsOnAst: true,
414
546
  }, ovTokens, overviewWouldBe || ovTokens);
415
547
  recordWithTrace({
416
- tool: 'project_overview',
548
+ tool: "project_overview",
417
549
  path: projectRoot,
418
550
  tokensReturned: ovTokens,
419
551
  tokensWouldBe: overviewWouldBe || ovTokens,
420
552
  timestamp: Date.now(),
421
- savingsCategory: 'compression',
553
+ savingsCategory: "compression",
422
554
  args: overviewArgs,
423
555
  });
424
556
  return overviewResult;
425
557
  }
426
- case 'related_files': {
558
+ case "related_files": {
427
559
  const relArgs = validateRelatedFilesArgs(args);
428
- const cachedRel = sessionCache?.get('related_files', relArgs);
560
+ const cachedRel = sessionCache?.get("related_files", relArgs);
429
561
  if (cachedRel) {
430
- recordWithTrace({ tool: 'related_files', path: relArgs.path, tokensReturned: cachedRel.tokenEstimate, tokensWouldBe: cachedRel.tokensWouldBe ?? cachedRel.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', absPath: resolve(projectRoot, relArgs.path), args: relArgs });
562
+ recordWithTrace({
563
+ tool: "related_files",
564
+ path: relArgs.path,
565
+ tokensReturned: cachedRel.tokenEstimate,
566
+ tokensWouldBe: cachedRel.tokensWouldBe ?? cachedRel.tokenEstimate,
567
+ timestamp: Date.now(),
568
+ sessionCacheHit: true,
569
+ savingsCategory: "cache",
570
+ absPath: resolve(projectRoot, relArgs.path),
571
+ args: relArgs,
572
+ });
431
573
  return cachedRel.result;
432
574
  }
433
575
  const relResult = await handleRelatedFiles(relArgs, projectRoot, astIndex);
434
- const relText = relResult.content[0]?.text ?? '';
576
+ const relText = relResult.content[0]?.text ?? "";
435
577
  const relTokens = estimateTokens(relText);
436
578
  const relWouldBe = await estimateRelatedFilesWorkflowTokens(relArgs.path, relResult.meta);
437
579
  const relDeps = [
438
580
  resolve(projectRoot, relArgs.path),
439
- ...relResult.meta.imports.map(f => resolve(projectRoot, f)),
440
- ...relResult.meta.importedBy.map(f => resolve(projectRoot, f)),
441
- ...relResult.meta.tests.map(f => resolve(projectRoot, f)),
581
+ ...relResult.meta.imports.map((f) => resolve(projectRoot, f)),
582
+ ...relResult.meta.importedBy.map((f) => resolve(projectRoot, f)),
583
+ ...relResult.meta.tests.map((f) => resolve(projectRoot, f)),
442
584
  ];
443
- sessionCache?.set('related_files', relArgs, relResult, {
585
+ sessionCache?.set("related_files", relArgs, relResult, {
444
586
  files: relDeps,
445
587
  dependsOnAst: true,
446
588
  }, relTokens, relWouldBe || relTokens);
447
589
  recordWithTrace({
448
- tool: 'related_files',
590
+ tool: "related_files",
449
591
  path: relArgs.path,
450
592
  tokensReturned: relTokens,
451
593
  tokensWouldBe: relWouldBe || relTokens,
452
594
  timestamp: Date.now(),
453
- savingsCategory: 'compression',
595
+ savingsCategory: "compression",
454
596
  absPath: resolve(projectRoot, relArgs.path),
455
597
  args: relArgs,
456
598
  });
457
599
  return relResult;
458
600
  }
459
- case 'outline': {
601
+ case "outline": {
460
602
  const outlineArgs = validateOutlineArgs(args);
461
- const cachedOutline = sessionCache?.get('outline', outlineArgs);
603
+ const cachedOutline = sessionCache?.get("outline", outlineArgs);
462
604
  if (cachedOutline) {
463
- recordWithTrace({ tool: 'outline', path: outlineArgs.path, tokensReturned: cachedOutline.tokenEstimate, tokensWouldBe: cachedOutline.tokensWouldBe ?? cachedOutline.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: outlineArgs });
605
+ recordWithTrace({
606
+ tool: "outline",
607
+ path: outlineArgs.path,
608
+ tokensReturned: cachedOutline.tokenEstimate,
609
+ tokensWouldBe: cachedOutline.tokensWouldBe ?? cachedOutline.tokenEstimate,
610
+ timestamp: Date.now(),
611
+ sessionCacheHit: true,
612
+ savingsCategory: "cache",
613
+ args: outlineArgs,
614
+ });
464
615
  return cachedOutline.result;
465
616
  }
466
617
  const outlineResult = await handleOutline(outlineArgs, projectRoot, astIndex);
467
- const outlineText = outlineResult.content[0]?.text ?? '';
618
+ const outlineText = outlineResult.content[0]?.text ?? "";
468
619
  const outlineTokens = estimateTokens(outlineText);
469
620
  const outlineWouldBe = await estimateOutlineWorkflowTokens(outlineArgs.path, outlineArgs.recursive ?? false, outlineArgs.max_depth ?? 2);
470
- sessionCache?.set('outline', outlineArgs, outlineResult, {
471
- files: [resolve(projectRoot, outlineArgs.path) + '/'],
621
+ sessionCache?.set("outline", outlineArgs, outlineResult, {
622
+ files: [resolve(projectRoot, outlineArgs.path) + "/"],
472
623
  dependsOnAst: true,
473
624
  }, outlineTokens, outlineWouldBe || outlineTokens);
474
625
  recordWithTrace({
475
- tool: 'outline',
626
+ tool: "outline",
476
627
  path: outlineArgs.path,
477
628
  tokensReturned: outlineTokens,
478
629
  tokensWouldBe: outlineWouldBe || outlineTokens,
479
630
  timestamp: Date.now(),
480
- savingsCategory: 'compression',
631
+ savingsCategory: "compression",
481
632
  args: outlineArgs,
482
633
  });
483
634
  return outlineResult;
484
635
  }
485
- case 'session_analytics': {
636
+ case "session_analytics": {
486
637
  const verbose = args?.verbose === true;
487
- return { content: [{ type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${analytics.report(verbose)}` }] };
638
+ return {
639
+ content: [
640
+ {
641
+ type: "text",
642
+ text: `TOKEN PILOT v${pkgVersion}\n\n${analytics.report(verbose)}`,
643
+ },
644
+ ],
645
+ };
488
646
  }
489
- case 'find_unused': {
647
+ case "find_unused": {
490
648
  const unusedArgs = validateFindUnusedArgs(args);
491
- const cachedUnused = sessionCache?.get('find_unused', unusedArgs);
649
+ const cachedUnused = sessionCache?.get("find_unused", unusedArgs);
492
650
  if (cachedUnused) {
493
- recordWithTrace({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: cachedUnused.tokenEstimate, tokensWouldBe: cachedUnused.tokensWouldBe ?? cachedUnused.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: unusedArgs });
651
+ recordWithTrace({
652
+ tool: "find_unused",
653
+ path: unusedArgs.module ?? "all",
654
+ tokensReturned: cachedUnused.tokenEstimate,
655
+ tokensWouldBe: cachedUnused.tokensWouldBe ?? cachedUnused.tokenEstimate,
656
+ timestamp: Date.now(),
657
+ sessionCacheHit: true,
658
+ savingsCategory: "cache",
659
+ args: unusedArgs,
660
+ });
494
661
  return cachedUnused.result;
495
662
  }
496
663
  const unusedResult = await handleFindUnused(unusedArgs, astIndex);
497
- const unusedText = unusedResult.content[0]?.text ?? '';
664
+ const unusedText = unusedResult.content[0]?.text ?? "";
498
665
  const unusedTokens = estimateTokens(unusedText);
499
666
  const unusedWouldBe = await estimateFindUsagesWorkflowTokens(unusedResult.meta.files);
500
- sessionCache?.set('find_unused', unusedArgs, unusedResult, { dependsOnAst: true }, unusedTokens, unusedWouldBe || unusedTokens);
501
- recordWithTrace({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: unusedTokens, tokensWouldBe: unusedWouldBe || unusedTokens, timestamp: Date.now(), savingsCategory: 'compression', args: unusedArgs });
667
+ sessionCache?.set("find_unused", unusedArgs, unusedResult, { dependsOnAst: true }, unusedTokens, unusedWouldBe || unusedTokens);
668
+ recordWithTrace({
669
+ tool: "find_unused",
670
+ path: unusedArgs.module ?? "all",
671
+ tokensReturned: unusedTokens,
672
+ tokensWouldBe: unusedWouldBe || unusedTokens,
673
+ timestamp: Date.now(),
674
+ savingsCategory: "compression",
675
+ args: unusedArgs,
676
+ });
502
677
  return unusedResult;
503
678
  }
504
- case 'code_audit': {
679
+ case "code_audit": {
505
680
  const auditArgs = validateCodeAuditArgs(args);
506
- const cachedAudit = sessionCache?.get('code_audit', auditArgs);
681
+ const cachedAudit = sessionCache?.get("code_audit", auditArgs);
507
682
  if (cachedAudit) {
508
- recordWithTrace({ tool: 'code_audit', path: auditArgs.check, tokensReturned: cachedAudit.tokenEstimate, tokensWouldBe: cachedAudit.tokensWouldBe ?? cachedAudit.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: auditArgs });
683
+ recordWithTrace({
684
+ tool: "code_audit",
685
+ path: auditArgs.check,
686
+ tokensReturned: cachedAudit.tokenEstimate,
687
+ tokensWouldBe: cachedAudit.tokensWouldBe ?? cachedAudit.tokenEstimate,
688
+ timestamp: Date.now(),
689
+ sessionCacheHit: true,
690
+ savingsCategory: "cache",
691
+ args: auditArgs,
692
+ });
509
693
  return cachedAudit.result;
510
694
  }
511
695
  const auditResult = await handleCodeAudit(auditArgs, projectRoot, astIndex);
512
- const auditText = auditResult.content[0]?.text ?? '';
696
+ const auditText = auditResult.content[0]?.text ?? "";
513
697
  const auditTokens = estimateTokens(auditText);
514
698
  const auditWouldBe = await estimateFindUsagesWorkflowTokens(auditResult.meta.files);
515
- sessionCache?.set('code_audit', auditArgs, auditResult, { dependsOnAst: true }, auditTokens, auditWouldBe || auditTokens);
516
- recordWithTrace({ tool: 'code_audit', path: auditArgs.check, tokensReturned: auditTokens, tokensWouldBe: auditWouldBe || auditTokens, timestamp: Date.now(), savingsCategory: 'compression', args: auditArgs });
699
+ sessionCache?.set("code_audit", auditArgs, auditResult, { dependsOnAst: true }, auditTokens, auditWouldBe || auditTokens);
700
+ recordWithTrace({
701
+ tool: "code_audit",
702
+ path: auditArgs.check,
703
+ tokensReturned: auditTokens,
704
+ tokensWouldBe: auditWouldBe || auditTokens,
705
+ timestamp: Date.now(),
706
+ savingsCategory: "compression",
707
+ args: auditArgs,
708
+ });
517
709
  return auditResult;
518
710
  }
519
- case 'module_info': {
711
+ case "module_info": {
520
712
  const moduleArgs = validateModuleInfoArgs(args);
521
- const cachedModule = sessionCache?.get('module_info', moduleArgs);
713
+ const cachedModule = sessionCache?.get("module_info", moduleArgs);
522
714
  if (cachedModule) {
523
- recordWithTrace({ tool: 'module_info', path: moduleArgs.module, tokensReturned: cachedModule.tokenEstimate, tokensWouldBe: cachedModule.tokensWouldBe ?? cachedModule.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: moduleArgs });
715
+ recordWithTrace({
716
+ tool: "module_info",
717
+ path: moduleArgs.module,
718
+ tokensReturned: cachedModule.tokenEstimate,
719
+ tokensWouldBe: cachedModule.tokensWouldBe ?? cachedModule.tokenEstimate,
720
+ timestamp: Date.now(),
721
+ sessionCacheHit: true,
722
+ savingsCategory: "cache",
723
+ args: moduleArgs,
724
+ });
524
725
  return cachedModule.result;
525
726
  }
526
727
  const moduleResult = await handleModuleInfo(moduleArgs, projectRoot, astIndex);
527
- const moduleText = moduleResult.content[0]?.text ?? '';
728
+ const moduleText = moduleResult.content[0]?.text ?? "";
528
729
  const moduleTokens = estimateTokens(moduleText);
529
730
  const moduleWouldBe = await estimateFindUsagesWorkflowTokens(moduleResult.meta.files);
530
- sessionCache?.set('module_info', moduleArgs, moduleResult, { dependsOnAst: true }, moduleTokens, moduleWouldBe || moduleTokens);
531
- recordWithTrace({ tool: 'module_info', path: moduleArgs.module, tokensReturned: moduleTokens, tokensWouldBe: moduleWouldBe || moduleTokens, timestamp: Date.now(), savingsCategory: 'compression', args: moduleArgs });
731
+ sessionCache?.set("module_info", moduleArgs, moduleResult, { dependsOnAst: true }, moduleTokens, moduleWouldBe || moduleTokens);
732
+ recordWithTrace({
733
+ tool: "module_info",
734
+ path: moduleArgs.module,
735
+ tokensReturned: moduleTokens,
736
+ tokensWouldBe: moduleWouldBe || moduleTokens,
737
+ timestamp: Date.now(),
738
+ savingsCategory: "compression",
739
+ args: moduleArgs,
740
+ });
532
741
  return moduleResult;
533
742
  }
534
- case 'smart_diff': {
743
+ case "smart_diff": {
535
744
  const sdArgs = validateSmartDiffArgs(args);
536
745
  const sdResult = await handleSmartDiff(sdArgs, projectRoot, astIndex);
537
- const sdText = sdResult.content[0]?.text ?? '';
746
+ const sdText = sdResult.content[0]?.text ?? "";
538
747
  const sdTokens = estimateTokens(sdText);
539
- recordWithTrace({ tool: 'smart_diff', path: sdArgs.path ?? sdArgs.scope ?? 'unstaged', tokensReturned: sdTokens, tokensWouldBe: sdResult.rawTokens || sdTokens, timestamp: Date.now(), savingsCategory: 'compression', args: sdArgs });
748
+ recordWithTrace({
749
+ tool: "smart_diff",
750
+ path: sdArgs.path ?? sdArgs.scope ?? "unstaged",
751
+ tokensReturned: sdTokens,
752
+ tokensWouldBe: sdResult.rawTokens || sdTokens,
753
+ timestamp: Date.now(),
754
+ savingsCategory: "compression",
755
+ args: sdArgs,
756
+ });
540
757
  return { content: sdResult.content };
541
758
  }
542
- case 'explore_area': {
759
+ case "explore_area": {
543
760
  const eaArgs = validateExploreAreaArgs(args);
544
- const cachedEa = sessionCache?.get('explore_area', eaArgs);
761
+ const cachedEa = sessionCache?.get("explore_area", eaArgs);
545
762
  if (cachedEa) {
546
- recordWithTrace({ tool: 'explore_area', path: eaArgs.path, tokensReturned: cachedEa.tokenEstimate, tokensWouldBe: cachedEa.tokensWouldBe ?? cachedEa.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: eaArgs });
763
+ recordWithTrace({
764
+ tool: "explore_area",
765
+ path: eaArgs.path,
766
+ tokensReturned: cachedEa.tokenEstimate,
767
+ tokensWouldBe: cachedEa.tokensWouldBe ?? cachedEa.tokenEstimate,
768
+ timestamp: Date.now(),
769
+ sessionCacheHit: true,
770
+ savingsCategory: "cache",
771
+ args: eaArgs,
772
+ });
547
773
  return cachedEa.result;
548
774
  }
549
775
  const eaResult = await handleExploreArea(eaArgs, projectRoot, astIndex);
550
- const eaText = eaResult.content[0]?.text ?? '';
776
+ const eaText = eaResult.content[0]?.text ?? "";
551
777
  const eaTokens = estimateTokens(eaText);
552
778
  const eaWouldBe = await estimateExploreAreaWorkflowTokens(eaResult.meta);
553
- sessionCache?.set('explore_area', eaArgs, eaResult, {
554
- files: [resolve(projectRoot, eaArgs.path) + '/'],
779
+ sessionCache?.set("explore_area", eaArgs, eaResult, {
780
+ files: [resolve(projectRoot, eaArgs.path) + "/"],
555
781
  dependsOnAst: true,
556
782
  dependsOnGit: true,
557
783
  }, eaTokens, eaWouldBe || eaTokens);
558
784
  recordWithTrace({
559
- tool: 'explore_area',
785
+ tool: "explore_area",
560
786
  path: eaArgs.path,
561
787
  tokensReturned: eaTokens,
562
788
  tokensWouldBe: eaWouldBe || eaTokens,
563
789
  timestamp: Date.now(),
564
- savingsCategory: 'compression',
790
+ savingsCategory: "compression",
565
791
  args: eaArgs,
566
792
  });
567
793
  return eaResult;
568
794
  }
569
- case 'smart_log': {
795
+ case "smart_log": {
570
796
  const slArgs = validateSmartLogArgs(args);
571
797
  const slResult = await handleSmartLog(slArgs, projectRoot);
572
- const slText = slResult.content[0]?.text ?? '';
798
+ const slText = slResult.content[0]?.text ?? "";
573
799
  const slTokens = estimateTokens(slText);
574
- recordWithTrace({ tool: 'smart_log', path: slArgs.path ?? 'all', tokensReturned: slTokens, tokensWouldBe: slResult.rawTokens || slTokens, timestamp: Date.now(), savingsCategory: 'compression', args: slArgs });
800
+ recordWithTrace({
801
+ tool: "smart_log",
802
+ path: slArgs.path ?? "all",
803
+ tokensReturned: slTokens,
804
+ tokensWouldBe: slResult.rawTokens || slTokens,
805
+ timestamp: Date.now(),
806
+ savingsCategory: "compression",
807
+ args: slArgs,
808
+ });
575
809
  return { content: slResult.content };
576
810
  }
577
- case 'test_summary': {
811
+ case "test_summary": {
578
812
  const tsArgs = validateTestSummaryArgs(args);
579
813
  const tsResult = await handleTestSummary(tsArgs, projectRoot);
580
- const tsText = tsResult.content[0]?.text ?? '';
814
+ const tsText = tsResult.content[0]?.text ?? "";
581
815
  const tsTokens = estimateTokens(tsText);
582
- recordWithTrace({ tool: 'test_summary', path: tsArgs.command, tokensReturned: tsTokens, tokensWouldBe: tsResult.rawTokens || tsTokens, timestamp: Date.now(), savingsCategory: 'compression', args: tsArgs });
816
+ recordWithTrace({
817
+ tool: "test_summary",
818
+ path: tsArgs.command,
819
+ tokensReturned: tsTokens,
820
+ tokensWouldBe: tsResult.rawTokens || tsTokens,
821
+ timestamp: Date.now(),
822
+ savingsCategory: "compression",
823
+ args: tsArgs,
824
+ });
583
825
  return { content: tsResult.content };
584
826
  }
585
- case 'session_snapshot': {
827
+ case "session_snapshot": {
586
828
  const snapshotArgs = args;
587
829
  if (!snapshotArgs.goal) {
588
- return { content: [{ type: 'text', text: 'Error: goal is required' }], isError: true };
830
+ return {
831
+ content: [{ type: "text", text: "Error: goal is required" }],
832
+ isError: true,
833
+ };
589
834
  }
590
835
  const snapshotResult = handleSessionSnapshot(snapshotArgs);
591
- const snapshotText = snapshotResult.content[0]?.text ?? '';
836
+ const snapshotText = snapshotResult.content[0]?.text ?? "";
592
837
  const snapshotTokens = estimateTokens(snapshotText);
593
- recordWithTrace({ tool: 'session_snapshot', tokensReturned: snapshotTokens, tokensWouldBe: snapshotTokens, timestamp: Date.now(), savingsCategory: 'compression' });
838
+ // TP-340: persist to .token-pilot/snapshots/ unless caller opts out.
839
+ if (snapshotArgs.persist !== false) {
840
+ try {
841
+ await persistSnapshot({ projectRoot, body: snapshotText });
842
+ }
843
+ catch {
844
+ /* best-effort — never fail the tool call */
845
+ }
846
+ }
847
+ recordWithTrace({
848
+ tool: "session_snapshot",
849
+ tokensReturned: snapshotTokens,
850
+ tokensWouldBe: snapshotTokens,
851
+ timestamp: Date.now(),
852
+ savingsCategory: "compression",
853
+ });
594
854
  return { content: snapshotResult.content };
595
855
  }
856
+ case "session_budget": {
857
+ const budgetArgs = args;
858
+ const budgetResult = await handleSessionBudget({ sessionId: budgetArgs.sessionId ?? "" }, projectRoot, {
859
+ baseThreshold: config.hooks.denyThreshold,
860
+ adaptiveThreshold: config.hooks.adaptiveThreshold,
861
+ adaptiveBudgetTokens: config.hooks.adaptiveBudgetTokens,
862
+ });
863
+ const budgetTokens = estimateTokens(budgetResult.content[0]?.text ?? "");
864
+ recordWithTrace({
865
+ tool: "session_budget",
866
+ tokensReturned: budgetTokens,
867
+ tokensWouldBe: budgetTokens,
868
+ timestamp: Date.now(),
869
+ savingsCategory: "compression",
870
+ });
871
+ return { content: budgetResult.content };
872
+ }
596
873
  default:
597
874
  return {
598
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
875
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
599
876
  isError: true,
600
877
  };
601
878
  }
@@ -603,7 +880,7 @@ export async function createServer(projectRoot, options) {
603
880
  catch (err) {
604
881
  const message = err instanceof Error ? err.message : String(err);
605
882
  return {
606
- content: [{ type: 'text', text: `Error: ${message}` }],
883
+ content: [{ type: "text", text: `Error: ${message}` }],
607
884
  isError: true,
608
885
  };
609
886
  }