token-pilot 0.8.2 → 0.9.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.
@@ -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,7 +1,5 @@
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';
5
3
  export declare function handleOutline(args: OutlineArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
6
4
  content: Array<{
7
5
  type: 'text';
@@ -22,7 +22,28 @@ export async function handleOutline(args, projectRoot, astIndex) {
22
22
  }],
23
23
  };
24
24
  }
25
- // List code files and subdirectories (1 level)
25
+ const recursive = args.recursive ?? false;
26
+ const maxDepth = args.max_depth ?? 2;
27
+ const sections = [];
28
+ await outlineDir(absPath, sections, 0, recursive ? maxDepth : 0, projectRoot, astIndex);
29
+ sections.push('HINT: Use outline(path) on subdirs, smart_read(path) for file structure, read_symbol(path, symbol) for source code.');
30
+ return { content: [{ type: 'text', text: sections.join('\n') }] };
31
+ }
32
+ /** Max output lines to prevent runaway recursive outlines */
33
+ const MAX_OUTLINE_LINES = 500;
34
+ /**
35
+ * Outline a single directory. When depth < maxDepth and recursive,
36
+ * recurse into subdirectories. Otherwise show file counts only.
37
+ */
38
+ async function outlineDir(absPath, sections, depth, maxDepth, projectRoot, astIndex) {
39
+ // Guard: stop if output is already too large
40
+ if (sections.length >= MAX_OUTLINE_LINES) {
41
+ if (!sections[sections.length - 1]?.startsWith('⚠')) {
42
+ sections.push(`⚠ Output truncated at ${MAX_OUTLINE_LINES} lines. Use outline() on specific subdirectories for details.`);
43
+ }
44
+ return;
45
+ }
46
+ // List code files and subdirectories
26
47
  const entries = await readdir(absPath, { withFileTypes: true });
27
48
  const codeFiles = [];
28
49
  const subdirs = [];
@@ -38,53 +59,63 @@ export async function handleOutline(args, projectRoot, astIndex) {
38
59
  }
39
60
  }
40
61
  if (codeFiles.length === 0 && subdirs.length === 0) {
41
- return {
42
- content: [{
43
- type: 'text',
44
- text: `No code files or subdirectories found in "${args.path}".`,
45
- }],
46
- };
62
+ if (depth === 0) {
63
+ sections.push(`No code files or subdirectories found in "${relative(projectRoot, absPath) || '.'}".`);
64
+ }
65
+ return;
47
66
  }
48
67
  // Sort
49
68
  codeFiles.sort();
50
69
  subdirs.sort();
51
70
  const relDir = relative(projectRoot, absPath) || '.';
71
+ const indent = ' '.repeat(depth);
52
72
  const totalLabel = codeFiles.length > 0 ? `${codeFiles.length} files` : '';
53
73
  const subLabel = subdirs.length > 0 ? `${subdirs.length} subdirs` : '';
54
74
  const countLabel = [totalLabel, subLabel].filter(Boolean).join(', ');
55
- const sections = [`OUTLINE: ${relDir}/ (${countLabel})`, ''];
56
- // Show subdirectories with file counts (recursive scan)
75
+ sections.push(`${indent}OUTLINE: ${relDir}/ (${countLabel})`);
76
+ sections.push('');
77
+ // Show subdirectories
57
78
  if (subdirs.length > 0) {
58
- for (const sub of subdirs) {
59
- const subPath = resolve(absPath, sub);
60
- const fileCount = await countCodeFiles(subPath);
61
- sections.push(` ${sub}/ (${fileCount} code files)`);
79
+ if (depth < maxDepth) {
80
+ // Recursive: outline each subdir
81
+ for (const sub of subdirs) {
82
+ const subPath = resolve(absPath, sub);
83
+ await outlineDir(subPath, sections, depth + 1, maxDepth, projectRoot, astIndex);
84
+ }
85
+ }
86
+ else {
87
+ // Non-recursive: show file counts only
88
+ for (const sub of subdirs) {
89
+ const subPath = resolve(absPath, sub);
90
+ const fileCount = await countCodeFiles(subPath);
91
+ sections.push(`${indent} ${sub}/ (${fileCount} code files)`);
92
+ }
93
+ sections.push('');
62
94
  }
63
- sections.push('');
64
95
  }
65
96
  // Show code files at this level with AST outline
97
+ const fileIndent = ' '.repeat(depth + 1);
98
+ const symbolIndent = depth + 2;
66
99
  for (const filePath of codeFiles) {
67
100
  const name = basename(filePath);
68
101
  try {
69
102
  const structure = await astIndex.outline(filePath);
70
103
  if (!structure) {
71
- sections.push(`${name} (no AST)`);
104
+ sections.push(`${fileIndent}${name} (no AST)`);
72
105
  sections.push('');
73
106
  continue;
74
107
  }
75
- sections.push(`${name} (${structure.meta.lines} lines)`);
108
+ sections.push(`${fileIndent}${name} (${structure.meta.lines} lines)`);
76
109
  for (const sym of structure.symbols) {
77
- formatCompactSymbol(sym, sections, 1);
110
+ formatCompactSymbol(sym, sections, symbolIndent);
78
111
  }
79
112
  sections.push('');
80
113
  }
81
114
  catch {
82
- sections.push(`${name} (outline failed)`);
115
+ sections.push(`${fileIndent}${name} (outline failed)`);
83
116
  sections.push('');
84
117
  }
85
118
  }
86
- sections.push('HINT: Use outline(path) on subdirs, smart_read(path) for file structure, read_symbol(path, symbol) for source code.');
87
- return { content: [{ type: 'text', text: sections.join('\n') }] };
88
119
  }
89
120
  /**
90
121
  * Format a symbol in compact outline form.
@@ -1,5 +1,6 @@
1
1
  import type { AstIndexClient } from '../ast-index/client.js';
2
- export declare function handleProjectOverview(projectRoot: string, astIndex: AstIndexClient): Promise<{
2
+ import type { ProjectOverviewArgs } from '../core/validation.js';
3
+ export declare function handleProjectOverview(args: ProjectOverviewArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
3
4
  content: Array<{
4
5
  type: 'text';
5
6
  text: string;