token-pilot 0.8.3 → 0.10.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +17 -10
- package/dist/ast-index/client.d.ts +15 -1
- package/dist/ast-index/client.js +179 -0
- package/dist/ast-index/types.d.ts +27 -1
- package/dist/ast-index/types.js +1 -1
- package/dist/core/project-detector.d.ts +42 -0
- package/dist/core/project-detector.js +362 -0
- package/dist/core/validation.d.ts +47 -4
- package/dist/core/validation.js +111 -8
- package/dist/handlers/explore-area.d.ts +9 -0
- package/dist/handlers/explore-area.js +280 -0
- package/dist/handlers/find-usages.d.ts +3 -3
- package/dist/handlers/find-usages.js +88 -13
- package/dist/handlers/module-info.d.ts +9 -0
- package/dist/handlers/module-info.js +123 -0
- package/dist/handlers/outline.d.ts +7 -3
- package/dist/handlers/outline.js +52 -21
- package/dist/handlers/project-overview.d.ts +2 -1
- package/dist/handlers/project-overview.js +146 -107
- package/dist/handlers/smart-diff.d.ts +35 -0
- package/dist/handlers/smart-diff.js +257 -0
- package/dist/server.js +98 -7
- package/package.json +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { resolve, relative, basename, dirname } from 'node:path';
|
|
5
|
+
import { resolveSafePath } from '../core/validation.js';
|
|
6
|
+
import { outlineDir, CODE_EXTENSIONS } from './outline.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
// ──────────────────────────────────────────────
|
|
9
|
+
// Constants
|
|
10
|
+
// ──────────────────────────────────────────────
|
|
11
|
+
const MAX_IMPORT_FILES = 20;
|
|
12
|
+
const MAX_OUTPUT_LINES = 500;
|
|
13
|
+
// ──────────────────────────────────────────────
|
|
14
|
+
// Handler
|
|
15
|
+
// ──────────────────────────────────────────────
|
|
16
|
+
export async function handleExploreArea(args, projectRoot, astIndex) {
|
|
17
|
+
// Resolve path — if it points to a file, use its parent directory
|
|
18
|
+
let absPath = resolveSafePath(projectRoot, args.path);
|
|
19
|
+
const pathStat = await stat(absPath).catch(() => null);
|
|
20
|
+
if (!pathStat) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: `Path "${args.path}" not found.` }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (!pathStat.isDirectory()) {
|
|
26
|
+
absPath = dirname(absPath);
|
|
27
|
+
}
|
|
28
|
+
const relDir = relative(projectRoot, absPath) || '.';
|
|
29
|
+
const include = args.include ?? ['outline', 'imports', 'tests', 'changes'];
|
|
30
|
+
// Collect code files for import/test analysis
|
|
31
|
+
const codeFiles = await listCodeFiles(absPath);
|
|
32
|
+
// Run all sections in parallel
|
|
33
|
+
const [outlineSection, importsSection, testsSection, changesSection] = await Promise.allSettled([
|
|
34
|
+
include.includes('outline') ? buildOutlineSection(absPath, projectRoot, astIndex) : Promise.resolve(null),
|
|
35
|
+
include.includes('imports') ? buildImportsSection(codeFiles, absPath, projectRoot, astIndex) : Promise.resolve(null),
|
|
36
|
+
include.includes('tests') ? buildTestsSection(codeFiles, absPath, projectRoot) : Promise.resolve(null),
|
|
37
|
+
include.includes('changes') ? buildChangesSection(relDir, projectRoot) : Promise.resolve(null),
|
|
38
|
+
]);
|
|
39
|
+
// Assemble output
|
|
40
|
+
const lines = [];
|
|
41
|
+
const subdirCount = await countSubdirs(absPath);
|
|
42
|
+
lines.push(`AREA: ${relDir}/ (${codeFiles.length} code files${subdirCount > 0 ? `, ${subdirCount} subdirs` : ''})`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
// Outline
|
|
45
|
+
const outlineLines = extractResult(outlineSection);
|
|
46
|
+
if (outlineLines) {
|
|
47
|
+
lines.push('STRUCTURE:');
|
|
48
|
+
lines.push(...outlineLines);
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
// Imports
|
|
52
|
+
const importLines = extractResult(importsSection);
|
|
53
|
+
if (importLines) {
|
|
54
|
+
lines.push(...importLines);
|
|
55
|
+
}
|
|
56
|
+
// Tests
|
|
57
|
+
const testLines = extractResult(testsSection);
|
|
58
|
+
if (testLines) {
|
|
59
|
+
lines.push(...testLines);
|
|
60
|
+
}
|
|
61
|
+
// Changes
|
|
62
|
+
const changeLines = extractResult(changesSection);
|
|
63
|
+
if (changeLines) {
|
|
64
|
+
lines.push(...changeLines);
|
|
65
|
+
}
|
|
66
|
+
// Truncate if needed
|
|
67
|
+
if (lines.length > MAX_OUTPUT_LINES) {
|
|
68
|
+
lines.length = MAX_OUTPUT_LINES;
|
|
69
|
+
lines.push('... truncated. Use outline() on specific subdirectories for details.');
|
|
70
|
+
}
|
|
71
|
+
lines.push('HINT: Use smart_read(file) for details, read_symbol(path, symbol) for source code, find_usages(symbol) for references.');
|
|
72
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
73
|
+
}
|
|
74
|
+
// ──────────────────────────────────────────────
|
|
75
|
+
// Outline section — reuses outlineDir from outline.ts
|
|
76
|
+
// ──────────────────────────────────────────────
|
|
77
|
+
async function buildOutlineSection(absPath, projectRoot, astIndex) {
|
|
78
|
+
const sections = [];
|
|
79
|
+
await outlineDir(absPath, sections, 0, 2, projectRoot, astIndex);
|
|
80
|
+
return sections;
|
|
81
|
+
}
|
|
82
|
+
// ──────────────────────────────────────────────
|
|
83
|
+
// Imports section — aggregate external deps + who imports this area
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
86
|
+
if (!astIndex.isAvailable() || astIndex.isDisabled() || astIndex.isOversized()) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const filesToAnalyze = codeFiles.slice(0, MAX_IMPORT_FILES);
|
|
90
|
+
const externalDeps = new Set();
|
|
91
|
+
const internalDeps = new Set();
|
|
92
|
+
const relDir = relative(projectRoot, absPath) || '.';
|
|
93
|
+
// Get imports for each file
|
|
94
|
+
const importResults = await Promise.allSettled(filesToAnalyze.map(f => astIndex.fileImports(f)));
|
|
95
|
+
for (const result of importResults) {
|
|
96
|
+
if (result.status !== 'fulfilled' || !result.value)
|
|
97
|
+
continue;
|
|
98
|
+
for (const imp of result.value) {
|
|
99
|
+
const source = imp.source;
|
|
100
|
+
if (!source)
|
|
101
|
+
continue;
|
|
102
|
+
if (source.startsWith('.') || source.startsWith('/')) {
|
|
103
|
+
// Internal import — track if it's outside this area
|
|
104
|
+
const resolved = resolve(absPath, source);
|
|
105
|
+
if (!resolved.startsWith(absPath)) {
|
|
106
|
+
const relImport = relative(projectRoot, resolved).replace(/\.[^.]+$/, '');
|
|
107
|
+
internalDeps.add(relImport);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// External package
|
|
112
|
+
const pkg = source.startsWith('@') ? source.split('/').slice(0, 2).join('/') : source.split('/')[0];
|
|
113
|
+
externalDeps.add(pkg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Find who imports files from this area (reverse dependencies)
|
|
118
|
+
const importedBy = new Set();
|
|
119
|
+
const fileBasenames = filesToAnalyze.map(f => basename(f).replace(/\.[^.]+$/, ''));
|
|
120
|
+
const refResults = await Promise.allSettled(fileBasenames.slice(0, 10).map(name => astIndex.refs(name, 10)));
|
|
121
|
+
for (const result of refResults) {
|
|
122
|
+
if (result.status !== 'fulfilled' || !result.value)
|
|
123
|
+
continue;
|
|
124
|
+
const refs = result.value;
|
|
125
|
+
if (refs.imports) {
|
|
126
|
+
for (const imp of refs.imports) {
|
|
127
|
+
const impFile = imp.path;
|
|
128
|
+
if (!impFile)
|
|
129
|
+
continue;
|
|
130
|
+
const relFile = relative(projectRoot, impFile);
|
|
131
|
+
// Only include files outside this area
|
|
132
|
+
if (!relFile.startsWith(relDir)) {
|
|
133
|
+
importedBy.add(relFile.replace(/\.[^.]+$/, ''));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const lines = [];
|
|
139
|
+
if (externalDeps.size > 0) {
|
|
140
|
+
const deps = Array.from(externalDeps).sort().slice(0, 20);
|
|
141
|
+
lines.push(`IMPORTS: ${deps.join(', ')}${externalDeps.size > 20 ? ` ... (${externalDeps.size} total)` : ''}`);
|
|
142
|
+
}
|
|
143
|
+
if (internalDeps.size > 0) {
|
|
144
|
+
const deps = Array.from(internalDeps).sort().slice(0, 10);
|
|
145
|
+
lines.push(`INTERNAL DEPS: ${deps.join(', ')}${internalDeps.size > 10 ? ` ... (${internalDeps.size} total)` : ''}`);
|
|
146
|
+
}
|
|
147
|
+
if (importedBy.size > 0) {
|
|
148
|
+
const importers = Array.from(importedBy).sort().slice(0, 10);
|
|
149
|
+
lines.push(`IMPORTED BY: ${importers.join(', ')}${importedBy.size > 10 ? ` ... (${importedBy.size} total)` : ''}`);
|
|
150
|
+
}
|
|
151
|
+
if (lines.length > 0)
|
|
152
|
+
lines.push('');
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
// ──────────────────────────────────────────────
|
|
156
|
+
// Tests section — find test/spec files matching area files
|
|
157
|
+
// ──────────────────────────────────────────────
|
|
158
|
+
async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
159
|
+
const testFiles = [];
|
|
160
|
+
const areaFileNames = new Set(codeFiles.map(f => basename(f).replace(/\.[^.]+$/, '')));
|
|
161
|
+
// Scan for test files: check area dir + common test dirs
|
|
162
|
+
const dirsToScan = [absPath];
|
|
163
|
+
// Check for sibling __tests__ or tests directory
|
|
164
|
+
const parent = dirname(absPath);
|
|
165
|
+
const areaName = basename(absPath);
|
|
166
|
+
const testDirCandidates = [
|
|
167
|
+
resolve(absPath, '__tests__'),
|
|
168
|
+
resolve(absPath, 'tests'),
|
|
169
|
+
resolve(absPath, 'test'),
|
|
170
|
+
resolve(parent, '__tests__', areaName),
|
|
171
|
+
resolve(parent, 'tests', areaName),
|
|
172
|
+
];
|
|
173
|
+
for (const testDir of testDirCandidates) {
|
|
174
|
+
const testDirStat = await stat(testDir).catch(() => null);
|
|
175
|
+
if (testDirStat?.isDirectory()) {
|
|
176
|
+
dirsToScan.push(testDir);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Also check project-level test directories
|
|
180
|
+
const projectTestDirs = [
|
|
181
|
+
resolve(projectRoot, 'tests'),
|
|
182
|
+
resolve(projectRoot, 'test'),
|
|
183
|
+
resolve(projectRoot, '__tests__'),
|
|
184
|
+
];
|
|
185
|
+
for (const testDir of projectTestDirs) {
|
|
186
|
+
if (dirsToScan.includes(testDir))
|
|
187
|
+
continue;
|
|
188
|
+
const testDirStat = await stat(testDir).catch(() => null);
|
|
189
|
+
if (testDirStat?.isDirectory()) {
|
|
190
|
+
dirsToScan.push(testDir);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const dir of dirsToScan) {
|
|
194
|
+
try {
|
|
195
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (!entry.isFile())
|
|
198
|
+
continue;
|
|
199
|
+
const name = entry.name;
|
|
200
|
+
if (name.includes('.test.') || name.includes('.spec.') || name.includes('_test.') || name.includes('_spec.')) {
|
|
201
|
+
// Check if this test corresponds to an area file
|
|
202
|
+
const testBase = name
|
|
203
|
+
.replace(/\.(test|spec)\./, '.')
|
|
204
|
+
.replace(/_(test|spec)\./, '.')
|
|
205
|
+
.replace(/\.[^.]+$/, '');
|
|
206
|
+
if (areaFileNames.has(testBase) || dir !== absPath) {
|
|
207
|
+
const relPath = relative(projectRoot, resolve(dir, name));
|
|
208
|
+
if (!testFiles.includes(relPath)) {
|
|
209
|
+
testFiles.push(relPath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { /* skip unreadable dirs */ }
|
|
216
|
+
}
|
|
217
|
+
if (testFiles.length === 0)
|
|
218
|
+
return [];
|
|
219
|
+
const lines = [];
|
|
220
|
+
lines.push(`TESTS: ${testFiles.join(', ')}`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
// ──────────────────────────────────────────────
|
|
225
|
+
// Changes section — recent git log for this area
|
|
226
|
+
// ──────────────────────────────────────────────
|
|
227
|
+
async function buildChangesSection(relDir, projectRoot) {
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execFileAsync('git', ['log', '--oneline', '-5', '--', relDir], { cwd: projectRoot, timeout: 5000 });
|
|
230
|
+
if (!stdout.trim())
|
|
231
|
+
return [];
|
|
232
|
+
const lines = [];
|
|
233
|
+
lines.push('RECENT CHANGES:');
|
|
234
|
+
for (const line of stdout.trim().split('\n')) {
|
|
235
|
+
lines.push(` ${line}`);
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ──────────────────────────────────────────────
|
|
245
|
+
// Helpers
|
|
246
|
+
// ──────────────────────────────────────────────
|
|
247
|
+
function extractResult(settled) {
|
|
248
|
+
if (settled.status === 'fulfilled' && settled.value && settled.value.length > 0) {
|
|
249
|
+
return settled.value;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
async function listCodeFiles(dirPath) {
|
|
254
|
+
try {
|
|
255
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
256
|
+
const files = [];
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (entry.isFile()) {
|
|
259
|
+
const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
|
|
260
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
261
|
+
files.push(resolve(dirPath, entry.name));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return files.sort();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function countSubdirs(dirPath) {
|
|
272
|
+
try {
|
|
273
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
274
|
+
return entries.filter(e => e.isDirectory()).length;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=explore-area.js.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { AstIndexClient } from '../ast-index/client.js';
|
|
2
|
-
|
|
3
|
-
symbol: string;
|
|
4
|
-
}
|
|
2
|
+
import type { FindUsagesArgs } from '../core/validation.js';
|
|
5
3
|
/**
|
|
6
4
|
* Find all usages of a symbol across the project.
|
|
7
5
|
*
|
|
@@ -9,6 +7,8 @@ export interface FindUsagesArgs {
|
|
|
9
7
|
* with `search` (text: catches imports and self-references that refs misses).
|
|
10
8
|
* Filter search results to exact word matches only (no substring matches).
|
|
11
9
|
* Deduplicate by file:line.
|
|
10
|
+
*
|
|
11
|
+
* v1.1: added scope, kind, limit, lang post-filters.
|
|
12
12
|
*/
|
|
13
13
|
export declare function handleFindUsages(args: FindUsagesArgs, astIndex: AstIndexClient): Promise<{
|
|
14
14
|
content: Array<{
|
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
function escapeRegex(s) {
|
|
5
5
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
6
|
}
|
|
7
|
+
/** Extension map for lang filter (best-effort) */
|
|
8
|
+
const LANG_EXT_MAP = {
|
|
9
|
+
typescript: ['.ts', '.tsx'],
|
|
10
|
+
javascript: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
11
|
+
php: ['.php'],
|
|
12
|
+
python: ['.py'],
|
|
13
|
+
rust: ['.rs'],
|
|
14
|
+
go: ['.go'],
|
|
15
|
+
java: ['.java'],
|
|
16
|
+
ruby: ['.rb'],
|
|
17
|
+
csharp: ['.cs'],
|
|
18
|
+
kotlin: ['.kt', '.kts'],
|
|
19
|
+
swift: ['.swift'],
|
|
20
|
+
dart: ['.dart'],
|
|
21
|
+
vue: ['.vue'],
|
|
22
|
+
svelte: ['.svelte'],
|
|
23
|
+
};
|
|
7
24
|
/**
|
|
8
25
|
* Find all usages of a symbol across the project.
|
|
9
26
|
*
|
|
@@ -11,6 +28,8 @@ function escapeRegex(s) {
|
|
|
11
28
|
* with `search` (text: catches imports and self-references that refs misses).
|
|
12
29
|
* Filter search results to exact word matches only (no substring matches).
|
|
13
30
|
* Deduplicate by file:line.
|
|
31
|
+
*
|
|
32
|
+
* v1.1: added scope, kind, limit, lang post-filters.
|
|
14
33
|
*/
|
|
15
34
|
export async function handleFindUsages(args, astIndex) {
|
|
16
35
|
if (astIndex.isDisabled() || astIndex.isOversized()) {
|
|
@@ -47,29 +66,89 @@ export async function handleFindUsages(args, astIndex) {
|
|
|
47
66
|
// Categorize additional results
|
|
48
67
|
const additionalImports = additional.filter(r => /\bimport\b/.test(r.text));
|
|
49
68
|
const additionalOther = additional.filter(r => !/\bimport\b/.test(r.text));
|
|
50
|
-
//
|
|
51
|
-
|
|
69
|
+
// Build mutable result arrays
|
|
70
|
+
let definitions = refs.definitions.map(d => ({ file: d.path, line: d.line, text: (d.signature ?? d.name).trim() }));
|
|
71
|
+
let allImports = [
|
|
52
72
|
...refs.imports.map(i => ({ file: i.path, line: i.line, text: (i.context ?? i.name).trim() })),
|
|
53
73
|
...additionalImports.map(r => ({ file: r.file, line: r.line, text: r.text })),
|
|
54
74
|
];
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
let allUsages = [
|
|
76
|
+
...refs.usages.map(u => ({ file: u.path, line: u.line, text: (u.context ?? u.name).trim() })),
|
|
77
|
+
...additionalOther,
|
|
78
|
+
];
|
|
79
|
+
// ─── Post-filters (v1.1) ───
|
|
80
|
+
// 1. Scope filter — by path prefix
|
|
81
|
+
if (args.scope) {
|
|
82
|
+
const scopePrefix = args.scope;
|
|
83
|
+
definitions = definitions.filter(d => d.file.includes(scopePrefix));
|
|
84
|
+
allImports = allImports.filter(i => i.file.includes(scopePrefix));
|
|
85
|
+
allUsages = allUsages.filter(u => u.file.includes(scopePrefix));
|
|
86
|
+
}
|
|
87
|
+
// 2. Lang filter — best-effort by file extension
|
|
88
|
+
if (args.lang) {
|
|
89
|
+
const langLower = args.lang.toLowerCase();
|
|
90
|
+
const exts = LANG_EXT_MAP[langLower] ?? [`.${langLower}`];
|
|
91
|
+
const matchesLang = (file) => exts.some(e => file.endsWith(e));
|
|
92
|
+
definitions = definitions.filter(d => matchesLang(d.file));
|
|
93
|
+
allImports = allImports.filter(i => matchesLang(i.file));
|
|
94
|
+
allUsages = allUsages.filter(u => matchesLang(u.file));
|
|
95
|
+
}
|
|
96
|
+
// 3. Kind filter — select sections
|
|
97
|
+
const kind = args.kind ?? 'all';
|
|
98
|
+
if (kind !== 'all') {
|
|
99
|
+
switch (kind) {
|
|
100
|
+
case 'definitions':
|
|
101
|
+
allImports = [];
|
|
102
|
+
allUsages = [];
|
|
103
|
+
break;
|
|
104
|
+
case 'imports':
|
|
105
|
+
definitions = [];
|
|
106
|
+
allUsages = [];
|
|
107
|
+
break;
|
|
108
|
+
case 'usages':
|
|
109
|
+
definitions = [];
|
|
110
|
+
allImports = [];
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 4. Limit — per category
|
|
115
|
+
const limit = args.limit ?? 50;
|
|
116
|
+
definitions = definitions.slice(0, limit);
|
|
117
|
+
allImports = allImports.slice(0, limit);
|
|
118
|
+
allUsages = allUsages.slice(0, limit);
|
|
119
|
+
// ─── Output ───
|
|
120
|
+
const totalCount = definitions.length + allImports.length + allUsages.length;
|
|
57
121
|
if (totalCount === 0) {
|
|
58
122
|
const hints = [`No usages found for "${args.symbol}".`];
|
|
123
|
+
if (args.scope)
|
|
124
|
+
hints.push(` (filtered by scope: "${args.scope}")`);
|
|
125
|
+
if (args.lang)
|
|
126
|
+
hints.push(` (filtered by lang: "${args.lang}")`);
|
|
127
|
+
if (args.kind && args.kind !== 'all')
|
|
128
|
+
hints.push(` (filtered by kind: "${args.kind}")`);
|
|
59
129
|
if (!astIndex.isAvailable()) {
|
|
60
130
|
hints.push('WARNING: ast-index is not available.');
|
|
61
131
|
}
|
|
62
132
|
return { content: [{ type: 'text', text: hints.join('\n') }] };
|
|
63
133
|
}
|
|
134
|
+
// Build header with active filters
|
|
135
|
+
const filterHints = [];
|
|
136
|
+
if (args.scope)
|
|
137
|
+
filterHints.push(`scope="${args.scope}"`);
|
|
138
|
+
if (args.lang)
|
|
139
|
+
filterHints.push(`lang=${args.lang}`);
|
|
140
|
+
if (args.kind && args.kind !== 'all')
|
|
141
|
+
filterHints.push(`kind=${args.kind}`);
|
|
142
|
+
const filterStr = filterHints.length > 0 ? ` [${filterHints.join(', ')}]` : '';
|
|
64
143
|
const lines = [
|
|
65
|
-
`REFS: "${args.symbol}" (${totalCount} total: ${
|
|
144
|
+
`REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} definitions, ${allImports.length} imports, ${allUsages.length} usages)${filterStr}`,
|
|
66
145
|
'',
|
|
67
146
|
];
|
|
68
|
-
if (
|
|
147
|
+
if (definitions.length > 0) {
|
|
69
148
|
lines.push('DEFINITIONS:');
|
|
70
|
-
for (const d of
|
|
71
|
-
lines.push(` ${d.
|
|
72
|
-
lines.push(` ${
|
|
149
|
+
for (const d of definitions) {
|
|
150
|
+
lines.push(` ${d.file}:${d.line}`);
|
|
151
|
+
lines.push(` ${d.text}`);
|
|
73
152
|
}
|
|
74
153
|
lines.push('');
|
|
75
154
|
}
|
|
@@ -81,10 +160,6 @@ export async function handleFindUsages(args, astIndex) {
|
|
|
81
160
|
}
|
|
82
161
|
lines.push('');
|
|
83
162
|
}
|
|
84
|
-
const allUsages = [
|
|
85
|
-
...refs.usages.map(u => ({ file: u.path, line: u.line, text: (u.context ?? u.name).trim() })),
|
|
86
|
-
...additionalOther,
|
|
87
|
-
];
|
|
88
163
|
if (allUsages.length > 0) {
|
|
89
164
|
lines.push('USAGES:');
|
|
90
165
|
for (const u of allUsages) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AstIndexClient } from '../ast-index/client.js';
|
|
2
|
+
import type { ModuleInfoArgs } from '../core/validation.js';
|
|
3
|
+
export declare function handleModuleInfo(args: ModuleInfoArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
|
|
4
|
+
content: Array<{
|
|
5
|
+
type: 'text';
|
|
6
|
+
text: string;
|
|
7
|
+
}>;
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=module-info.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
export async function handleModuleInfo(args, projectRoot, astIndex) {
|
|
3
|
+
// Degradation check
|
|
4
|
+
if (astIndex.isDisabled() || astIndex.isOversized()) {
|
|
5
|
+
return {
|
|
6
|
+
content: [{
|
|
7
|
+
type: 'text',
|
|
8
|
+
text: '⚠ ast-index unavailable — module_info requires ast-index.\n' +
|
|
9
|
+
'DEGRADED: Use find_usages() + related_files() as alternatives for dependency analysis.',
|
|
10
|
+
}],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const check = args.check ?? 'all';
|
|
14
|
+
const sections = [];
|
|
15
|
+
// Resolve module
|
|
16
|
+
const moduleList = await astIndex.modules(args.module);
|
|
17
|
+
const moduleName = moduleList.length > 0 ? moduleList[0].name : args.module;
|
|
18
|
+
const modulePath = moduleList.length > 0 ? moduleList[0].path : args.module;
|
|
19
|
+
sections.push(`MODULE: ${moduleName} (${modulePath})`);
|
|
20
|
+
if (moduleList.length === 0) {
|
|
21
|
+
sections.push('');
|
|
22
|
+
sections.push(`⚠ Module "${args.module}" not found by ast-index.`);
|
|
23
|
+
sections.push('');
|
|
24
|
+
// List available modules as hint
|
|
25
|
+
const allModules = await astIndex.modules();
|
|
26
|
+
if (allModules.length > 0) {
|
|
27
|
+
sections.push(`Available modules (${allModules.length}):`);
|
|
28
|
+
for (const m of allModules.slice(0, 20)) {
|
|
29
|
+
sections.push(` ${m.name} (${m.path})`);
|
|
30
|
+
}
|
|
31
|
+
if (allModules.length > 20) {
|
|
32
|
+
sections.push(` ... and ${allModules.length - 20} more`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
sections.push('No modules detected. ast-index module analysis requires a modular project structure.');
|
|
37
|
+
sections.push('HINT: Use find_usages() for cross-file symbol references, related_files() for import graphs.');
|
|
38
|
+
}
|
|
39
|
+
return { content: [{ type: 'text', text: sections.join('\n') }] };
|
|
40
|
+
}
|
|
41
|
+
sections.push('');
|
|
42
|
+
// Run requested checks in parallel
|
|
43
|
+
const checks = check === 'all'
|
|
44
|
+
? ['deps', 'dependents', 'api', 'unused-deps']
|
|
45
|
+
: [check];
|
|
46
|
+
const results = await Promise.allSettled(checks.map(async (c) => {
|
|
47
|
+
switch (c) {
|
|
48
|
+
case 'deps': return { type: 'deps', data: await astIndex.moduleDeps(args.module) };
|
|
49
|
+
case 'dependents': return { type: 'dependents', data: await astIndex.moduleDependents(args.module) };
|
|
50
|
+
case 'api': return { type: 'api', data: await astIndex.moduleApi(args.module) };
|
|
51
|
+
case 'unused-deps': return { type: 'unused-deps', data: await astIndex.unusedDeps(args.module) };
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
for (const r of results) {
|
|
55
|
+
if (r.status !== 'fulfilled')
|
|
56
|
+
continue;
|
|
57
|
+
const { type, data } = r.value;
|
|
58
|
+
switch (type) {
|
|
59
|
+
case 'deps': {
|
|
60
|
+
if (data.length > 0) {
|
|
61
|
+
sections.push(`DEPENDENCIES (${data.length}):`);
|
|
62
|
+
for (const d of data) {
|
|
63
|
+
const typeHint = d.type ? ` [${d.type}]` : '';
|
|
64
|
+
sections.push(` → ${d.name} (${d.path})${typeHint}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
sections.push('DEPENDENCIES: none detected');
|
|
69
|
+
}
|
|
70
|
+
sections.push('');
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'dependents': {
|
|
74
|
+
if (data.length > 0) {
|
|
75
|
+
sections.push(`DEPENDENTS (${data.length} modules depend on this):`);
|
|
76
|
+
for (const d of data) {
|
|
77
|
+
sections.push(` ← ${d.name} (${d.path})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
sections.push('DEPENDENTS: none — this module is a leaf');
|
|
82
|
+
}
|
|
83
|
+
sections.push('');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'api': {
|
|
87
|
+
if (data.length > 0) {
|
|
88
|
+
sections.push(`PUBLIC API (${data.length} symbols):`);
|
|
89
|
+
for (const a of data) {
|
|
90
|
+
const loc = `${rel(projectRoot, a.file)}:${a.line}`;
|
|
91
|
+
const sig = a.signature ? ` — ${a.signature}` : '';
|
|
92
|
+
sections.push(` ${a.kind} ${a.name}${sig} (${loc})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
sections.push('PUBLIC API: none detected');
|
|
97
|
+
}
|
|
98
|
+
sections.push('');
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'unused-deps': {
|
|
102
|
+
if (data.length > 0) {
|
|
103
|
+
sections.push(`UNUSED DEPENDENCIES (${data.length}):`);
|
|
104
|
+
for (const d of data) {
|
|
105
|
+
const reason = d.reason ? ` — ${d.reason}` : ' — imported but no symbols used';
|
|
106
|
+
sections.push(` ⚠ ${d.name} (${d.path})${reason}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
sections.push('UNUSED DEPENDENCIES: none — all dependencies are used');
|
|
111
|
+
}
|
|
112
|
+
sections.push('');
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
sections.push('HINT: Use smart_read() on module files, find_usages() for cross-module references.');
|
|
118
|
+
return { content: [{ type: 'text', text: sections.join('\n') }] };
|
|
119
|
+
}
|
|
120
|
+
function rel(projectRoot, absPath) {
|
|
121
|
+
return relative(projectRoot, absPath) || absPath;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=module-info.js.map
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { AstIndexClient } from '../ast-index/client.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
2
|
+
import type { OutlineArgs } from '../core/validation.js';
|
|
3
|
+
export declare const CODE_EXTENSIONS: Set<string>;
|
|
5
4
|
export declare function handleOutline(args: OutlineArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
|
|
6
5
|
content: Array<{
|
|
7
6
|
type: 'text';
|
|
8
7
|
text: string;
|
|
9
8
|
}>;
|
|
10
9
|
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Outline a single directory. When depth < maxDepth and recursive,
|
|
12
|
+
* recurse into subdirectories. Otherwise show file counts only.
|
|
13
|
+
*/
|
|
14
|
+
export declare function outlineDir(absPath: string, sections: string[], depth: number, maxDepth: number, projectRoot: string, astIndex: AstIndexClient): Promise<void>;
|
|
11
15
|
//# sourceMappingURL=outline.d.ts.map
|