token-pilot 0.14.2 → 0.16.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 (39) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +25 -8
  3. package/dist/ast-index/client.d.ts +2 -89
  4. package/dist/ast-index/client.js +49 -742
  5. package/dist/ast-index/enricher.d.ts +10 -0
  6. package/dist/ast-index/enricher.js +202 -0
  7. package/dist/ast-index/parser.d.ts +31 -0
  8. package/dist/ast-index/parser.js +340 -0
  9. package/dist/ast-index/regex-parser-python.d.ts +8 -0
  10. package/dist/ast-index/regex-parser-python.js +132 -0
  11. package/dist/ast-index/regex-parser.d.ts +8 -0
  12. package/dist/ast-index/regex-parser.js +118 -0
  13. package/dist/config/defaults.js +1 -0
  14. package/dist/core/session-analytics.d.ts +2 -2
  15. package/dist/core/session-analytics.js +78 -61
  16. package/dist/core/symbol-resolver.d.ts +0 -1
  17. package/dist/core/symbol-resolver.js +3 -12
  18. package/dist/core/validation.d.ts +12 -0
  19. package/dist/core/validation.js +62 -2
  20. package/dist/handlers/code-audit.js +2 -2
  21. package/dist/handlers/find-unused.js +1 -1
  22. package/dist/handlers/find-usages.d.ts +1 -1
  23. package/dist/handlers/find-usages.js +93 -25
  24. package/dist/handlers/read-for-edit.d.ts +1 -0
  25. package/dist/handlers/read-for-edit.js +65 -0
  26. package/dist/handlers/read-symbols.d.ts +18 -0
  27. package/dist/handlers/read-symbols.js +142 -0
  28. package/dist/handlers/smart-diff.js +23 -0
  29. package/dist/handlers/smart-read.js +14 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +6 -5
  32. package/dist/server/token-estimates.d.ts +31 -0
  33. package/dist/server/token-estimates.js +204 -0
  34. package/dist/server/tool-definitions.d.ts +1070 -0
  35. package/dist/server/tool-definitions.js +316 -0
  36. package/dist/server.js +23 -480
  37. package/dist/types.d.ts +1 -0
  38. package/package.json +1 -1
  39. package/skills/guide/SKILL.md +64 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Regex-based fallback parser for Python.
3
+ * Used when the ast-index binary is unavailable.
4
+ * Extracts top-level symbols (classes, functions, variables) and class methods.
5
+ */
6
+ const PY_RESERVED = new Set([
7
+ 'if', 'else', 'elif', 'for', 'while', 'try', 'except', 'finally',
8
+ 'with', 'return', 'yield', 'raise', 'pass', 'break', 'continue',
9
+ 'import', 'from', 'as', 'del', 'assert', 'lambda', 'not', 'and', 'or',
10
+ ]);
11
+ // Top-level patterns (indent 0)
12
+ const PY_CLASS_RE = /^class\s+(\w+)\s*[\(:]/;
13
+ const PY_FUNC_RE = /^(?:async\s+)?def\s+(\w+)\s*\(/;
14
+ const PY_ASSIGN_RE = /^([A-Z][A-Z_0-9]+)\s*[=:]/; // MODULE_CONSTANT = ...
15
+ // Method patterns inside class body (indented 4+ spaces or 1+ tab)
16
+ const PY_METHOD_RE = /^(\s{4,}|\t+)(?:async\s+)?def\s+(\w+)\s*\(/;
17
+ // Decorator
18
+ const PY_DECORATOR_RE = /^@(\w[\w.]*)/;
19
+ export function parsePythonRegex(content) {
20
+ const lines = content.split('\n');
21
+ const entries = [];
22
+ let currentClass = null;
23
+ let pendingDecorators = [];
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const line = lines[i];
26
+ const lineNum = i + 1;
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith('#'))
29
+ continue;
30
+ const indent = line.match(/^(\s*)/)?.[0].length ?? 0;
31
+ // Collect decorators
32
+ const decMatch = trimmed.match(PY_DECORATOR_RE);
33
+ if (decMatch) {
34
+ pendingDecorators.push(`@${decMatch[1]}`);
35
+ continue;
36
+ }
37
+ // Close current class when we hit a non-empty line at indent 0
38
+ // that is a new definition (not a continuation)
39
+ if (currentClass && indent === 0 && (PY_CLASS_RE.test(trimmed) || PY_FUNC_RE.test(trimmed) || PY_ASSIGN_RE.test(trimmed))) {
40
+ currentClass.end_line = lineNum - 1;
41
+ currentClass = null;
42
+ }
43
+ // Top-level class
44
+ if (indent === 0) {
45
+ const classMatch = trimmed.match(PY_CLASS_RE);
46
+ if (classMatch && !PY_RESERVED.has(classMatch[1])) {
47
+ const entry = {
48
+ name: classMatch[1],
49
+ kind: 'class',
50
+ start_line: pendingDecorators.length > 0 ? lineNum - pendingDecorators.length : lineNum,
51
+ end_line: 0,
52
+ signature: trimmed.slice(0, 120),
53
+ decorators: pendingDecorators.length > 0 ? [...pendingDecorators] : undefined,
54
+ children: [],
55
+ };
56
+ entries.push(entry);
57
+ currentClass = entry;
58
+ pendingDecorators = [];
59
+ continue;
60
+ }
61
+ // Top-level function
62
+ const funcMatch = trimmed.match(PY_FUNC_RE);
63
+ if (funcMatch && !PY_RESERVED.has(funcMatch[1])) {
64
+ entries.push({
65
+ name: funcMatch[1],
66
+ kind: 'function',
67
+ start_line: pendingDecorators.length > 0 ? lineNum - pendingDecorators.length : lineNum,
68
+ end_line: 0,
69
+ signature: trimmed.slice(0, 120),
70
+ is_async: trimmed.startsWith('async '),
71
+ decorators: pendingDecorators.length > 0 ? [...pendingDecorators] : undefined,
72
+ });
73
+ pendingDecorators = [];
74
+ continue;
75
+ }
76
+ // Module-level constant
77
+ const assignMatch = trimmed.match(PY_ASSIGN_RE);
78
+ if (assignMatch) {
79
+ entries.push({
80
+ name: assignMatch[1],
81
+ kind: 'variable',
82
+ start_line: lineNum,
83
+ end_line: lineNum,
84
+ signature: trimmed.slice(0, 120),
85
+ });
86
+ pendingDecorators = [];
87
+ continue;
88
+ }
89
+ }
90
+ // Methods inside class body
91
+ if (currentClass && indent >= 4) {
92
+ const methodMatch = line.match(PY_METHOD_RE);
93
+ if (methodMatch) {
94
+ const name = methodMatch[2];
95
+ if (name && !PY_RESERVED.has(name)) {
96
+ const decoratorStart = pendingDecorators.length > 0 ? lineNum - pendingDecorators.length : lineNum;
97
+ currentClass.children.push({
98
+ name,
99
+ kind: 'method',
100
+ start_line: decoratorStart,
101
+ end_line: lineNum + 5,
102
+ signature: trimmed.slice(0, 120),
103
+ is_async: trimmed.includes('async '),
104
+ visibility: name.startsWith('__') && name.endsWith('__') ? 'public'
105
+ : name.startsWith('_') ? 'private' : 'public',
106
+ decorators: pendingDecorators.length > 0 ? [...pendingDecorators] : undefined,
107
+ });
108
+ pendingDecorators = [];
109
+ }
110
+ continue;
111
+ }
112
+ }
113
+ // Non-matching line resets decorators
114
+ if (!decMatch) {
115
+ pendingDecorators = [];
116
+ }
117
+ }
118
+ // Close last class
119
+ if (currentClass) {
120
+ currentClass.end_line = lines.length;
121
+ }
122
+ // Fill in end_line for entries
123
+ for (let i = 0; i < entries.length; i++) {
124
+ if (entries[i].end_line === 0) {
125
+ entries[i].end_line = i < entries.length - 1
126
+ ? entries[i + 1].start_line - 1
127
+ : lines.length;
128
+ }
129
+ }
130
+ return entries;
131
+ }
132
+ //# sourceMappingURL=regex-parser-python.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Regex-based fallback parser for TypeScript/JavaScript.
3
+ * Used when the ast-index binary is unavailable.
4
+ * Extracts top-level symbols and class/interface members.
5
+ */
6
+ import type { AstIndexOutlineEntry } from './types.js';
7
+ export declare function parseTypeScriptRegex(content: string): AstIndexOutlineEntry[];
8
+ //# sourceMappingURL=regex-parser.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Regex-based fallback parser for TypeScript/JavaScript.
3
+ * Used when the ast-index binary is unavailable.
4
+ * Extracts top-level symbols and class/interface members.
5
+ */
6
+ const RESERVED = new Set([
7
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'return', 'throw', 'try', 'catch',
8
+ 'const', 'let', 'var', 'import', 'export', 'default', 'new', 'typeof', 'instanceof',
9
+ 'await', 'yield', 'delete', 'void', 'in', 'of', 'case', 'break', 'continue',
10
+ ]);
11
+ // Top-level declaration patterns (matched against trimmed line at indent 0)
12
+ const TOP_LEVEL = [
13
+ { re: /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/, kind: 'class' },
14
+ { re: /^(?:export\s+)?interface\s+(\w+)/, kind: 'interface' },
15
+ { re: /^(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+(\w+)/, kind: 'enum' },
16
+ { re: /^(?:export\s+)?type\s+(\w+)\s*[=<]/, kind: 'type' },
17
+ { re: /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)/, kind: 'function' },
18
+ // const/let arrow or function expression: export const foo = (async)? (fn | arrow)
19
+ { re: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::[^=\n]+)?\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*=>|<)/, kind: 'function' },
20
+ ];
21
+ // Method pattern inside class/interface body (indented 2+ spaces)
22
+ const METHOD_RE = /^(\s{2,})(?:(?:public|private|protected|static|abstract|override|readonly|async)\s+)*(?:get\s+|set\s+)?(\w+)\s*(?:<[^(]*>)?\s*\(/;
23
+ export function parseTypeScriptRegex(content) {
24
+ const lines = content.split('\n');
25
+ const entries = [];
26
+ // Track which class/interface we're currently inside using brace depth
27
+ let braceDepth = 0;
28
+ let currentClass = null;
29
+ let classOpenDepth = -1;
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i];
32
+ const lineNum = i + 1;
33
+ const trimmed = line.trim();
34
+ if (!trimmed)
35
+ continue;
36
+ // Count brace changes on this line (skip strings/comments roughly)
37
+ for (let j = 0; j < line.length; j++) {
38
+ const ch = line[j];
39
+ if (ch === '/' && line[j + 1] === '/')
40
+ break; // line comment
41
+ if (ch === '{')
42
+ braceDepth++;
43
+ else if (ch === '}')
44
+ braceDepth--;
45
+ }
46
+ // Close current class when we return to its opening depth
47
+ if (currentClass && braceDepth <= classOpenDepth) {
48
+ currentClass.end_line = lineNum;
49
+ currentClass = null;
50
+ classOpenDepth = -1;
51
+ }
52
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
53
+ continue;
54
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
55
+ // Top-level declarations (indent 0 only)
56
+ if (indent === 0) {
57
+ let hit = false;
58
+ for (const p of TOP_LEVEL) {
59
+ const m = trimmed.match(p.re);
60
+ if (!m)
61
+ continue;
62
+ const name = m[1];
63
+ if (!name || RESERVED.has(name))
64
+ continue;
65
+ const entry = {
66
+ name,
67
+ kind: p.kind,
68
+ start_line: lineNum,
69
+ end_line: 0,
70
+ signature: trimmed.slice(0, 120),
71
+ children: p.kind === 'class' || p.kind === 'interface' || p.kind === 'enum' ? [] : undefined,
72
+ };
73
+ entries.push(entry);
74
+ if (entry.children !== undefined) {
75
+ currentClass = entry;
76
+ // Class opens on this line — brace depth after counting this line
77
+ classOpenDepth = braceDepth - 1;
78
+ }
79
+ hit = true;
80
+ break;
81
+ }
82
+ if (hit)
83
+ continue;
84
+ }
85
+ // Method declarations inside class/interface body
86
+ if (currentClass && indent >= 2) {
87
+ const m = line.match(METHOD_RE);
88
+ if (m) {
89
+ const name = m[2];
90
+ if (name && !RESERVED.has(name)) {
91
+ // Exclude arrow assignments (those have = before the ()
92
+ // and plain function calls like this.foo() or obj.method()
93
+ const beforeParen = line.indexOf('(');
94
+ const segment = line.slice(0, beforeParen);
95
+ if (!segment.includes('.') && !segment.includes('=')) {
96
+ currentClass.children.push({
97
+ name,
98
+ kind: 'method',
99
+ start_line: lineNum,
100
+ end_line: lineNum + 5,
101
+ signature: trimmed.slice(0, 120),
102
+ });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ // Fill in end_line for top-level entries that didn't close via brace tracking
109
+ for (let i = 0; i < entries.length; i++) {
110
+ if (entries[i].end_line === 0) {
111
+ entries[i].end_line = i < entries.length - 1
112
+ ? entries[i + 1].start_line - 1
113
+ : lines.length;
114
+ }
115
+ }
116
+ return entries;
117
+ }
118
+ //# sourceMappingURL=regex-parser.js.map
@@ -21,6 +21,7 @@ export const DEFAULT_CONFIG = {
21
21
  enabled: true,
22
22
  interceptRead: true,
23
23
  autoInstall: true,
24
+ denyThreshold: 300,
24
25
  },
25
26
  context: {
26
27
  estimateTokens: true,
@@ -26,9 +26,9 @@ export declare class SessionAnalytics {
26
26
  setContextModeStatus(status: ContextModeStatus): void;
27
27
  record(call: ToolCall): void;
28
28
  /**
29
- * Generate a session report.
29
+ * Generate session report. Compact by default (~5 lines), verbose=true for full breakdown.
30
30
  */
31
- report(): string;
31
+ report(verbose?: boolean): string;
32
32
  reset(): void;
33
33
  }
34
34
  //# sourceMappingURL=session-analytics.d.ts.map
@@ -15,76 +15,102 @@ export class SessionAnalytics {
15
15
  this.calls.push(call);
16
16
  }
17
17
  /**
18
- * Generate a session report.
18
+ * Generate session report. Compact by default (~5 lines), verbose=true for full breakdown.
19
19
  */
20
- report() {
20
+ report(verbose = false) {
21
21
  const duration = formatDuration(Date.now() - this.sessionStart);
22
22
  const totalReturned = this.calls.reduce((s, c) => s + c.tokensReturned, 0);
23
23
  const totalWouldBe = this.calls.reduce((s, c) => s + c.tokensWouldBe, 0);
24
24
  const saved = totalWouldBe > 0 ? Math.round((1 - totalReturned / totalWouldBe) * 100) : 0;
25
- // Group by tool
25
+ // --- Shared data ---
26
26
  const byTool = new Map();
27
27
  for (const c of this.calls) {
28
- const existing = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0, wouldBe: 0 };
29
- existing.count++;
30
- existing.tokens += c.tokensReturned;
31
- existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
32
- existing.wouldBe += c.tokensWouldBe;
33
- byTool.set(c.tool, existing);
28
+ const e = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0, wouldBe: 0 };
29
+ e.count++;
30
+ e.tokens += c.tokensReturned;
31
+ e.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
32
+ e.wouldBe += c.tokensWouldBe;
33
+ byTool.set(c.tool, e);
34
34
  }
35
- const lines = [
36
- `SESSION ANALYTICS (${duration})`,
37
- '',
38
- `Total tool calls: ${this.calls.length}`,
39
- `Tokens returned: ~${totalReturned}`,
40
- `Tokens saved: ~${totalWouldBe - totalReturned} (${saved}% reduction)`,
41
- '',
42
- 'By tool:',
43
- ];
44
35
  const sortedTools = Array.from(byTool.entries()).sort((a, b) => b[1].saved - a[1].saved);
45
- for (const [tool, stats] of sortedTools) {
46
- const reduction = stats.wouldBe > 0
47
- ? Math.round((1 - stats.tokens / stats.wouldBe) * 100)
48
- : 0;
49
- lines.push(` ${tool}: ${stats.count} calls, ~${stats.tokens} tokens returned, ~${stats.saved} saved (${reduction}% reduction)`);
50
- }
51
- // Top files by savings
52
36
  const byFile = new Map();
53
37
  for (const c of this.calls) {
54
38
  if (c.path) {
55
- const current = byFile.get(c.path) ?? 0;
56
- byFile.set(c.path, current + Math.max(0, c.tokensWouldBe - c.tokensReturned));
39
+ byFile.set(c.path, (byFile.get(c.path) ?? 0) + Math.max(0, c.tokensWouldBe - c.tokensReturned));
57
40
  }
58
41
  }
59
- const topFiles = Array.from(byFile.entries())
60
- .sort((a, b) => b[1] - a[1])
61
- .slice(0, 5);
42
+ const cacheHits = this.calls.filter(c => c.sessionCacheHit);
43
+ // --- Compact report ---
44
+ const tokensSaved = totalWouldBe - totalReturned;
45
+ const lines = [
46
+ `SESSION ANALYTICS (${duration})`,
47
+ `Calls: ${this.calls.length} · Tokens returned: ~${totalReturned} · Saved: ~${tokensSaved} (${saved}%)`,
48
+ ];
49
+ if (this.calls.length > 0) {
50
+ const toolParts = sortedTools.slice(0, 5).map(([tool, s]) => {
51
+ const pct = s.wouldBe > 0 ? Math.round((1 - s.tokens / s.wouldBe) * 100) : 0;
52
+ return `${tool} ${s.count}× (${pct}%)`;
53
+ });
54
+ lines.push(`Tools: ${toolParts.join(' · ')}`);
55
+ }
56
+ const topFiles = Array.from(byFile.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3);
62
57
  if (topFiles.length > 0) {
58
+ const fileParts = topFiles.map(([f, s]) => `${f} ~${s}`);
59
+ lines.push(`Top files: ${fileParts.join(' · ')}`);
60
+ }
61
+ const extras = [];
62
+ if (cacheHits.length > 0) {
63
+ const hitRate = Math.round(cacheHits.length / this.calls.length * 100);
64
+ extras.push(`Cache: ${cacheHits.length}/${this.calls.length} hits (${hitRate}%)`);
65
+ }
66
+ if (this.contextModeStatus.detected) {
67
+ extras.push(`context-mode: active (${this.contextModeStatus.source})`);
68
+ }
69
+ if (extras.length > 0) {
70
+ lines.push(extras.join(' · '));
71
+ }
72
+ if (!verbose)
73
+ return lines.join('\n');
74
+ // --- Verbose additions ---
75
+ lines.push('');
76
+ lines.push('--- DETAILED BREAKDOWN ---');
77
+ // Full per-tool table
78
+ if (sortedTools.length > 0) {
79
+ lines.push('');
80
+ lines.push('By tool:');
81
+ for (const [tool, stats] of sortedTools) {
82
+ const reduction = stats.wouldBe > 0 ? Math.round((1 - stats.tokens / stats.wouldBe) * 100) : 0;
83
+ lines.push(` ${tool}: ${stats.count} calls, ~${stats.tokens} tokens returned, ~${stats.saved} saved (${reduction}%)`);
84
+ }
85
+ }
86
+ // Top 5 files
87
+ const allTopFiles = Array.from(byFile.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5);
88
+ if (allTopFiles.length > 0) {
63
89
  lines.push('');
64
90
  lines.push('Top files by savings:');
65
- for (const [file, saved] of topFiles) {
66
- lines.push(` ${file}: ~${saved} tokens saved`);
91
+ for (const [file, fileSaved] of allTopFiles) {
92
+ lines.push(` ${file}: ~${fileSaved} tokens saved`);
67
93
  }
68
94
  }
69
- const lowValueTools = sortedTools
95
+ // Low-value tools
96
+ const lowValue = sortedTools
70
97
  .map(([tool, stats]) => ({
71
98
  tool,
72
99
  reduction: stats.wouldBe > 0 ? Math.round((1 - stats.tokens / stats.wouldBe) * 100) : 0,
73
100
  count: stats.count,
74
101
  }))
75
- .filter((tool) => tool.reduction < 20);
76
- if (lowValueTools.length > 0) {
102
+ .filter(t => t.reduction < 20);
103
+ if (lowValue.length > 0) {
77
104
  lines.push('');
78
105
  lines.push('Needs improvement:');
79
- for (const tool of lowValueTools.slice(0, 5)) {
80
- lines.push(` ${tool.tool}: only ${tool.reduction}% reduction across ${tool.count} call${tool.count === 1 ? '' : 's'}`);
106
+ for (const t of lowValue.slice(0, 5)) {
107
+ lines.push(` ${t.tool}: only ${t.reduction}% reduction across ${t.count} call${t.count === 1 ? '' : 's'}`);
81
108
  }
82
109
  }
83
- // Savings breakdown by category
110
+ // Savings by category
84
111
  const byCategory = { compression: 0, cache: 0, dedup: 0, none: 0 };
85
112
  for (const c of this.calls) {
86
- const cat = c.savingsCategory ?? 'none';
87
- byCategory[cat] += Math.max(0, c.tokensWouldBe - c.tokensReturned);
113
+ byCategory[c.savingsCategory ?? 'none'] += Math.max(0, c.tokensWouldBe - c.tokensReturned);
88
114
  }
89
115
  if (totalWouldBe > totalReturned) {
90
116
  lines.push('');
@@ -96,35 +122,27 @@ export class SessionAnalytics {
96
122
  if (byCategory.dedup > 0)
97
123
  lines.push(` Dedup (already in context): ~${byCategory.dedup} tokens`);
98
124
  }
99
- // Session cache hits
100
- const cacheHits = this.calls.filter(c => c.sessionCacheHit);
125
+ // Session cache detail
101
126
  if (cacheHits.length > 0) {
102
127
  const cacheTokensSaved = cacheHits.reduce((s, c) => s + Math.max(0, c.tokensWouldBe - c.tokensReturned), 0);
103
128
  lines.push('');
104
129
  lines.push(`Session cache: ${cacheHits.length} hits / ${this.calls.length} calls (${Math.round(cacheHits.length / this.calls.length * 100)}% hit rate, ~${cacheTokensSaved} tokens saved)`);
105
130
  }
106
- // Dedup reminders served
107
- const dedupCalls = this.calls.filter(c => c.savingsCategory === 'dedup');
108
- if (dedupCalls.length > 0) {
109
- lines.push('');
110
- lines.push(`Compact reminders/dedup: ${dedupCalls.length} calls (avoided full re-reads)`);
111
- }
112
- // Delegation stats
131
+ // Delegation
113
132
  const delegated = this.calls.filter(c => c.delegatedToContextMode);
114
133
  if (delegated.length > 0) {
115
- lines.push('');
116
134
  lines.push(`Delegated to context-mode: ${delegated.length} calls`);
117
135
  }
118
- // Per-intent breakdown (Track 2)
136
+ // Per-intent breakdown
119
137
  const callsWithIntent = this.calls.filter(c => c.intent);
120
138
  if (callsWithIntent.length > 0) {
121
139
  const byIntent = new Map();
122
140
  for (const c of callsWithIntent) {
123
141
  const intent = c.intent;
124
- const existing = byIntent.get(intent) ?? { count: 0, saved: 0 };
125
- existing.count++;
126
- existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
127
- byIntent.set(intent, existing);
142
+ const e = byIntent.get(intent) ?? { count: 0, saved: 0 };
143
+ e.count++;
144
+ e.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
145
+ byIntent.set(intent, e);
128
146
  }
129
147
  lines.push('');
130
148
  lines.push('Per-intent breakdown:');
@@ -135,30 +153,29 @@ export class SessionAnalytics {
135
153
  }
136
154
  }
137
155
  }
138
- // Decision insights (Track 0)
156
+ // Decision insights
139
157
  const tracedCalls = this.calls.filter(c => c.decisionTrace);
140
158
  if (tracedCalls.length > 0) {
141
- const alreadyInContextCount = tracedCalls.filter(c => c.decisionTrace.alreadyInContext).length;
159
+ const alreadyInContext = tracedCalls.filter(c => c.decisionTrace.alreadyInContext).length;
142
160
  const totalEstimated = tracedCalls.reduce((s, c) => s + c.decisionTrace.estimatedCost, 0);
143
161
  const totalActual = tracedCalls.reduce((s, c) => s + c.decisionTrace.actualCost, 0);
144
162
  const avgReduction = totalEstimated > 0 ? Math.round((1 - totalActual / totalEstimated) * 100) : 0;
145
163
  const missedSavings = tracedCalls.filter(c => c.decisionTrace.cheaperAlternative).length;
146
164
  lines.push('');
147
165
  lines.push('Decision insights:');
148
- lines.push(` Files already in context: ${alreadyInContextCount} of ${tracedCalls.length} calls (${Math.round(alreadyInContextCount / tracedCalls.length * 100)}%)`);
166
+ lines.push(` Files already in context: ${alreadyInContext} of ${tracedCalls.length} calls (${Math.round(alreadyInContext / tracedCalls.length * 100)}%)`);
149
167
  lines.push(` Avg cost reduction: ${avgReduction}% (estimated → actual)`);
150
168
  if (missedSavings > 0) {
151
- lines.push(` Missed savings opportunities: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
169
+ lines.push(` Missed savings: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
152
170
  }
153
171
  }
154
- // Context-mode companion status
172
+ // Context-mode
155
173
  if (this.contextModeStatus.detected) {
156
174
  lines.push('');
157
175
  lines.push('--- Combined Architecture ---');
158
176
  lines.push(`context-mode: active (detected via ${this.contextModeStatus.source})`);
159
177
  lines.push('Token Pilot handles: code files (AST-level structural reading)');
160
178
  lines.push('context-mode handles: shell output, logs, large data files (BM25-indexed)');
161
- lines.push('TIP: Use /context-mode:stats for context-mode savings breakdown.');
162
179
  }
163
180
  return lines.join('\n');
164
181
  }
@@ -15,7 +15,6 @@ export declare class SymbolResolver {
15
15
  contextBefore?: number;
16
16
  contextAfter?: number;
17
17
  }): string;
18
- private mapKind;
19
18
  /**
20
19
  * Hierarchical search: AuthService → children → login
21
20
  */
@@ -1,3 +1,4 @@
1
+ import { mapKind } from '../ast-index/parser.js';
1
2
  export class SymbolResolver {
2
3
  astIndex;
3
4
  constructor(astIndex) {
@@ -63,7 +64,7 @@ export class SymbolResolver {
63
64
  symbol: {
64
65
  name: detail.name,
65
66
  qualifiedName: qualifiedName,
66
- kind: this.mapKind(detail.kind),
67
+ kind: mapKind(detail.kind),
67
68
  signature: detail.signature ?? detail.name,
68
69
  location: {
69
70
  startLine: detail.start_line,
@@ -96,7 +97,7 @@ export class SymbolResolver {
96
97
  symbol: {
97
98
  name: leafDetail.name,
98
99
  qualifiedName: qualifiedName,
99
- kind: this.mapKind(leafDetail.kind),
100
+ kind: mapKind(leafDetail.kind),
100
101
  signature: leafDetail.signature ?? leafDetail.name,
101
102
  location: {
102
103
  startLine: leafDetail.start_line,
@@ -133,16 +134,6 @@ export class SymbolResolver {
133
134
  }
134
135
  return output.join('\n');
135
136
  }
136
- mapKind(kind) {
137
- const map = {
138
- function: 'function', class: 'class', method: 'method',
139
- property: 'property', variable: 'variable', type: 'type',
140
- interface: 'interface', enum: 'enum', constant: 'constant',
141
- namespace: 'namespace', struct: 'class', trait: 'interface',
142
- impl: 'class', module: 'namespace',
143
- };
144
- return map[kind.toLowerCase()] ?? 'function';
145
- }
146
137
  /**
147
138
  * Hierarchical search: AuthService → children → login
148
139
  */
@@ -23,6 +23,16 @@ export declare function validateReadSymbolArgs(args: unknown): {
23
23
  context_after?: number;
24
24
  show?: 'full' | 'head' | 'tail' | 'outline';
25
25
  };
26
+ /**
27
+ * Validate read_symbols arguments (batch multi-symbol read).
28
+ */
29
+ export declare function validateReadSymbolsArgs(args: unknown): {
30
+ path: string;
31
+ symbols: string[];
32
+ context_before?: number;
33
+ context_after?: number;
34
+ show?: 'full' | 'head' | 'tail' | 'outline';
35
+ };
26
36
  /**
27
37
  * Validate read_range arguments.
28
38
  */
@@ -48,6 +58,7 @@ export interface FindUsagesArgs {
48
58
  kind?: 'definitions' | 'imports' | 'usages' | 'all';
49
59
  limit?: number;
50
60
  lang?: string;
61
+ context_lines?: number;
51
62
  }
52
63
  export declare function validateFindUsagesArgs(args: unknown): FindUsagesArgs;
53
64
  /**
@@ -62,6 +73,7 @@ export declare function validateSmartReadManyArgs(args: unknown): {
62
73
  export declare function validateReadForEditArgs(args: unknown): {
63
74
  path: string;
64
75
  symbol?: string;
76
+ symbols?: string[];
65
77
  line?: number;
66
78
  context?: number;
67
79
  include_callers?: boolean;