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.
@@ -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
package/dist/server.js CHANGED
@@ -27,9 +27,10 @@ import { handleReadForEdit } from './handlers/read-for-edit.js';
27
27
  import { handleRelatedFiles } from './handlers/related-files.js';
28
28
  import { handleOutline } from './handlers/outline.js';
29
29
  import { handleCodeAudit } from './handlers/code-audit.js';
30
+ import { handleModuleInfo } from './handlers/module-info.js';
30
31
  import { detectContextMode } from './integration/context-mode-detector.js';
31
32
  import { estimateTokens } from './core/token-estimator.js';
32
- import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, } from './core/validation.js';
33
+ import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, } from './core/validation.js';
33
34
  export async function createServer(projectRoot, options) {
34
35
  const config = await loadConfig(projectRoot);
35
36
  const astIndex = new AstIndexClient(projectRoot, config.astIndex.timeout, {
@@ -198,6 +199,7 @@ export async function createServer(projectRoot, options) {
198
199
  '• Text pattern search/counting → Grep (regex, count mode)',
199
200
  '• Security audit → Grep for: password, token, secret, credential, hardcoded, api_key, TODO.*security',
200
201
  '• Deep dive into specific code → read_symbol (after finding issues)',
202
+ '• Module architecture → module_info (deps, dependents, public API, unused deps)',
201
203
  '',
202
204
  'WORKFLOW: project_overview → smart_read → read_symbol → read_for_edit → edit → read_diff',
203
205
  ].join('\n'),
@@ -291,21 +293,31 @@ export async function createServer(projectRoot, options) {
291
293
  // --- Search & navigation ---
292
294
  {
293
295
  name: 'find_usages',
294
- description: 'Use INSTEAD OF Grep/ripgrep for finding symbol references. Semantic search across the project — groups results by: definitions, imports, usages.',
296
+ description: 'Use INSTEAD OF Grep/ripgrep for finding symbol references. Semantic search across the project — groups results by: definitions, imports, usages. (v1.1: added scope, kind, limit, lang filters)',
295
297
  inputSchema: {
296
298
  type: 'object',
297
299
  properties: {
298
300
  symbol: { type: 'string', description: 'Symbol name to find usages of' },
301
+ scope: { type: 'string', description: 'Filter results by path prefix (e.g., "src/Domain/")' },
302
+ kind: { type: 'string', enum: ['definitions', 'imports', 'usages', 'all'], description: 'Show only specific section (default: "all")' },
303
+ limit: { type: 'number', description: 'Max results per category (default: 50, max: 500)' },
304
+ lang: { type: 'string', description: 'Filter by language/extension (e.g., "php", "typescript")' },
299
305
  },
300
306
  required: ['symbol'],
301
307
  },
302
308
  },
303
309
  {
304
310
  name: 'project_overview',
305
- description: 'START HERE for unfamiliar codebases. Shows project type, architecture, framework detection, directory structure with symbol counts. Use before exploring code.',
311
+ description: 'START HERE for unfamiliar codebases. Shows project type (dual-detection: ast-index + config files), architecture, framework detection, quality tools, CI, directory map. (v1.1: added include filter)',
306
312
  inputSchema: {
307
313
  type: 'object',
308
- properties: {},
314
+ properties: {
315
+ include: {
316
+ type: 'array',
317
+ items: { type: 'string', enum: ['stack', 'ci', 'quality', 'architecture'] },
318
+ description: 'Sections to include (default: all). Use ["stack"] for quick type check, ["quality","ci"] for tooling overview.',
319
+ },
320
+ },
309
321
  },
310
322
  },
311
323
  {
@@ -321,11 +333,13 @@ export async function createServer(projectRoot, options) {
321
333
  },
322
334
  {
323
335
  name: 'outline',
324
- description: 'Use INSTEAD OF listing dir + reading each file. One call returns all symbols (classes, functions, methods, routes) for every code file in a directory.',
336
+ description: 'Use INSTEAD OF listing dir + reading each file. One call returns all symbols (classes, functions, methods, routes) for every code file in a directory. (v1.1: added recursive, max_depth)',
325
337
  inputSchema: {
326
338
  type: 'object',
327
339
  properties: {
328
340
  path: { type: 'string', description: 'Directory path' },
341
+ recursive: { type: 'boolean', description: 'Recursively outline subdirectories (default: false)' },
342
+ max_depth: { type: 'number', description: 'Max recursion depth when recursive=true (default: 2, max: 5)' },
329
343
  },
330
344
  required: ['path'],
331
345
  },
@@ -371,6 +385,22 @@ export async function createServer(projectRoot, options) {
371
385
  required: ['check'],
372
386
  },
373
387
  },
388
+ {
389
+ name: 'module_info',
390
+ description: 'Analyze module dependencies, dependents, public API, and unused deps. Use for architecture understanding and dependency cleanup.',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {
394
+ module: { type: 'string', description: 'Module name or path pattern (e.g., "auth", "src/Domain/")' },
395
+ check: {
396
+ type: 'string',
397
+ enum: ['deps', 'dependents', 'api', 'unused-deps', 'all'],
398
+ description: 'What to check: "deps" (dependencies), "dependents" (who depends on this), "api" (public symbols), "unused-deps" (dead dependencies), "all" (everything). Default: "all"',
399
+ },
400
+ },
401
+ required: ['module'],
402
+ },
403
+ },
374
404
  ],
375
405
  }));
376
406
  // Helper: get real full-file token count for honest analytics
@@ -476,7 +506,8 @@ export async function createServer(projectRoot, options) {
476
506
  return usagesResult;
477
507
  }
478
508
  case 'project_overview': {
479
- const overviewResult = await handleProjectOverview(projectRoot, astIndex);
509
+ const overviewArgs = validateProjectOverviewArgs(args);
510
+ const overviewResult = await handleProjectOverview(overviewArgs, projectRoot, astIndex);
480
511
  const overviewText = overviewResult.content[0]?.text ?? '';
481
512
  overviewResult.content[0] = { type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${overviewText}` };
482
513
  const ovTokens = estimateTokens(overviewResult.content[0].text);
@@ -513,6 +544,15 @@ export async function createServer(projectRoot, options) {
513
544
  analytics.record({ tool: 'code_audit', path: auditArgs.check, tokensReturned: estimateTokens(auditText), tokensWouldBe: estimateTokens(auditText), timestamp: Date.now() });
514
545
  return auditResult;
515
546
  }
547
+ case 'module_info': {
548
+ const moduleArgs = validateModuleInfoArgs(args);
549
+ const moduleResult = await handleModuleInfo(moduleArgs, projectRoot, astIndex);
550
+ const moduleText = moduleResult.content[0]?.text ?? '';
551
+ // Estimate: manual analysis would require reading all module files + grepping deps
552
+ const moduleWouldBe = estimateTokens(moduleText) * 5;
553
+ analytics.record({ tool: 'module_info', path: moduleArgs.module, tokensReturned: estimateTokens(moduleText), tokensWouldBe: moduleWouldBe, timestamp: Date.now() });
554
+ return moduleResult;
555
+ }
516
556
  default:
517
557
  return {
518
558
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-pilot",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Save 60-80% tokens when AI reads code — MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",