kyawthiha-nextjs-agent-cli 1.0.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/README.md +188 -0
- package/dist/agent/agent.js +340 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/prompts/agent-prompt.js +527 -0
- package/dist/agent/summarizer.js +97 -0
- package/dist/agent/tools/ast-tools.js +601 -0
- package/dist/agent/tools/code-tools.js +1059 -0
- package/dist/agent/tools/file-tools.js +199 -0
- package/dist/agent/tools/index.js +25 -0
- package/dist/agent/tools/search-tools.js +404 -0
- package/dist/agent/tools/shell-tools.js +334 -0
- package/dist/agent/types.js +4 -0
- package/dist/cli/commands/config.js +61 -0
- package/dist/cli/commands/start.js +236 -0
- package/dist/cli/index.js +12 -0
- package/dist/utils/cred-store.js +70 -0
- package/dist/utils/logger.js +9 -0
- package/package.json +52 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST tools for the AI Agent
|
|
3
|
+
* Uses TypeScript Compiler API for accurate code structure analysis
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
/**
|
|
9
|
+
* Parse a TypeScript/JavaScript file and extract symbols using TS Compiler API
|
|
10
|
+
*/
|
|
11
|
+
function parseFileSymbols(filePath, content) {
|
|
12
|
+
const symbols = [];
|
|
13
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
14
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
15
|
+
return symbols;
|
|
16
|
+
}
|
|
17
|
+
// Determine script kind
|
|
18
|
+
let scriptKind;
|
|
19
|
+
switch (ext) {
|
|
20
|
+
case '.tsx':
|
|
21
|
+
scriptKind = ts.ScriptKind.TSX;
|
|
22
|
+
break;
|
|
23
|
+
case '.jsx':
|
|
24
|
+
scriptKind = ts.ScriptKind.JSX;
|
|
25
|
+
break;
|
|
26
|
+
case '.js':
|
|
27
|
+
case '.mjs':
|
|
28
|
+
case '.cjs':
|
|
29
|
+
scriptKind = ts.ScriptKind.JS;
|
|
30
|
+
break;
|
|
31
|
+
default: scriptKind = ts.ScriptKind.TS;
|
|
32
|
+
}
|
|
33
|
+
// Create source file
|
|
34
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
35
|
+
/**
|
|
36
|
+
* Get the kind name for a node
|
|
37
|
+
*/
|
|
38
|
+
function getKindName(node) {
|
|
39
|
+
if (ts.isFunctionDeclaration(node))
|
|
40
|
+
return 'function';
|
|
41
|
+
if (ts.isArrowFunction(node))
|
|
42
|
+
return 'arrow_function';
|
|
43
|
+
if (ts.isFunctionExpression(node))
|
|
44
|
+
return 'function_expression';
|
|
45
|
+
if (ts.isMethodDeclaration(node))
|
|
46
|
+
return 'method';
|
|
47
|
+
if (ts.isClassDeclaration(node))
|
|
48
|
+
return 'class';
|
|
49
|
+
if (ts.isInterfaceDeclaration(node))
|
|
50
|
+
return 'interface';
|
|
51
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
52
|
+
return 'type';
|
|
53
|
+
if (ts.isEnumDeclaration(node))
|
|
54
|
+
return 'enum';
|
|
55
|
+
if (ts.isVariableDeclaration(node))
|
|
56
|
+
return 'variable';
|
|
57
|
+
if (ts.isPropertyDeclaration(node))
|
|
58
|
+
return 'property';
|
|
59
|
+
if (ts.isGetAccessor(node))
|
|
60
|
+
return 'getter';
|
|
61
|
+
if (ts.isSetAccessor(node))
|
|
62
|
+
return 'setter';
|
|
63
|
+
if (ts.isConstructorDeclaration(node))
|
|
64
|
+
return 'constructor';
|
|
65
|
+
if (ts.isModuleDeclaration(node))
|
|
66
|
+
return 'namespace';
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if a node is exported
|
|
71
|
+
*/
|
|
72
|
+
function isExported(node) {
|
|
73
|
+
if (!ts.canHaveModifiers(node))
|
|
74
|
+
return false;
|
|
75
|
+
const mods = ts.getModifiers(node);
|
|
76
|
+
if (!mods)
|
|
77
|
+
return false;
|
|
78
|
+
return mods.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get modifiers as strings
|
|
82
|
+
*/
|
|
83
|
+
function getModifiers(node) {
|
|
84
|
+
if (!ts.canHaveModifiers(node))
|
|
85
|
+
return [];
|
|
86
|
+
const mods = ts.getModifiers(node);
|
|
87
|
+
if (!mods)
|
|
88
|
+
return [];
|
|
89
|
+
return mods
|
|
90
|
+
.map(m => {
|
|
91
|
+
switch (m.kind) {
|
|
92
|
+
case ts.SyntaxKind.PublicKeyword: return 'public';
|
|
93
|
+
case ts.SyntaxKind.PrivateKeyword: return 'private';
|
|
94
|
+
case ts.SyntaxKind.ProtectedKeyword: return 'protected';
|
|
95
|
+
case ts.SyntaxKind.StaticKeyword: return 'static';
|
|
96
|
+
case ts.SyntaxKind.ReadonlyKeyword: return 'readonly';
|
|
97
|
+
case ts.SyntaxKind.AsyncKeyword: return 'async';
|
|
98
|
+
case ts.SyntaxKind.AbstractKeyword: return 'abstract';
|
|
99
|
+
case ts.SyntaxKind.ExportKeyword: return 'export';
|
|
100
|
+
case ts.SyntaxKind.DefaultKeyword: return 'default';
|
|
101
|
+
default: return '';
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.filter(m => m);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get signature for a node
|
|
108
|
+
*/
|
|
109
|
+
function getSignature(node) {
|
|
110
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
let sig = lines[line]?.trim() || '';
|
|
113
|
+
// Clean up signature - remove body
|
|
114
|
+
sig = sig.replace(/\{[\s\S]*$/, '').trim();
|
|
115
|
+
if (sig.endsWith('{'))
|
|
116
|
+
sig = sig.slice(0, -1).trim();
|
|
117
|
+
// Limit length
|
|
118
|
+
if (sig.length > 100)
|
|
119
|
+
sig = sig.substring(0, 97) + '...';
|
|
120
|
+
return sig;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get name from a node
|
|
124
|
+
*/
|
|
125
|
+
function getName(node) {
|
|
126
|
+
if ('name' in node && node.name) {
|
|
127
|
+
const name = node.name;
|
|
128
|
+
if (ts.isIdentifier(name)) {
|
|
129
|
+
return name.text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Walk the AST and collect symbols
|
|
136
|
+
*/
|
|
137
|
+
function visit(node, parentName) {
|
|
138
|
+
const kind = getKindName(node);
|
|
139
|
+
// Skip unknown or unimportant nodes
|
|
140
|
+
if (kind === 'unknown') {
|
|
141
|
+
ts.forEachChild(node, child => visit(child, parentName));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Handle variable declarations specially
|
|
145
|
+
if (ts.isVariableStatement(node)) {
|
|
146
|
+
const isExp = isExported(node);
|
|
147
|
+
const mods = getModifiers(node);
|
|
148
|
+
node.declarationList.declarations.forEach(decl => {
|
|
149
|
+
const name = getName(decl);
|
|
150
|
+
if (!name)
|
|
151
|
+
return;
|
|
152
|
+
// Check if it's a function (arrow function or function expression)
|
|
153
|
+
let declKind = 'variable';
|
|
154
|
+
if (decl.initializer) {
|
|
155
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
156
|
+
declKind = 'function';
|
|
157
|
+
}
|
|
158
|
+
else if (ts.isClassExpression(decl.initializer)) {
|
|
159
|
+
declKind = 'class';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(decl.getStart());
|
|
163
|
+
const { line: endLine } = sourceFile.getLineAndCharacterOfPosition(decl.getEnd());
|
|
164
|
+
symbols.push({
|
|
165
|
+
name,
|
|
166
|
+
kind: declKind,
|
|
167
|
+
line: line + 1,
|
|
168
|
+
endLine: endLine + 1,
|
|
169
|
+
signature: getSignature(decl),
|
|
170
|
+
exported: isExp,
|
|
171
|
+
modifiers: mods,
|
|
172
|
+
parent: parentName
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const name = getName(node);
|
|
178
|
+
if (name) {
|
|
179
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
180
|
+
const { line: endLine } = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
181
|
+
symbols.push({
|
|
182
|
+
name,
|
|
183
|
+
kind,
|
|
184
|
+
line: line + 1,
|
|
185
|
+
endLine: endLine + 1,
|
|
186
|
+
signature: getSignature(node),
|
|
187
|
+
exported: isExported(node),
|
|
188
|
+
modifiers: getModifiers(node),
|
|
189
|
+
parent: parentName
|
|
190
|
+
});
|
|
191
|
+
// For classes, recursively process members
|
|
192
|
+
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
193
|
+
ts.forEachChild(node, child => visit(child, name));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
ts.forEachChild(node, child => visit(child, parentName));
|
|
198
|
+
}
|
|
199
|
+
visit(sourceFile);
|
|
200
|
+
return symbols;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Tool: List Symbols
|
|
204
|
+
* List all code symbols in a file using TypeScript Compiler API
|
|
205
|
+
*/
|
|
206
|
+
export const listSymbolsTool = {
|
|
207
|
+
name: 'list_symbols',
|
|
208
|
+
description: `List all code symbols (functions, classes, interfaces, types) in a file.
|
|
209
|
+
Uses TypeScript Compiler API for accurate parsing.
|
|
210
|
+
|
|
211
|
+
Output format:
|
|
212
|
+
L<line>-<endLine> [kind] name (modifiers)
|
|
213
|
+
signature
|
|
214
|
+
|
|
215
|
+
Filter by kind: function, class, interface, type, enum, method, variable`,
|
|
216
|
+
parameters: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
filePath: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'Path to the file to analyze'
|
|
222
|
+
},
|
|
223
|
+
kind: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'Filter by symbol kind',
|
|
226
|
+
enum: ['function', 'class', 'interface', 'type', 'enum', 'method', 'variable', 'all']
|
|
227
|
+
},
|
|
228
|
+
exportedOnly: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Show only exported symbols',
|
|
231
|
+
enum: ['true', 'false']
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
required: ['filePath']
|
|
235
|
+
},
|
|
236
|
+
execute: async (input) => {
|
|
237
|
+
try {
|
|
238
|
+
const filePath = input.filePath;
|
|
239
|
+
const filterKind = input.kind || 'all';
|
|
240
|
+
const exportedOnly = input.exportedOnly === 'true';
|
|
241
|
+
// Read file
|
|
242
|
+
let content;
|
|
243
|
+
try {
|
|
244
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error.code === 'ENOENT') {
|
|
248
|
+
return `Error: File not found: ${filePath}`;
|
|
249
|
+
}
|
|
250
|
+
return `Error reading file: ${error.message}`;
|
|
251
|
+
}
|
|
252
|
+
const symbols = parseFileSymbols(filePath, content);
|
|
253
|
+
// Filter
|
|
254
|
+
let filtered = symbols;
|
|
255
|
+
if (filterKind !== 'all') {
|
|
256
|
+
filtered = filtered.filter(s => s.kind === filterKind);
|
|
257
|
+
}
|
|
258
|
+
if (exportedOnly) {
|
|
259
|
+
filtered = filtered.filter(s => s.exported);
|
|
260
|
+
}
|
|
261
|
+
if (filtered.length === 0) {
|
|
262
|
+
return `No symbols found in ${path.basename(filePath)}${filterKind !== 'all' ? ` (kind: ${filterKind})` : ''}`;
|
|
263
|
+
}
|
|
264
|
+
// Format output
|
|
265
|
+
const output = [];
|
|
266
|
+
output.push(`File: ${path.basename(filePath)}`);
|
|
267
|
+
output.push(`Symbols: ${filtered.length}\n`);
|
|
268
|
+
// Group by kind
|
|
269
|
+
const byKind = {};
|
|
270
|
+
for (const sym of filtered) {
|
|
271
|
+
if (!byKind[sym.kind])
|
|
272
|
+
byKind[sym.kind] = [];
|
|
273
|
+
byKind[sym.kind].push(sym);
|
|
274
|
+
}
|
|
275
|
+
for (const [kind, syms] of Object.entries(byKind)) {
|
|
276
|
+
output.push(`=== ${kind.toUpperCase()}S (${syms.length}) ===`);
|
|
277
|
+
for (const sym of syms) {
|
|
278
|
+
const exportMark = sym.exported ? 'export ' : '';
|
|
279
|
+
const mods = sym.modifiers.filter(m => m !== 'export').join(' ');
|
|
280
|
+
const parentMark = sym.parent ? ` (in ${sym.parent})` : '';
|
|
281
|
+
output.push(`L${sym.line}-${sym.endLine}: ${exportMark}${mods ? mods + ' ' : ''}${sym.name}${parentMark}`);
|
|
282
|
+
output.push(` ${sym.signature}`);
|
|
283
|
+
}
|
|
284
|
+
output.push('');
|
|
285
|
+
}
|
|
286
|
+
return output.join('\n');
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return `Error: ${error.message}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Tool: Find Definition
|
|
295
|
+
* Find where a symbol is defined using TS Compiler API
|
|
296
|
+
*/
|
|
297
|
+
export const findDefinitionTool = {
|
|
298
|
+
name: 'find_definition',
|
|
299
|
+
description: `Find where a symbol (function, class, variable) is defined.
|
|
300
|
+
Uses TypeScript Compiler API for accurate matching.
|
|
301
|
+
|
|
302
|
+
Returns file path, line range, kind, and signature.`,
|
|
303
|
+
parameters: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
symbol: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'Name of the symbol to find'
|
|
309
|
+
},
|
|
310
|
+
searchPath: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
description: 'File or directory to search in'
|
|
313
|
+
},
|
|
314
|
+
kind: {
|
|
315
|
+
type: 'string',
|
|
316
|
+
description: 'Optional: filter by kind',
|
|
317
|
+
enum: ['function', 'class', 'interface', 'type', 'enum', 'variable', 'all']
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
required: ['symbol', 'searchPath']
|
|
321
|
+
},
|
|
322
|
+
execute: async (input) => {
|
|
323
|
+
try {
|
|
324
|
+
const symbol = input.symbol;
|
|
325
|
+
const searchPath = input.searchPath;
|
|
326
|
+
const filterKind = input.kind || 'all';
|
|
327
|
+
// Check if path exists
|
|
328
|
+
let stat;
|
|
329
|
+
try {
|
|
330
|
+
stat = await fs.stat(searchPath);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return `Error: Path not found: ${searchPath}`;
|
|
334
|
+
}
|
|
335
|
+
const results = [];
|
|
336
|
+
const processFile = async (filePath) => {
|
|
337
|
+
try {
|
|
338
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
339
|
+
const symbols = parseFileSymbols(filePath, content);
|
|
340
|
+
for (const sym of symbols) {
|
|
341
|
+
if (sym.name === symbol) {
|
|
342
|
+
if (filterKind === 'all' || sym.kind === filterKind) {
|
|
343
|
+
results.push({ file: filePath, symbol: sym });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Skip unreadable files
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
if (stat.isFile()) {
|
|
353
|
+
await processFile(searchPath);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Walk directory
|
|
357
|
+
const walk = async (dir) => {
|
|
358
|
+
try {
|
|
359
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
if (entry.name === 'node_modules' ||
|
|
362
|
+
entry.name === '.git' ||
|
|
363
|
+
entry.name === 'dist' ||
|
|
364
|
+
entry.name === '.next' ||
|
|
365
|
+
entry.name.startsWith('.')) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const fullPath = path.join(dir, entry.name);
|
|
369
|
+
if (entry.isDirectory()) {
|
|
370
|
+
await walk(fullPath);
|
|
371
|
+
}
|
|
372
|
+
else if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
|
|
373
|
+
await processFile(fullPath);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Skip
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
await walk(searchPath);
|
|
382
|
+
}
|
|
383
|
+
if (results.length === 0) {
|
|
384
|
+
return `No definition found for "${symbol}"${filterKind !== 'all' ? ` (kind: ${filterKind})` : ''}`;
|
|
385
|
+
}
|
|
386
|
+
const output = [];
|
|
387
|
+
output.push(`Found ${results.length} definition(s) for "${symbol}":\n`);
|
|
388
|
+
for (const { file, symbol: sym } of results) {
|
|
389
|
+
const relPath = stat.isDirectory() ? path.relative(searchPath, file) : path.basename(file);
|
|
390
|
+
output.push(`=== ${relPath}:${sym.line}-${sym.endLine} ===`);
|
|
391
|
+
output.push(`Kind: ${sym.kind}`);
|
|
392
|
+
output.push(`Exported: ${sym.exported}`);
|
|
393
|
+
output.push(`Modifiers: ${sym.modifiers.join(', ') || 'none'}`);
|
|
394
|
+
output.push(`Signature: ${sym.signature}`);
|
|
395
|
+
if (sym.parent) {
|
|
396
|
+
output.push(`Parent: ${sym.parent}`);
|
|
397
|
+
}
|
|
398
|
+
output.push('');
|
|
399
|
+
}
|
|
400
|
+
return output.join('\n');
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return `Error: ${error.message}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Tool: Find References
|
|
409
|
+
* Find all usages of a symbol
|
|
410
|
+
*/
|
|
411
|
+
export const findReferencesTool = {
|
|
412
|
+
name: 'find_references',
|
|
413
|
+
description: `Find all usages/references of a symbol in the codebase.
|
|
414
|
+
Uses word boundary matching for accurate results.
|
|
415
|
+
|
|
416
|
+
Returns list of files and lines where the symbol is used.`,
|
|
417
|
+
parameters: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
symbol: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: 'Symbol name to find references for'
|
|
423
|
+
},
|
|
424
|
+
searchPath: {
|
|
425
|
+
type: 'string',
|
|
426
|
+
description: 'Directory to search in'
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
required: ['symbol', 'searchPath']
|
|
430
|
+
},
|
|
431
|
+
execute: async (input) => {
|
|
432
|
+
try {
|
|
433
|
+
const symbol = input.symbol;
|
|
434
|
+
const searchPath = input.searchPath;
|
|
435
|
+
// Check path exists
|
|
436
|
+
try {
|
|
437
|
+
await fs.access(searchPath);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return `Error: Path not found: ${searchPath}`;
|
|
441
|
+
}
|
|
442
|
+
const references = [];
|
|
443
|
+
// Word boundary regex for accurate matching
|
|
444
|
+
const symbolRegex = new RegExp(`\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
445
|
+
const walk = async (dir) => {
|
|
446
|
+
try {
|
|
447
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
448
|
+
for (const entry of entries) {
|
|
449
|
+
if (entry.name === 'node_modules' ||
|
|
450
|
+
entry.name === '.git' ||
|
|
451
|
+
entry.name === 'dist' ||
|
|
452
|
+
entry.name === '.next' ||
|
|
453
|
+
entry.name.startsWith('.')) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const fullPath = path.join(dir, entry.name);
|
|
457
|
+
if (entry.isDirectory()) {
|
|
458
|
+
await walk(fullPath);
|
|
459
|
+
}
|
|
460
|
+
else if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
|
|
461
|
+
try {
|
|
462
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
463
|
+
const lines = content.split('\n');
|
|
464
|
+
const symbols = parseFileSymbols(fullPath, content);
|
|
465
|
+
const definitionLines = new Set(symbols.filter(s => s.name === symbol).map(s => s.line));
|
|
466
|
+
lines.forEach((line, idx) => {
|
|
467
|
+
if (symbolRegex.test(line)) {
|
|
468
|
+
references.push({
|
|
469
|
+
file: fullPath,
|
|
470
|
+
line: idx + 1,
|
|
471
|
+
content: line.trim(),
|
|
472
|
+
isDefinition: definitionLines.has(idx + 1)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// Skip
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Skip
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
await walk(searchPath);
|
|
488
|
+
if (references.length === 0) {
|
|
489
|
+
return `No references found for "${symbol}"`;
|
|
490
|
+
}
|
|
491
|
+
// Separate definitions from usages
|
|
492
|
+
const definitions = references.filter(r => r.isDefinition);
|
|
493
|
+
const usages = references.filter(r => !r.isDefinition);
|
|
494
|
+
// Group by file
|
|
495
|
+
const byFile = {};
|
|
496
|
+
for (const ref of references) {
|
|
497
|
+
const relPath = path.relative(searchPath, ref.file);
|
|
498
|
+
if (!byFile[relPath])
|
|
499
|
+
byFile[relPath] = [];
|
|
500
|
+
byFile[relPath].push({ line: ref.line, content: ref.content, isDefinition: ref.isDefinition });
|
|
501
|
+
}
|
|
502
|
+
const output = [];
|
|
503
|
+
output.push(`Found ${references.length} references to "${symbol}"`);
|
|
504
|
+
output.push(` Definitions: ${definitions.length}`);
|
|
505
|
+
output.push(` Usages: ${usages.length}`);
|
|
506
|
+
output.push(` Files: ${Object.keys(byFile).length}\n`);
|
|
507
|
+
for (const [file, refs] of Object.entries(byFile)) {
|
|
508
|
+
const defCount = refs.filter(r => r.isDefinition).length;
|
|
509
|
+
const useCount = refs.length - defCount;
|
|
510
|
+
output.push(`=== ${file} (${defCount} def, ${useCount} use) ===`);
|
|
511
|
+
for (const ref of refs.slice(0, 15)) {
|
|
512
|
+
const marker = ref.isDefinition ? '[DEF]' : '[USE]';
|
|
513
|
+
output.push(`L${ref.line} ${marker}: ${ref.content.substring(0, 80)}${ref.content.length > 80 ? '...' : ''}`);
|
|
514
|
+
}
|
|
515
|
+
if (refs.length > 15) {
|
|
516
|
+
output.push(`... and ${refs.length - 15} more in this file`);
|
|
517
|
+
}
|
|
518
|
+
output.push('');
|
|
519
|
+
}
|
|
520
|
+
return output.join('\n');
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
return `Error: ${error.message}`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Tool: Get File Outline
|
|
529
|
+
* Get a quick outline of a file's structure
|
|
530
|
+
*/
|
|
531
|
+
export const fileOutlineTool = {
|
|
532
|
+
name: 'file_outline',
|
|
533
|
+
description: `Get a quick structural outline of a file.
|
|
534
|
+
Shows the hierarchy of classes, functions, and their members.
|
|
535
|
+
|
|
536
|
+
Best for understanding file structure at a glance.`,
|
|
537
|
+
parameters: {
|
|
538
|
+
type: 'object',
|
|
539
|
+
properties: {
|
|
540
|
+
filePath: {
|
|
541
|
+
type: 'string',
|
|
542
|
+
description: 'Path to the file'
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
required: ['filePath']
|
|
546
|
+
},
|
|
547
|
+
execute: async (input) => {
|
|
548
|
+
try {
|
|
549
|
+
const filePath = input.filePath;
|
|
550
|
+
let content;
|
|
551
|
+
try {
|
|
552
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (error.code === 'ENOENT') {
|
|
556
|
+
return `Error: File not found: ${filePath}`;
|
|
557
|
+
}
|
|
558
|
+
return `Error reading file: ${error.message}`;
|
|
559
|
+
}
|
|
560
|
+
const symbols = parseFileSymbols(filePath, content);
|
|
561
|
+
const totalLines = content.split('\n').length;
|
|
562
|
+
if (symbols.length === 0) {
|
|
563
|
+
return `No symbols found in ${path.basename(filePath)} (${totalLines} lines)`;
|
|
564
|
+
}
|
|
565
|
+
// Build tree structure
|
|
566
|
+
const output = [];
|
|
567
|
+
output.push(`File: ${path.basename(filePath)}`);
|
|
568
|
+
output.push(`Lines: ${totalLines}`);
|
|
569
|
+
output.push(`Symbols: ${symbols.length}\n`);
|
|
570
|
+
// Group top-level and nested symbols
|
|
571
|
+
const topLevel = symbols.filter(s => !s.parent);
|
|
572
|
+
const nested = symbols.filter(s => s.parent);
|
|
573
|
+
for (const sym of topLevel) {
|
|
574
|
+
const exp = sym.exported ? 'export ' : '';
|
|
575
|
+
const mods = sym.modifiers.filter(m => m !== 'export').join(' ');
|
|
576
|
+
output.push(`${exp}${mods ? mods + ' ' : ''}${sym.kind} ${sym.name} [L${sym.line}-${sym.endLine}]`);
|
|
577
|
+
// Add nested members
|
|
578
|
+
const members = nested.filter(n => n.parent === sym.name);
|
|
579
|
+
for (const member of members) {
|
|
580
|
+
const memberMods = member.modifiers.join(' ');
|
|
581
|
+
output.push(` └─ ${memberMods ? memberMods + ' ' : ''}${member.kind} ${member.name} [L${member.line}]`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return output.join('\n');
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
return `Error: ${error.message}`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* Get all AST tools
|
|
593
|
+
*/
|
|
594
|
+
export function getAstTools() {
|
|
595
|
+
return [
|
|
596
|
+
listSymbolsTool,
|
|
597
|
+
findDefinitionTool,
|
|
598
|
+
findReferencesTool,
|
|
599
|
+
fileOutlineTool,
|
|
600
|
+
];
|
|
601
|
+
}
|