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.
- package/CHANGELOG.md +17 -0
- package/README.md +8 -8
- package/dist/ast-index/client.d.ts +0 -89
- package/dist/ast-index/client.js +26 -743
- 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.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 +1 -1
- package/dist/core/session-analytics.js +33 -122
- package/dist/core/symbol-resolver.d.ts +0 -1
- package/dist/core/symbol-resolver.js +3 -12
- package/dist/handlers/code-audit.js +2 -2
- package/dist/handlers/find-unused.js +1 -1
- package/dist/handlers/find-usages.js +34 -26
- package/dist/handlers/smart-read.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -7
- package/dist/server/token-estimates.d.ts +31 -0
- package/dist/server/token-estimates.js +204 -0
- package/dist/server/tool-definitions.d.ts +958 -0
- package/dist/server/tool-definitions.js +288 -0
- package/dist/server.js +8 -477
- package/dist/types.d.ts +1 -0
- package/package.json +14 -12
- package/skills/guide/SKILL.md +64 -0
package/dist/ast-index/client.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
149
|
+
const entries = parseOutlineText(result);
|
|
177
150
|
if (entries.length === 0)
|
|
178
151
|
return null;
|
|
179
|
-
return await
|
|
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 =
|
|
160
|
+
const entries = parseOutlineText(result);
|
|
189
161
|
if (entries.length === 0)
|
|
190
162
|
return null;
|
|
191
|
-
return await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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
|