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.
@@ -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
+ }