ucn 3.7.17 → 3.7.19

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/mcp/server.js CHANGED
@@ -31,20 +31,20 @@ try {
31
31
  // ============================================================================
32
32
 
33
33
  const { ProjectIndex } = require('../core/project');
34
- const { findProjectRoot, isTestFile } = require('../core/discovery');
35
- const { detectLanguage } = require('../core/parser');
34
+ const { findProjectRoot } = require('../core/discovery');
36
35
  const output = require('../core/output');
37
- const { pickBestDefinition, addTestExclusions } = require('../core/shared');
36
+ const { pickBestDefinition } = require('../core/shared');
37
+ const { getMcpCommandEnum, normalizeParams } = require('../core/registry');
38
+ const { execute } = require('../core/execute');
39
+ const { ExpandCache, renderExpandItem } = require('../core/expand-cache');
38
40
 
39
41
  // ============================================================================
40
42
  // INDEX CACHE
41
43
  // ============================================================================
42
44
 
43
45
  const indexCache = new Map(); // projectDir → { index, checkedAt }
44
- const expandCache = new Map(); // projectDir:symbolName → { items, root, symbolName, usedAt }
45
- const lastContextKey = new Map(); // projectRoot → expandCache key
46
46
  const MAX_CACHE_SIZE = 10;
47
- const MAX_EXPAND_CACHE_SIZE = 50;
47
+ const expandCacheInstance = new ExpandCache();
48
48
 
49
49
  function getIndex(projectDir) {
50
50
  const absDir = path.resolve(projectDir);
@@ -68,11 +68,8 @@ function getIndex(projectDir) {
68
68
  } else {
69
69
  index.build(null, { quiet: true, forceRebuild: loaded });
70
70
  index.saveCache();
71
- // Clear expandCache entries for this project — stale after rebuild
72
- for (const [key, val] of expandCache) {
73
- if (val.root === root) expandCache.delete(key);
74
- }
75
- lastContextKey.delete(root);
71
+ // Clear expand cache entries for this project — stale after rebuild
72
+ expandCacheInstance.clearForRoot(root);
76
73
  }
77
74
 
78
75
  // LRU eviction
@@ -87,11 +84,7 @@ function getIndex(projectDir) {
87
84
  }
88
85
  if (oldestKey) {
89
86
  indexCache.delete(oldestKey);
90
- // Clean up associated expandCache and lastContextKey entries
91
- for (const [key, val] of expandCache) {
92
- if (val.root === oldestKey) expandCache.delete(key);
93
- }
94
- lastContextKey.delete(oldestKey);
87
+ expandCacheInstance.clearForRoot(oldestKey);
95
88
  }
96
89
  }
97
90
 
@@ -112,11 +105,6 @@ const server = new McpServer({
112
105
  // TOOL HELPERS
113
106
  // ============================================================================
114
107
 
115
- function parseExclude(excludeStr) {
116
- if (!excludeStr) return [];
117
- return excludeStr.split(',').map(s => s.trim()).filter(Boolean);
118
- }
119
-
120
108
  const MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
121
109
 
122
110
  function toolResult(text) {
@@ -228,14 +216,7 @@ server.registerTool(
228
216
  {
229
217
  description: TOOL_DESCRIPTION,
230
218
  inputSchema: z.object({
231
- command: z.enum([
232
- 'about', 'context', 'impact', 'smart', 'trace',
233
- 'find', 'usages', 'fn', 'class', 'example',
234
- 'related', 'tests', 'verify', 'plan', 'typedef',
235
- 'expand', 'toc', 'search', 'deadcode',
236
- 'imports', 'exporters', 'file_exports', 'graph', 'lines',
237
- 'api', 'stats', 'diff_impact', 'stacktrace'
238
- ]),
219
+ command: z.enum(getMcpCommandEnum()),
239
220
  project_dir: z.string().describe('Absolute or relative path to the project root directory'),
240
221
  name: z.string().optional().describe('Symbol name to analyze. For fn: comma-separated for bulk (e.g. "parse,format"). For find: supports glob patterns (e.g. "handle*").'),
241
222
  file: z.string().optional().describe('File path (imports/exporters/graph/file_exports/lines/api/diff_impact) or filter pattern for disambiguation (e.g. "parser", "src/core")'),
@@ -289,11 +270,13 @@ server.registerTool(
289
270
  // UNDERSTANDING CODE
290
271
  // ==================================================================
291
272
 
273
+ // ── Commands using shared executor ─────────────────────────
274
+
292
275
  case 'about': {
293
- const err = requireName(name);
294
- if (err) return err;
295
276
  const index = getIndex(project_dir);
296
- const result = index.about(name, { file, exclude: parseExclude(exclude), withTypes: with_types || false, includeMethods: include_methods ?? undefined, includeUncertain: include_uncertain || false, all: all || false, maxCallers: top, maxCallees: top });
277
+ const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top });
278
+ const { ok, result, error } = execute(index, 'about', ep);
279
+ if (!ok) return toolError(error);
297
280
  return toolResult(output.formatAbout(result, {
298
281
  allHint: 'Repeat with all=true to show all.',
299
282
  methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.'
@@ -301,66 +284,38 @@ server.registerTool(
301
284
  }
302
285
 
303
286
  case 'context': {
304
- const err = requireName(name);
305
- if (err) return err;
306
287
  const index = getIndex(project_dir);
307
- const ctx = index.context(name, {
308
- includeMethods: include_methods,
309
- includeUncertain: include_uncertain || false,
310
- file,
311
- exclude: parseExclude(exclude)
312
- });
313
- if (!ctx) return toolResult(`Symbol "${name}" not found.`);
288
+ const ep = normalizeParams({ name, file, exclude, include_methods, include_uncertain });
289
+ const { ok, result: ctx, error } = execute(index, 'context', ep);
290
+ if (!ok) return toolResult(error); // context uses soft error (not toolError)
314
291
  const { text, expandable } = output.formatContext(ctx, {
315
292
  expandHint: 'Use expand command with item number to see code for any item.'
316
293
  });
317
- if (expandable.length > 0) {
318
- const cacheKey = `${index.root}:${name}:${file || ''}`;
319
- // LRU eviction for expandCache
320
- if (expandCache.size >= MAX_EXPAND_CACHE_SIZE && !expandCache.has(cacheKey)) {
321
- let oldestKey = null;
322
- let oldestTime = Infinity;
323
- for (const [key, val] of expandCache) {
324
- if ((val.usedAt || 0) < oldestTime) {
325
- oldestTime = val.usedAt || 0;
326
- oldestKey = key;
327
- }
328
- }
329
- if (oldestKey) expandCache.delete(oldestKey);
330
- }
331
- expandCache.set(cacheKey, { items: expandable, root: index.root, symbolName: name, usedAt: Date.now() });
332
- lastContextKey.set(index.root, cacheKey);
333
- }
294
+ expandCacheInstance.save(index.root, name, file, expandable);
334
295
  return toolResult(text);
335
296
  }
336
297
 
337
298
  case 'impact': {
338
- const err = requireName(name);
339
- if (err) return err;
340
299
  const index = getIndex(project_dir);
341
- const result = index.impact(name, { file, exclude: parseExclude(exclude) });
300
+ const ep = normalizeParams({ name, file, exclude });
301
+ const { ok, result, error } = execute(index, 'impact', ep);
302
+ if (!ok) return toolError(error);
342
303
  return toolResult(output.formatImpact(result));
343
304
  }
344
305
 
345
306
  case 'smart': {
346
- const err = requireName(name);
347
- if (err) return err;
348
307
  const index = getIndex(project_dir);
349
- const result = index.smart(name, {
350
- file,
351
- withTypes: with_types || false,
352
- includeMethods: include_methods,
353
- includeUncertain: include_uncertain || false
354
- });
355
- if (!result) return toolResult(`Function "${name}" not found.`);
308
+ const ep = normalizeParams({ name, file, with_types, include_methods, include_uncertain });
309
+ const { ok, result, error } = execute(index, 'smart', ep);
310
+ if (!ok) return toolResult(error); // soft error
356
311
  return toolResult(output.formatSmart(result));
357
312
  }
358
313
 
359
314
  case 'trace': {
360
- const err = requireName(name);
361
- if (err) return err;
362
315
  const index = getIndex(project_dir);
363
- const result = index.trace(name, { depth: depth ?? 3, file, all: depth !== undefined, includeMethods: include_methods, includeUncertain: include_uncertain || false });
316
+ const ep = normalizeParams({ name, file, depth, all, include_methods, include_uncertain });
317
+ const { ok, result, error } = execute(index, 'trace', ep);
318
+ if (!ok) return toolError(error);
364
319
  return toolResult(output.formatTrace(result, {
365
320
  allHint: 'Set depth to expand all children.',
366
321
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
@@ -368,96 +323,73 @@ server.registerTool(
368
323
  }
369
324
 
370
325
  case 'example': {
371
- const err = requireName(name);
372
- if (err) return err;
373
326
  const index = getIndex(project_dir);
374
- const exResult = index.example(name);
375
- if (!exResult) return toolResult(`No usage examples found for "${name}".`);
376
- return toolResult(output.formatExample(exResult, name));
327
+ const { ok, result, error } = execute(index, 'example', { name });
328
+ if (!ok) return toolError(error);
329
+ if (!result) return toolResult(`No usage examples found for "${name}".`);
330
+ return toolResult(output.formatExample(result, name));
377
331
  }
378
332
 
379
333
  case 'related': {
380
- const err = requireName(name);
381
- if (err) return err;
382
334
  const index = getIndex(project_dir);
383
- const result = index.related(name, { file, top, all: all || false });
335
+ const { ok, result, error } = execute(index, 'related', { name, file, top, all });
336
+ if (!ok) return toolError(error);
384
337
  if (!result) return toolResult(`Symbol "${name}" not found.`);
385
338
  return toolResult(output.formatRelated(result, {
386
- showAll: all || false,
387
- top,
339
+ showAll: all || false, top,
388
340
  allHint: 'Repeat with all=true to show all.'
389
341
  }));
390
342
  }
391
343
 
392
- // ==================================================================
393
- // FINDING CODE
394
- // ==================================================================
344
+ // ── Finding Code ────────────────────────────────────────────
395
345
 
396
346
  case 'find': {
397
- const err = requireName(name);
398
- if (err) return err;
399
347
  const index = getIndex(project_dir);
400
- const excludeArr = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
401
- const found = index.find(name, { file, exclude: excludeArr, exact: exact || false, in: inPath });
402
- return toolResult(output.formatFind(found, name, top));
348
+ const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
349
+ const { ok, result, error } = execute(index, 'find', ep);
350
+ if (!ok) return toolError(error);
351
+ return toolResult(output.formatFind(result, name, top));
403
352
  }
404
353
 
405
354
  case 'usages': {
406
- const err = requireName(name);
407
- if (err) return err;
408
355
  const index = getIndex(project_dir);
409
- const excludeArr = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
410
- const result = index.usages(name, {
411
- exclude: excludeArr,
412
- codeOnly: code_only || false,
413
- context: ctxLines || 0,
414
- in: inPath
415
- });
356
+ const ep = normalizeParams({ name, exclude, include_tests, code_only, context: ctxLines, in: inPath });
357
+ const { ok, result, error } = execute(index, 'usages', ep);
358
+ if (!ok) return toolError(error);
416
359
  return toolResult(output.formatUsages(result, name));
417
360
  }
418
361
 
419
362
  case 'toc': {
420
363
  const index = getIndex(project_dir);
421
- const toc = index.getToc({ detailed: detailed || false, topLevel: top_level || false, all: all || false, top });
422
- return toolResult(output.formatToc(toc, {
364
+ const ep = normalizeParams({ detailed, top_level, all, top });
365
+ const { ok, result, error } = execute(index, 'toc', ep);
366
+ if (!ok) return toolError(error);
367
+ return toolResult(output.formatToc(result, {
423
368
  topHint: 'Set top=N or use detailed=false for compact view.'
424
369
  }));
425
370
  }
426
371
 
427
372
  case 'search': {
428
- if (!term || !term.trim()) {
429
- return toolError('Search term is required.');
430
- }
431
373
  const index = getIndex(project_dir);
432
- const searchExclude = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
433
- const result = index.search(term, {
434
- codeOnly: code_only || false,
435
- context: ctxLines || 0,
436
- caseSensitive: case_sensitive || false,
437
- exclude: searchExclude,
438
- in: inPath || undefined,
439
- regex: regex
440
- });
374
+ const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex });
375
+ const { ok, result, error } = execute(index, 'search', ep);
376
+ if (!ok) return toolError(error);
441
377
  return toolResult(output.formatSearch(result, term));
442
378
  }
443
379
 
444
380
  case 'tests': {
445
- const err = requireName(name);
446
- if (err) return err;
447
381
  const index = getIndex(project_dir);
448
- const result = index.tests(name, { callsOnly: calls_only });
382
+ const ep = normalizeParams({ name, calls_only });
383
+ const { ok, result, error } = execute(index, 'tests', ep);
384
+ if (!ok) return toolError(error);
449
385
  return toolResult(output.formatTests(result, name));
450
386
  }
451
387
 
452
388
  case 'deadcode': {
453
389
  const index = getIndex(project_dir);
454
- const result = index.deadcode({
455
- exclude: parseExclude(exclude),
456
- in: inPath || undefined,
457
- includeExported: include_exported || false,
458
- includeDecorated: include_decorated || false,
459
- includeTests: include_tests || false
460
- });
390
+ const ep = normalizeParams({ exclude, in: inPath, include_exported, include_decorated, include_tests });
391
+ const { ok, result, error } = execute(index, 'deadcode', ep);
392
+ if (!ok) return toolError(error);
461
393
  return toolResult(output.formatDeadcode(result, {
462
394
  top: top || 0,
463
395
  decoratedHint: !include_decorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
@@ -465,9 +397,96 @@ server.registerTool(
465
397
  }));
466
398
  }
467
399
 
468
- // ==================================================================
469
- // EXTRACTING CODE
470
- // ==================================================================
400
+ // ── File Dependencies ───────────────────────────────────────
401
+
402
+ case 'imports': {
403
+ const index = getIndex(project_dir);
404
+ const { ok, result, error } = execute(index, 'imports', { file });
405
+ if (!ok) return toolError(error);
406
+ return toolResult(output.formatImports(result, file));
407
+ }
408
+
409
+ case 'exporters': {
410
+ const index = getIndex(project_dir);
411
+ const { ok, result, error } = execute(index, 'exporters', { file });
412
+ if (!ok) return toolError(error);
413
+ return toolResult(output.formatExporters(result, file));
414
+ }
415
+
416
+ case 'file_exports': {
417
+ const index = getIndex(project_dir);
418
+ const { ok, result, error } = execute(index, 'fileExports', { file });
419
+ if (!ok) return toolError(error);
420
+ return toolResult(output.formatFileExports(result, file));
421
+ }
422
+
423
+ case 'graph': {
424
+ const index = getIndex(project_dir);
425
+ const { ok, result, error } = execute(index, 'graph', { file, direction, depth, all });
426
+ if (!ok) return toolError(error);
427
+ return toolResult(output.formatGraph(result, {
428
+ showAll: all || depth !== undefined,
429
+ maxDepth: depth ?? 2, file,
430
+ depthHint: 'Set depth parameter for deeper graph.',
431
+ allHint: 'Set depth to expand all children.'
432
+ }));
433
+ }
434
+
435
+ // ── Refactoring ─────────────────────────────────────────────
436
+
437
+ case 'verify': {
438
+ const index = getIndex(project_dir);
439
+ const { ok, result, error } = execute(index, 'verify', { name, file });
440
+ if (!ok) return toolError(error);
441
+ return toolResult(output.formatVerify(result));
442
+ }
443
+
444
+ case 'plan': {
445
+ const index = getIndex(project_dir);
446
+ const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file });
447
+ const { ok, result, error } = execute(index, 'plan', ep);
448
+ if (!ok) return toolError(error);
449
+ return toolResult(output.formatPlan(result));
450
+ }
451
+
452
+ case 'diff_impact': {
453
+ const index = getIndex(project_dir);
454
+ const { ok, result, error } = execute(index, 'diffImpact', { base, staged, file });
455
+ if (!ok) return toolError(error);
456
+ return toolResult(output.formatDiffImpact(result));
457
+ }
458
+
459
+ // ── Other ───────────────────────────────────────────────────
460
+
461
+ case 'typedef': {
462
+ const index = getIndex(project_dir);
463
+ const { ok, result, error } = execute(index, 'typedef', { name, exact });
464
+ if (!ok) return toolError(error);
465
+ return toolResult(output.formatTypedef(result, name));
466
+ }
467
+
468
+ case 'stacktrace': {
469
+ const index = getIndex(project_dir);
470
+ const { ok, result, error } = execute(index, 'stacktrace', { stack });
471
+ if (!ok) return toolError(error);
472
+ return toolResult(output.formatStackTrace(result));
473
+ }
474
+
475
+ case 'api': {
476
+ const index = getIndex(project_dir);
477
+ const { ok, result, error } = execute(index, 'api', { file });
478
+ if (!ok) return toolError(error);
479
+ return toolResult(output.formatApi(result, file || '.'));
480
+ }
481
+
482
+ case 'stats': {
483
+ const index = getIndex(project_dir);
484
+ const { ok, result, error } = execute(index, 'stats', { functions });
485
+ if (!ok) return toolError(error);
486
+ return toolResult(output.formatStats(result, { top: top || 0 }));
487
+ }
488
+
489
+ // ── Extracting Code (adapter-specific) ──────────────────────
471
490
 
472
491
  case 'fn': {
473
492
  const err = requireName(name);
@@ -486,6 +505,19 @@ server.registerTool(
486
505
  continue;
487
506
  }
488
507
 
508
+ // Show all definitions when all=true and multiple matches
509
+ if (matches.length > 1 && !file && all) {
510
+ for (const m of matches) {
511
+ const mPathCheck = resolveAndValidatePath(index, m.relativePath || path.relative(index.root, m.file));
512
+ if (typeof mPathCheck !== 'string') return mPathCheck;
513
+ const mCode = fs.readFileSync(m.file, 'utf-8');
514
+ const mLines = mCode.split('\n');
515
+ const mFnCode = mLines.slice(m.startLine - 1, m.endLine).join('\n');
516
+ parts.push(output.formatFn(m, mFnCode));
517
+ }
518
+ continue;
519
+ }
520
+
489
521
  const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
490
522
  const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
491
523
  if (typeof fnPathCheck !== 'string') return fnPathCheck;
@@ -495,7 +527,7 @@ server.registerTool(
495
527
 
496
528
  let note = '';
497
529
  if (matches.length > 1 && !file) {
498
- note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n`;
530
+ note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter or all=true to show all.\n`;
499
531
  }
500
532
  parts.push(note + output.formatFn(match, fnCode));
501
533
  }
@@ -516,6 +548,20 @@ server.registerTool(
516
548
  return toolResult(`Class "${name}" not found.`);
517
549
  }
518
550
 
551
+ // Show all definitions when all=true and multiple matches
552
+ if (matches.length > 1 && !file && all) {
553
+ const allParts = [];
554
+ for (const m of matches) {
555
+ const mPathCheck = resolveAndValidatePath(index, m.relativePath || path.relative(index.root, m.file));
556
+ if (typeof mPathCheck !== 'string') return mPathCheck;
557
+ const mCode = fs.readFileSync(m.file, 'utf-8');
558
+ const mLines = mCode.split('\n');
559
+ const clsCode = mLines.slice(m.startLine - 1, m.endLine).join('\n');
560
+ allParts.push(output.formatClass(m, clsCode));
561
+ }
562
+ return toolResult(allParts.join('\n\n'));
563
+ }
564
+
519
565
  const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
520
566
  // Validate file is within project root
521
567
  const clsPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
@@ -620,199 +666,19 @@ server.registerTool(
620
666
  return toolError('Item number is required (e.g. item=1).');
621
667
  }
622
668
  const index = getIndex(project_dir);
623
- // Look up from the most recent context call for this project
624
- const recentKey = lastContextKey.get(index.root);
625
- const recentCache = recentKey ? expandCache.get(recentKey) : null;
626
-
627
- let match = null;
628
- let cachedItemCount = 0;
629
-
630
- if (recentCache && recentCache.items) {
631
- // Strict: only expand from the most recent context call
632
- recentCache.usedAt = Date.now(); // LRU: refresh on access
633
- cachedItemCount = recentCache.items.length;
634
- match = recentCache.items.find(i => i.num === item);
635
- } else {
636
- // No recent context — fallback to any cached context for this project
637
- for (const [key, cached] of expandCache) {
638
- if (cached.root === index.root && cached.items) {
639
- cached.usedAt = Date.now(); // LRU: refresh on access
640
- cachedItemCount = Math.max(cachedItemCount, cached.items.length);
641
- const found = cached.items.find(i => i.num === item);
642
- if (found) { match = found; break; }
643
- }
644
- }
645
- }
669
+ const { match, itemCount, symbolName } = expandCacheInstance.lookup(index.root, item);
646
670
 
647
- if (!match && cachedItemCount === 0) {
671
+ if (!match && itemCount === 0) {
648
672
  return toolError('No expandable items found. Run context command first to get numbered items.');
649
673
  }
650
674
  if (!match) {
651
- const scopeHint = recentCache ? ` (from last context for "${recentCache.symbolName}")` : '';
652
- return toolError(`Item ${item} not found${scopeHint}. Available items: 1-${cachedItemCount}`);
653
- }
654
-
655
- const filePath = match.file || (index.root && match.relativePath ? path.join(index.root, match.relativePath) : null);
656
- if (!filePath || !fs.existsSync(filePath)) {
657
- return toolError(`Cannot locate file for ${match.name}`);
658
- }
659
- // Validate file is within project root
660
- try {
661
- const realPath = fs.realpathSync(filePath);
662
- const realRoot = fs.realpathSync(index.root);
663
- if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
664
- return toolError(`File is outside project root: ${match.name}`);
665
- }
666
- } catch (e) {
667
- return toolError(`Cannot resolve file path for ${match.name}`);
668
- }
669
-
670
- const content = fs.readFileSync(filePath, 'utf-8');
671
- const fileLines = content.split('\n');
672
- const startLine = match.startLine || match.line || 1;
673
- const endLine = match.endLine || startLine + 20;
674
-
675
- const lines = [];
676
- lines.push(`[${match.num}] ${match.name} (${match.type})`);
677
- lines.push(`${match.relativePath}:${startLine}-${endLine}`);
678
- lines.push('\u2550'.repeat(60));
679
-
680
- for (let i = startLine - 1; i < Math.min(endLine, fileLines.length); i++) {
681
- lines.push(fileLines[i]);
682
- }
683
-
684
- return toolResult(lines.join('\n'));
685
- }
686
-
687
- // ==================================================================
688
- // FILE DEPENDENCIES
689
- // ==================================================================
690
-
691
- case 'imports': {
692
- if (!file) {
693
- return toolError('File parameter is required for imports command.');
694
- }
695
- const index = getIndex(project_dir);
696
- const result = index.imports(file);
697
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
698
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
699
- return toolResult(output.formatImports(result, file));
700
- }
701
-
702
- case 'exporters': {
703
- if (!file) {
704
- return toolError('File parameter is required for exporters command.');
705
- }
706
- const index = getIndex(project_dir);
707
- const result = index.exporters(file);
708
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
709
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
710
- return toolResult(output.formatExporters(result, file));
711
- }
712
-
713
- case 'file_exports': {
714
- if (!file) {
715
- return toolError('File parameter is required for file_exports command.');
716
- }
717
- const index = getIndex(project_dir);
718
- const result = index.fileExports(file);
719
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
720
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
721
- return toolResult(output.formatFileExports(result, file));
722
- }
723
-
724
- case 'graph': {
725
- if (!file) {
726
- return toolError('File parameter is required for graph command.');
727
- }
728
- const index = getIndex(project_dir);
729
- const result = index.graph(file, { direction: direction || 'both', maxDepth: depth ?? 2 });
730
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
731
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
732
- return toolResult(output.formatGraph(result, {
733
- showAll: all || depth !== undefined,
734
- file,
735
- depthHint: 'Set depth parameter for deeper graph.',
736
- allHint: 'Set depth to expand all children.'
737
- }));
738
- }
739
-
740
- // ==================================================================
741
- // REFACTORING
742
- // ==================================================================
743
-
744
- case 'verify': {
745
- const err = requireName(name);
746
- if (err) return err;
747
- const index = getIndex(project_dir);
748
- const result = index.verify(name, { file });
749
- return toolResult(output.formatVerify(result));
750
- }
751
-
752
- case 'plan': {
753
- const err = requireName(name);
754
- if (err) return err;
755
- if (!add_param && !remove_param && !rename_to) {
756
- return toolError('Plan requires an operation: add_param, remove_param, or rename_to');
757
- }
758
- const index = getIndex(project_dir);
759
- const result = index.plan(name, {
760
- addParam: add_param,
761
- removeParam: remove_param,
762
- renameTo: rename_to,
763
- defaultValue: default_value,
764
- file
765
- });
766
- return toolResult(output.formatPlan(result));
767
- }
768
-
769
- case 'diff_impact': {
770
- // Validate git ref format to prevent argument injection
771
- if (base && !/^[a-zA-Z0-9._\-~\/^@{}:]+$/.test(base)) {
772
- return toolError(`Invalid git ref format: ${base}`);
773
- }
774
- const index = getIndex(project_dir);
775
- const result = index.diffImpact({
776
- base: base || 'HEAD',
777
- staged: staged || false,
778
- file: file || undefined
779
- });
780
- return toolResult(output.formatDiffImpact(result));
781
- }
782
-
783
- // ==================================================================
784
- // OTHER
785
- // ==================================================================
786
-
787
- case 'typedef': {
788
- const err = requireName(name);
789
- if (err) return err;
790
- const index = getIndex(project_dir);
791
- const result = index.typedef(name, { exact: exact || false });
792
- return toolResult(output.formatTypedef(result, name));
793
- }
794
-
795
- case 'stacktrace': {
796
- if (!stack || !stack.trim()) {
797
- return toolError('Stack trace text is required.');
675
+ const scopeHint = symbolName ? ` (from last context for "${symbolName}")` : '';
676
+ return toolError(`Item ${item} not found${scopeHint}. Available items: 1-${itemCount}`);
798
677
  }
799
- const index = getIndex(project_dir);
800
- const result = index.parseStackTrace(stack);
801
- return toolResult(output.formatStackTrace(result));
802
- }
803
678
 
804
- case 'api': {
805
- const index = getIndex(project_dir);
806
- const result = index.api(file || undefined);
807
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
808
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
809
- return toolResult(output.formatApi(result, file || '.'));
810
- }
811
-
812
- case 'stats': {
813
- const index = getIndex(project_dir);
814
- const stats = index.getStats({ functions: functions || false });
815
- return toolResult(output.formatStats(stats, { top: top || 30 }));
679
+ const rendered = renderExpandItem(match, index.root, { validateRoot: true });
680
+ if (!rendered.ok) return toolError(rendered.error);
681
+ return toolResult(rendered.text);
816
682
  }
817
683
 
818
684
  default: