token-pilot 0.14.2 → 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.
- package/CHANGELOG.md +17 -0
- package/dist/ast-index/client.d.ts +0 -89
- package/dist/ast-index/client.js +26 -743
- package/dist/ast-index/enricher.d.ts +10 -0
- package/dist/ast-index/enricher.js +202 -0
- package/dist/ast-index/parser.d.ts +31 -0
- package/dist/ast-index/parser.js +340 -0
- package/dist/ast-index/regex-parser.d.ts +8 -0
- package/dist/ast-index/regex-parser.js +118 -0
- package/dist/config/defaults.js +1 -0
- package/dist/core/session-analytics.d.ts +1 -1
- package/dist/core/session-analytics.js +33 -122
- package/dist/core/symbol-resolver.d.ts +0 -1
- package/dist/core/symbol-resolver.js +3 -12
- package/dist/handlers/code-audit.js +2 -2
- package/dist/handlers/find-unused.js +1 -1
- package/dist/handlers/find-usages.js +34 -26
- package/dist/handlers/smart-read.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -5
- package/dist/server/token-estimates.d.ts +31 -0
- package/dist/server/token-estimates.js +204 -0
- package/dist/server/tool-definitions.d.ts +958 -0
- package/dist/server/tool-definitions.js +288 -0
- package/dist/server.js +8 -477
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/skills/guide/SKILL.md +64 -0
|
@@ -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
|
package/dist/config/defaults.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
: 0;
|
|
49
|
-
|
|
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
|
-
|
|
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,
|
|
56
|
+
.slice(0, 3);
|
|
62
57
|
if (topFiles.length > 0) {
|
|
63
|
-
|
|
64
|
-
lines.push(
|
|
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
|
-
//
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
lines.push(
|
|
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
|
}
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
183
|
+
`REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} def · ${allImports.length} imports · ${allUsages.length} usages)${filterStr}`,
|
|
155
184
|
'',
|
|
156
185
|
];
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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 <=
|
|
177
|
+
if (lineCount <= denyThreshold) {
|
|
177
178
|
process.exit(0);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -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
|