token-pilot 0.30.0 → 0.30.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -4
- package/README.md +24 -0
- package/agents/tp-api-surface-tracker.md +1 -1
- package/agents/tp-audit-scanner.md +1 -1
- package/agents/tp-commit-writer.md +1 -1
- package/agents/tp-context-engineer.md +1 -1
- package/agents/tp-dead-code-finder.md +1 -1
- package/agents/tp-debugger.md +1 -1
- package/agents/tp-dep-health.md +1 -1
- package/agents/tp-doc-writer.md +1 -1
- package/agents/tp-history-explorer.md +1 -1
- package/agents/tp-impact-analyzer.md +1 -1
- package/agents/tp-incident-timeline.md +1 -1
- package/agents/tp-incremental-builder.md +1 -1
- package/agents/tp-migration-scout.md +1 -1
- package/agents/tp-onboard.md +1 -1
- package/agents/tp-performance-profiler.md +1 -1
- package/agents/tp-pr-reviewer.md +1 -1
- package/agents/tp-refactor-planner.md +1 -1
- package/agents/tp-review-impact.md +1 -1
- package/agents/tp-run.md +1 -1
- package/agents/tp-session-restorer.md +1 -1
- package/agents/tp-ship-coordinator.md +1 -1
- package/agents/tp-spec-writer.md +1 -1
- package/agents/tp-test-coverage-gapper.md +1 -1
- package/agents/tp-test-triage.md +1 -1
- package/agents/tp-test-writer.md +1 -1
- package/dist/ast-index/client.d.ts +17 -2
- package/dist/ast-index/client.js +233 -107
- package/dist/core/edit-prep-state.d.ts +42 -0
- package/dist/core/edit-prep-state.js +108 -0
- package/dist/handlers/explore-area.js +6 -1
- package/dist/handlers/read-for-edit.d.ts +5 -5
- package/dist/handlers/read-for-edit.js +188 -110
- package/dist/hooks/installer.js +18 -0
- package/dist/hooks/pre-bash.d.ts +9 -0
- package/dist/hooks/pre-bash.js +48 -0
- package/dist/hooks/pre-edit.d.ts +69 -0
- package/dist/hooks/pre-edit.js +104 -0
- package/dist/hooks/pre-grep.d.ts +10 -0
- package/dist/hooks/pre-grep.js +38 -2
- package/dist/index.d.ts +30 -0
- package/dist/index.js +83 -20
- package/dist/server/tool-definitions.js +18 -6
- package/dist/server.js +21 -5
- package/docs/installation.md +27 -1
- package/hooks/hooks.json +18 -0
- package/package.json +1 -1
- package/start.sh +19 -9
package/dist/ast-index/client.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { findBinary, installBinary } from
|
|
4
|
-
import { parseFileCount, parseOutlineText, parseImportsText, parseImplementationsText, parseHierarchyText, parseAgrepText, parseTodoText, parseDeprecatedText, parseAnnotationsText, parseModuleListText, parseModuleDepText, parseUnusedDepsText, parseModuleApiText, } from
|
|
5
|
-
import { buildFileStructure } from
|
|
6
|
-
import { parseTypeScriptRegex } from
|
|
7
|
-
import { parsePythonRegex } from
|
|
8
|
-
const TS_JS_EXTENSIONS = new Set([
|
|
9
|
-
const PYTHON_EXTENSIONS = new Set([
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
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";
|
|
6
|
+
import { parseTypeScriptRegex } from "./regex-parser.js";
|
|
7
|
+
import { parsePythonRegex } from "./regex-parser-python.js";
|
|
8
|
+
const TS_JS_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs"]);
|
|
9
|
+
const PYTHON_EXTENSIONS = new Set(["py", "pyw"]);
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
export class AstIndexClient {
|
|
12
12
|
static MAX_INDEX_FILES = 50_000;
|
|
@@ -21,6 +21,9 @@ export class AstIndexClient {
|
|
|
21
21
|
autoInstall;
|
|
22
22
|
astGrepAvailable = null;
|
|
23
23
|
astGrepBinDir = null;
|
|
24
|
+
// Periodic-update timer and overlap guard (see startPeriodicUpdate below)
|
|
25
|
+
periodicTimer = null;
|
|
26
|
+
periodicUpdateInFlight = false;
|
|
24
27
|
constructor(projectRoot, timeout = 5000, options) {
|
|
25
28
|
this.projectRoot = projectRoot;
|
|
26
29
|
this.timeout = timeout;
|
|
@@ -37,7 +40,7 @@ export class AstIndexClient {
|
|
|
37
40
|
}
|
|
38
41
|
// 2. Auto-install if enabled
|
|
39
42
|
if (this.autoInstall) {
|
|
40
|
-
console.error(
|
|
43
|
+
console.error("[token-pilot] ast-index not found, downloading...");
|
|
41
44
|
try {
|
|
42
45
|
const installed = await installBinary((msg) => console.error(`[token-pilot] ${msg}`));
|
|
43
46
|
this.binaryPath = installed.path;
|
|
@@ -47,20 +50,20 @@ export class AstIndexClient {
|
|
|
47
50
|
console.error(`[token-pilot] Auto-install failed: ${err instanceof Error ? err.message : err}`);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
throw new Error(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
throw new Error("ast-index binary not found and auto-install failed.\n" +
|
|
54
|
+
"Install manually: npx token-pilot install-ast-index\n" +
|
|
55
|
+
"Or: cargo install ast-index");
|
|
53
56
|
}
|
|
54
57
|
async ensureIndex() {
|
|
55
58
|
if (this.indexed)
|
|
56
59
|
return;
|
|
57
60
|
if (this.indexDisabled) {
|
|
58
|
-
throw new Error(
|
|
61
|
+
throw new Error("ast-index: index build disabled — project root is too broad (e.g. /). " +
|
|
59
62
|
'Configure mcpServers with "args": ["/path/to/project"] to set the correct project root.');
|
|
60
63
|
}
|
|
61
64
|
if (this.indexOversized) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
|
|
65
|
+
throw new Error("ast-index disabled: previous build indexed >50k files (likely node_modules). " +
|
|
66
|
+
"Ensure node_modules is in .gitignore, then restart the MCP server.");
|
|
64
67
|
}
|
|
65
68
|
// Deduplicate concurrent calls — all waiters share one build
|
|
66
69
|
if (this.indexPromise)
|
|
@@ -76,26 +79,32 @@ export class AstIndexClient {
|
|
|
76
79
|
async buildIndex() {
|
|
77
80
|
let existingFileCount = 0;
|
|
78
81
|
try {
|
|
79
|
-
const stats = await this.exec([
|
|
82
|
+
const stats = await this.exec(["--format", "json", "stats"]);
|
|
80
83
|
existingFileCount = parseFileCount(stats);
|
|
81
84
|
}
|
|
82
|
-
catch {
|
|
85
|
+
catch {
|
|
86
|
+
/* no index yet */
|
|
87
|
+
}
|
|
83
88
|
if (existingFileCount > AstIndexClient.MAX_INDEX_FILES) {
|
|
84
89
|
console.error(`[token-pilot] ast-index: existing index has ${existingFileCount} files (>${AstIndexClient.MAX_INDEX_FILES}) — likely includes node_modules. Clearing.`);
|
|
85
90
|
try {
|
|
86
|
-
await this.exec([
|
|
91
|
+
await this.exec(["clear"]);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* best effort */
|
|
87
95
|
}
|
|
88
|
-
catch { /* best effort */ }
|
|
89
96
|
existingFileCount = 0;
|
|
90
97
|
}
|
|
91
98
|
if (existingFileCount > 0) {
|
|
92
99
|
console.error(`[token-pilot] ast-index: updating index (${existingFileCount} files)...`);
|
|
93
100
|
try {
|
|
94
|
-
await this.exec([
|
|
101
|
+
await this.exec(["update"], 30000);
|
|
95
102
|
try {
|
|
96
|
-
existingFileCount = parseFileCount(await this.exec([
|
|
103
|
+
existingFileCount = parseFileCount(await this.exec(["--format", "json", "stats"]));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* keep previous count */
|
|
97
107
|
}
|
|
98
|
-
catch { /* keep previous count */ }
|
|
99
108
|
if (existingFileCount > AstIndexClient.MAX_INDEX_FILES) {
|
|
100
109
|
return this.handleOversizedIndex(existingFileCount);
|
|
101
110
|
}
|
|
@@ -107,10 +116,10 @@ export class AstIndexClient {
|
|
|
107
116
|
console.error(`[token-pilot] ast-index: update failed, falling back to rebuild — ${updateErr instanceof Error ? updateErr.message : updateErr}`);
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
|
-
console.error(
|
|
119
|
+
console.error("[token-pilot] ast-index: building index (this may take a moment)...");
|
|
111
120
|
try {
|
|
112
|
-
await this.exec([
|
|
113
|
-
const fileCount = parseFileCount(await this.exec([
|
|
121
|
+
await this.exec(["rebuild"], 120000);
|
|
122
|
+
const fileCount = parseFileCount(await this.exec(["--format", "json", "stats"]).catch(() => ""));
|
|
114
123
|
if (fileCount > AstIndexClient.MAX_INDEX_FILES) {
|
|
115
124
|
return this.handleOversizedIndex(fileCount);
|
|
116
125
|
}
|
|
@@ -119,8 +128,8 @@ export class AstIndexClient {
|
|
|
119
128
|
}
|
|
120
129
|
catch (buildErr) {
|
|
121
130
|
const errMsg = buildErr instanceof Error ? buildErr.message : String(buildErr);
|
|
122
|
-
if (errMsg.includes(
|
|
123
|
-
const count = parseFileCount(await this.exec([
|
|
131
|
+
if (errMsg.includes("lock") || errMsg.includes("already running")) {
|
|
132
|
+
const count = parseFileCount(await this.exec(["--format", "json", "stats"]).catch(() => ""));
|
|
124
133
|
if (count > 0 && count <= AstIndexClient.MAX_INDEX_FILES) {
|
|
125
134
|
this.indexed = true;
|
|
126
135
|
console.error(`[token-pilot] ast-index: using existing index (${count} files, rebuild skipped due to lock)`);
|
|
@@ -138,9 +147,11 @@ export class AstIndexClient {
|
|
|
138
147
|
this.indexOversized = true;
|
|
139
148
|
this.indexed = false;
|
|
140
149
|
try {
|
|
141
|
-
await this.exec([
|
|
150
|
+
await this.exec(["clear"]);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* best effort */
|
|
142
154
|
}
|
|
143
|
-
catch { /* best effort */ }
|
|
144
155
|
console.error(`[token-pilot] ast-index: ${fileCount} files indexed (>${AstIndexClient.MAX_INDEX_FILES}) — ` +
|
|
145
156
|
`likely includes node_modules. Index cleared.\n` +
|
|
146
157
|
` → Ensure node_modules is in .gitignore\n` +
|
|
@@ -149,7 +160,7 @@ export class AstIndexClient {
|
|
|
149
160
|
}
|
|
150
161
|
async outline(filePath) {
|
|
151
162
|
try {
|
|
152
|
-
const result = await this.exec([
|
|
163
|
+
const result = await this.exec(["outline", filePath]);
|
|
153
164
|
const entries = parseOutlineText(result);
|
|
154
165
|
if (entries.length === 0)
|
|
155
166
|
return null;
|
|
@@ -160,7 +171,7 @@ export class AstIndexClient {
|
|
|
160
171
|
return this.regexFallback(filePath);
|
|
161
172
|
try {
|
|
162
173
|
await this.ensureIndex();
|
|
163
|
-
const result = await this.exec([
|
|
174
|
+
const result = await this.exec(["outline", filePath]);
|
|
164
175
|
const entries = parseOutlineText(result);
|
|
165
176
|
if (entries.length === 0)
|
|
166
177
|
return null;
|
|
@@ -174,15 +185,17 @@ export class AstIndexClient {
|
|
|
174
185
|
}
|
|
175
186
|
/** Regex-based fallback for TS/JS/Python when ast-index binary is unavailable. */
|
|
176
187
|
async regexFallback(filePath) {
|
|
177
|
-
const ext = filePath.split(
|
|
178
|
-
const parser = TS_JS_EXTENSIONS.has(ext)
|
|
179
|
-
|
|
188
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
189
|
+
const parser = TS_JS_EXTENSIONS.has(ext)
|
|
190
|
+
? parseTypeScriptRegex
|
|
191
|
+
: PYTHON_EXTENSIONS.has(ext)
|
|
192
|
+
? parsePythonRegex
|
|
180
193
|
: null;
|
|
181
194
|
if (!parser)
|
|
182
195
|
return null;
|
|
183
196
|
try {
|
|
184
|
-
const { readFile } = await import(
|
|
185
|
-
const content = await readFile(filePath,
|
|
197
|
+
const { readFile } = await import("node:fs/promises");
|
|
198
|
+
const content = await readFile(filePath, "utf-8");
|
|
186
199
|
const entries = parser(content);
|
|
187
200
|
if (entries.length === 0)
|
|
188
201
|
return null;
|
|
@@ -194,24 +207,38 @@ export class AstIndexClient {
|
|
|
194
207
|
}
|
|
195
208
|
async symbol(name) {
|
|
196
209
|
try {
|
|
197
|
-
const result = await this.exec([
|
|
210
|
+
const result = await this.exec(["symbol", name, "--format", "json"]);
|
|
198
211
|
const raw = JSON.parse(result);
|
|
199
212
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
200
213
|
const first = raw[0];
|
|
201
|
-
return {
|
|
214
|
+
return {
|
|
215
|
+
name: first.name,
|
|
216
|
+
kind: first.kind,
|
|
217
|
+
file: first.path,
|
|
218
|
+
start_line: first.line,
|
|
219
|
+
signature: first.signature,
|
|
220
|
+
};
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
|
-
catch {
|
|
223
|
+
catch {
|
|
224
|
+
/* fall through to ensureIndex path */
|
|
225
|
+
}
|
|
205
226
|
if (this.indexDisabled || this.indexOversized)
|
|
206
227
|
return null;
|
|
207
228
|
try {
|
|
208
229
|
await this.ensureIndex();
|
|
209
|
-
const result = await this.exec([
|
|
230
|
+
const result = await this.exec(["symbol", name, "--format", "json"]);
|
|
210
231
|
const raw = JSON.parse(result);
|
|
211
232
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
212
233
|
return null;
|
|
213
234
|
const first = raw[0];
|
|
214
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
name: first.name,
|
|
237
|
+
kind: first.kind,
|
|
238
|
+
file: first.path,
|
|
239
|
+
start_line: first.line,
|
|
240
|
+
signature: first.signature,
|
|
241
|
+
};
|
|
215
242
|
}
|
|
216
243
|
catch (err) {
|
|
217
244
|
console.error(`[token-pilot] ast-index symbol failed: ${err instanceof Error ? err.message : err}`);
|
|
@@ -220,41 +247,51 @@ export class AstIndexClient {
|
|
|
220
247
|
}
|
|
221
248
|
async search(query, options) {
|
|
222
249
|
await this.ensureIndex();
|
|
223
|
-
const args = [
|
|
250
|
+
const args = ["search", query, "--format", "json"];
|
|
224
251
|
if (options?.inFile)
|
|
225
|
-
args.push(
|
|
252
|
+
args.push("--in-file", options.inFile);
|
|
226
253
|
if (options?.type)
|
|
227
|
-
args.push(
|
|
254
|
+
args.push("--type", options.type);
|
|
228
255
|
if (options?.maxResults)
|
|
229
|
-
args.push(
|
|
256
|
+
args.push("--limit", String(options.maxResults));
|
|
230
257
|
if (options?.fuzzy)
|
|
231
|
-
args.push(
|
|
258
|
+
args.push("--fuzzy");
|
|
232
259
|
try {
|
|
233
260
|
const result = await this.exec(args);
|
|
234
261
|
const parsed = JSON.parse(result);
|
|
235
262
|
// ast-index returns { content_matches: [], symbols: [], files: [], references: [] }
|
|
236
263
|
// Merge all result types — content_matches alone is often empty
|
|
237
264
|
const all = [
|
|
238
|
-
...(Array.isArray(parsed.content_matches)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
265
|
+
...(Array.isArray(parsed.content_matches)
|
|
266
|
+
? parsed.content_matches
|
|
267
|
+
: []),
|
|
268
|
+
...(Array.isArray(parsed.symbols)
|
|
269
|
+
? parsed.symbols.map((s) => ({
|
|
270
|
+
path: s.path ?? s.file,
|
|
271
|
+
line: s.line,
|
|
272
|
+
content: s.signature ?? s.name,
|
|
273
|
+
}))
|
|
274
|
+
: []),
|
|
275
|
+
...(Array.isArray(parsed.files)
|
|
276
|
+
? parsed.files.map((f) => ({
|
|
277
|
+
path: f.path ?? f.file,
|
|
278
|
+
line: f.line ?? 1,
|
|
279
|
+
content: f.path ?? f.file,
|
|
280
|
+
}))
|
|
281
|
+
: []),
|
|
245
282
|
...(Array.isArray(parsed.references) ? parsed.references : []),
|
|
246
283
|
];
|
|
247
|
-
const matches = all.length > 0 ? all :
|
|
284
|
+
const matches = all.length > 0 ? all : Array.isArray(parsed) ? parsed : [];
|
|
248
285
|
const mapped = matches
|
|
249
286
|
.map((m) => ({
|
|
250
|
-
file: m.path ?? m.file ??
|
|
251
|
-
line: typeof m.line ===
|
|
252
|
-
text: m.content ?? m.text ?? m.signature ??
|
|
287
|
+
file: m.path ?? m.file ?? "",
|
|
288
|
+
line: typeof m.line === "number" ? m.line : 0,
|
|
289
|
+
text: m.content ?? m.text ?? m.signature ?? "",
|
|
253
290
|
}))
|
|
254
|
-
.filter(r => r.file !==
|
|
291
|
+
.filter((r) => r.file !== "" && r.text !== "");
|
|
255
292
|
// Deduplicate by file:line
|
|
256
293
|
const seen = new Set();
|
|
257
|
-
return mapped.filter(r => {
|
|
294
|
+
return mapped.filter((r) => {
|
|
258
295
|
const key = `${r.file}:${r.line}`;
|
|
259
296
|
if (seen.has(key))
|
|
260
297
|
return false;
|
|
@@ -270,11 +307,21 @@ export class AstIndexClient {
|
|
|
270
307
|
async usages(symbolName) {
|
|
271
308
|
await this.ensureIndex();
|
|
272
309
|
try {
|
|
273
|
-
const result = await this.exec([
|
|
310
|
+
const result = await this.exec([
|
|
311
|
+
"usages",
|
|
312
|
+
symbolName,
|
|
313
|
+
"--format",
|
|
314
|
+
"json",
|
|
315
|
+
]);
|
|
274
316
|
const raw = JSON.parse(result);
|
|
275
317
|
if (!Array.isArray(raw))
|
|
276
318
|
return [];
|
|
277
|
-
return raw.map(u => ({
|
|
319
|
+
return raw.map((u) => ({
|
|
320
|
+
file: u.path,
|
|
321
|
+
line: u.line,
|
|
322
|
+
text: u.context,
|
|
323
|
+
kind: "reference",
|
|
324
|
+
}));
|
|
278
325
|
}
|
|
279
326
|
catch (err) {
|
|
280
327
|
console.error(`[token-pilot] ast-index usages failed: ${err instanceof Error ? err.message : err}`);
|
|
@@ -284,7 +331,12 @@ export class AstIndexClient {
|
|
|
284
331
|
async implementations(name) {
|
|
285
332
|
await this.ensureIndex();
|
|
286
333
|
try {
|
|
287
|
-
const result = await this.exec([
|
|
334
|
+
const result = await this.exec([
|
|
335
|
+
"implementations",
|
|
336
|
+
name,
|
|
337
|
+
"--format",
|
|
338
|
+
"json",
|
|
339
|
+
]);
|
|
288
340
|
try {
|
|
289
341
|
return JSON.parse(result);
|
|
290
342
|
}
|
|
@@ -300,11 +352,11 @@ export class AstIndexClient {
|
|
|
300
352
|
async hierarchy(name, options) {
|
|
301
353
|
await this.ensureIndex();
|
|
302
354
|
try {
|
|
303
|
-
const args = [
|
|
355
|
+
const args = ["hierarchy", name, "--format", "json"];
|
|
304
356
|
if (options?.inFile)
|
|
305
|
-
args.push(
|
|
357
|
+
args.push("--in-file", options.inFile);
|
|
306
358
|
if (options?.module)
|
|
307
|
-
args.push(
|
|
359
|
+
args.push("--module", options.module);
|
|
308
360
|
const result = await this.exec(args);
|
|
309
361
|
try {
|
|
310
362
|
return JSON.parse(result);
|
|
@@ -320,7 +372,7 @@ export class AstIndexClient {
|
|
|
320
372
|
}
|
|
321
373
|
async stats() {
|
|
322
374
|
try {
|
|
323
|
-
return await this.exec([
|
|
375
|
+
return await this.exec(["stats"]);
|
|
324
376
|
}
|
|
325
377
|
catch {
|
|
326
378
|
return null;
|
|
@@ -329,8 +381,11 @@ export class AstIndexClient {
|
|
|
329
381
|
async listFiles() {
|
|
330
382
|
try {
|
|
331
383
|
await this.ensureIndex();
|
|
332
|
-
const result = await this.exec([
|
|
333
|
-
return result
|
|
384
|
+
const result = await this.exec(["files"], 15000);
|
|
385
|
+
return result
|
|
386
|
+
.split("\n")
|
|
387
|
+
.map((l) => l.trim())
|
|
388
|
+
.filter((l) => l.length > 0);
|
|
334
389
|
}
|
|
335
390
|
catch (err) {
|
|
336
391
|
console.error(`[token-pilot] ast-index files failed: ${err instanceof Error ? err.message : err}`);
|
|
@@ -340,7 +395,14 @@ export class AstIndexClient {
|
|
|
340
395
|
async refs(symbolName, limit = 20) {
|
|
341
396
|
await this.ensureIndex();
|
|
342
397
|
try {
|
|
343
|
-
const result = await this.exec([
|
|
398
|
+
const result = await this.exec([
|
|
399
|
+
"refs",
|
|
400
|
+
symbolName,
|
|
401
|
+
"--limit",
|
|
402
|
+
String(limit),
|
|
403
|
+
"--format",
|
|
404
|
+
"json",
|
|
405
|
+
]);
|
|
344
406
|
return JSON.parse(result);
|
|
345
407
|
}
|
|
346
408
|
catch (err) {
|
|
@@ -351,11 +413,11 @@ export class AstIndexClient {
|
|
|
351
413
|
async map(options) {
|
|
352
414
|
await this.ensureIndex();
|
|
353
415
|
try {
|
|
354
|
-
const args = [
|
|
416
|
+
const args = ["map", "--format", "json"];
|
|
355
417
|
if (options?.module)
|
|
356
|
-
args.push(
|
|
418
|
+
args.push("--module", options.module);
|
|
357
419
|
if (options?.limit)
|
|
358
|
-
args.push(
|
|
420
|
+
args.push("--limit", String(options.limit));
|
|
359
421
|
const result = await this.exec(args, 15000);
|
|
360
422
|
return JSON.parse(result);
|
|
361
423
|
}
|
|
@@ -367,7 +429,7 @@ export class AstIndexClient {
|
|
|
367
429
|
async conventions() {
|
|
368
430
|
await this.ensureIndex();
|
|
369
431
|
try {
|
|
370
|
-
const result = await this.exec([
|
|
432
|
+
const result = await this.exec(["conventions", "--format", "json"]);
|
|
371
433
|
return JSON.parse(result);
|
|
372
434
|
}
|
|
373
435
|
catch (err) {
|
|
@@ -378,7 +440,14 @@ export class AstIndexClient {
|
|
|
378
440
|
async callers(functionName, limit = 50) {
|
|
379
441
|
await this.ensureIndex();
|
|
380
442
|
try {
|
|
381
|
-
const result = await this.exec([
|
|
443
|
+
const result = await this.exec([
|
|
444
|
+
"callers",
|
|
445
|
+
functionName,
|
|
446
|
+
"--limit",
|
|
447
|
+
String(limit),
|
|
448
|
+
"--format",
|
|
449
|
+
"json",
|
|
450
|
+
]);
|
|
382
451
|
const parsed = JSON.parse(result);
|
|
383
452
|
return Array.isArray(parsed) ? parsed : [];
|
|
384
453
|
}
|
|
@@ -390,7 +459,14 @@ export class AstIndexClient {
|
|
|
390
459
|
async callTree(functionName, depth = 3) {
|
|
391
460
|
await this.ensureIndex();
|
|
392
461
|
try {
|
|
393
|
-
const result = await this.exec([
|
|
462
|
+
const result = await this.exec([
|
|
463
|
+
"call-tree",
|
|
464
|
+
functionName,
|
|
465
|
+
"--depth",
|
|
466
|
+
String(depth),
|
|
467
|
+
"--format",
|
|
468
|
+
"json",
|
|
469
|
+
]);
|
|
394
470
|
return JSON.parse(result);
|
|
395
471
|
}
|
|
396
472
|
catch (err) {
|
|
@@ -401,9 +477,9 @@ export class AstIndexClient {
|
|
|
401
477
|
async changed(base) {
|
|
402
478
|
await this.ensureIndex();
|
|
403
479
|
try {
|
|
404
|
-
const args = [
|
|
480
|
+
const args = ["changed", "--format", "json"];
|
|
405
481
|
if (base)
|
|
406
|
-
args.push(
|
|
482
|
+
args.push("--base", base);
|
|
407
483
|
const result = await this.exec(args, 15000);
|
|
408
484
|
const parsed = JSON.parse(result);
|
|
409
485
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -416,13 +492,13 @@ export class AstIndexClient {
|
|
|
416
492
|
async unusedSymbols(options) {
|
|
417
493
|
await this.ensureIndex();
|
|
418
494
|
try {
|
|
419
|
-
const args = [
|
|
495
|
+
const args = ["unused-symbols", "--format", "json"];
|
|
420
496
|
if (options?.module)
|
|
421
|
-
args.push(
|
|
497
|
+
args.push("--module", options.module);
|
|
422
498
|
if (options?.exportOnly)
|
|
423
|
-
args.push(
|
|
499
|
+
args.push("--export-only");
|
|
424
500
|
if (options?.limit)
|
|
425
|
-
args.push(
|
|
501
|
+
args.push("--limit", String(options.limit));
|
|
426
502
|
const result = await this.exec(args, 15000);
|
|
427
503
|
const parsed = JSON.parse(result);
|
|
428
504
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -435,7 +511,7 @@ export class AstIndexClient {
|
|
|
435
511
|
async fileImports(filePath) {
|
|
436
512
|
await this.ensureIndex();
|
|
437
513
|
try {
|
|
438
|
-
const result = await this.exec([
|
|
514
|
+
const result = await this.exec(["imports", filePath]);
|
|
439
515
|
return parseImportsText(result);
|
|
440
516
|
}
|
|
441
517
|
catch (err) {
|
|
@@ -448,19 +524,26 @@ export class AstIndexClient {
|
|
|
448
524
|
if (this.astGrepAvailable !== null)
|
|
449
525
|
return this.astGrepAvailable;
|
|
450
526
|
try {
|
|
451
|
-
await execFileAsync(
|
|
527
|
+
await execFileAsync("sg", ["--version"], { timeout: 3000 });
|
|
452
528
|
this.astGrepAvailable = true;
|
|
453
529
|
return true;
|
|
454
530
|
}
|
|
455
|
-
catch {
|
|
531
|
+
catch {
|
|
532
|
+
/* not in PATH */
|
|
533
|
+
}
|
|
456
534
|
try {
|
|
457
|
-
const localBinDir = new URL(
|
|
458
|
-
|
|
535
|
+
const localBinDir = new URL("../../node_modules/.bin", import.meta.url)
|
|
536
|
+
.pathname;
|
|
537
|
+
await execFileAsync(localBinDir + "/sg", ["--version"], {
|
|
538
|
+
timeout: 3000,
|
|
539
|
+
});
|
|
459
540
|
this.astGrepBinDir = localBinDir;
|
|
460
541
|
this.astGrepAvailable = true;
|
|
461
542
|
return true;
|
|
462
543
|
}
|
|
463
|
-
catch {
|
|
544
|
+
catch {
|
|
545
|
+
/* not found locally either */
|
|
546
|
+
}
|
|
464
547
|
this.astGrepAvailable = false;
|
|
465
548
|
return false;
|
|
466
549
|
}
|
|
@@ -470,14 +553,14 @@ export class AstIndexClient {
|
|
|
470
553
|
await this.ensureIndex();
|
|
471
554
|
const available = await this.checkAstGrep();
|
|
472
555
|
if (!available) {
|
|
473
|
-
throw new Error(
|
|
474
|
-
|
|
475
|
-
|
|
556
|
+
throw new Error("ast-grep (sg) not installed — required for structural pattern search.\n" +
|
|
557
|
+
"Install: brew install ast-grep OR npm i -g @ast-grep/cli\n" +
|
|
558
|
+
"Alternative: use Grep/ripgrep for text-based pattern search.");
|
|
476
559
|
}
|
|
477
560
|
const limit = options?.limit ?? 50;
|
|
478
|
-
const args = [
|
|
561
|
+
const args = ["agrep", pattern];
|
|
479
562
|
if (options?.lang)
|
|
480
|
-
args.push(
|
|
563
|
+
args.push("--lang", options.lang);
|
|
481
564
|
try {
|
|
482
565
|
const result = await this.exec(args, 15000);
|
|
483
566
|
return parseAgrepText(result).slice(0, limit);
|
|
@@ -492,7 +575,7 @@ export class AstIndexClient {
|
|
|
492
575
|
return [];
|
|
493
576
|
await this.ensureIndex();
|
|
494
577
|
try {
|
|
495
|
-
const result = await this.exec([
|
|
578
|
+
const result = await this.exec(["todo"], 15000);
|
|
496
579
|
return parseTodoText(result);
|
|
497
580
|
}
|
|
498
581
|
catch (err) {
|
|
@@ -505,7 +588,7 @@ export class AstIndexClient {
|
|
|
505
588
|
return [];
|
|
506
589
|
await this.ensureIndex();
|
|
507
590
|
try {
|
|
508
|
-
const result = await this.exec([
|
|
591
|
+
const result = await this.exec(["deprecated"], 15000);
|
|
509
592
|
return parseDeprecatedText(result);
|
|
510
593
|
}
|
|
511
594
|
catch (err) {
|
|
@@ -518,7 +601,7 @@ export class AstIndexClient {
|
|
|
518
601
|
return [];
|
|
519
602
|
await this.ensureIndex();
|
|
520
603
|
try {
|
|
521
|
-
const result = await this.exec([
|
|
604
|
+
const result = await this.exec(["annotations", name], 15000);
|
|
522
605
|
return parseAnnotationsText(result, name);
|
|
523
606
|
}
|
|
524
607
|
catch (err) {
|
|
@@ -530,19 +613,51 @@ export class AstIndexClient {
|
|
|
530
613
|
if (!this.indexed || this.indexDisabled || this.indexOversized)
|
|
531
614
|
return;
|
|
532
615
|
try {
|
|
533
|
-
await this.exec([
|
|
616
|
+
await this.exec(["update"], 15000);
|
|
534
617
|
}
|
|
535
618
|
catch (err) {
|
|
536
619
|
console.error(`[token-pilot] ast-index incremental update failed: ${err instanceof Error ? err.message : err}`);
|
|
537
620
|
}
|
|
538
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Periodic safety-net so long sessions don't drift when FileWatcher misses
|
|
624
|
+
* events (Docker bind mounts, NFS, files changed by sibling tools). We
|
|
625
|
+
* explicitly avoid spawning `ast-index watch` as a daemon — it duplicates
|
|
626
|
+
* our FileWatcher, needs PID/lifecycle management, and goes zombie if the
|
|
627
|
+
* MCP server is killed with SIGKILL.
|
|
628
|
+
*
|
|
629
|
+
* Default cadence is 5 minutes. `unref()` lets the process exit naturally
|
|
630
|
+
* even if a tick is pending. An in-flight guard prevents overlapping runs
|
|
631
|
+
* when a single update exceeds the interval (rare — timeout is 15 s).
|
|
632
|
+
*/
|
|
633
|
+
startPeriodicUpdate(intervalMs = 5 * 60 * 1000) {
|
|
634
|
+
if (this.periodicTimer)
|
|
635
|
+
return;
|
|
636
|
+
this.periodicTimer = setInterval(() => {
|
|
637
|
+
if (this.periodicUpdateInFlight)
|
|
638
|
+
return;
|
|
639
|
+
if (!this.indexed || this.indexDisabled || this.indexOversized)
|
|
640
|
+
return;
|
|
641
|
+
this.periodicUpdateInFlight = true;
|
|
642
|
+
void this.incrementalUpdate().finally(() => {
|
|
643
|
+
this.periodicUpdateInFlight = false;
|
|
644
|
+
});
|
|
645
|
+
}, intervalMs);
|
|
646
|
+
this.periodicTimer.unref?.();
|
|
647
|
+
}
|
|
648
|
+
stopPeriodicUpdate() {
|
|
649
|
+
if (this.periodicTimer) {
|
|
650
|
+
clearInterval(this.periodicTimer);
|
|
651
|
+
this.periodicTimer = null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
539
654
|
// --- Module analysis methods ---
|
|
540
655
|
async modules(pattern) {
|
|
541
656
|
if (this.indexDisabled || this.indexOversized)
|
|
542
657
|
return [];
|
|
543
658
|
await this.ensureIndex();
|
|
544
659
|
try {
|
|
545
|
-
const cmdArgs = pattern ? [
|
|
660
|
+
const cmdArgs = pattern ? ["module", pattern] : ["module"];
|
|
546
661
|
const result = await this.exec(cmdArgs, 15000);
|
|
547
662
|
return parseModuleListText(result);
|
|
548
663
|
}
|
|
@@ -556,7 +671,7 @@ export class AstIndexClient {
|
|
|
556
671
|
return [];
|
|
557
672
|
await this.ensureIndex();
|
|
558
673
|
try {
|
|
559
|
-
const result = await this.exec([
|
|
674
|
+
const result = await this.exec(["deps", module], 15000);
|
|
560
675
|
return parseModuleDepText(result);
|
|
561
676
|
}
|
|
562
677
|
catch (err) {
|
|
@@ -569,7 +684,7 @@ export class AstIndexClient {
|
|
|
569
684
|
return [];
|
|
570
685
|
await this.ensureIndex();
|
|
571
686
|
try {
|
|
572
|
-
const result = await this.exec([
|
|
687
|
+
const result = await this.exec(["dependents", module], 15000);
|
|
573
688
|
return parseModuleDepText(result);
|
|
574
689
|
}
|
|
575
690
|
catch (err) {
|
|
@@ -582,7 +697,7 @@ export class AstIndexClient {
|
|
|
582
697
|
return [];
|
|
583
698
|
await this.ensureIndex();
|
|
584
699
|
try {
|
|
585
|
-
const result = await this.exec([
|
|
700
|
+
const result = await this.exec(["unused-deps", module], 15000);
|
|
586
701
|
return parseUnusedDepsText(result);
|
|
587
702
|
}
|
|
588
703
|
catch (err) {
|
|
@@ -595,7 +710,7 @@ export class AstIndexClient {
|
|
|
595
710
|
return [];
|
|
596
711
|
await this.ensureIndex();
|
|
597
712
|
try {
|
|
598
|
-
const result = await this.exec([
|
|
713
|
+
const result = await this.exec(["api", module], 15000);
|
|
599
714
|
return parseModuleApiText(result);
|
|
600
715
|
}
|
|
601
716
|
catch (err) {
|
|
@@ -625,16 +740,27 @@ export class AstIndexClient {
|
|
|
625
740
|
}
|
|
626
741
|
async exec(args, timeoutMs) {
|
|
627
742
|
if (!this.binaryPath) {
|
|
628
|
-
throw new Error(
|
|
743
|
+
throw new Error("ast-index not initialized. Call init() first.");
|
|
744
|
+
}
|
|
745
|
+
// ast-index v3.39+ honours AST_INDEX_WALK_UP=1 — read-commands then
|
|
746
|
+
// traverse past nested VCS markers (submodule .git, inner Cargo.toml,
|
|
747
|
+
// nested settings.gradle) to reuse a parent-level index if one exists.
|
|
748
|
+
// Without this, running `search`/`outline` from a monorepo subdir stops
|
|
749
|
+
// at the nearest marker and finds nothing when the subdir has no DB.
|
|
750
|
+
// Safe default: pure-additive, no effect when projectRoot already sits
|
|
751
|
+
// at the index root.
|
|
752
|
+
const env = {
|
|
753
|
+
...process.env,
|
|
754
|
+
AST_INDEX_WALK_UP: "1",
|
|
755
|
+
};
|
|
756
|
+
if (this.astGrepBinDir) {
|
|
757
|
+
env.PATH = `${this.astGrepBinDir}:${process.env.PATH ?? ""}`;
|
|
629
758
|
}
|
|
630
|
-
const env = this.astGrepBinDir
|
|
631
|
-
? { ...process.env, PATH: `${this.astGrepBinDir}:${process.env.PATH ?? ''}` }
|
|
632
|
-
: undefined;
|
|
633
759
|
const { stdout, stderr } = await execFileAsync(this.binaryPath, args, {
|
|
634
760
|
timeout: timeoutMs ?? this.timeout,
|
|
635
761
|
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
636
762
|
cwd: this.projectRoot,
|
|
637
|
-
|
|
763
|
+
env,
|
|
638
764
|
});
|
|
639
765
|
if (stderr) {
|
|
640
766
|
console.error(`[token-pilot] ast-index stderr (${args[0]}): ${stderr.trim()}`);
|