ucn 3.8.15 → 3.8.16

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/cli/index.js CHANGED
@@ -14,7 +14,7 @@ const { parseFile, detectLanguage } = require('../core/parser');
14
14
  const { ProjectIndex } = require('../core/project');
15
15
  const { expandGlob, findProjectRoot } = require('../core/discovery');
16
16
  const output = require('../core/output');
17
- // pickBestDefinition moved to execute.js — no longer needed here
17
+ const { escapeRegExp } = require('../core/shared');
18
18
  const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
19
19
  const { execute } = require('../core/execute');
20
20
  const { ExpandCache } = require('../core/expand-cache');
@@ -115,6 +115,7 @@ function parseFlags(tokens) {
115
115
  showConfidence: !tokens.includes('--no-confidence'),
116
116
  minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
117
117
  framework: getValueFlag('--framework'),
118
+ stack: getValueFlag('--stack'),
118
119
  workers: (() => {
119
120
  const v = getValueFlag('--workers');
120
121
  if (v === null) return undefined;
@@ -327,11 +328,11 @@ function runFileCommand(filePath, command, arg) {
327
328
  fn: { name: arg, file: relativePath, ...flags },
328
329
  class: { name: arg, file: relativePath, ...flags },
329
330
  find: { name: arg, file: relativePath, ...flags },
330
- usages: { name: arg, ...flags },
331
+ usages: { name: arg, file: relativePath, ...flags },
331
332
  search: { term: arg, ...flags },
332
333
  lines: { file: relativePath, range: arg },
333
- typedef: { name: arg, ...flags },
334
- api: { file: relativePath },
334
+ typedef: { name: arg, file: relativePath, ...flags },
335
+ api: { file: relativePath, limit: flags.limit },
335
336
  };
336
337
 
337
338
  const { ok, result, error, note } = execute(index, canonical, paramsByCommand[canonical]);
@@ -427,6 +428,11 @@ function runProjectCommand(rootDir, command, arg) {
427
428
  if (!usedCache) {
428
429
  index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers });
429
430
  needsCacheSave = flags.cache;
431
+ // Clear stale expand cache — line ranges may have shifted after rebuild
432
+ try {
433
+ const expandPath = path.join(index.root, '.ucn-cache', 'expandable.json');
434
+ if (fs.existsSync(expandPath)) fs.unlinkSync(expandPath);
435
+ } catch (_) { /* best-effort */ }
430
436
  }
431
437
 
432
438
  try {
@@ -439,7 +445,7 @@ function runProjectCommand(rootDir, command, arg) {
439
445
  // Map from camelCase flag name to CLI flag string
440
446
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
441
447
  // Flags that are global (not command-specific) or have truthy defaults — skip warning for these
442
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', 'stack', 'showConfidence']);
448
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', 'showConfidence']);
443
449
  for (const [key, value] of Object.entries(flags)) {
444
450
  if (globalFlags.has(key)) continue;
445
451
  // Skip falsy/default values (0, undefined, false, empty array)
@@ -487,7 +493,7 @@ function runProjectCommand(rootDir, command, arg) {
487
493
  }
488
494
 
489
495
  case 'example': {
490
- const { ok, result, error } = execute(index, 'example', { name: arg });
496
+ const { ok, result, error } = execute(index, 'example', { name: arg, file: flags.file, className: flags.className });
491
497
  if (!ok) fail(error);
492
498
  printOutput(result,
493
499
  r => output.formatExampleJson(r, arg),
@@ -550,7 +556,7 @@ function runProjectCommand(rootDir, command, arg) {
550
556
  const items = cached?.items || [];
551
557
  const match = items.find(i => i.num === expandNum);
552
558
  const { ok, result, error } = execute(index, 'expand', {
553
- match, itemNum: expandNum, itemCount: items.length
559
+ match, itemNum: expandNum, itemCount: items.length, validateRoot: true
554
560
  });
555
561
  if (!ok) fail(error);
556
562
  console.log(result.text);
@@ -558,21 +564,23 @@ function runProjectCommand(rootDir, command, arg) {
558
564
  }
559
565
 
560
566
  case 'smart': {
561
- const { ok, result, error } = execute(index, 'smart', { name: arg, ...flags });
567
+ const { ok, result, error, note } = execute(index, 'smart', { name: arg, ...flags });
562
568
  if (!ok) fail(error);
563
569
  printOutput(result, output.formatSmartJson, r => output.formatSmart(r, {
564
570
  uncertainHint: 'use --include-uncertain to include all'
565
571
  }));
572
+ if (note) console.error(note);
566
573
  break;
567
574
  }
568
575
 
569
576
  case 'about': {
570
- const { ok, result, error } = execute(index, 'about', { name: arg, ...flags });
577
+ const { ok, result, error, note } = execute(index, 'about', { name: arg, ...flags });
571
578
  if (!ok) fail(error);
572
579
  printOutput(result,
573
580
  output.formatAboutJson,
574
581
  r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence })
575
582
  );
583
+ if (note) console.error(note);
576
584
  break;
577
585
  }
578
586
 
@@ -616,7 +624,7 @@ function runProjectCommand(rootDir, command, arg) {
616
624
  }
617
625
 
618
626
  case 'stacktrace': {
619
- const { ok, result, error } = execute(index, 'stacktrace', { stack: arg });
627
+ const { ok, result, error } = execute(index, 'stacktrace', { stack: flags.stack || arg });
620
628
  if (!ok) fail(error);
621
629
  printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
622
630
  break;
@@ -641,7 +649,7 @@ function runProjectCommand(rootDir, command, arg) {
641
649
 
642
650
  case 'fn': {
643
651
  requireArg(arg, 'Usage: ucn . fn <name>');
644
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all });
652
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all, className: flags.className });
645
653
  if (!ok) fail(error);
646
654
  if (note) console.error(note);
647
655
  printOutput(result, output.formatFnResultJson, output.formatFnResult);
@@ -650,7 +658,7 @@ function runProjectCommand(rootDir, command, arg) {
650
658
 
651
659
  case 'class': {
652
660
  requireArg(arg, 'Usage: ucn . class <name>');
653
- const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
661
+ const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines, className: flags.className });
654
662
  if (!ok) fail(error);
655
663
  if (note) console.error(note);
656
664
  printOutput(result, output.formatClassResultJson, output.formatClassResult);
@@ -659,8 +667,9 @@ function runProjectCommand(rootDir, command, arg) {
659
667
 
660
668
  case 'lines': {
661
669
  requireArg(arg, 'Usage: ucn . lines <range> --file <path>');
662
- const { ok, result, error } = execute(index, 'lines', { file: flags.file, range: arg });
670
+ const { ok, result, error, note } = execute(index, 'lines', { file: flags.file, range: arg });
663
671
  if (!ok) fail(error);
672
+ if (note) console.error(note);
664
673
  printOutput(result, output.formatLinesJson, r => output.formatLines(r));
665
674
  break;
666
675
  }
@@ -694,7 +703,7 @@ function runProjectCommand(rootDir, command, arg) {
694
703
  const { ok, result, error } = execute(index, 'fileExports', { file: filePath });
695
704
  if (!ok) fail(error);
696
705
  printOutput(result,
697
- r => JSON.stringify({ file: filePath, exports: r }, null, 2),
706
+ output.formatFileExportsJson,
698
707
  r => output.formatFileExports(r, filePath)
699
708
  );
700
709
  break;
@@ -705,11 +714,7 @@ function runProjectCommand(rootDir, command, arg) {
705
714
  const { ok, result, error } = execute(index, 'graph', { file: filePath, direction: flags.direction, depth: flags.depth, all: flags.all });
706
715
  if (!ok) fail(error);
707
716
  printOutput(result,
708
- r => JSON.stringify({
709
- root: path.relative(index.root, r.root),
710
- nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
711
- edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
712
- }, null, 2),
717
+ output.formatGraphJson,
713
718
  r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: filePath })
714
719
  );
715
720
  break;
@@ -725,7 +730,7 @@ function runProjectCommand(rootDir, command, arg) {
725
730
  // ── Remaining commands ──────────────────────────────────────────
726
731
 
727
732
  case 'typedef': {
728
- const { ok, result, error } = execute(index, 'typedef', { name: arg, exact: flags.exact });
733
+ const { ok, result, error } = execute(index, 'typedef', { name: arg, exact: flags.exact, file: flags.file, className: flags.className });
729
734
  if (!ok) fail(error);
730
735
  printOutput(result,
731
736
  r => output.formatTypedefJson(r, arg),
@@ -735,7 +740,7 @@ function runProjectCommand(rootDir, command, arg) {
735
740
  }
736
741
 
737
742
  case 'tests': {
738
- const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly });
743
+ const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className });
739
744
  if (!ok) fail(error);
740
745
  printOutput(result,
741
746
  r => output.formatTestsJson(r, arg),
@@ -794,8 +799,9 @@ function runProjectCommand(rootDir, command, arg) {
794
799
  }
795
800
 
796
801
  case 'entrypoints': {
797
- const { ok, result, error } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file, exclude: flags.exclude });
802
+ const { ok, result, error, note } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file, exclude: flags.exclude, includeTests: flags.includeTests, limit: flags.limit });
798
803
  if (!ok) fail(error);
804
+ if (note) console.error(note);
799
805
  printOutput(result,
800
806
  output.formatEntrypointsJson,
801
807
  r => output.formatEntrypoints(r)
@@ -814,8 +820,9 @@ function runProjectCommand(rootDir, command, arg) {
814
820
  }
815
821
 
816
822
  case 'diffImpact': {
817
- const { ok, result, error } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file });
823
+ const { ok, result, error, note } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file, limit: flags.limit, all: flags.all });
818
824
  if (!ok) fail(error);
825
+ if (note) console.error(note);
819
826
  printOutput(result, output.formatDiffImpactJson, r => output.formatDiffImpact(r, { all: flags.all }));
820
827
  break;
821
828
  }
@@ -1062,10 +1069,6 @@ function searchGlobFiles(files, term) {
1062
1069
  // HELPERS
1063
1070
  // ============================================================================
1064
1071
 
1065
- function escapeRegExp(text) {
1066
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1067
- }
1068
-
1069
1072
  function isCommentOrString(line) {
1070
1073
  const trimmed = line.trim();
1071
1074
  return trimmed.startsWith('//') ||
@@ -1279,6 +1282,8 @@ Flags can be added per-command: context myFunc --include-methods
1279
1282
  if (input === 'rebuild') {
1280
1283
  console.log('Rebuilding index...');
1281
1284
  index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
1285
+ // Clear expand cache — stale line ranges after rebuild
1286
+ if (iExpandCache) iExpandCache.clearForRoot(index.root);
1282
1287
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
1283
1288
  rl.prompt();
1284
1289
  return;
@@ -1344,7 +1349,7 @@ const INTERACTIVE_DISPATCH = {
1344
1349
  trace: { params: 'name', format: (r) => output.formatTrace(r) },
1345
1350
  reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
1346
1351
  related: { params: 'name', format: (r, _a, f) => output.formatRelated(r, { all: f.all, top: f.top }) },
1347
- example: { params: (a) => ({ name: a }), format: (r, a) => output.formatExample(r, a) },
1352
+ example: { params: 'name', format: (r, a) => output.formatExample(r, a) },
1348
1353
 
1349
1354
  // ── Finding Code ─────────────────────────────────────────────────
1350
1355
  find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
@@ -1364,12 +1369,12 @@ const INTERACTIVE_DISPATCH = {
1364
1369
  // ── Refactoring Helpers ──────────────────────────────────────────
1365
1370
  plan: { params: 'name', format: (r) => output.formatPlan(r) },
1366
1371
  verify: { params: 'name', format: (r) => output.formatVerify(r) },
1367
- diffImpact: { params: 'flags', format: (r, _a, f) => output.formatDiffImpact(r, { all: f.all }) },
1368
- entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude }), format: (r) => output.formatEntrypoints(r) },
1372
+ diffImpact: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit, all: f.all }), format: (r, _a, f) => output.formatDiffImpact(r, { all: f.all }) },
1373
+ entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude, includeTests: f.includeTests, limit: f.limit }), format: (r) => output.formatEntrypoints(r) },
1369
1374
 
1370
1375
  // ── Other ────────────────────────────────────────────────────────
1371
1376
  api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file || '.') },
1372
- stacktrace: { params: (a) => ({ stack: a }), format: (r) => output.formatStackTrace(r) },
1377
+ stacktrace: { params: (a, f) => ({ stack: f.stack || a }), format: (r) => output.formatStackTrace(r) },
1373
1378
  stats: { params: 'flags', format: (r, _a, f) => output.formatStats(r, { top: f.top }) },
1374
1379
  };
1375
1380
 
@@ -1395,8 +1400,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1395
1400
  switch (command) {
1396
1401
 
1397
1402
  case 'fn': {
1398
- if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]'); return; }
1399
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all });
1403
+ if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>] [--class-name=<class>]'); return; }
1404
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all, className: iflags.className });
1400
1405
  if (!ok) { console.log(error); return; }
1401
1406
  if (note) console.log(note);
1402
1407
  console.log(output.formatFnResult(result));
@@ -1443,7 +1448,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1443
1448
  itemCount = items.length;
1444
1449
  }
1445
1450
  const { ok, result, error } = execute(index, 'expand', {
1446
- match, itemNum: expandNum, itemCount, symbolName
1451
+ match, itemNum: expandNum, itemCount, symbolName, validateRoot: true
1447
1452
  });
1448
1453
  if (!ok) { console.log(error); return; }
1449
1454
  console.log(result.text);
@@ -1451,7 +1456,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1451
1456
  }
1452
1457
 
1453
1458
  case 'context': {
1454
- const { ok, result, error } = execute(index, 'context', { name: arg, ...iflags });
1459
+ const { ok, result, error, note } = execute(index, 'context', { name: arg, ...iflags });
1455
1460
  if (!ok) { console.log(error); return; }
1456
1461
  const { text, expandable } = output.formatContext(result, {
1457
1462
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
@@ -1460,6 +1465,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1460
1465
  showConfidence: iflags.showConfidence,
1461
1466
  });
1462
1467
  console.log(text);
1468
+ if (note) console.log(note);
1463
1469
  if (cache) {
1464
1470
  cache.save(index.root, arg, iflags.file, expandable);
1465
1471
  } else {
@@ -1469,8 +1475,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1469
1475
  }
1470
1476
 
1471
1477
  case 'deadcode': {
1472
- const { ok, result, error } = execute(index, 'deadcode', iflags);
1478
+ const { ok, result, error, note } = execute(index, 'deadcode', iflags);
1473
1479
  if (!ok) { console.log(error); return; }
1480
+ if (note) console.log(note);
1474
1481
  console.log(output.formatDeadcode(result, {
1475
1482
  top: iflags.top,
1476
1483
  decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
@@ -1480,8 +1487,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1480
1487
  }
1481
1488
 
1482
1489
  case 'search': {
1483
- const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...iflags });
1490
+ const { ok, result, error, structural, note } = execute(index, 'search', { term: arg, ...iflags });
1484
1491
  if (!ok) { console.log(error); return; }
1492
+ if (note) console.log(note);
1485
1493
  if (structural) {
1486
1494
  console.log(output.formatStructuralSearch(result));
1487
1495
  } else {
package/core/execute.js CHANGED
@@ -62,14 +62,14 @@ function splitClassMethod(name) {
62
62
  /**
63
63
  * Apply Class.method syntax to params object.
64
64
  * If name contains ".", splits it and sets p.name and p.className.
65
- * Only applies if p.className is not already set.
65
+ * When p.className is already set, still split the name to extract the method
66
+ * part (explicit --class-name takes precedence over the class from dot notation).
66
67
  */
67
68
  function applyClassMethodSyntax(p) {
68
- if (p.className) return; // already set explicitly
69
69
  const split = splitClassMethod(p.name);
70
70
  if (split) {
71
71
  p.name = split.methodName;
72
- p.className = split.className;
72
+ if (!p.className) p.className = split.className;
73
73
  }
74
74
  }
75
75
 
@@ -343,7 +343,8 @@ const HANDLERS = {
343
343
  withTypes: p.withTypes || false,
344
344
  });
345
345
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
346
- return { ok: true, result };
346
+ const tNote = truncationNote(index);
347
+ return { ok: true, result, ...(tNote && { note: tNote }) };
347
348
  },
348
349
 
349
350
  trace: (index, p) => {
package/core/registry.js CHANGED
@@ -106,7 +106,7 @@ const FLAG_APPLICABILITY = {
106
106
  example: ['file', 'className'],
107
107
  related: ['file', 'className', 'top', 'all'],
108
108
  // Finding code
109
- find: ['file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all'],
109
+ find: ['file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all', 'depth'],
110
110
  usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
111
111
  toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
112
112
  search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
package/core/search.js CHANGED
@@ -161,7 +161,10 @@ function usages(index, name, options = {}) {
161
161
  const usagesList = [];
162
162
 
163
163
  // Resolve file pattern for --file filter
164
- const fileFilter = options.file ? index.resolveFilePathForQuery(options.file) : null;
164
+ const fileFilterRaw = options.file ? index.resolveFilePathForQuery(options.file) : null;
165
+ // resolveFilePathForQuery may return error objects for ambiguous/not-found — fall back to substring matching
166
+ const fileFilter = typeof fileFilterRaw === 'string' ? fileFilterRaw : null;
167
+ const fileSubstring = options.file || null; // fallback for unresolved patterns
165
168
 
166
169
  // Get definitions (filtered)
167
170
  let allDefinitions = index.symbols.get(name) || [];
@@ -170,6 +173,8 @@ function usages(index, name, options = {}) {
170
173
  }
171
174
  if (fileFilter) {
172
175
  allDefinitions = allDefinitions.filter(d => d.file === fileFilter);
176
+ } else if (fileSubstring) {
177
+ allDefinitions = allDefinitions.filter(d => d.relativePath && d.relativePath.includes(fileSubstring));
173
178
  }
174
179
  const definitions = options.exclude || options.in
175
180
  ? allDefinitions.filter(d => index.matchesFilters(d.relativePath, options))
@@ -187,9 +192,11 @@ function usages(index, name, options = {}) {
187
192
 
188
193
  // Scan all files for usages
189
194
  for (const [filePath, fileEntry] of index.files) {
190
- // Apply --file filter
195
+ // Apply --file filter (exact match if resolved, substring fallback otherwise)
191
196
  if (fileFilter && filePath !== fileFilter) {
192
197
  continue;
198
+ } else if (!fileFilter && fileSubstring && !fileEntry.relativePath.includes(fileSubstring)) {
199
+ continue;
193
200
  }
194
201
  // Apply filters
195
202
  if (!index.matchesFilters(fileEntry.relativePath, options)) {
@@ -838,9 +845,21 @@ function tests(index, nameOrFile, options = {}) {
838
845
  const callPattern = new RegExp(escapeRegExp(searchTerm) + '\\s*\\(');
839
846
  const strPattern = new RegExp("['\"`]" + escapeRegExp(searchTerm) + "['\"`]");
840
847
 
848
+ // When className is provided, build a pattern to scope matches.
849
+ // We require the test file to also reference the class name (import, instantiation, or receiver).
850
+ const classNameFilter = options.className
851
+ ? new RegExp('\\b' + escapeRegExp(options.className) + '\\b')
852
+ : null;
853
+
841
854
  for (const { path: testPath, entry } of testFiles) {
842
855
  try {
843
856
  const content = index._readFile(testPath);
857
+
858
+ // className scoping: skip test files that don't reference the class at all
859
+ if (classNameFilter && !classNameFilter.test(content)) {
860
+ continue;
861
+ }
862
+
844
863
  const lines = content.split('\n');
845
864
  const matches = [];
846
865
 
package/mcp/server.js CHANGED
@@ -178,13 +178,6 @@ function resolveAndValidatePath(index, file) {
178
178
  return resolved;
179
179
  }
180
180
 
181
- function requireName(name) {
182
- if (!name || !name.trim()) {
183
- return toolError('Symbol name is required.');
184
- }
185
- return null;
186
- }
187
-
188
181
  // ============================================================================
189
182
  // CONSOLIDATED TOOL REGISTRATION
190
183
  // ============================================================================
@@ -332,19 +325,21 @@ server.registerTool(
332
325
 
333
326
  case 'about': {
334
327
  index = getIndex(project_dir, ep);
335
- const { ok, result, error } = execute(index, 'about', ep);
336
- if (!ok) return tr(error); // soft error — won't kill sibling calls
337
- return tr(output.formatAbout(result, {
328
+ const { ok, result, error, note } = execute(index, 'about', ep);
329
+ if (!ok) return toolError(error);
330
+ let aboutText = output.formatAbout(result, {
338
331
  allHint: 'Repeat with all=true to show all.',
339
332
  methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.',
340
333
  showConfidence: ep.showConfidence !== false,
341
- }));
334
+ });
335
+ if (note) aboutText += '\n\n' + note;
336
+ return tr(aboutText);
342
337
  }
343
338
 
344
339
  case 'context': {
345
340
  index = getIndex(project_dir, ep);
346
341
  const { ok, result: ctx, error, note } = execute(index, 'context', ep);
347
- if (!ok) return tr(error); // context uses soft error (not toolError)
342
+ if (!ok) return toolError(error);
348
343
  const { text, expandable } = output.formatContext(ctx, {
349
344
  expandHint: 'Use expand command with item number to see code for any item.',
350
345
  showConfidence: ep.showConfidence !== false,
@@ -358,7 +353,7 @@ server.registerTool(
358
353
  case 'impact': {
359
354
  index = getIndex(project_dir, ep);
360
355
  const { ok, result, error, note } = execute(index, 'impact', ep);
361
- if (!ok) return tr(error); // soft error
356
+ if (!ok) return toolError(error);
362
357
  let impactText = output.formatImpact(result);
363
358
  if (note) impactText += '\n\n' + note;
364
359
  return tr(impactText);
@@ -367,7 +362,7 @@ server.registerTool(
367
362
  case 'blast': {
368
363
  index = getIndex(project_dir, ep);
369
364
  const { ok, result, error, note } = execute(index, 'blast', ep);
370
- if (!ok) return tr(error); // soft error
365
+ if (!ok) return toolError(error);
371
366
  let blastText = output.formatBlast(result, {
372
367
  allHint: 'Set depth to expand all children.',
373
368
  });
@@ -377,15 +372,17 @@ server.registerTool(
377
372
 
378
373
  case 'smart': {
379
374
  index = getIndex(project_dir, ep);
380
- const { ok, result, error } = execute(index, 'smart', ep);
381
- if (!ok) return tr(error); // soft error
382
- return tr(output.formatSmart(result));
375
+ const { ok, result, error, note } = execute(index, 'smart', ep);
376
+ if (!ok) return toolError(error);
377
+ let smartText = output.formatSmart(result);
378
+ if (note) smartText += '\n\n' + note;
379
+ return tr(smartText);
383
380
  }
384
381
 
385
382
  case 'trace': {
386
383
  index = getIndex(project_dir, ep);
387
384
  const { ok, result, error, note } = execute(index, 'trace', ep);
388
- if (!ok) return tr(error); // soft error
385
+ if (!ok) return toolError(error);
389
386
  let traceText = output.formatTrace(result, {
390
387
  allHint: 'Set depth to expand all children.',
391
388
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
@@ -397,7 +394,7 @@ server.registerTool(
397
394
  case 'reverse_trace': {
398
395
  index = getIndex(project_dir, ep);
399
396
  const { ok, result, error, note } = execute(index, 'reverseTrace', ep);
400
- if (!ok) return tr(error);
397
+ if (!ok) return toolError(error);
401
398
  let rtText = output.formatReverseTrace(result, {
402
399
  allHint: 'Set depth to expand all children.',
403
400
  });
@@ -408,16 +405,14 @@ server.registerTool(
408
405
  case 'example': {
409
406
  index = getIndex(project_dir, ep);
410
407
  const { ok, result, error } = execute(index, 'example', ep);
411
- if (!ok) return tr(error);
412
- if (!result) return tr(`No usage examples found for "${ep.name}".`);
408
+ if (!ok) return toolError(error);
413
409
  return tr(output.formatExample(result, ep.name));
414
410
  }
415
411
 
416
412
  case 'related': {
417
413
  index = getIndex(project_dir, ep);
418
414
  const { ok, result, error, note } = execute(index, 'related', ep);
419
- if (!ok) return tr(error);
420
- if (!result) return tr(`Symbol "${ep.name}" not found.`);
415
+ if (!ok) return toolError(error);
421
416
  let relText = output.formatRelated(result, {
422
417
  all: ep.all || false, top: ep.top,
423
418
  allHint: 'Repeat with all=true to show all.'
@@ -431,7 +426,7 @@ server.registerTool(
431
426
  case 'find': {
432
427
  index = getIndex(project_dir, ep);
433
428
  const { ok, result, error, note } = execute(index, 'find', ep);
434
- if (!ok) return tr(error); // soft error
429
+ if (!ok) return toolError(error);
435
430
  let text = output.formatFind(result, ep.name, ep.top);
436
431
  if (note) text += '\n\n' + note;
437
432
  return tr(text);
@@ -440,7 +435,7 @@ server.registerTool(
440
435
  case 'usages': {
441
436
  index = getIndex(project_dir, ep);
442
437
  const { ok, result, error, note } = execute(index, 'usages', ep);
443
- if (!ok) return tr(error); // soft error
438
+ if (!ok) return toolError(error);
444
439
  let text = output.formatUsages(result, ep.name);
445
440
  if (note) text += '\n\n' + note;
446
441
  return tr(text);
@@ -449,7 +444,7 @@ server.registerTool(
449
444
  case 'toc': {
450
445
  index = getIndex(project_dir, ep);
451
446
  const { ok, result, error, note } = execute(index, 'toc', ep);
452
- if (!ok) return tr(error); // soft error
447
+ if (!ok) return toolError(error);
453
448
  let text = output.formatToc(result, {
454
449
  topHint: 'Set top=N or use detailed=false for compact view.'
455
450
  });
@@ -459,25 +454,31 @@ server.registerTool(
459
454
 
460
455
  case 'search': {
461
456
  index = getIndex(project_dir, ep);
462
- const { ok, result, error, structural } = execute(index, 'search', ep);
463
- if (!ok) return tr(error); // soft error
457
+ const { ok, result, error, structural, note } = execute(index, 'search', ep);
458
+ if (!ok) return toolError(error);
459
+ let searchText;
464
460
  if (structural) {
465
- return tr(output.formatStructuralSearch(result));
461
+ searchText = output.formatStructuralSearch(result);
462
+ } else {
463
+ searchText = output.formatSearch(result, ep.term);
466
464
  }
467
- return tr(output.formatSearch(result, ep.term));
465
+ if (note) searchText += '\n\n' + note;
466
+ return tr(searchText);
468
467
  }
469
468
 
470
469
  case 'tests': {
471
470
  index = getIndex(project_dir, ep);
472
- const { ok, result, error } = execute(index, 'tests', ep);
473
- if (!ok) return tr(error); // soft error
474
- return tr(output.formatTests(result, ep.name));
471
+ const { ok, result, error, note } = execute(index, 'tests', ep);
472
+ if (!ok) return toolError(error);
473
+ let testsText = output.formatTests(result, ep.name);
474
+ if (note) testsText += '\n\n' + note;
475
+ return tr(testsText);
475
476
  }
476
477
 
477
478
  case 'affected_tests': {
478
479
  index = getIndex(project_dir, ep);
479
480
  const { ok, result, error, note } = execute(index, 'affectedTests', ep);
480
- if (!ok) return tr(error);
481
+ if (!ok) return toolError(error);
481
482
  let atText = output.formatAffectedTests(result, { all: ep.all });
482
483
  if (note) atText += '\n\n' + note;
483
484
  return tr(atText);
@@ -486,7 +487,7 @@ server.registerTool(
486
487
  case 'deadcode': {
487
488
  index = getIndex(project_dir, ep);
488
489
  const { ok, result, error, note } = execute(index, 'deadcode', ep);
489
- if (!ok) return tr(error); // soft error
490
+ if (!ok) return toolError(error);
490
491
  const dcNote = note;
491
492
  let dcText = output.formatDeadcode(result, {
492
493
  top: ep.top || 0,
@@ -499,9 +500,11 @@ server.registerTool(
499
500
 
500
501
  case 'entrypoints': {
501
502
  index = getIndex(project_dir, ep);
502
- const { ok, result, error } = execute(index, 'entrypoints', ep);
503
- if (!ok) return tr(error);
504
- return tr(output.formatEntrypoints(result));
503
+ const { ok, result, error, note } = execute(index, 'entrypoints', ep);
504
+ if (!ok) return toolError(error);
505
+ let epText = output.formatEntrypoints(result);
506
+ if (note) epText += '\n\n' + note;
507
+ return tr(epText);
505
508
  }
506
509
 
507
510
  // ── File Dependencies ───────────────────────────────────────
@@ -509,28 +512,28 @@ server.registerTool(
509
512
  case 'imports': {
510
513
  index = getIndex(project_dir, ep);
511
514
  const { ok, result, error } = execute(index, 'imports', ep);
512
- if (!ok) return tr(error); // soft error
515
+ if (!ok) return toolError(error);
513
516
  return tr(output.formatImports(result, ep.file));
514
517
  }
515
518
 
516
519
  case 'exporters': {
517
520
  index = getIndex(project_dir, ep);
518
521
  const { ok, result, error } = execute(index, 'exporters', ep);
519
- if (!ok) return tr(error); // soft error
522
+ if (!ok) return toolError(error);
520
523
  return tr(output.formatExporters(result, ep.file));
521
524
  }
522
525
 
523
526
  case 'file_exports': {
524
527
  index = getIndex(project_dir, ep);
525
528
  const { ok, result, error } = execute(index, 'fileExports', ep);
526
- if (!ok) return tr(error); // soft error
529
+ if (!ok) return toolError(error);
527
530
  return tr(output.formatFileExports(result, ep.file));
528
531
  }
529
532
 
530
533
  case 'graph': {
531
534
  index = getIndex(project_dir, ep);
532
535
  const { ok, result, error } = execute(index, 'graph', ep);
533
- if (!ok) return tr(error); // soft error
536
+ if (!ok) return toolError(error);
534
537
  return tr(output.formatGraph(result, {
535
538
  showAll: ep.all || ep.depth !== undefined,
536
539
  maxDepth: ep.depth ?? 2, file: ep.file,
@@ -542,7 +545,7 @@ server.registerTool(
542
545
  case 'circular_deps': {
543
546
  index = getIndex(project_dir, ep);
544
547
  const { ok, result, error } = execute(index, 'circularDeps', ep);
545
- if (!ok) return tr(error);
548
+ if (!ok) return toolError(error);
546
549
  return tr(output.formatCircularDeps(result));
547
550
  }
548
551
 
@@ -551,22 +554,24 @@ server.registerTool(
551
554
  case 'verify': {
552
555
  index = getIndex(project_dir, ep);
553
556
  const { ok, result, error } = execute(index, 'verify', ep);
554
- if (!ok) return tr(error); // soft error
557
+ if (!ok) return toolError(error);
555
558
  return tr(output.formatVerify(result));
556
559
  }
557
560
 
558
561
  case 'plan': {
559
562
  index = getIndex(project_dir, ep);
560
563
  const { ok, result, error } = execute(index, 'plan', ep);
561
- if (!ok) return tr(error); // soft error
564
+ if (!ok) return toolError(error);
562
565
  return tr(output.formatPlan(result));
563
566
  }
564
567
 
565
568
  case 'diff_impact': {
566
569
  index = getIndex(project_dir, ep);
567
- const { ok, result, error } = execute(index, 'diffImpact', ep);
568
- if (!ok) return tr(error); // soft error — e.g. "not a git repo"
569
- return tr(output.formatDiffImpact(result, { all: ep.all }));
570
+ const { ok, result, error, note } = execute(index, 'diffImpact', ep);
571
+ if (!ok) return toolError(error);
572
+ let diText = output.formatDiffImpact(result, { all: ep.all });
573
+ if (note) diText += '\n\n' + note;
574
+ return tr(diText);
570
575
  }
571
576
 
572
577
  // ── Other ───────────────────────────────────────────────────
@@ -574,21 +579,21 @@ server.registerTool(
574
579
  case 'typedef': {
575
580
  index = getIndex(project_dir, ep);
576
581
  const { ok, result, error } = execute(index, 'typedef', ep);
577
- if (!ok) return tr(error); // soft error
582
+ if (!ok) return toolError(error);
578
583
  return tr(output.formatTypedef(result, ep.name));
579
584
  }
580
585
 
581
586
  case 'stacktrace': {
582
587
  index = getIndex(project_dir, ep);
583
588
  const { ok, result, error } = execute(index, 'stacktrace', ep);
584
- if (!ok) return tr(error); // soft error
589
+ if (!ok) return toolError(error);
585
590
  return tr(output.formatStackTrace(result));
586
591
  }
587
592
 
588
593
  case 'api': {
589
594
  index = getIndex(project_dir, ep);
590
595
  const { ok, result, error, note } = execute(index, 'api', ep);
591
- if (!ok) return tr(error); // soft error
596
+ if (!ok) return toolError(error);
592
597
  let apiText = output.formatApi(result, ep.file || '.');
593
598
  if (note) apiText += '\n\n' + note;
594
599
  return tr(apiText);
@@ -596,19 +601,19 @@ server.registerTool(
596
601
 
597
602
  case 'stats': {
598
603
  index = getIndex(project_dir, ep);
599
- const { ok, result, error } = execute(index, 'stats', ep);
600
- if (!ok) return tr(error); // soft error
601
- return tr(output.formatStats(result, { top: ep.top || 0 }));
604
+ const { ok, result, error, note } = execute(index, 'stats', ep);
605
+ if (!ok) return toolError(error);
606
+ let statsText = output.formatStats(result, { top: ep.top || 0 });
607
+ if (note) statsText += '\n\n' + note;
608
+ return tr(statsText);
602
609
  }
603
610
 
604
611
  // ── Extracting Code (via execute) ────────────────────────────
605
612
 
606
613
  case 'fn': {
607
- const err = requireName(ep.name);
608
- if (err) return err;
609
614
  index = getIndex(project_dir, ep);
610
615
  const { ok, result, error, note } = execute(index, 'fn', ep);
611
- if (!ok) return tr(error); // soft error
616
+ if (!ok) return toolError(error);
612
617
  // MCP path security: validate all result files are within project root
613
618
  for (const entry of result.entries) {
614
619
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -619,14 +624,9 @@ server.registerTool(
619
624
  }
620
625
 
621
626
  case 'class': {
622
- const err = requireName(ep.name);
623
- if (err) return err;
624
- if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
625
- return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
626
- }
627
627
  index = getIndex(project_dir, ep);
628
628
  const { ok, result, error, note } = execute(index, 'class', ep);
629
- if (!ok) return tr(error); // soft error (class not found)
629
+ if (!ok) return toolError(error); // soft error (class not found)
630
630
  // MCP path security: validate all result files are within project root
631
631
  for (const entry of result.entries) {
632
632
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -639,7 +639,7 @@ server.registerTool(
639
639
  case 'lines': {
640
640
  index = getIndex(project_dir, ep);
641
641
  const { ok, result, error } = execute(index, 'lines', ep);
642
- if (!ok) return tr(error); // soft error
642
+ if (!ok) return toolError(error);
643
643
  // MCP path security: validate file is within project root
644
644
  const check = resolveAndValidatePath(index, result.relativePath);
645
645
  if (typeof check !== 'string') return check;
@@ -657,7 +657,7 @@ server.registerTool(
657
657
  itemCount: lookup.itemCount, symbolName: lookup.symbolName,
658
658
  validateRoot: true
659
659
  });
660
- if (!ok) return tr(error); // soft error
660
+ if (!ok) return toolError(error);
661
661
  return tr(result.text);
662
662
  }
663
663
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.8.15",
3
+ "version": "3.8.16",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",