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
package/dist/core/validation.js
CHANGED
|
@@ -60,6 +60,44 @@ export function validateReadSymbolArgs(args) {
|
|
|
60
60
|
show,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate read_symbols arguments (batch multi-symbol read).
|
|
65
|
+
*/
|
|
66
|
+
export function validateReadSymbolsArgs(args) {
|
|
67
|
+
if (!args || typeof args !== 'object') {
|
|
68
|
+
throw new Error('Arguments must be an object.');
|
|
69
|
+
}
|
|
70
|
+
const a = args;
|
|
71
|
+
if (typeof a.path !== 'string' || a.path.length === 0) {
|
|
72
|
+
throw new Error('Required parameter "path" must be a non-empty string.');
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(a.symbols) || a.symbols.length === 0) {
|
|
75
|
+
throw new Error('Required parameter "symbols" must be a non-empty array of strings.');
|
|
76
|
+
}
|
|
77
|
+
if (a.symbols.length > 10) {
|
|
78
|
+
throw new Error('"symbols" can contain at most 10 symbols.');
|
|
79
|
+
}
|
|
80
|
+
for (const s of a.symbols) {
|
|
81
|
+
if (typeof s !== 'string' || s.length === 0) {
|
|
82
|
+
throw new Error('Each symbol in "symbols" must be a non-empty string.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let show;
|
|
86
|
+
if (a.show !== undefined && a.show !== null) {
|
|
87
|
+
const valid = ['full', 'head', 'tail', 'outline'];
|
|
88
|
+
if (typeof a.show !== 'string' || !valid.includes(a.show)) {
|
|
89
|
+
throw new Error('"show" must be one of: full, head, tail, outline.');
|
|
90
|
+
}
|
|
91
|
+
show = a.show;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
path: a.path,
|
|
95
|
+
symbols: a.symbols,
|
|
96
|
+
context_before: optionalNumber(a.context_before, 'context_before'),
|
|
97
|
+
context_after: optionalNumber(a.context_after, 'context_after'),
|
|
98
|
+
show,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
63
101
|
/**
|
|
64
102
|
* Validate read_range arguments.
|
|
65
103
|
*/
|
|
@@ -118,12 +156,17 @@ export function validateFindUsagesArgs(args) {
|
|
|
118
156
|
if (limit !== undefined && (limit < 1 || limit > 500)) {
|
|
119
157
|
throw new Error('"limit" must be between 1 and 500.');
|
|
120
158
|
}
|
|
159
|
+
const context_lines = optionalNumber(a.context_lines, 'context_lines');
|
|
160
|
+
if (context_lines !== undefined && (context_lines < 0 || context_lines > 10)) {
|
|
161
|
+
throw new Error('"context_lines" must be between 0 and 10.');
|
|
162
|
+
}
|
|
121
163
|
return {
|
|
122
164
|
symbol: a.symbol,
|
|
123
165
|
scope: optionalString(a.scope, 'scope'),
|
|
124
166
|
kind,
|
|
125
167
|
limit,
|
|
126
168
|
lang: optionalString(a.lang, 'lang'),
|
|
169
|
+
context_lines,
|
|
127
170
|
};
|
|
128
171
|
}
|
|
129
172
|
/**
|
|
@@ -176,12 +219,29 @@ export function validateReadForEditArgs(args) {
|
|
|
176
219
|
if (typeof a.path !== 'string' || a.path.length === 0) {
|
|
177
220
|
throw new Error('Required parameter "path" must be a non-empty string.');
|
|
178
221
|
}
|
|
179
|
-
if (!a.symbol && !a.line) {
|
|
180
|
-
throw new Error('Either "symbol" or "line" must be provided.');
|
|
222
|
+
if (!a.symbol && !a.line && (!Array.isArray(a.symbols) || a.symbols.length === 0)) {
|
|
223
|
+
throw new Error('Either "symbol", "symbols", or "line" must be provided.');
|
|
224
|
+
}
|
|
225
|
+
// Validate symbols array (batch mode)
|
|
226
|
+
let symbols;
|
|
227
|
+
if (a.symbols !== undefined && a.symbols !== null) {
|
|
228
|
+
if (!Array.isArray(a.symbols)) {
|
|
229
|
+
throw new Error('"symbols" must be an array of strings.');
|
|
230
|
+
}
|
|
231
|
+
if (a.symbols.length > 10) {
|
|
232
|
+
throw new Error('"symbols" can contain at most 10 symbols.');
|
|
233
|
+
}
|
|
234
|
+
for (const s of a.symbols) {
|
|
235
|
+
if (typeof s !== 'string' || s.length === 0) {
|
|
236
|
+
throw new Error('Each symbol in "symbols" must be a non-empty string.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
symbols = a.symbols;
|
|
181
240
|
}
|
|
182
241
|
return {
|
|
183
242
|
path: a.path,
|
|
184
243
|
symbol: optionalString(a.symbol, 'symbol'),
|
|
244
|
+
symbols,
|
|
185
245
|
line: optionalNumber(a.line, 'line'),
|
|
186
246
|
context: optionalNumber(a.context, 'context'),
|
|
187
247
|
include_callers: optionalBool(a.include_callers, 'include_callers'),
|
|
@@ -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({
|
|
@@ -10,7 +10,7 @@ import type { FindUsagesArgs } from '../core/validation.js';
|
|
|
10
10
|
*
|
|
11
11
|
* v1.1: added scope, kind, limit, lang post-filters.
|
|
12
12
|
*/
|
|
13
|
-
export declare function handleFindUsages(args: FindUsagesArgs, astIndex: AstIndexClient): Promise<{
|
|
13
|
+
export declare function handleFindUsages(args: FindUsagesArgs, astIndex: AstIndexClient, projectRoot?: string): Promise<{
|
|
14
14
|
content: Array<{
|
|
15
15
|
type: 'text';
|
|
16
16
|
text: string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
2
4
|
/**
|
|
3
5
|
* Escape special regex characters in a string.
|
|
@@ -22,6 +24,81 @@ const LANG_EXT_MAP = {
|
|
|
22
24
|
vue: ['.vue'],
|
|
23
25
|
svelte: ['.svelte'],
|
|
24
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Render a section (DEFINITIONS/IMPORTS/USAGES) grouped by file.
|
|
29
|
+
* Single match per file → one line. Multiple → file header + indented lines.
|
|
30
|
+
*/
|
|
31
|
+
function renderSection(title, items) {
|
|
32
|
+
if (items.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
const lines = [`${title}:`];
|
|
35
|
+
const byFile = new Map();
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const arr = byFile.get(item.file) ?? [];
|
|
38
|
+
arr.push({ line: item.line, text: item.text });
|
|
39
|
+
byFile.set(item.file, arr);
|
|
40
|
+
}
|
|
41
|
+
for (const [file, matches] of byFile) {
|
|
42
|
+
matches.sort((a, b) => a.line - b.line);
|
|
43
|
+
if (matches.length === 1) {
|
|
44
|
+
lines.push(` ${file}:${matches[0].line} ${matches[0].text}`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
lines.push(` ${file}:`);
|
|
48
|
+
for (const m of matches) {
|
|
49
|
+
lines.push(` :${m.line} ${m.text}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push('');
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Render a section with surrounding source context lines.
|
|
58
|
+
*/
|
|
59
|
+
async function renderSectionWithContext(title, items, contextLines, projectRoot) {
|
|
60
|
+
if (items.length === 0)
|
|
61
|
+
return [];
|
|
62
|
+
const lines = [`${title}:`];
|
|
63
|
+
const byFile = new Map();
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const arr = byFile.get(item.file) ?? [];
|
|
66
|
+
arr.push({ line: item.line, text: item.text });
|
|
67
|
+
byFile.set(item.file, arr);
|
|
68
|
+
}
|
|
69
|
+
for (const [file, matches] of byFile) {
|
|
70
|
+
matches.sort((a, b) => a.line - b.line);
|
|
71
|
+
lines.push(` ${file}:`);
|
|
72
|
+
// Read file for context
|
|
73
|
+
let fileLines = null;
|
|
74
|
+
try {
|
|
75
|
+
const content = await readFile(resolve(projectRoot, file), 'utf-8');
|
|
76
|
+
fileLines = content.split('\n');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// File unreadable — fall back to text-only
|
|
80
|
+
for (const m of matches) {
|
|
81
|
+
lines.push(` :${m.line} ${m.text}`);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (let mi = 0; mi < matches.length; mi++) {
|
|
86
|
+
const m = matches[mi];
|
|
87
|
+
const start = Math.max(0, m.line - 1 - contextLines);
|
|
88
|
+
const end = Math.min(fileLines.length, m.line + contextLines);
|
|
89
|
+
for (let i = start; i < end; i++) {
|
|
90
|
+
const lineNum = i + 1;
|
|
91
|
+
const marker = lineNum === m.line ? '>' : ' ';
|
|
92
|
+
lines.push(` ${marker} ${lineNum} | ${fileLines[i]}`);
|
|
93
|
+
}
|
|
94
|
+
if (mi < matches.length - 1) {
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
lines.push('');
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
25
102
|
/**
|
|
26
103
|
* Find all usages of a symbol across the project.
|
|
27
104
|
*
|
|
@@ -32,7 +109,7 @@ const LANG_EXT_MAP = {
|
|
|
32
109
|
*
|
|
33
110
|
* v1.1: added scope, kind, limit, lang post-filters.
|
|
34
111
|
*/
|
|
35
|
-
export async function handleFindUsages(args, astIndex) {
|
|
112
|
+
export async function handleFindUsages(args, astIndex, projectRoot) {
|
|
36
113
|
if (astIndex.isDisabled() || astIndex.isOversized()) {
|
|
37
114
|
return {
|
|
38
115
|
content: [{
|
|
@@ -40,7 +117,7 @@ export async function handleFindUsages(args, astIndex) {
|
|
|
40
117
|
text: 'find_usages is disabled: ' + (astIndex.isDisabled()
|
|
41
118
|
? 'project root not detected. Call smart_read() on any project file first — this auto-detects the project root and enables ast-index tools.'
|
|
42
119
|
: 'ast-index built >50k files (likely includes node_modules). Ensure node_modules is in .gitignore.')
|
|
43
|
-
+ '\nAlternative: use
|
|
120
|
+
+ '\nAlternative: use Grep to find symbol references.',
|
|
44
121
|
}],
|
|
45
122
|
meta: { files: [], definitions: 0, imports: 0, usages: 0, total: 0 },
|
|
46
123
|
};
|
|
@@ -151,32 +228,23 @@ export async function handleFindUsages(args, astIndex) {
|
|
|
151
228
|
filterHints.push(`kind=${args.kind}`);
|
|
152
229
|
const filterStr = filterHints.length > 0 ? ` [${filterHints.join(', ')}]` : '';
|
|
153
230
|
const lines = [
|
|
154
|
-
`REFS: "${args.symbol}" (${totalCount} total: ${definitions.length}
|
|
231
|
+
`REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} def · ${allImports.length} imports · ${allUsages.length} usages)${filterStr}`,
|
|
155
232
|
'',
|
|
156
233
|
];
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
lines.push(
|
|
234
|
+
if (args.context_lines !== undefined && args.context_lines > 0 && projectRoot) {
|
|
235
|
+
const [defSection, impSection, useSection] = await Promise.all([
|
|
236
|
+
renderSectionWithContext('DEFINITIONS', definitions, args.context_lines, projectRoot),
|
|
237
|
+
renderSectionWithContext('IMPORTS', allImports, args.context_lines, projectRoot),
|
|
238
|
+
renderSectionWithContext('USAGES', allUsages, args.context_lines, projectRoot),
|
|
239
|
+
]);
|
|
240
|
+
lines.push(...defSection);
|
|
241
|
+
lines.push(...impSection);
|
|
242
|
+
lines.push(...useSection);
|
|
164
243
|
}
|
|
165
|
-
|
|
166
|
-
lines.push('
|
|
167
|
-
|
|
168
|
-
|
|
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('');
|
|
244
|
+
else {
|
|
245
|
+
lines.push(...renderSection('DEFINITIONS', definitions));
|
|
246
|
+
lines.push(...renderSection('IMPORTS', allImports));
|
|
247
|
+
lines.push(...renderSection('USAGES', allUsages));
|
|
180
248
|
}
|
|
181
249
|
lines.push('HINT: Use read_symbol() or read_range() to load specific results.');
|
|
182
250
|
// Confidence metadata
|
|
@@ -39,6 +39,71 @@ export async function handleReadForEdit(args, projectRoot, symbolResolver, fileC
|
|
|
39
39
|
lastAccess: Date.now(),
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
+
// --- Batch mode: multiple symbols ---
|
|
43
|
+
if (args.symbols && args.symbols.length > 0) {
|
|
44
|
+
let structure = cached?.structure;
|
|
45
|
+
if (!structure) {
|
|
46
|
+
structure = await astIndex.outline(absPath) ?? undefined;
|
|
47
|
+
}
|
|
48
|
+
const sections = [];
|
|
49
|
+
sections.push(`--- EDIT CONTEXT (BATCH: ${args.symbols.length} symbols) ---`);
|
|
50
|
+
sections.push(`FILE: ${args.path}`);
|
|
51
|
+
sections.push('');
|
|
52
|
+
let resolved_count = 0;
|
|
53
|
+
for (let i = 0; i < args.symbols.length; i++) {
|
|
54
|
+
const symName = args.symbols[i];
|
|
55
|
+
const resolved = await symbolResolver.resolve(symName, structure);
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
sections.push(`=== SYMBOL ${i + 1}/${args.symbols.length}: ${symName} — NOT FOUND ===`);
|
|
58
|
+
sections.push('');
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
resolved_count++;
|
|
62
|
+
const symbolLines = resolved.endLine - resolved.startLine + 1;
|
|
63
|
+
const MAX_EDIT_LINES = 60;
|
|
64
|
+
let effStart = resolved.startLine;
|
|
65
|
+
let effEnd;
|
|
66
|
+
let label;
|
|
67
|
+
if (symbolLines <= MAX_EDIT_LINES) {
|
|
68
|
+
effEnd = resolved.endLine;
|
|
69
|
+
label = `${symName} [L${effStart}-${effEnd}] (${symbolLines} lines, full)`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
effEnd = effStart + MAX_EDIT_LINES - 1;
|
|
73
|
+
label = `${symName} [L${effStart}-${resolved.endLine}] (showing first ${MAX_EDIT_LINES} of ${symbolLines} lines)`;
|
|
74
|
+
}
|
|
75
|
+
const rangeStart = Math.max(1, effStart - ctx);
|
|
76
|
+
const rangeEnd = Math.min(lines.length, effEnd + ctx);
|
|
77
|
+
const rawCode = lines.slice(rangeStart - 1, rangeEnd).join('\n');
|
|
78
|
+
sections.push(`=== SYMBOL ${i + 1}/${args.symbols.length}: ${label} ===`);
|
|
79
|
+
sections.push('');
|
|
80
|
+
sections.push(rawCode);
|
|
81
|
+
sections.push('');
|
|
82
|
+
// Track each symbol
|
|
83
|
+
contextRegistry.trackLoad(absPath, {
|
|
84
|
+
type: 'symbol',
|
|
85
|
+
symbolName: symName,
|
|
86
|
+
startLine: rangeStart,
|
|
87
|
+
endLine: rangeEnd,
|
|
88
|
+
tokens: estimateTokens(rawCode),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
sections.push('--- END EDIT CONTEXT ---');
|
|
92
|
+
sections.push('');
|
|
93
|
+
sections.push(`To edit: use exact text from each section as old_string in Edit tool.`);
|
|
94
|
+
if (resolved_count < args.symbols.length) {
|
|
95
|
+
sections.push(`WARNING: ${args.symbols.length - resolved_count} symbol(s) not found. Use smart_read to see available symbols.`);
|
|
96
|
+
}
|
|
97
|
+
const confidenceMeta = assessConfidence({
|
|
98
|
+
symbolResolved: resolved_count > 0,
|
|
99
|
+
fullFile: false,
|
|
100
|
+
truncated: false,
|
|
101
|
+
astAvailable: true,
|
|
102
|
+
});
|
|
103
|
+
sections.push(formatConfidence(confidenceMeta));
|
|
104
|
+
const output = sections.join('\n');
|
|
105
|
+
return { content: [{ type: 'text', text: output }] };
|
|
106
|
+
}
|
|
42
107
|
let startLine;
|
|
43
108
|
let endLine;
|
|
44
109
|
let targetLabel;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AstIndexClient } from '../ast-index/client.js';
|
|
2
|
+
import type { SymbolResolver } from '../core/symbol-resolver.js';
|
|
3
|
+
import type { FileCache } from '../core/file-cache.js';
|
|
4
|
+
import type { ContextRegistry } from '../core/context-registry.js';
|
|
5
|
+
export interface ReadSymbolsArgs {
|
|
6
|
+
path: string;
|
|
7
|
+
symbols: string[];
|
|
8
|
+
context_before?: number;
|
|
9
|
+
context_after?: number;
|
|
10
|
+
show?: 'full' | 'head' | 'tail' | 'outline';
|
|
11
|
+
}
|
|
12
|
+
export declare function handleReadSymbols(args: ReadSymbolsArgs, projectRoot: string, symbolResolver: SymbolResolver, fileCache: FileCache, contextRegistry: ContextRegistry, astIndex?: AstIndexClient, advisoryReminders?: boolean): Promise<{
|
|
13
|
+
content: Array<{
|
|
14
|
+
type: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=read-symbols.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { estimateTokens } from '../core/token-estimator.js';
|
|
3
|
+
import { resolveSafePath } from '../core/validation.js';
|
|
4
|
+
import { assessConfidence, formatConfidence } from '../core/confidence.js';
|
|
5
|
+
export async function handleReadSymbols(args, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, advisoryReminders = true) {
|
|
6
|
+
const absPath = resolveSafePath(projectRoot, args.path);
|
|
7
|
+
// Get file content ONCE
|
|
8
|
+
const cached = fileCache.get(absPath);
|
|
9
|
+
let lines;
|
|
10
|
+
if (cached) {
|
|
11
|
+
lines = cached.lines;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const content = await readFile(absPath, 'utf-8');
|
|
15
|
+
lines = content.split('\n');
|
|
16
|
+
}
|
|
17
|
+
// Get AST structure ONCE
|
|
18
|
+
let structure = cached?.structure;
|
|
19
|
+
if (!structure && astIndex) {
|
|
20
|
+
structure = await astIndex.outline(absPath) ?? undefined;
|
|
21
|
+
}
|
|
22
|
+
const N = args.symbols.length;
|
|
23
|
+
const sections = [];
|
|
24
|
+
// Show mode constants (same as read_symbol.ts)
|
|
25
|
+
const MAX_SYMBOL_LINES = 300;
|
|
26
|
+
const MAX_FULL_LINES = 500;
|
|
27
|
+
const HEAD = 50;
|
|
28
|
+
const TAIL = 30;
|
|
29
|
+
let anyTruncated = false;
|
|
30
|
+
let anyResolved = false;
|
|
31
|
+
let totalTokens = 0;
|
|
32
|
+
for (let i = 0; i < N; i++) {
|
|
33
|
+
const symbolName = args.symbols[i];
|
|
34
|
+
const idx = i + 1;
|
|
35
|
+
const resolved = await symbolResolver.resolve(symbolName, structure);
|
|
36
|
+
if (!resolved) {
|
|
37
|
+
sections.push(`SYMBOL ${idx}/${N}: ${symbolName}\n` +
|
|
38
|
+
`ERROR: Symbol "${symbolName}" not found in ${args.path}.\n` +
|
|
39
|
+
`HINT: Use smart_read("${args.path}") to see available symbols.`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
anyResolved = true;
|
|
43
|
+
const source = symbolResolver.extractSource(resolved, lines, {
|
|
44
|
+
contextBefore: args.context_before ?? 2,
|
|
45
|
+
contextAfter: args.context_after ?? 0,
|
|
46
|
+
});
|
|
47
|
+
const loc = `[L${resolved.startLine}-${resolved.endLine}]`;
|
|
48
|
+
const lineCount = resolved.endLine - resolved.startLine + 1;
|
|
49
|
+
// Determine effective show mode
|
|
50
|
+
const showMode = args.show ?? (lineCount > MAX_SYMBOL_LINES ? 'outline' : 'full');
|
|
51
|
+
let displaySource = source;
|
|
52
|
+
let truncated = false;
|
|
53
|
+
if (showMode === 'full') {
|
|
54
|
+
if (lineCount > MAX_FULL_LINES) {
|
|
55
|
+
const sourceLines = source.split('\n');
|
|
56
|
+
displaySource = sourceLines.slice(0, MAX_FULL_LINES).join('\n');
|
|
57
|
+
displaySource += `\n\n ... truncated at ${MAX_FULL_LINES} lines (${lineCount - MAX_FULL_LINES} more). Use show="head"/"tail" for targeted view.`;
|
|
58
|
+
truncated = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (showMode === 'head') {
|
|
62
|
+
const sourceLines = source.split('\n');
|
|
63
|
+
displaySource = sourceLines.slice(0, HEAD).join('\n');
|
|
64
|
+
if (lineCount > HEAD) {
|
|
65
|
+
displaySource += `\n\n ... ${lineCount - HEAD} more lines. Use show="tail" or read_symbol("${args.path}", "MethodName") for specific parts.`;
|
|
66
|
+
truncated = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (showMode === 'tail') {
|
|
70
|
+
const sourceLines = source.split('\n');
|
|
71
|
+
displaySource = sourceLines.slice(-TAIL).join('\n');
|
|
72
|
+
if (lineCount > TAIL) {
|
|
73
|
+
displaySource = ` ... ${lineCount - TAIL} lines above ...\n\n` + displaySource;
|
|
74
|
+
truncated = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// 'outline' mode: head + method list + tail
|
|
79
|
+
if (lineCount > HEAD + TAIL) {
|
|
80
|
+
const sourceLines = source.split('\n');
|
|
81
|
+
const head = sourceLines.slice(0, HEAD).join('\n');
|
|
82
|
+
const tail = sourceLines.slice(-TAIL).join('\n');
|
|
83
|
+
const omitted = sourceLines.length - HEAD - TAIL;
|
|
84
|
+
let methodOutline = '';
|
|
85
|
+
if (resolved.symbol.children && resolved.symbol.children.length > 0) {
|
|
86
|
+
const methodLines = resolved.symbol.children.map(c => {
|
|
87
|
+
const mLoc = `[L${c.location.startLine}-${c.location.endLine}]`;
|
|
88
|
+
return ` ${c.visibility === 'private' ? '🔒 ' : ''}${c.name}${c.kind === 'method' || c.kind === 'function' ? '()' : ''} ${mLoc} (${c.location.lineCount} lines)`;
|
|
89
|
+
});
|
|
90
|
+
methodOutline = `\nMETHODS (${resolved.symbol.children.length}):\n${methodLines.join('\n')}\n`;
|
|
91
|
+
}
|
|
92
|
+
displaySource = [
|
|
93
|
+
head,
|
|
94
|
+
'',
|
|
95
|
+
` ... ${omitted} lines omitted — use read_symbol("${args.path}", "MethodName") to read specific methods ...`,
|
|
96
|
+
methodOutline,
|
|
97
|
+
tail,
|
|
98
|
+
].join('\n');
|
|
99
|
+
truncated = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (truncated)
|
|
103
|
+
anyTruncated = true;
|
|
104
|
+
const symbolLines = [
|
|
105
|
+
`SYMBOL ${idx}/${N}: ${symbolName} (${resolved.symbol.kind}) ${loc} (${lineCount} lines${truncated ? `, show=${showMode}` : ''})`,
|
|
106
|
+
'',
|
|
107
|
+
displaySource,
|
|
108
|
+
];
|
|
109
|
+
if (resolved.symbol.references.length > 0) {
|
|
110
|
+
symbolLines.push('');
|
|
111
|
+
symbolLines.push(`REFERENCES: ${resolved.symbol.references.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
sections.push(symbolLines.join('\n'));
|
|
114
|
+
// Track each symbol
|
|
115
|
+
const sectionTokens = estimateTokens(symbolLines.join('\n'));
|
|
116
|
+
totalTokens += sectionTokens;
|
|
117
|
+
contextRegistry.trackLoad(absPath, {
|
|
118
|
+
type: 'symbol',
|
|
119
|
+
symbolName,
|
|
120
|
+
startLine: resolved.startLine,
|
|
121
|
+
endLine: resolved.endLine,
|
|
122
|
+
tokens: sectionTokens,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (cached?.hash) {
|
|
126
|
+
contextRegistry.setContentHash(absPath, cached.hash);
|
|
127
|
+
}
|
|
128
|
+
const header = `FILE: ${args.path} | SYMBOLS: ${N} requested`;
|
|
129
|
+
const body = sections.join('\n\n---\n\n');
|
|
130
|
+
const footer = 'CONTEXT TRACKED: These symbols are now in your context.';
|
|
131
|
+
const output = [header, '', body, '', footer].join('\n');
|
|
132
|
+
// Confidence metadata (aggregate)
|
|
133
|
+
const confidenceMeta = assessConfidence({
|
|
134
|
+
symbolResolved: anyResolved,
|
|
135
|
+
truncated: anyTruncated,
|
|
136
|
+
fullFile: false,
|
|
137
|
+
hasCallers: false,
|
|
138
|
+
astAvailable: !!structure,
|
|
139
|
+
});
|
|
140
|
+
return { content: [{ type: 'text', text: output + formatConfidence(confidenceMeta) }] };
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=read-symbols.js.map
|
|
@@ -221,6 +221,29 @@ function formatSmartDiff(allFiles, processedFiles, symbolChanges, args, rawToken
|
|
|
221
221
|
const lines = [];
|
|
222
222
|
lines.push(`CHANGES: ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}, +${totalAdded} -${totalRemoved} (${scopeLabel})`);
|
|
223
223
|
lines.push('');
|
|
224
|
+
// Affected symbols summary
|
|
225
|
+
const allSymbolChanges = [];
|
|
226
|
+
for (const [file, changes] of symbolChanges) {
|
|
227
|
+
for (const sc of changes) {
|
|
228
|
+
allSymbolChanges.push({ file, symbol: sc });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (allSymbolChanges.length > 0) {
|
|
232
|
+
lines.push('AFFECTED SYMBOLS:');
|
|
233
|
+
// Group by changeType
|
|
234
|
+
const modified = allSymbolChanges.filter(s => s.symbol.changeType === 'MODIFIED');
|
|
235
|
+
const added = allSymbolChanges.filter(s => s.symbol.changeType === 'ADDED');
|
|
236
|
+
const removed = allSymbolChanges.filter(s => s.symbol.changeType === 'REMOVED');
|
|
237
|
+
for (const [label, group] of [['MODIFIED', modified], ['ADDED', added], ['REMOVED', removed]]) {
|
|
238
|
+
if (group.length === 0)
|
|
239
|
+
continue;
|
|
240
|
+
for (const item of group) {
|
|
241
|
+
const parens = ['function', 'method'].includes(item.symbol.kind) ? '()' : '';
|
|
242
|
+
lines.push(` ${label}: ${item.file}::${item.symbol.name}${parens}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
224
247
|
for (const fd of processedFiles) {
|
|
225
248
|
if (lines.length >= MAX_OUTPUT_LINES) {
|
|
226
249
|
lines.push(`... truncated (${allFiles.length - processedFiles.indexOf(fd)} more files)`);
|
|
@@ -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
|
}
|