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.
@@ -1,7 +1,7 @@
1
1
  import { readdir, stat } from 'node:fs/promises';
2
2
  import { resolve, basename, relative } from 'node:path';
3
3
  import { resolveSafePath } from '../core/validation.js';
4
- const CODE_EXTENSIONS = new Set([
4
+ export const CODE_EXTENSIONS = new Set([
5
5
  'ts', 'tsx', 'js', 'jsx', 'mjs', 'py', 'go', 'rs', 'java', 'kt', 'kts',
6
6
  'swift', 'cs', 'cpp', 'cc', 'cxx', 'hpp', 'c', 'h', 'php', 'rb', 'scala',
7
7
  'dart', 'lua', 'sh', 'bash', 'sql', 'r', 'vue', 'svelte', 'pl', 'pm',
@@ -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
+ export 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;
@@ -1,78 +1,109 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { resolve, basename } from 'node:path';
3
- export async function handleProjectOverview(projectRoot, astIndex) {
1
+ import { detectProject } from '../core/project-detector.js';
2
+ export async function handleProjectOverview(args, projectRoot, astIndex) {
4
3
  const lines = [];
5
- // 1. Project info from package.json / Cargo.toml etc.
6
- const projectInfo = await detectProjectInfo(projectRoot);
7
- if (projectInfo) {
8
- lines.push(`PROJECT: ${projectInfo.name} v${projectInfo.version}`);
9
- if (projectInfo.description)
10
- lines.push(` ${projectInfo.description}`);
11
- lines.push('');
12
- }
13
- else {
14
- lines.push(`PROJECT: ${basename(projectRoot)}`);
15
- lines.push('');
16
- }
17
- // 2. ast-index map — directory structure with file counts and symbol kinds
4
+ // 1. Dual detection: ast-index + config scanner
5
+ let astIndexType;
6
+ let mapData = null;
7
+ let convData = null;
18
8
  if (astIndex.isAvailable() && !astIndex.isOversized() && !astIndex.isDisabled()) {
19
- const [mapData, convData] = await Promise.all([
9
+ [mapData, convData] = await Promise.all([
20
10
  astIndex.map(),
21
11
  astIndex.conventions(),
22
12
  ]);
23
13
  if (mapData) {
24
- lines.push(`TYPE: ${mapData.project_type} (${mapData.file_count} files)`);
25
- lines.push('');
26
- // Conventions
27
- if (convData) {
28
- if (convData.architecture.length > 0) {
29
- lines.push(`ARCHITECTURE: ${convData.architecture.join(', ')}`);
30
- }
31
- const fwList = [];
32
- for (const [category, frameworks] of Object.entries(convData.frameworks)) {
33
- for (const fw of frameworks) {
34
- fwList.push(`${fw.name} (${category})`);
35
- }
36
- }
37
- if (fwList.length > 0) {
38
- lines.push(`FRAMEWORKS: ${fwList.join(', ')}`);
39
- }
40
- if (convData.naming_patterns.length > 0) {
41
- const patterns = convData.naming_patterns
42
- .slice(0, 8)
43
- .map(p => `${p.suffix}(${p.count})`)
44
- .join(', ');
45
- lines.push(`PATTERNS: ${patterns}`);
46
- }
47
- lines.push('');
48
- }
49
- // Directory map
50
- lines.push('MAP:');
51
- for (const group of mapData.groups) {
52
- const kinds = group.kinds
53
- ? ' — ' + Object.entries(group.kinds).map(([k, v]) => `${v} ${k}`).join(', ')
54
- : '';
55
- lines.push(` ${group.path} (${group.file_count} files${kinds})`);
56
- }
57
- lines.push('');
14
+ astIndexType = mapData.project_type;
15
+ }
16
+ }
17
+ const detection = await detectProject(projectRoot, astIndexType);
18
+ // Determine which sections to include
19
+ const include = args.include ?? ['stack', 'ci', 'quality', 'architecture'];
20
+ const showStack = include.includes('stack');
21
+ const showCI = include.includes('ci');
22
+ const showQuality = include.includes('quality');
23
+ const showArch = include.includes('architecture');
24
+ // 2. Project identity
25
+ lines.push(`PROJECT: ${detection.projectName} v${detection.projectVersion}`);
26
+ if (detection.projectDescription)
27
+ lines.push(` ${detection.projectDescription}`);
28
+ lines.push('');
29
+ // 3. TYPE — dual detection
30
+ if (showStack) {
31
+ if (astIndexType) {
32
+ lines.push(`TYPE (ast-index): ${astIndexType}${mapData ? ` (${mapData.file_count} files)` : ''}`);
33
+ }
34
+ if (detection.configStacks.length > 0) {
35
+ const configLine = formatConfigStacks(detection);
36
+ lines.push(`TYPE (config): ${configLine}`);
37
+ }
38
+ if (detection.configStacks.length === 0 && !astIndexType) {
39
+ lines.push('TYPE: unknown (no config files found)');
40
+ }
41
+ // Confidence
42
+ lines.push(`CONFIDENCE: ${detection.confidence}${getConfidenceHint(detection)}`);
43
+ lines.push('');
44
+ }
45
+ // 4. Architecture & frameworks (from ast-index conventions)
46
+ if (showArch && convData) {
47
+ if (convData.architecture.length > 0) {
48
+ lines.push(`ARCHITECTURE: ${convData.architecture.join(', ')}`);
49
+ }
50
+ // Merge framework info: ast-index conventions + config detection
51
+ const fwList = buildFrameworkList(convData, detection);
52
+ if (fwList.length > 0) {
53
+ lines.push(`FRAMEWORKS: ${fwList.join(', ')}`);
54
+ }
55
+ if (convData.naming_patterns.length > 0) {
56
+ const patterns = convData.naming_patterns
57
+ .slice(0, 8)
58
+ .map(p => `${p.suffix}(${p.count})`)
59
+ .join(', ');
60
+ lines.push(`PATTERNS: ${patterns}`);
61
+ }
62
+ lines.push('');
63
+ }
64
+ // 5. Quality tools
65
+ if (showQuality && detection.qualityTools.length > 0) {
66
+ lines.push(`QUALITY: ${detection.qualityTools.join(', ')}`);
67
+ }
68
+ // 6. CI pipelines
69
+ if (showCI && detection.ciPipelines.length > 0) {
70
+ lines.push(`CI: ${detection.ciPipelines.join(', ')}`);
71
+ }
72
+ // Docker
73
+ if (showCI && detection.hasDocker) {
74
+ lines.push('DOCKER: yes');
75
+ }
76
+ if ((showQuality && detection.qualityTools.length > 0) || (showCI && detection.ciPipelines.length > 0)) {
77
+ lines.push('');
78
+ }
79
+ // 7. Directory map (from ast-index)
80
+ if (showArch && mapData) {
81
+ lines.push('MAP:');
82
+ for (const group of mapData.groups) {
83
+ const kinds = group.kinds
84
+ ? ' — ' + Object.entries(group.kinds).map(([k, v]) => `${v} ${k}`).join(', ')
85
+ : '';
86
+ lines.push(` ${group.path} (${group.file_count} files${kinds})`);
58
87
  }
59
- else {
60
- // Fallback to stats
61
- try {
62
- const statsText = await astIndex.stats();
63
- if (statsText) {
64
- const filesMatch = statsText.match(/Files:\s*(\d+)/);
65
- const symbolsMatch = statsText.match(/Symbols:\s*(\d+)/);
66
- if (filesMatch)
67
- lines.push(`Files indexed: ${filesMatch[1]}`);
68
- if (symbolsMatch)
69
- lines.push(`Symbols: ${symbolsMatch[1]}`);
70
- lines.push('');
71
- }
88
+ lines.push('');
89
+ }
90
+ else if (showArch && !mapData && astIndex.isAvailable() && !astIndex.isDisabled() && !astIndex.isOversized()) {
91
+ // Fallback to stats
92
+ try {
93
+ const statsText = await astIndex.stats();
94
+ if (statsText) {
95
+ const filesMatch = statsText.match(/Files:\s*(\d+)/);
96
+ const symbolsMatch = statsText.match(/Symbols:\s*(\d+)/);
97
+ if (filesMatch)
98
+ lines.push(`Files indexed: ${filesMatch[1]}`);
99
+ if (symbolsMatch)
100
+ lines.push(`Symbols: ${symbolsMatch[1]}`);
101
+ lines.push('');
72
102
  }
73
- catch { /* ignore */ }
74
103
  }
104
+ catch { /* ignore */ }
75
105
  }
106
+ // 8. Degradation warnings
76
107
  if (astIndex.isDisabled()) {
77
108
  lines.push('⚠ ast-index: project root not detected. Call smart_read() on any project file first.');
78
109
  lines.push(' Working tools: smart_read, smart_read_many, outline, read_symbol, read_range');
@@ -88,47 +119,55 @@ export async function handleProjectOverview(projectRoot, astIndex) {
88
119
  lines.push('HINT: Use smart_read() on files, find_usages() for symbol references, outline() for directory overview.');
89
120
  return { content: [{ type: 'text', text: lines.join('\n') }] };
90
121
  }
91
- async function detectProjectInfo(projectRoot) {
92
- try {
93
- const pkg = JSON.parse(await readFile(resolve(projectRoot, 'package.json'), 'utf-8'));
94
- return {
95
- name: pkg.name ?? basename(projectRoot),
96
- version: pkg.version ?? '0.0.0',
97
- description: pkg.description,
98
- type: 'Node.js/TypeScript',
99
- };
100
- }
101
- catch { /* not a node project */ }
102
- try {
103
- const composer = JSON.parse(await readFile(resolve(projectRoot, 'composer.json'), 'utf-8'));
104
- return {
105
- name: composer.name ?? 'unknown',
106
- version: composer.version ?? '0.0.0',
107
- description: composer.description,
108
- type: 'PHP',
109
- };
110
- }
111
- catch { /* not a php project */ }
112
- try {
113
- const cargo = await readFile(resolve(projectRoot, 'Cargo.toml'), 'utf-8');
114
- const name = cargo.match(/^name\s*=\s*"(.+?)"/m)?.[1] ?? 'unknown';
115
- const version = cargo.match(/^version\s*=\s*"(.+?)"/m)?.[1] ?? '0.0.0';
116
- return { name, version, type: 'Rust' };
117
- }
118
- catch { /* not a rust project */ }
119
- try {
120
- const pyproject = await readFile(resolve(projectRoot, 'pyproject.toml'), 'utf-8');
121
- const name = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1] ?? 'unknown';
122
- const version = pyproject.match(/^version\s*=\s*"(.+?)"/m)?.[1] ?? '0.0.0';
123
- return { name, version, type: 'Python' };
124
- }
125
- catch { /* not a python project */ }
126
- try {
127
- const gomod = await readFile(resolve(projectRoot, 'go.mod'), 'utf-8');
128
- const name = gomod.match(/^module\s+(.+)/m)?.[1] ?? 'unknown';
129
- return { name, version: '0.0.0', type: 'Go' };
130
- }
131
- catch { /* not a go project */ }
132
- return null;
122
+ // ──────────────────────────────────────────────
123
+ // Formatters
124
+ // ──────────────────────────────────────────────
125
+ function formatConfigStacks(detection) {
126
+ if (detection.configStacks.length === 0)
127
+ return 'unknown';
128
+ const parts = [];
129
+ for (const stack of detection.configStacks) {
130
+ let part = stack.type;
131
+ if (stack.langVersion)
132
+ part = stack.langVersion;
133
+ if (stack.framework)
134
+ part += ` (${stack.framework})`;
135
+ parts.push(part);
136
+ }
137
+ if (detection.primaryStack && detection.configStacks.length > 1) {
138
+ // Put primary first, mark others
139
+ const primaryIdx = detection.configStacks.indexOf(detection.primaryStack);
140
+ if (primaryIdx > 0) {
141
+ const [primary] = parts.splice(primaryIdx, 1);
142
+ parts.unshift(primary);
143
+ }
144
+ return parts[0] + (parts.length > 1 ? ` + ${parts.slice(1).join(', ')}` : '');
145
+ }
146
+ return parts.join(', ');
147
+ }
148
+ function getConfidenceHint(detection) {
149
+ if (detection.confidence === 'low') {
150
+ return ` — ast-index and config files disagree on project type`;
151
+ }
152
+ if (detection.confidence === 'medium' && detection.configStacks.length > 1) {
153
+ return ` multi-stack project detected`;
154
+ }
155
+ return '';
156
+ }
157
+ function buildFrameworkList(convData, detection) {
158
+ const fwSet = new Set();
159
+ // From ast-index conventions
160
+ for (const [category, frameworks] of Object.entries(convData.frameworks)) {
161
+ for (const fw of frameworks) {
162
+ fwSet.add(`${fw.name} (${category})`);
163
+ }
164
+ }
165
+ // From config detection (may have version info that conventions don't)
166
+ for (const stack of detection.configStacks) {
167
+ if (stack.framework && !Array.from(fwSet).some(f => f.includes(stack.framework.split(' ')[0]))) {
168
+ fwSet.add(stack.framework);
169
+ }
170
+ }
171
+ return Array.from(fwSet);
133
172
  }
134
173
  //# sourceMappingURL=project-overview.js.map
@@ -0,0 +1,35 @@
1
+ import type { AstIndexClient } from '../ast-index/client.js';
2
+ import type { SmartDiffArgs } from '../core/validation.js';
3
+ import type { FileStructure } from '../types.js';
4
+ interface FileDiff {
5
+ path: string;
6
+ oldPath?: string;
7
+ addedLines: number;
8
+ removedLines: number;
9
+ hunks: DiffHunk[];
10
+ isBinary: boolean;
11
+ isNew: boolean;
12
+ isDeleted: boolean;
13
+ }
14
+ interface DiffHunk {
15
+ newStart: number;
16
+ newCount: number;
17
+ lines: string[];
18
+ }
19
+ interface SymbolChange {
20
+ name: string;
21
+ kind: string;
22
+ changeType: 'MODIFIED' | 'ADDED' | 'REMOVED';
23
+ lineRange: string;
24
+ }
25
+ export declare function handleSmartDiff(args: SmartDiffArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
26
+ content: Array<{
27
+ type: 'text';
28
+ text: string;
29
+ }>;
30
+ rawTokens: number;
31
+ }>;
32
+ export declare function parseUnifiedDiff(raw: string): FileDiff[];
33
+ export declare function mapHunksToSymbols(hunks: DiffHunk[], structure: FileStructure): SymbolChange[];
34
+ export {};
35
+ //# sourceMappingURL=smart-diff.d.ts.map