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.
@@ -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
- export interface FindUsagesArgs {
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
- // Merge imports: refs.imports + additional imports
51
- const allImports = [
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
- // Total count
56
- const totalCount = refs.definitions.length + allImports.length + refs.usages.length + additionalOther.length;
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: ${refs.definitions.length} definitions, ${allImports.length} imports, ${refs.usages.length + additionalOther.length} usages)`,
144
+ `REFS: "${args.symbol}" (${totalCount} total: ${definitions.length} definitions, ${allImports.length} imports, ${allUsages.length} usages)${filterStr}`,
66
145
  '',
67
146
  ];
68
- if (refs.definitions.length > 0) {
147
+ if (definitions.length > 0) {
69
148
  lines.push('DEFINITIONS:');
70
- for (const d of refs.definitions) {
71
- lines.push(` ${d.path}:${d.line}`);
72
- lines.push(` ${(d.signature ?? d.name).trim()}`);
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
- export interface OutlineArgs {
3
- path: string;
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