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.
- package/CHANGELOG.md +34 -0
- package/README.md +25 -8
- package/dist/ast-index/client.d.ts +2 -89
- package/dist/ast-index/client.js +49 -742
- 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-python.d.ts +8 -0
- package/dist/ast-index/regex-parser-python.js +132 -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 +2 -2
- package/dist/core/session-analytics.js +78 -61
- package/dist/core/symbol-resolver.d.ts +0 -1
- package/dist/core/symbol-resolver.js +3 -12
- package/dist/core/validation.d.ts +12 -0
- package/dist/core/validation.js +62 -2
- package/dist/handlers/code-audit.js +2 -2
- package/dist/handlers/find-unused.js +1 -1
- package/dist/handlers/find-usages.d.ts +1 -1
- package/dist/handlers/find-usages.js +93 -25
- package/dist/handlers/read-for-edit.d.ts +1 -0
- package/dist/handlers/read-for-edit.js +65 -0
- package/dist/handlers/read-symbols.d.ts +18 -0
- package/dist/handlers/read-symbols.js +142 -0
- package/dist/handlers/smart-diff.js +23 -0
- 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 +1070 -0
- package/dist/server/tool-definitions.js +316 -0
- package/dist/server.js +23 -480
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- 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
|
package/dist/config/defaults.js
CHANGED
|
@@ -26,9 +26,9 @@ export declare class SessionAnalytics {
|
|
|
26
26
|
setContextModeStatus(status: ContextModeStatus): void;
|
|
27
27
|
record(call: ToolCall): void;
|
|
28
28
|
/**
|
|
29
|
-
* Generate
|
|
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
|
|
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
|
-
//
|
|
25
|
+
// --- Shared data ---
|
|
26
26
|
const byTool = new Map();
|
|
27
27
|
for (const c of this.calls) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
byTool.set(c.tool,
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
66
|
-
lines.push(` ${file}: ~${
|
|
91
|
+
for (const [file, fileSaved] of allTopFiles) {
|
|
92
|
+
lines.push(` ${file}: ~${fileSaved} tokens saved`);
|
|
67
93
|
}
|
|
68
94
|
}
|
|
69
|
-
|
|
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(
|
|
76
|
-
if (
|
|
102
|
+
.filter(t => t.reduction < 20);
|
|
103
|
+
if (lowValue.length > 0) {
|
|
77
104
|
lines.push('');
|
|
78
105
|
lines.push('Needs improvement:');
|
|
79
|
-
for (const
|
|
80
|
-
lines.push(` ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
byIntent.set(intent,
|
|
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
|
|
156
|
+
// Decision insights
|
|
139
157
|
const tracedCalls = this.calls.filter(c => c.decisionTrace);
|
|
140
158
|
if (tracedCalls.length > 0) {
|
|
141
|
-
const
|
|
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: ${
|
|
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
|
|
169
|
+
lines.push(` Missed savings: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
|
|
152
170
|
}
|
|
153
171
|
}
|
|
154
|
-
// Context-mode
|
|
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
|
}
|
|
@@ -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
|
*/
|
|
@@ -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;
|