token-pilot 0.14.1 → 0.15.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.
@@ -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,7 +26,7 @@ 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 a compact session report (~5 lines).
30
30
  */
31
31
  report(): string;
32
32
  reset(): void;
@@ -1,5 +1,4 @@
1
1
  import { formatDuration } from './format-duration.js';
2
- import { ALL_INTENTS } from './intent-classifier.js';
3
2
  /**
4
3
  * Tracks token savings and tool usage across a session.
5
4
  * When context-mode is detected, includes unified reporting.
@@ -15,150 +14,62 @@ export class SessionAnalytics {
15
14
  this.calls.push(call);
16
15
  }
17
16
  /**
18
- * Generate a session report.
17
+ * Generate a compact session report (~5 lines).
19
18
  */
20
19
  report() {
21
20
  const duration = formatDuration(Date.now() - this.sessionStart);
22
21
  const totalReturned = this.calls.reduce((s, c) => s + c.tokensReturned, 0);
23
22
  const totalWouldBe = this.calls.reduce((s, c) => s + c.tokensWouldBe, 0);
24
23
  const saved = totalWouldBe > 0 ? Math.round((1 - totalReturned / totalWouldBe) * 100) : 0;
25
- // Group by tool
26
- const byTool = new Map();
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);
34
- }
35
24
  const lines = [
36
25
  `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:',
26
+ `Calls: ${this.calls.length} · Tokens returned: ~${totalReturned} · Saved: ~${totalWouldBe - totalReturned} (${saved}%)`,
43
27
  ];
44
- 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)`);
28
+ // By tool — top 5 by savings, all on one line
29
+ if (this.calls.length > 0) {
30
+ const byTool = new Map();
31
+ for (const c of this.calls) {
32
+ const e = byTool.get(c.tool) ?? { count: 0, tokens: 0, wouldBe: 0 };
33
+ e.count++;
34
+ e.tokens += c.tokensReturned;
35
+ e.wouldBe += c.tokensWouldBe;
36
+ byTool.set(c.tool, e);
37
+ }
38
+ const sorted = Array.from(byTool.entries())
39
+ .sort((a, b) => (b[1].wouldBe - b[1].tokens) - (a[1].wouldBe - a[1].tokens))
40
+ .slice(0, 5);
41
+ const toolParts = sorted.map(([tool, s]) => {
42
+ const pct = s.wouldBe > 0 ? Math.round((1 - s.tokens / s.wouldBe) * 100) : 0;
43
+ return `${tool} ${s.count}× (${pct}%)`;
44
+ });
45
+ lines.push(`Tools: ${toolParts.join(' · ')}`);
50
46
  }
51
- // Top files by savings
47
+ // Top files by savings (top 3)
52
48
  const byFile = new Map();
53
49
  for (const c of this.calls) {
54
50
  if (c.path) {
55
- const current = byFile.get(c.path) ?? 0;
56
- byFile.set(c.path, current + Math.max(0, c.tokensWouldBe - c.tokensReturned));
51
+ byFile.set(c.path, (byFile.get(c.path) ?? 0) + Math.max(0, c.tokensWouldBe - c.tokensReturned));
57
52
  }
58
53
  }
59
54
  const topFiles = Array.from(byFile.entries())
60
55
  .sort((a, b) => b[1] - a[1])
61
- .slice(0, 5);
56
+ .slice(0, 3);
62
57
  if (topFiles.length > 0) {
63
- lines.push('');
64
- lines.push('Top files by savings:');
65
- for (const [file, saved] of topFiles) {
66
- lines.push(` ${file}: ~${saved} tokens saved`);
67
- }
68
- }
69
- const lowValueTools = sortedTools
70
- .map(([tool, stats]) => ({
71
- tool,
72
- reduction: stats.wouldBe > 0 ? Math.round((1 - stats.tokens / stats.wouldBe) * 100) : 0,
73
- count: stats.count,
74
- }))
75
- .filter((tool) => tool.reduction < 20);
76
- if (lowValueTools.length > 0) {
77
- lines.push('');
78
- 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'}`);
81
- }
82
- }
83
- // Savings breakdown by category
84
- const byCategory = { compression: 0, cache: 0, dedup: 0, none: 0 };
85
- for (const c of this.calls) {
86
- const cat = c.savingsCategory ?? 'none';
87
- byCategory[cat] += Math.max(0, c.tokensWouldBe - c.tokensReturned);
88
- }
89
- if (totalWouldBe > totalReturned) {
90
- lines.push('');
91
- lines.push('Savings breakdown:');
92
- if (byCategory.compression > 0)
93
- lines.push(` Compression (AST/structured): ~${byCategory.compression} tokens`);
94
- if (byCategory.cache > 0)
95
- lines.push(` Cache hits (session cache): ~${byCategory.cache} tokens`);
96
- if (byCategory.dedup > 0)
97
- lines.push(` Dedup (already in context): ~${byCategory.dedup} tokens`);
58
+ const fileParts = topFiles.map(([f, s]) => `${f} ~${s}`);
59
+ lines.push(`Top files: ${fileParts.join(' · ')}`);
98
60
  }
99
- // Session cache hits
61
+ // Cache hits + context-mode on one line
62
+ const extras = [];
100
63
  const cacheHits = this.calls.filter(c => c.sessionCacheHit);
101
64
  if (cacheHits.length > 0) {
102
- const cacheTokensSaved = cacheHits.reduce((s, c) => s + Math.max(0, c.tokensWouldBe - c.tokensReturned), 0);
103
- lines.push('');
104
- lines.push(`Session cache: ${cacheHits.length} hits / ${this.calls.length} calls (${Math.round(cacheHits.length / this.calls.length * 100)}% hit rate, ~${cacheTokensSaved} tokens saved)`);
65
+ const hitRate = Math.round(cacheHits.length / this.calls.length * 100);
66
+ extras.push(`Cache: ${cacheHits.length}/${this.calls.length} hits (${hitRate}%)`);
105
67
  }
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
113
- const delegated = this.calls.filter(c => c.delegatedToContextMode);
114
- if (delegated.length > 0) {
115
- lines.push('');
116
- lines.push(`Delegated to context-mode: ${delegated.length} calls`);
117
- }
118
- // Per-intent breakdown (Track 2)
119
- const callsWithIntent = this.calls.filter(c => c.intent);
120
- if (callsWithIntent.length > 0) {
121
- const byIntent = new Map();
122
- for (const c of callsWithIntent) {
123
- 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);
128
- }
129
- lines.push('');
130
- lines.push('Per-intent breakdown:');
131
- for (const intent of ALL_INTENTS) {
132
- const stats = byIntent.get(intent);
133
- if (stats) {
134
- lines.push(` ${intent}: ${stats.count} call${stats.count === 1 ? '' : 's'}, ~${stats.saved} tokens saved`);
135
- }
136
- }
137
- }
138
- // Decision insights (Track 0)
139
- const tracedCalls = this.calls.filter(c => c.decisionTrace);
140
- if (tracedCalls.length > 0) {
141
- const alreadyInContextCount = tracedCalls.filter(c => c.decisionTrace.alreadyInContext).length;
142
- const totalEstimated = tracedCalls.reduce((s, c) => s + c.decisionTrace.estimatedCost, 0);
143
- const totalActual = tracedCalls.reduce((s, c) => s + c.decisionTrace.actualCost, 0);
144
- const avgReduction = totalEstimated > 0 ? Math.round((1 - totalActual / totalEstimated) * 100) : 0;
145
- const missedSavings = tracedCalls.filter(c => c.decisionTrace.cheaperAlternative).length;
146
- lines.push('');
147
- lines.push('Decision insights:');
148
- lines.push(` Files already in context: ${alreadyInContextCount} of ${tracedCalls.length} calls (${Math.round(alreadyInContextCount / tracedCalls.length * 100)}%)`);
149
- lines.push(` Avg cost reduction: ${avgReduction}% (estimated → actual)`);
150
- if (missedSavings > 0) {
151
- lines.push(` Missed savings opportunities: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
152
- }
153
- }
154
- // Context-mode companion status
155
68
  if (this.contextModeStatus.detected) {
156
- lines.push('');
157
- lines.push('--- Combined Architecture ---');
158
- lines.push(`context-mode: active (detected via ${this.contextModeStatus.source})`);
159
- lines.push('Token Pilot handles: code files (AST-level structural reading)');
160
- 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.');
69
+ extras.push(`context-mode: active (${this.contextModeStatus.source})`);
70
+ }
71
+ if (extras.length > 0) {
72
+ lines.push(extras.join(' · '));
162
73
  }
163
74
  return lines.join('\n');
164
75
  }
@@ -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
  */
@@ -87,7 +87,7 @@ async function handleTodo(limit, projectRoot, astIndex) {
87
87
  return {
88
88
  content: [{
89
89
  type: 'text',
90
- text: 'TODO/FIXME COMMENTS: none found.\n\nHINT: ast-index may not detect all comment formats. Try Grep: grep -rn "TODO\\|FIXME\\|HACK" --include="*.ts"',
90
+ text: 'TODO/FIXME COMMENTS: none found.\n\nHINT: ast-index may not detect all comment formats. Try Grep with pattern "TODO|FIXME|HACK".',
91
91
  }],
92
92
  meta: { files: [] },
93
93
  };
@@ -146,7 +146,7 @@ async function handleAnnotations(name, limit, projectRoot, astIndex) {
146
146
  return {
147
147
  content: [{
148
148
  type: 'text',
149
- text: `ANNOTATIONS @${name}: none found.\n\nHINT: Try Grep for text-based search: grep -rn "@${name}" --include="*.ts"`,
149
+ text: `ANNOTATIONS @${name}: none found.\n\nHINT: Try Grep with pattern "@${name}" for text-based search.`,
150
150
  }],
151
151
  meta: { files: [] },
152
152
  };
@@ -18,7 +18,7 @@ export async function handleFindUnused(args, astIndex) {
18
18
  return { content: [{ type: 'text', text: 'find_unused is disabled: ' + (astIndex.isDisabled()
19
19
  ? 'project root not detected. Call smart_read() on any project file first — this auto-detects the project root and enables ast-index tools.'
20
20
  : 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.') +
21
- '\nAlternative: use grep_search to find unused exports manually.' }], meta: { files: [] } };
21
+ '\nAlternative: use Grep to find unused exports manually.' }], meta: { files: [] } };
22
22
  }
23
23
  const requestLimit = (args.limit ?? 30) + 20; // extra to compensate for filtering
24
24
  const unused = await astIndex.unusedSymbols({
@@ -22,6 +22,35 @@ const LANG_EXT_MAP = {
22
22
  vue: ['.vue'],
23
23
  svelte: ['.svelte'],
24
24
  };
25
+ /**
26
+ * Render a section (DEFINITIONS/IMPORTS/USAGES) grouped by file.
27
+ * Single match per file → one line. Multiple → file header + indented lines.
28
+ */
29
+ function renderSection(title, items) {
30
+ if (items.length === 0)
31
+ return [];
32
+ const lines = [`${title}:`];
33
+ const byFile = new Map();
34
+ for (const item of items) {
35
+ const arr = byFile.get(item.file) ?? [];
36
+ arr.push({ line: item.line, text: item.text });
37
+ byFile.set(item.file, arr);
38
+ }
39
+ for (const [file, matches] of byFile) {
40
+ matches.sort((a, b) => a.line - b.line);
41
+ if (matches.length === 1) {
42
+ lines.push(` ${file}:${matches[0].line} ${matches[0].text}`);
43
+ }
44
+ else {
45
+ lines.push(` ${file}:`);
46
+ for (const m of matches) {
47
+ lines.push(` :${m.line} ${m.text}`);
48
+ }
49
+ }
50
+ }
51
+ lines.push('');
52
+ return lines;
53
+ }
25
54
  /**
26
55
  * Find all usages of a symbol across the project.
27
56
  *
@@ -40,7 +69,7 @@ export async function handleFindUsages(args, astIndex) {
40
69
  text: 'find_usages is disabled: ' + (astIndex.isDisabled()
41
70
  ? 'project root not detected. Call smart_read() on any project file first — this auto-detects the project root and enables ast-index tools.'
42
71
  : 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.')
43
- + '\nAlternative: use grep_search to find symbol references.',
72
+ + '\nAlternative: use Grep to find symbol references.',
44
73
  }],
45
74
  meta: { files: [], definitions: 0, imports: 0, usages: 0, total: 0 },
46
75
  };
@@ -151,33 +180,12 @@ export async function handleFindUsages(args, astIndex) {
151
180
  filterHints.push(`kind=${args.kind}`);
152
181
  const filterStr = filterHints.length > 0 ? ` [${filterHints.join(', ')}]` : '';
153
182
  const lines = [
154
- `REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} definitions, ${allImports.length} imports, ${allUsages.length} usages)${filterStr}`,
183
+ `REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} def · ${allImports.length} imports · ${allUsages.length} usages)${filterStr}`,
155
184
  '',
156
185
  ];
157
- if (definitions.length > 0) {
158
- lines.push('DEFINITIONS:');
159
- for (const d of definitions) {
160
- lines.push(` ${d.file}:${d.line}`);
161
- lines.push(` ${d.text}`);
162
- }
163
- lines.push('');
164
- }
165
- if (allImports.length > 0) {
166
- lines.push('IMPORTS:');
167
- for (const i of allImports) {
168
- lines.push(` ${i.file}:${i.line}`);
169
- lines.push(` ${i.text}`);
170
- }
171
- lines.push('');
172
- }
173
- if (allUsages.length > 0) {
174
- lines.push('USAGES:');
175
- for (const u of allUsages) {
176
- lines.push(` ${u.file}:${u.line}`);
177
- lines.push(` ${u.text}`);
178
- }
179
- lines.push('');
180
- }
186
+ lines.push(...renderSection('DEFINITIONS', definitions));
187
+ lines.push(...renderSection('IMPORTS', allImports));
188
+ lines.push(...renderSection('USAGES', allUsages));
181
189
  lines.push('HINT: Use read_symbol() or read_range() to load specific results.');
182
190
  // Confidence metadata
183
191
  const confidenceMeta = assessConfidence({
@@ -4,6 +4,9 @@ import { formatOutline } from '../formatters/structure.js';
4
4
  import { estimateTokens, formatSavings } from '../core/token-estimator.js';
5
5
  import { resolveSafePath } from '../core/validation.js';
6
6
  import { isNonCodeStructured, handleNonCodeRead } from './non-code.js';
7
+ import { parseTypeScriptRegex } from '../ast-index/regex-parser.js';
8
+ import { buildFileStructure } from '../ast-index/enricher.js';
9
+ const TS_JS_EXTENSIONS = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs']);
7
10
  import { assessConfidence, formatConfidence } from '../core/confidence.js';
8
11
  export async function handleSmartRead(args, projectRoot, astIndex, fileCache, contextRegistry, config) {
9
12
  const absPath = resolveSafePath(projectRoot, args.path);
@@ -55,7 +58,7 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
55
58
  const isStale = cached ? await fileCache.isStale(absPath) : true;
56
59
  if (!cached || isStale) {
57
60
  // 4. Get structure from ast-index
58
- const structure = await astIndex.outline(absPath);
61
+ let structure = await astIndex.outline(absPath);
59
62
  if (!structure) {
60
63
  // ast-index doesn't support this file type
61
64
  // Try non-code structural summary (JSON, YAML, Markdown, TOML)
@@ -64,6 +67,16 @@ export async function handleSmartRead(args, projectRoot, astIndex, fileCache, co
64
67
  if (nonCodeResult)
65
68
  return nonCodeResult;
66
69
  }
70
+ // Regex fallback for TS/JS when binary is unavailable
71
+ const ext = absPath.split('.').pop()?.toLowerCase() ?? '';
72
+ if (TS_JS_EXTENSIONS.has(ext)) {
73
+ const regexEntries = parseTypeScriptRegex(content);
74
+ if (regexEntries.length > 0) {
75
+ structure = await buildFileStructure(absPath, regexEntries);
76
+ }
77
+ }
78
+ }
79
+ if (!structure) {
67
80
  // Fallback: return truncated preview instead of full raw content
68
81
  const previewLines = 60;
69
82
  const truncated = lines.length > previewLines;
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare const CODE_EXTENSIONS: Set<string>;
3
3
  export declare function getVersion(): string;
4
4
  export declare function main(cliArgs?: string[]): Promise<void>;
5
5
  export declare function startServer(cliArgs?: string[]): Promise<void>;
6
- export declare function handleHookRead(filePathArg?: string): void;
6
+ export declare function handleHookRead(filePathArg?: string, denyThreshold?: number): void;
7
7
  export declare function handleHookEdit(): void;
8
8
  export declare function handleInstallHook(projectRoot: string): Promise<void>;
9
9
  export declare function handleUninstallHook(projectRoot: string): Promise<void>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { readFileSync } from 'node:fs';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
@@ -10,7 +10,6 @@ import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion } from './
10
10
  import { loadConfig } from './config/loader.js';
11
11
  import { isDangerousRoot } from './core/validation.js';
12
12
  const execFileAsync = promisify(execFile);
13
- const HOOK_DENY_THRESHOLD = 500;
14
13
  export const CODE_EXTENSIONS = new Set([
15
14
  'ts', 'tsx', 'js', 'jsx', 'mjs', 'py', 'go', 'rs', 'java', 'kt', 'kts',
16
15
  'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
@@ -30,9 +29,11 @@ export function getVersion() {
30
29
  }
31
30
  export async function main(cliArgs = process.argv.slice(2)) {
32
31
  switch (cliArgs[0]) {
33
- case 'hook-read':
34
- handleHookRead(cliArgs[1]);
32
+ case 'hook-read': {
33
+ const cfg = await loadConfig(process.cwd());
34
+ handleHookRead(cliArgs[1], cfg.hooks.denyThreshold);
35
35
  return;
36
+ }
36
37
  case 'hook-edit':
37
38
  handleHookEdit();
38
39
  return;
@@ -139,7 +140,7 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
139
140
  process.exit(0);
140
141
  });
141
142
  }
142
- export function handleHookRead(filePathArg) {
143
+ export function handleHookRead(filePathArg, denyThreshold = 300) {
143
144
  // Parse stdin (Claude Code hook format) to get tool_input
144
145
  let filePath = filePathArg;
145
146
  let hasOffset = false;
@@ -173,7 +174,7 @@ export function handleHookRead(filePathArg) {
173
174
  try {
174
175
  const content = readFileSync(filePath, 'utf-8');
175
176
  lineCount = content.split('\n').length;
176
- if (lineCount <= HOOK_DENY_THRESHOLD) {
177
+ if (lineCount <= denyThreshold) {
177
178
  process.exit(0);
178
179
  }
179
180
  }
@@ -449,7 +450,7 @@ MCP Tools (18):
449
450
  `);
450
451
  process.exit(0);
451
452
  }
452
- const isDirectRun = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
453
+ const isDirectRun = process.argv[1] !== undefined && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
453
454
  if (isDirectRun) {
454
455
  main().catch(err => {
455
456
  console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Token estimation functions for analytics.
3
+ * Used to calculate "tokens would be" for honest savings reporting.
4
+ */
5
+ import type { FileCache } from '../core/file-cache.js';
6
+ import type { SavingsCategory } from '../core/session-analytics.js';
7
+ /**
8
+ * Creates token estimation functions bound to a project context.
9
+ * Uses getter for projectRoot since it may change on auto-detect.
10
+ */
11
+ export declare function createTokenEstimates(getProjectRoot: () => string, fileCache: FileCache): {
12
+ fullFileTokens: (relativePath: string) => Promise<number>;
13
+ estimateProjectOverviewWorkflowTokens: (includeSections: Array<"stack" | "ci" | "quality" | "architecture">) => Promise<number>;
14
+ estimateOutlineWorkflowTokens: (relativePath: string, recursive: boolean, maxDepth: number) => Promise<number>;
15
+ estimateRelatedFilesWorkflowTokens: (targetPath: string, meta?: {
16
+ imports?: string[];
17
+ importedBy?: string[];
18
+ tests?: string[];
19
+ }) => Promise<number>;
20
+ estimateFindUsagesWorkflowTokens: (files: string[]) => Promise<number>;
21
+ estimateExploreAreaWorkflowTokens: (meta: {
22
+ codeFiles?: string[];
23
+ testFiles?: string[];
24
+ internalDeps?: string[];
25
+ importedBy?: string[];
26
+ externalDeps?: string[];
27
+ changeCount?: number;
28
+ }) => Promise<number>;
29
+ detectSavingsCategory: (text: string) => SavingsCategory;
30
+ };
31
+ //# sourceMappingURL=token-estimates.d.ts.map