ucn 3.7.18 → 3.7.20

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);
@@ -647,200 +666,19 @@ server.registerTool(
647
666
  return toolError('Item number is required (e.g. item=1).');
648
667
  }
649
668
  const index = getIndex(project_dir);
650
- // Look up from the most recent context call for this project
651
- const recentKey = lastContextKey.get(index.root);
652
- const recentCache = recentKey ? expandCache.get(recentKey) : null;
653
-
654
- let match = null;
655
- let cachedItemCount = 0;
656
-
657
- if (recentCache && recentCache.items) {
658
- // Strict: only expand from the most recent context call
659
- recentCache.usedAt = Date.now(); // LRU: refresh on access
660
- cachedItemCount = recentCache.items.length;
661
- match = recentCache.items.find(i => i.num === item);
662
- } else {
663
- // No recent context — fallback to any cached context for this project
664
- for (const [key, cached] of expandCache) {
665
- if (cached.root === index.root && cached.items) {
666
- cached.usedAt = Date.now(); // LRU: refresh on access
667
- cachedItemCount = Math.max(cachedItemCount, cached.items.length);
668
- const found = cached.items.find(i => i.num === item);
669
- if (found) { match = found; break; }
670
- }
671
- }
672
- }
669
+ const { match, itemCount, symbolName } = expandCacheInstance.lookup(index.root, item);
673
670
 
674
- if (!match && cachedItemCount === 0) {
671
+ if (!match && itemCount === 0) {
675
672
  return toolError('No expandable items found. Run context command first to get numbered items.');
676
673
  }
677
674
  if (!match) {
678
- const scopeHint = recentCache ? ` (from last context for "${recentCache.symbolName}")` : '';
679
- return toolError(`Item ${item} not found${scopeHint}. Available items: 1-${cachedItemCount}`);
680
- }
681
-
682
- const filePath = match.file || (index.root && match.relativePath ? path.join(index.root, match.relativePath) : null);
683
- if (!filePath || !fs.existsSync(filePath)) {
684
- return toolError(`Cannot locate file for ${match.name}`);
685
- }
686
- // Validate file is within project root
687
- try {
688
- const realPath = fs.realpathSync(filePath);
689
- const realRoot = fs.realpathSync(index.root);
690
- if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
691
- return toolError(`File is outside project root: ${match.name}`);
692
- }
693
- } catch (e) {
694
- return toolError(`Cannot resolve file path for ${match.name}`);
695
- }
696
-
697
- const content = fs.readFileSync(filePath, 'utf-8');
698
- const fileLines = content.split('\n');
699
- const startLine = match.startLine || match.line || 1;
700
- const endLine = match.endLine || startLine + 20;
701
-
702
- const lines = [];
703
- lines.push(`[${match.num}] ${match.name} (${match.type})`);
704
- lines.push(`${match.relativePath}:${startLine}-${endLine}`);
705
- lines.push('\u2550'.repeat(60));
706
-
707
- for (let i = startLine - 1; i < Math.min(endLine, fileLines.length); i++) {
708
- lines.push(fileLines[i]);
709
- }
710
-
711
- return toolResult(lines.join('\n'));
712
- }
713
-
714
- // ==================================================================
715
- // FILE DEPENDENCIES
716
- // ==================================================================
717
-
718
- case 'imports': {
719
- if (!file) {
720
- return toolError('File parameter is required for imports command.');
721
- }
722
- const index = getIndex(project_dir);
723
- const result = index.imports(file);
724
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
725
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
726
- return toolResult(output.formatImports(result, file));
727
- }
728
-
729
- case 'exporters': {
730
- if (!file) {
731
- return toolError('File parameter is required for exporters command.');
732
- }
733
- const index = getIndex(project_dir);
734
- const result = index.exporters(file);
735
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
736
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
737
- return toolResult(output.formatExporters(result, file));
738
- }
739
-
740
- case 'file_exports': {
741
- if (!file) {
742
- return toolError('File parameter is required for file_exports command.');
743
- }
744
- const index = getIndex(project_dir);
745
- const result = index.fileExports(file);
746
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
747
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
748
- return toolResult(output.formatFileExports(result, file));
749
- }
750
-
751
- case 'graph': {
752
- if (!file) {
753
- return toolError('File parameter is required for graph command.');
754
- }
755
- const index = getIndex(project_dir);
756
- const result = index.graph(file, { direction: direction || 'both', maxDepth: depth ?? 2 });
757
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
758
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
759
- return toolResult(output.formatGraph(result, {
760
- showAll: all || depth !== undefined,
761
- maxDepth: depth ?? 2,
762
- file,
763
- depthHint: 'Set depth parameter for deeper graph.',
764
- allHint: 'Set depth to expand all children.'
765
- }));
766
- }
767
-
768
- // ==================================================================
769
- // REFACTORING
770
- // ==================================================================
771
-
772
- case 'verify': {
773
- const err = requireName(name);
774
- if (err) return err;
775
- const index = getIndex(project_dir);
776
- const result = index.verify(name, { file });
777
- return toolResult(output.formatVerify(result));
778
- }
779
-
780
- case 'plan': {
781
- const err = requireName(name);
782
- if (err) return err;
783
- if (!add_param && !remove_param && !rename_to) {
784
- return toolError('Plan requires an operation: add_param, remove_param, or rename_to');
785
- }
786
- const index = getIndex(project_dir);
787
- const result = index.plan(name, {
788
- addParam: add_param,
789
- removeParam: remove_param,
790
- renameTo: rename_to,
791
- defaultValue: default_value,
792
- file
793
- });
794
- return toolResult(output.formatPlan(result));
795
- }
796
-
797
- case 'diff_impact': {
798
- // Validate git ref format to prevent argument injection
799
- if (base && !/^[a-zA-Z0-9._\-~\/^@{}:]+$/.test(base)) {
800
- return toolError(`Invalid git ref format: ${base}`);
801
- }
802
- const index = getIndex(project_dir);
803
- const result = index.diffImpact({
804
- base: base || 'HEAD',
805
- staged: staged || false,
806
- file: file || undefined
807
- });
808
- return toolResult(output.formatDiffImpact(result));
809
- }
810
-
811
- // ==================================================================
812
- // OTHER
813
- // ==================================================================
814
-
815
- case 'typedef': {
816
- const err = requireName(name);
817
- if (err) return err;
818
- const index = getIndex(project_dir);
819
- const result = index.typedef(name, { exact: exact || false });
820
- return toolResult(output.formatTypedef(result, name));
821
- }
822
-
823
- case 'stacktrace': {
824
- if (!stack || !stack.trim()) {
825
- 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}`);
826
677
  }
827
- const index = getIndex(project_dir);
828
- const result = index.parseStackTrace(stack);
829
- return toolResult(output.formatStackTrace(result));
830
- }
831
-
832
- case 'api': {
833
- const index = getIndex(project_dir);
834
- const result = index.api(file || undefined);
835
- if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
836
- if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
837
- return toolResult(output.formatApi(result, file || '.'));
838
- }
839
678
 
840
- case 'stats': {
841
- const index = getIndex(project_dir);
842
- const stats = index.getStats({ functions: functions || false });
843
- return toolResult(output.formatStats(stats, { top: top || 0 }));
679
+ const rendered = renderExpandItem(match, index.root, { validateRoot: true });
680
+ if (!rendered.ok) return toolError(rendered.error);
681
+ return toolResult(rendered.text);
844
682
  }
845
683
 
846
684
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.18",
3
+ "version": "3.7.20",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
6
6
  "main": "index.js",