token-pilot 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import { stat } from 'node:fs/promises';
4
- import { createHash } from 'node:crypto';
5
- import { readFile } from 'node:fs/promises';
6
3
  import { findBinary, installBinary } from './binary-manager.js';
4
+ import { parseFileCount, parseOutlineText, parseImportsText, parseImplementationsText, parseHierarchyText, parseAgrepText, parseTodoText, parseDeprecatedText, parseAnnotationsText, parseModuleListText, parseModuleDepText, parseUnusedDepsText, parseModuleApiText, } from './parser.js';
5
+ import { buildFileStructure } from './enricher.js';
7
6
  const execFileAsync = promisify(execFile);
8
7
  export class AstIndexClient {
9
8
  static MAX_INDEX_FILES = 50_000;
@@ -51,12 +50,10 @@ export class AstIndexClient {
51
50
  async ensureIndex() {
52
51
  if (this.indexed)
53
52
  return;
54
- // Project root is too broad (/, home dir) — refuse to build
55
53
  if (this.indexDisabled) {
56
54
  throw new Error('ast-index: index build disabled — project root is too broad (e.g. /). ' +
57
55
  'Configure mcpServers with "args": ["/path/to/project"] to set the correct project root.');
58
56
  }
59
- // If a previous build found >50k files, don't retry
60
57
  if (this.indexOversized) {
61
58
  throw new Error('ast-index disabled: previous build indexed >50k files (likely node_modules). ' +
62
59
  'Ensure node_modules is in .gitignore, then restart the MCP server.');
@@ -73,14 +70,12 @@ export class AstIndexClient {
73
70
  }
74
71
  }
75
72
  async buildIndex() {
76
- // Check if index already exists and has files
77
73
  let existingFileCount = 0;
78
74
  try {
79
75
  const stats = await this.exec(['--format', 'json', 'stats']);
80
- existingFileCount = this.parseFileCount(stats);
76
+ existingFileCount = parseFileCount(stats);
81
77
  }
82
78
  catch { /* no index yet */ }
83
- // Guard: existing index is oversized (node_modules leak from previous build)
84
79
  if (existingFileCount > AstIndexClient.MAX_INDEX_FILES) {
85
80
  console.error(`[token-pilot] ast-index: existing index has ${existingFileCount} files (>${AstIndexClient.MAX_INDEX_FILES}) — likely includes node_modules. Clearing.`);
86
81
  try {
@@ -88,19 +83,15 @@ export class AstIndexClient {
88
83
  }
89
84
  catch { /* best effort */ }
90
85
  existingFileCount = 0;
91
- // Fall through to rebuild — maybe .gitignore was fixed
92
86
  }
93
87
  if (existingFileCount > 0) {
94
- // Index exists — use incremental update (fast)
95
88
  console.error(`[token-pilot] ast-index: updating index (${existingFileCount} files)...`);
96
89
  try {
97
90
  await this.exec(['update'], 30000);
98
- // Re-check count after update
99
91
  try {
100
- existingFileCount = this.parseFileCount(await this.exec(['--format', 'json', 'stats']));
92
+ existingFileCount = parseFileCount(await this.exec(['--format', 'json', 'stats']));
101
93
  }
102
94
  catch { /* keep previous count */ }
103
- // Guard: update may have grown index beyond limit
104
95
  if (existingFileCount > AstIndexClient.MAX_INDEX_FILES) {
105
96
  return this.handleOversizedIndex(existingFileCount);
106
97
  }
@@ -112,12 +103,10 @@ export class AstIndexClient {
112
103
  console.error(`[token-pilot] ast-index: update failed, falling back to rebuild — ${updateErr instanceof Error ? updateErr.message : updateErr}`);
113
104
  }
114
105
  }
115
- // No index or update failed — full rebuild
116
106
  console.error('[token-pilot] ast-index: building index (this may take a moment)...');
117
107
  try {
118
108
  await this.exec(['rebuild'], 120000);
119
- const fileCount = this.parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
120
- // Guard: rebuild produced oversized index
109
+ const fileCount = parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
121
110
  if (fileCount > AstIndexClient.MAX_INDEX_FILES) {
122
111
  return this.handleOversizedIndex(fileCount);
123
112
  }
@@ -125,10 +114,9 @@ export class AstIndexClient {
125
114
  console.error(`[token-pilot] ast-index: index built (${fileCount} files)`);
126
115
  }
127
116
  catch (buildErr) {
128
- // If rebuild failed due to lock, check if index is usable anyway
129
117
  const errMsg = buildErr instanceof Error ? buildErr.message : String(buildErr);
130
118
  if (errMsg.includes('lock') || errMsg.includes('already running')) {
131
- const count = this.parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
119
+ const count = parseFileCount(await this.exec(['--format', 'json', 'stats']).catch(() => ''));
132
120
  if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
133
121
  this.indexed = true;
134
122
  console.error(`[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`);
@@ -142,7 +130,6 @@ export class AstIndexClient {
142
130
  throw buildErr;
143
131
  }
144
132
  }
145
- /** Mark index as oversized — disables index-dependent tools, outline still works */
146
133
  async handleOversizedIndex(fileCount) {
147
134
  this.indexOversized = true;
148
135
  this.indexed = false;
@@ -156,39 +143,24 @@ export class AstIndexClient {
156
143
  ` → Tools disabled: find_unused, find_usages, related_files, project_overview\n` +
157
144
  ` → Tools still working: outline, smart_read, smart_read_many, read_symbol`);
158
145
  }
159
- /** Extract file count from stats output (JSON or text) */
160
- parseFileCount(statsText) {
161
- // Try JSON first (--format json)
162
- try {
163
- const json = JSON.parse(statsText);
164
- if (json?.stats?.file_count !== undefined)
165
- return json.stats.file_count;
166
- }
167
- catch { /* not JSON, fall through */ }
168
- // Fallback: text format
169
- const match = statsText.match(/Files:\s*(\d+)/);
170
- return match ? parseInt(match[1], 10) : 0;
171
- }
172
146
  async outline(filePath) {
173
- // outline parses a single file — try directly without requiring full index
174
147
  try {
175
148
  const result = await this.exec(['outline', filePath]);
176
- const entries = this.parseOutlineText(result);
149
+ const entries = parseOutlineText(result);
177
150
  if (entries.length === 0)
178
151
  return null;
179
- return await this.buildFileStructure(filePath, entries);
152
+ return await buildFileStructure(filePath, entries);
180
153
  }
181
154
  catch {
182
- // Direct call failed — try building index first (unless disabled/oversized)
183
155
  if (this.indexDisabled || this.indexOversized)
184
156
  return null;
185
157
  try {
186
158
  await this.ensureIndex();
187
159
  const result = await this.exec(['outline', filePath]);
188
- const entries = this.parseOutlineText(result);
160
+ const entries = parseOutlineText(result);
189
161
  if (entries.length === 0)
190
162
  return null;
191
- return await this.buildFileStructure(filePath, entries);
163
+ return await buildFileStructure(filePath, entries);
192
164
  }
193
165
  catch (err) {
194
166
  console.error(`[token-pilot] ast-index outline failed for ${filePath}: ${err instanceof Error ? err.message : err}`);
@@ -196,77 +168,7 @@ export class AstIndexClient {
196
168
  }
197
169
  }
198
170
  }
199
- /**
200
- * Parse text output from `ast-index outline`:
201
- * Outline of src/file.ts:
202
- * :10 ClassName [class]
203
- * :11 propName [property]
204
- * :14 methodName [function]
205
- */
206
- parseOutlineText(text) {
207
- const lines = text.split('\n');
208
- const entries = [];
209
- const classStack = [];
210
- for (const line of lines) {
211
- // Match: optional whitespace, :LINE_NUM, SYMBOL_NAME, [KIND]
212
- const match = line.match(/^(\s*):(\d+)\s+(\S+)\s+\[(\w+)\]/);
213
- if (!match)
214
- continue;
215
- const indent = match[1].length;
216
- const entry = {
217
- name: match[3],
218
- kind: match[4],
219
- start_line: parseInt(match[2], 10),
220
- end_line: 0, // computed later
221
- };
222
- // Pop stack until we find a parent with less indent
223
- while (classStack.length > 0 && classStack[classStack.length - 1].indent >= indent) {
224
- classStack.pop();
225
- }
226
- if (classStack.length > 0) {
227
- // This is a child of the top of stack
228
- const parent = classStack[classStack.length - 1].entry;
229
- if (!parent.children)
230
- parent.children = [];
231
- parent.children.push(entry);
232
- }
233
- else {
234
- entries.push(entry);
235
- }
236
- // Push classes/interfaces onto stack as potential parents
237
- if (['class', 'interface', 'struct', 'enum', 'impl', 'trait', 'namespace', 'module'].includes(entry.kind.toLowerCase())) {
238
- classStack.push({ entry, indent });
239
- }
240
- }
241
- // Compute end_line for all entries
242
- this.computeEndLines(entries);
243
- return entries;
244
- }
245
- /** Compute end_line from sequential start positions */
246
- computeEndLines(entries) {
247
- for (let i = 0; i < entries.length; i++) {
248
- // Children first (recursive)
249
- if (entries[i].children?.length) {
250
- this.computeEndLines(entries[i].children);
251
- }
252
- if (i < entries.length - 1) {
253
- // end = next sibling's start - 1
254
- entries[i].end_line = entries[i + 1].start_line - 1;
255
- }
256
- else {
257
- // last entry: estimate based on children or use start + reasonable default
258
- const children = entries[i].children;
259
- if (children?.length) {
260
- entries[i].end_line = children[children.length - 1].end_line + 1;
261
- }
262
- else {
263
- entries[i].end_line = entries[i].start_line + 10; // estimated
264
- }
265
- }
266
- }
267
- }
268
171
  async symbol(name) {
269
- // Try directly first (works if index exists from a previous session)
270
172
  try {
271
173
  const result = await this.exec(['symbol', name, '--format', 'json']);
272
174
  const raw = JSON.parse(result);
@@ -276,7 +178,6 @@ export class AstIndexClient {
276
178
  }
277
179
  }
278
180
  catch { /* fall through to ensureIndex path */ }
279
- // Direct call failed — try building index (unless disabled/oversized)
280
181
  if (this.indexDisabled || this.indexOversized)
281
182
  return null;
282
183
  try {
@@ -317,7 +218,6 @@ export class AstIndexClient {
317
218
  })) : []),
318
219
  ...(Array.isArray(parsed.references) ? parsed.references : []),
319
220
  ];
320
- // Fallback: if parsed is an array directly
321
221
  const matches = all.length > 0 ? all : (Array.isArray(parsed) ? parsed : []);
322
222
  const mapped = matches
323
223
  .map((m) => ({
@@ -326,7 +226,7 @@ export class AstIndexClient {
326
226
  text: m.content ?? m.text ?? m.signature ?? '',
327
227
  }))
328
228
  .filter(r => r.file !== '' && r.text !== '');
329
- // Deduplicate by file:line (merge of 4 categories creates dupes)
229
+ // Deduplicate by file:line
330
230
  const seen = new Set();
331
231
  return mapped.filter(r => {
332
232
  const key = `${r.file}:${r.line}`;
@@ -348,12 +248,7 @@ export class AstIndexClient {
348
248
  const raw = JSON.parse(result);
349
249
  if (!Array.isArray(raw))
350
250
  return [];
351
- return raw.map(u => ({
352
- file: u.path,
353
- line: u.line,
354
- text: u.context,
355
- kind: 'reference',
356
- }));
251
+ return raw.map(u => ({ file: u.path, line: u.line, text: u.context, kind: 'reference' }));
357
252
  }
358
253
  catch (err) {
359
254
  console.error(`[token-pilot] ast-index usages failed: ${err instanceof Error ? err.message : err}`);
@@ -368,8 +263,7 @@ export class AstIndexClient {
368
263
  return JSON.parse(result);
369
264
  }
370
265
  catch {
371
- // JSON parse failed — parse text format as fallback
372
- return this.parseImplementationsText(result);
266
+ return parseImplementationsText(result);
373
267
  }
374
268
  }
375
269
  catch (err) {
@@ -385,8 +279,7 @@ export class AstIndexClient {
385
279
  return JSON.parse(result);
386
280
  }
387
281
  catch {
388
- // JSON parse failed — parse text format as fallback
389
- return this.parseHierarchyText(result, name);
282
+ return parseHierarchyText(result, name);
390
283
  }
391
284
  }
392
285
  catch (err) {
@@ -394,62 +287,6 @@ export class AstIndexClient {
394
287
  return null;
395
288
  }
396
289
  }
397
- parseImplementationsText(text) {
398
- const results = [];
399
- // Parse lines like: "class ClassName (file.php:42)"
400
- for (const line of text.split('\n')) {
401
- const m = line.match(/^\s*(class|interface|trait|struct|impl)\s+(\S+)\s+\((.+):(\d+)\)/);
402
- if (m) {
403
- results.push({ kind: m[1], name: m[2], file: m[3], line: parseInt(m[4], 10) });
404
- }
405
- }
406
- return results;
407
- }
408
- parseHierarchyText(text, rootName) {
409
- if (!text.trim())
410
- return null;
411
- // Parse ast-index hierarchy text output:
412
- // Hierarchy for 'ClassName':
413
- // Parents:
414
- // ParentClass (extends)
415
- // Children:
416
- // ChildClass (implements) (file.ts:42)
417
- const lines = text.split('\n');
418
- const parents = [];
419
- const childNodes = [];
420
- let section = 'none';
421
- for (const line of lines) {
422
- const trimmed = line.trim();
423
- if (trimmed === 'Parents:') {
424
- section = 'parents';
425
- continue;
426
- }
427
- if (trimmed === 'Children:') {
428
- section = 'children';
429
- continue;
430
- }
431
- if (trimmed.startsWith('Hierarchy for') || !trimmed)
432
- continue;
433
- // Match: SymbolName (relationship) (file:line) — file:line is optional
434
- const m = trimmed.match(/^(\S+)\s+\((\w+)\)(?:\s+\((.+):(\d+)\))?/);
435
- if (m && section !== 'none') {
436
- const node = {
437
- name: m[1],
438
- kind: m[2], // extends, implements, etc.
439
- children: [],
440
- file: m[3],
441
- line: m[4] ? parseInt(m[4], 10) : undefined,
442
- };
443
- if (section === 'parents')
444
- parents.push(node);
445
- else
446
- childNodes.push(node);
447
- }
448
- }
449
- if (parents.length === 0 && childNodes.length === 0)
450
- return null;
451
- return { name: rootName, kind: 'class', children: childNodes, parents };
452
- }
453
290
  async stats() {
454
291
  try {
455
292
  return await this.exec(['stats']);
@@ -458,10 +295,6 @@ export class AstIndexClient {
458
295
  return null;
459
296
  }
460
297
  }
461
- /**
462
- * List all files known to the ast-index.
463
- * Parses the `files` command output which lists one file per line.
464
- */
465
298
  async listFiles() {
466
299
  try {
467
300
  await this.ensureIndex();
@@ -473,10 +306,6 @@ export class AstIndexClient {
473
306
  return [];
474
307
  }
475
308
  }
476
- /**
477
- * Cross-references: definitions + imports + usages in one call.
478
- * Replaces separate symbol() + usages() calls.
479
- */
480
309
  async refs(symbolName, limit = 20) {
481
310
  await this.ensureIndex();
482
311
  try {
@@ -488,9 +317,6 @@ export class AstIndexClient {
488
317
  return { definitions: [], imports: [], usages: [] };
489
318
  }
490
319
  }
491
- /**
492
- * Project map: directory structure with file counts and symbol kinds.
493
- */
494
320
  async map(options) {
495
321
  await this.ensureIndex();
496
322
  try {
@@ -507,9 +333,6 @@ export class AstIndexClient {
507
333
  return null;
508
334
  }
509
335
  }
510
- /**
511
- * Detect project conventions: architecture, frameworks, naming patterns.
512
- */
513
336
  async conventions() {
514
337
  await this.ensureIndex();
515
338
  try {
@@ -521,9 +344,6 @@ export class AstIndexClient {
521
344
  return null;
522
345
  }
523
346
  }
524
- /**
525
- * Find callers of a function.
526
- */
527
347
  async callers(functionName, limit = 50) {
528
348
  await this.ensureIndex();
529
349
  try {
@@ -536,9 +356,6 @@ export class AstIndexClient {
536
356
  return [];
537
357
  }
538
358
  }
539
- /**
540
- * Show call hierarchy tree (callers tree up).
541
- */
542
359
  async callTree(functionName, depth = 3) {
543
360
  await this.ensureIndex();
544
361
  try {
@@ -550,9 +367,6 @@ export class AstIndexClient {
550
367
  return null;
551
368
  }
552
369
  }
553
- /**
554
- * Show changed symbols since base branch (git diff).
555
- */
556
370
  async changed(base) {
557
371
  await this.ensureIndex();
558
372
  try {
@@ -568,9 +382,6 @@ export class AstIndexClient {
568
382
  return [];
569
383
  }
570
384
  }
571
- /**
572
- * Find potentially unused symbols.
573
- */
574
385
  async unusedSymbols(options) {
575
386
  await this.ensureIndex();
576
387
  try {
@@ -590,72 +401,27 @@ export class AstIndexClient {
590
401
  return [];
591
402
  }
592
403
  }
593
- /**
594
- * Get imports for a specific file.
595
- * Parses text output: " { X, Y } from 'source';"
596
- */
597
404
  async fileImports(filePath) {
598
405
  await this.ensureIndex();
599
406
  try {
600
407
  const result = await this.exec(['imports', filePath]);
601
- return this.parseImportsText(result);
408
+ return parseImportsText(result);
602
409
  }
603
410
  catch (err) {
604
411
  console.error(`[token-pilot] ast-index imports failed: ${err instanceof Error ? err.message : err}`);
605
412
  return [];
606
413
  }
607
414
  }
608
- parseImportsText(text) {
609
- const entries = [];
610
- for (const line of text.split('\n')) {
611
- const trimmed = line.trim();
612
- if (!trimmed || trimmed.startsWith('Imports in') || trimmed.startsWith('Total:'))
613
- continue;
614
- // Match: { X, Y } from 'source'
615
- const braceMatch = trimmed.match(/^\{\s*(.+?)\s*\}\s+from\s+['"](.+?)['"]/);
616
- if (braceMatch) {
617
- entries.push({
618
- specifiers: braceMatch[1].split(',').map(s => s.trim()),
619
- source: braceMatch[2],
620
- });
621
- continue;
622
- }
623
- // Match: * as X from 'source'
624
- const nsMatch = trimmed.match(/^\*\s+as\s+(\S+)\s+from\s+['"](.+?)['"]/);
625
- if (nsMatch) {
626
- entries.push({
627
- specifiers: [nsMatch[1]],
628
- source: nsMatch[2],
629
- isNamespace: true,
630
- });
631
- continue;
632
- }
633
- // Match: X from 'source' (default import)
634
- const defaultMatch = trimmed.match(/^(\w+)\s+from\s+['"](.+?)['"]/);
635
- if (defaultMatch) {
636
- entries.push({
637
- specifiers: [defaultMatch[1]],
638
- source: defaultMatch[2],
639
- isDefault: true,
640
- });
641
- continue;
642
- }
643
- }
644
- return entries;
645
- }
646
415
  // --- Code audit commands ---
647
- /** Check if ast-grep (sg) is available for structural pattern search */
648
416
  async checkAstGrep() {
649
417
  if (this.astGrepAvailable !== null)
650
418
  return this.astGrepAvailable;
651
- // Try system PATH first
652
419
  try {
653
420
  await execFileAsync('sg', ['--version'], { timeout: 3000 });
654
421
  this.astGrepAvailable = true;
655
422
  return true;
656
423
  }
657
424
  catch { /* not in PATH */ }
658
- // Try node_modules/.bin/sg (from optionalDependencies or source installs)
659
425
  try {
660
426
  const localBinDir = new URL('../../node_modules/.bin', import.meta.url).pathname;
661
427
  await execFileAsync(localBinDir + '/sg', ['--version'], { timeout: 3000 });
@@ -667,7 +433,6 @@ export class AstIndexClient {
667
433
  this.astGrepAvailable = false;
668
434
  return false;
669
435
  }
670
- /** Structural pattern search via ast-grep. Requires ast-grep (sg) installed. */
671
436
  async agrep(pattern, options) {
672
437
  if (this.indexDisabled || this.indexOversized)
673
438
  return [];
@@ -684,129 +449,52 @@ export class AstIndexClient {
684
449
  args.push('--lang', options.lang);
685
450
  try {
686
451
  const result = await this.exec(args, 15000);
687
- return this.parseAgrepText(result).slice(0, limit);
452
+ return parseAgrepText(result).slice(0, limit);
688
453
  }
689
454
  catch (err) {
690
455
  console.error(`[token-pilot] ast-index agrep failed: ${err instanceof Error ? err.message : err}`);
691
456
  return [];
692
457
  }
693
458
  }
694
- parseAgrepText(text) {
695
- const results = [];
696
- for (const line of text.split('\n')) {
697
- if (!line.trim())
698
- continue;
699
- // Format: file:line:matched_text OR file:line: matched_text
700
- const match = line.match(/^(.+?):(\d+):(.*)$/);
701
- if (match) {
702
- results.push({
703
- file: match[1],
704
- line: parseInt(match[2], 10),
705
- text: match[3].trim(),
706
- });
707
- }
708
- }
709
- return results;
710
- }
711
- /** Find TODO/FIXME/HACK comments in the project */
712
459
  async todo() {
713
460
  if (this.indexDisabled || this.indexOversized)
714
461
  return [];
715
462
  await this.ensureIndex();
716
463
  try {
717
464
  const result = await this.exec(['todo'], 15000);
718
- return this.parseTodoText(result);
465
+ return parseTodoText(result);
719
466
  }
720
467
  catch (err) {
721
468
  console.error(`[token-pilot] ast-index todo failed: ${err instanceof Error ? err.message : err}`);
722
469
  return [];
723
470
  }
724
471
  }
725
- parseTodoText(text) {
726
- const results = [];
727
- for (const line of text.split('\n')) {
728
- if (!line.trim())
729
- continue;
730
- // Try format: file:line: KIND: message OR file:line: KIND message
731
- const match = line.match(/^(.+?):(\d+):\s*(TODO|FIXME|HACK|XXX|NOTE|WARN(?:ING)?)[:\s]+(.*)$/i);
732
- if (match) {
733
- results.push({
734
- file: match[1],
735
- line: parseInt(match[2], 10),
736
- kind: match[3].toUpperCase(),
737
- text: match[4].trim(),
738
- });
739
- }
740
- }
741
- return results;
742
- }
743
- /** Find @Deprecated symbols in the project */
744
472
  async deprecated() {
745
473
  if (this.indexDisabled || this.indexOversized)
746
474
  return [];
747
475
  await this.ensureIndex();
748
476
  try {
749
477
  const result = await this.exec(['deprecated'], 15000);
750
- return this.parseDeprecatedText(result);
478
+ return parseDeprecatedText(result);
751
479
  }
752
480
  catch (err) {
753
481
  console.error(`[token-pilot] ast-index deprecated failed: ${err instanceof Error ? err.message : err}`);
754
482
  return [];
755
483
  }
756
484
  }
757
- parseDeprecatedText(text) {
758
- const results = [];
759
- for (const line of text.split('\n')) {
760
- if (!line.trim())
761
- continue;
762
- // Try format: kind name (file:line) - message OR kind name (file:line)
763
- const match = line.match(/^(\w+)\s+(\S+)\s+\((.+?):(\d+)\)(?:\s*-\s*(.+))?$/);
764
- if (match) {
765
- results.push({
766
- kind: match[1],
767
- name: match[2],
768
- file: match[3],
769
- line: parseInt(match[4], 10),
770
- message: match[5]?.trim(),
771
- });
772
- }
773
- }
774
- return results;
775
- }
776
- /** Find symbols with a specific annotation/decorator */
777
485
  async annotations(name) {
778
486
  if (this.indexDisabled || this.indexOversized)
779
487
  return [];
780
488
  await this.ensureIndex();
781
489
  try {
782
490
  const result = await this.exec(['annotations', name], 15000);
783
- return this.parseAnnotationsText(result, name);
491
+ return parseAnnotationsText(result, name);
784
492
  }
785
493
  catch (err) {
786
494
  console.error(`[token-pilot] ast-index annotations failed: ${err instanceof Error ? err.message : err}`);
787
495
  return [];
788
496
  }
789
497
  }
790
- parseAnnotationsText(text, annotationName) {
791
- const results = [];
792
- for (const line of text.split('\n')) {
793
- if (!line.trim())
794
- continue;
795
- // Try format: kind name (file:line) OR @Annotation kind name (file:line)
796
- const match = line.match(/^(?:@\S+\s+)?(\w+)\s+(\S+)\s+\((.+?):(\d+)\)$/);
797
- if (match) {
798
- results.push({
799
- kind: match[1],
800
- name: match[2],
801
- file: match[3],
802
- line: parseInt(match[4], 10),
803
- annotation: annotationName,
804
- });
805
- }
806
- }
807
- return results;
808
- }
809
- /** Trigger incremental index update (called by file watcher after edits) */
810
498
  async incrementalUpdate() {
811
499
  if (!this.indexed || this.indexDisabled || this.indexOversized)
812
500
  return;
@@ -817,8 +505,7 @@ export class AstIndexClient {
817
505
  console.error(`[token-pilot] ast-index incremental update failed: ${err instanceof Error ? err.message : err}`);
818
506
  }
819
507
  }
820
- // --- Module analysis methods (ast-index v3.27.0) ---
821
- /** List project modules matching optional pattern */
508
+ // --- Module analysis methods ---
822
509
  async modules(pattern) {
823
510
  if (this.indexDisabled || this.indexOversized)
824
511
  return [];
@@ -826,207 +513,89 @@ export class AstIndexClient {
826
513
  try {
827
514
  const cmdArgs = pattern ? ['module', pattern] : ['module'];
828
515
  const result = await this.exec(cmdArgs, 15000);
829
- return this.parseModuleListText(result);
516
+ return parseModuleListText(result);
830
517
  }
831
518
  catch (err) {
832
519
  console.error(`[token-pilot] ast-index module failed: ${err instanceof Error ? err.message : err}`);
833
520
  return [];
834
521
  }
835
522
  }
836
- /** Get dependencies of a module */
837
523
  async moduleDeps(module) {
838
524
  if (this.indexDisabled || this.indexOversized)
839
525
  return [];
840
526
  await this.ensureIndex();
841
527
  try {
842
528
  const result = await this.exec(['deps', module], 15000);
843
- return this.parseModuleDepText(result);
529
+ return parseModuleDepText(result);
844
530
  }
845
531
  catch (err) {
846
532
  console.error(`[token-pilot] ast-index deps failed: ${err instanceof Error ? err.message : err}`);
847
533
  return [];
848
534
  }
849
535
  }
850
- /** Get modules that depend on this module */
851
536
  async moduleDependents(module) {
852
537
  if (this.indexDisabled || this.indexOversized)
853
538
  return [];
854
539
  await this.ensureIndex();
855
540
  try {
856
541
  const result = await this.exec(['dependents', module], 15000);
857
- return this.parseModuleDepText(result);
542
+ return parseModuleDepText(result);
858
543
  }
859
544
  catch (err) {
860
545
  console.error(`[token-pilot] ast-index dependents failed: ${err instanceof Error ? err.message : err}`);
861
546
  return [];
862
547
  }
863
548
  }
864
- /** Find unused dependencies of a module */
865
549
  async unusedDeps(module) {
866
550
  if (this.indexDisabled || this.indexOversized)
867
551
  return [];
868
552
  await this.ensureIndex();
869
553
  try {
870
554
  const result = await this.exec(['unused-deps', module], 15000);
871
- return this.parseUnusedDepsText(result);
555
+ return parseUnusedDepsText(result);
872
556
  }
873
557
  catch (err) {
874
558
  console.error(`[token-pilot] ast-index unused-deps failed: ${err instanceof Error ? err.message : err}`);
875
559
  return [];
876
560
  }
877
561
  }
878
- /** Get public API of a module */
879
562
  async moduleApi(module) {
880
563
  if (this.indexDisabled || this.indexOversized)
881
564
  return [];
882
565
  await this.ensureIndex();
883
566
  try {
884
567
  const result = await this.exec(['api', module], 15000);
885
- return this.parseModuleApiText(result);
568
+ return parseModuleApiText(result);
886
569
  }
887
570
  catch (err) {
888
571
  console.error(`[token-pilot] ast-index api failed: ${err instanceof Error ? err.message : err}`);
889
572
  return [];
890
573
  }
891
574
  }
892
- // Parsers for module commands (text format — JSON may not be supported for all)
893
- parseModuleListText(text) {
894
- const results = [];
895
- for (const line of text.split('\n')) {
896
- if (!line.trim())
897
- continue;
898
- // Try JSON first
899
- try {
900
- const parsed = JSON.parse(line);
901
- if (Array.isArray(parsed))
902
- return parsed;
903
- }
904
- catch { /* not JSON, parse as text */ }
905
- // Format: name (path) — N files OR name (path) OR path
906
- const match = line.match(/^(\S+)\s+\((.+?)\)(?:\s*—\s*(\d+)\s+files?)?$/);
907
- if (match) {
908
- results.push({
909
- name: match[1],
910
- path: match[2],
911
- file_count: match[3] ? parseInt(match[3], 10) : undefined,
912
- });
913
- }
914
- else {
915
- // Fallback: treat entire line as a path-based module
916
- const trimmed = line.trim();
917
- if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('─')) {
918
- const name = trimmed.split('/').pop() ?? trimmed;
919
- results.push({ name, path: trimmed });
920
- }
921
- }
922
- }
923
- return results;
924
- }
925
- parseModuleDepText(text) {
926
- const results = [];
927
- for (const line of text.split('\n')) {
928
- if (!line.trim())
929
- continue;
930
- // Try JSON first
931
- try {
932
- const parsed = JSON.parse(line);
933
- if (Array.isArray(parsed))
934
- return parsed;
935
- }
936
- catch { /* not JSON, parse as text */ }
937
- // Format: → name (path) OR ← name (path) OR name (path) OR name
938
- const match = line.match(/^[→←\-\s]*(\S+)(?:\s+\((.+?)\))?(?:\s+\[(direct|transitive)\])?$/);
939
- if (match) {
940
- results.push({
941
- name: match[1],
942
- path: match[2] ?? match[1],
943
- type: match[3],
944
- });
945
- }
946
- }
947
- return results;
948
- }
949
- parseUnusedDepsText(text) {
950
- const results = [];
951
- for (const line of text.split('\n')) {
952
- if (!line.trim())
953
- continue;
954
- // Try JSON first
955
- try {
956
- const parsed = JSON.parse(line);
957
- if (Array.isArray(parsed))
958
- return parsed;
959
- }
960
- catch { /* not JSON, parse as text */ }
961
- // Format: ⚠ name (path) — reason OR name (path) OR name — reason
962
- const match = line.match(/^[⚠!\s]*(\S+)(?:\s+\((.+?)\))?(?:\s*[—\-]+\s*(.+))?$/);
963
- if (match) {
964
- results.push({
965
- name: match[1],
966
- path: match[2] ?? match[1],
967
- reason: match[3]?.trim(),
968
- });
969
- }
970
- }
971
- return results;
972
- }
973
- parseModuleApiText(text) {
974
- const results = [];
975
- for (const line of text.split('\n')) {
976
- if (!line.trim())
977
- continue;
978
- // Try JSON first
979
- try {
980
- const parsed = JSON.parse(line);
981
- if (Array.isArray(parsed))
982
- return parsed;
983
- }
984
- catch { /* not JSON, parse as text */ }
985
- // Format: kind name (file:line) OR kind name signature (file:line)
986
- const match = line.match(/^(\w+)\s+(\S+)(?:\s+(.*?))?\s+\((.+?):(\d+)\)$/);
987
- if (match) {
988
- results.push({
989
- kind: match[1],
990
- name: match[2],
991
- signature: match[3]?.trim() || undefined,
992
- file: match[4],
993
- line: parseInt(match[5], 10),
994
- });
995
- }
996
- }
997
- return results;
998
- }
999
575
  // --- Utility methods ---
1000
576
  isAvailable() {
1001
577
  return this.binaryPath !== null;
1002
578
  }
1003
- /** Returns true if the index was built but found >50k files (node_modules leak) */
1004
579
  isOversized() {
1005
580
  return this.indexOversized;
1006
581
  }
1007
- /** Returns true if index building is disabled (dangerous root like /) */
1008
582
  isDisabled() {
1009
583
  return this.indexDisabled;
1010
584
  }
1011
- /** Disable index building (e.g. project root is / or home dir) */
1012
585
  disableIndex() {
1013
586
  this.indexDisabled = true;
1014
587
  }
1015
- /** Re-enable index building after auto-detecting a valid project root */
1016
588
  enableIndex() {
1017
589
  this.indexDisabled = false;
1018
590
  }
1019
- /** Update project root (e.g. after auto-detecting from file path) */
1020
591
  updateProjectRoot(newRoot) {
1021
592
  this.projectRoot = newRoot;
1022
- this.indexed = false; // Force re-check
593
+ this.indexed = false;
1023
594
  }
1024
595
  async exec(args, timeoutMs) {
1025
596
  if (!this.binaryPath) {
1026
597
  throw new Error('ast-index not initialized. Call init() first.');
1027
598
  }
1028
- // If ast-grep was found in node_modules/.bin, inject it into PATH
1029
- // so ast-index can find sg when running agrep
1030
599
  const env = this.astGrepBinDir
1031
600
  ? { ...process.env, PATH: `${this.astGrepBinDir}:${process.env.PATH ?? ''}` }
1032
601
  : undefined;
@@ -1041,291 +610,5 @@ export class AstIndexClient {
1041
610
  }
1042
611
  return stdout;
1043
612
  }
1044
- async buildFileStructure(filePath, entries) {
1045
- const content = await readFile(filePath, 'utf-8');
1046
- const lines = content.split('\n');
1047
- const fileStat = await stat(filePath);
1048
- // Fix last entry end_line to use actual file line count
1049
- this.fixLastEndLine(entries, lines.length);
1050
- // Enrich classes that ast-index returned without children (language-specific)
1051
- const lang = this.detectLanguage(filePath);
1052
- if (lang === 'Python') {
1053
- this.enrichPythonClassMethods(entries, lines);
1054
- }
1055
- else if (lang === 'PHP') {
1056
- this.enrichPHPClassMethods(entries, lines);
1057
- }
1058
- // Enrich entries with signatures from file content
1059
- this.enrichSignatures(entries, lines);
1060
- return {
1061
- path: filePath,
1062
- language: lang,
1063
- meta: {
1064
- lines: lines.length,
1065
- bytes: fileStat.size,
1066
- lastModified: fileStat.mtimeMs,
1067
- contentHash: createHash('sha256').update(content).digest('hex'),
1068
- },
1069
- imports: [],
1070
- exports: [],
1071
- symbols: entries.map(e => this.mapOutlineEntry(e)),
1072
- };
1073
- }
1074
- /**
1075
- * Python: ast-index doesn't return methods inside classes.
1076
- * Parse file content to extract `def` methods for classes without children.
1077
- */
1078
- enrichPythonClassMethods(entries, lines) {
1079
- for (const entry of entries) {
1080
- if (entry.kind.toLowerCase() !== 'class')
1081
- continue;
1082
- if (entry.children && entry.children.length > 0)
1083
- continue;
1084
- const classStartIdx = entry.start_line - 1; // 0-based
1085
- const classEndIdx = entry.end_line - 1;
1086
- // Detect class body indent: look for first `def ` inside class range
1087
- let bodyIndent = -1;
1088
- for (let i = classStartIdx + 1; i <= classEndIdx && i < lines.length; i++) {
1089
- const defMatch = lines[i].match(/^(\s+)def\s/);
1090
- if (defMatch) {
1091
- bodyIndent = defMatch[1].length;
1092
- break;
1093
- }
1094
- }
1095
- if (bodyIndent < 0)
1096
- continue; // no methods found
1097
- const methods = [];
1098
- for (let i = classStartIdx + 1; i <= classEndIdx && i < lines.length; i++) {
1099
- const line = lines[i];
1100
- // Match `def method_name(` at the detected indent level
1101
- const match = line.match(new RegExp(`^\\s{${bodyIndent}}def\\s+(\\w+)\\s*\\(`));
1102
- if (!match)
1103
- continue;
1104
- const methodName = match[1];
1105
- const methodLine = i + 1; // 1-based
1106
- // Check for async/static/decorators
1107
- const isAsync = line.includes('async def');
1108
- const isStatic = i > 0 && /^\s*@staticmethod/.test(lines[i - 1]);
1109
- const isClassMethod = i > 0 && /^\s*@classmethod/.test(lines[i - 1]);
1110
- // Collect decorators above
1111
- const decorators = [];
1112
- for (let d = i - 1; d >= classStartIdx; d--) {
1113
- const decMatch = lines[d].match(new RegExp(`^\\s{${bodyIndent}}@(\\w+)`));
1114
- if (decMatch) {
1115
- decorators.unshift(`@${decMatch[1]}`);
1116
- }
1117
- else {
1118
- break;
1119
- }
1120
- }
1121
- // Determine visibility from name
1122
- const visibility = methodName.startsWith('__') && !methodName.endsWith('__')
1123
- ? 'private'
1124
- : methodName.startsWith('_')
1125
- ? 'protected'
1126
- : 'public';
1127
- methods.push({
1128
- name: methodName,
1129
- kind: isStatic || isClassMethod ? 'function' : 'method',
1130
- start_line: methodLine,
1131
- end_line: 0, // computed below
1132
- signature: line.trim(),
1133
- visibility,
1134
- is_async: isAsync,
1135
- is_static: isStatic,
1136
- decorators: decorators.length > 0 ? decorators : undefined,
1137
- });
1138
- }
1139
- // Compute end_lines for methods
1140
- for (let m = 0; m < methods.length; m++) {
1141
- if (m < methods.length - 1) {
1142
- // End before next method (or its first decorator)
1143
- const nextStart = methods[m + 1].start_line;
1144
- // Walk back from next method to skip decorators/blank lines
1145
- let endLine = nextStart - 1;
1146
- for (let k = nextStart - 2; k >= methods[m].start_line; k--) {
1147
- const l = lines[k];
1148
- if (l.trim() === '' || new RegExp(`^\\s{${bodyIndent}}@`).test(l)) {
1149
- endLine = k; // 0-based → will be used as 1-based below
1150
- }
1151
- else {
1152
- break;
1153
- }
1154
- }
1155
- methods[m].end_line = endLine;
1156
- }
1157
- else {
1158
- // Last method ends at class end
1159
- methods[m].end_line = entry.end_line;
1160
- }
1161
- }
1162
- entry.children = methods;
1163
- }
1164
- }
1165
- /**
1166
- * PHP: ast-index doesn't return methods inside classes.
1167
- * Parse file content to extract `function` methods for classes without children.
1168
- */
1169
- enrichPHPClassMethods(entries, lines) {
1170
- for (const entry of entries) {
1171
- if (entry.kind.toLowerCase() !== 'class')
1172
- continue;
1173
- if (entry.children && entry.children.length > 0)
1174
- continue;
1175
- const classStartIdx = entry.start_line - 1;
1176
- const classEndIdx = entry.end_line - 1;
1177
- // Detect class body indent: look for first `function ` inside class range
1178
- let bodyIndent = -1;
1179
- for (let i = classStartIdx + 1; i <= classEndIdx && i < lines.length; i++) {
1180
- const fnMatch = lines[i].match(/^(\s+)(?:public|private|protected|static|\s)*function\s/);
1181
- if (fnMatch) {
1182
- bodyIndent = fnMatch[1].length;
1183
- break;
1184
- }
1185
- }
1186
- if (bodyIndent < 0)
1187
- continue;
1188
- const methods = [];
1189
- for (let i = classStartIdx + 1; i <= classEndIdx && i < lines.length; i++) {
1190
- const line = lines[i];
1191
- // Match PHP method: [visibility] [static] function name(
1192
- const match = line.match(new RegExp(`^\\s{${bodyIndent}}(?:(public|private|protected)\\s+)?(?:(static)\\s+)?function\\s+(\\w+)\\s*\\(`));
1193
- if (!match)
1194
- continue;
1195
- const visibility = match[1] ?? 'public';
1196
- const isStatic = !!match[2];
1197
- const methodName = match[3];
1198
- const methodLine = i + 1;
1199
- methods.push({
1200
- name: methodName,
1201
- kind: isStatic ? 'function' : 'method',
1202
- start_line: methodLine,
1203
- end_line: 0,
1204
- signature: line.trim(),
1205
- visibility,
1206
- is_static: isStatic,
1207
- });
1208
- }
1209
- // Compute end_lines
1210
- for (let m = 0; m < methods.length; m++) {
1211
- if (m < methods.length - 1) {
1212
- methods[m].end_line = methods[m + 1].start_line - 1;
1213
- }
1214
- else {
1215
- methods[m].end_line = entry.end_line;
1216
- }
1217
- }
1218
- entry.children = methods;
1219
- }
1220
- }
1221
- /** Fix the last entry's end_line to use actual file line count */
1222
- fixLastEndLine(entries, totalLines) {
1223
- if (entries.length === 0)
1224
- return;
1225
- const last = entries[entries.length - 1];
1226
- last.end_line = totalLines;
1227
- // Recursively fix children
1228
- if (last.children?.length) {
1229
- this.fixLastEndLine(last.children, last.end_line - 1);
1230
- }
1231
- }
1232
- /** Read actual signature lines from file content */
1233
- enrichSignatures(entries, lines) {
1234
- for (const entry of entries) {
1235
- if (!entry.signature) {
1236
- const lineIdx = entry.start_line - 1;
1237
- if (lineIdx >= 0 && lineIdx < lines.length) {
1238
- entry.signature = lines[lineIdx].trim();
1239
- }
1240
- }
1241
- if (entry.children?.length) {
1242
- this.enrichSignatures(entry.children, lines);
1243
- }
1244
- }
1245
- }
1246
- mapOutlineEntry(entry) {
1247
- return {
1248
- name: entry.name,
1249
- qualifiedName: entry.name, // Will be enriched with parent context
1250
- kind: this.mapKind(entry.kind),
1251
- signature: entry.signature ?? entry.name,
1252
- location: {
1253
- startLine: entry.start_line,
1254
- endLine: entry.end_line,
1255
- lineCount: entry.end_line - entry.start_line + 1,
1256
- },
1257
- visibility: this.mapVisibility(entry.visibility),
1258
- async: entry.is_async ?? false,
1259
- static: entry.is_static ?? false,
1260
- decorators: entry.decorators ?? [],
1261
- children: (entry.children ?? []).map(c => this.mapOutlineEntry(c)),
1262
- doc: entry.doc ?? null,
1263
- references: [],
1264
- };
1265
- }
1266
- mapKind(kind) {
1267
- const map = {
1268
- function: 'function',
1269
- class: 'class',
1270
- method: 'method',
1271
- property: 'property',
1272
- variable: 'variable',
1273
- type: 'type',
1274
- interface: 'interface',
1275
- enum: 'enum',
1276
- constant: 'constant',
1277
- namespace: 'namespace',
1278
- struct: 'class',
1279
- trait: 'interface',
1280
- impl: 'class',
1281
- module: 'namespace',
1282
- };
1283
- return map[kind.toLowerCase()] ?? 'function';
1284
- }
1285
- mapVisibility(vis) {
1286
- if (!vis)
1287
- return 'default';
1288
- const map = {
1289
- public: 'public',
1290
- private: 'private',
1291
- protected: 'protected',
1292
- pub: 'public',
1293
- export: 'public',
1294
- };
1295
- return map[vis.toLowerCase()] ?? 'default';
1296
- }
1297
- detectLanguage(filePath) {
1298
- const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
1299
- const map = {
1300
- ts: 'TypeScript', tsx: 'TypeScript',
1301
- js: 'JavaScript', jsx: 'JavaScript', mjs: 'JavaScript',
1302
- py: 'Python',
1303
- go: 'Go',
1304
- rs: 'Rust',
1305
- java: 'Java',
1306
- kt: 'Kotlin', kts: 'Kotlin',
1307
- swift: 'Swift',
1308
- cs: 'C#',
1309
- cpp: 'C++', cc: 'C++', cxx: 'C++', hpp: 'C++',
1310
- c: 'C', h: 'C',
1311
- php: 'PHP',
1312
- rb: 'Ruby',
1313
- scala: 'Scala',
1314
- dart: 'Dart',
1315
- lua: 'Lua',
1316
- sh: 'Bash', bash: 'Bash',
1317
- sql: 'SQL',
1318
- r: 'R',
1319
- vue: 'Vue',
1320
- svelte: 'Svelte',
1321
- pl: 'Perl', pm: 'Perl',
1322
- ex: 'Elixir', exs: 'Elixir',
1323
- groovy: 'Groovy',
1324
- m: 'Objective-C',
1325
- proto: 'Protocol Buffers',
1326
- bsl: 'BSL',
1327
- };
1328
- return map[ext] ?? 'Unknown';
1329
- }
1330
613
  }
1331
614
  //# sourceMappingURL=client.js.map