ucn 3.7.11 → 3.7.13

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.
@@ -92,6 +92,7 @@ ucn deadcode --exclude=test # Skip test files (most useful)
92
92
  | Quick project overview | `ucn toc` | Every file with function/class counts and line counts |
93
93
  | Find by glob pattern | `ucn find "handle*"` | Locate definitions matching a glob (supports * and ?) |
94
94
  | Text search with context | `ucn search term --context=3` | Like grep -C 3, shows surrounding lines |
95
+ | Regex search (default) | `ucn search '\d+'` | Search supports regex by default (alternation, character classes, etc.) |
95
96
  | Text search filtered | `ucn search term --exclude=test` | Search only in matching files |
96
97
  | Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
97
98
  | Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
@@ -126,6 +127,8 @@ ucn [target] <command> [name] [--flags]
126
127
  | `--staged` | Analyze staged changes (diff-impact) |
127
128
  | `--no-cache` | Force re-index after editing files |
128
129
  | `--context=N` | Lines of surrounding context in `usages`/`search` output |
130
+ | `--no-regex` | Force plain text search (regex is default) |
131
+ | `--functions` | Show per-function line counts in `stats` (complexity audit) |
129
132
 
130
133
  ## Workflow Integration
131
134
 
@@ -1 +1 @@
1
- ghu_gwrBjpCa3Gl8WuJxABQ8XOgRdbPfFf36TmIA
1
+ ghu_OKOjTMtrqEazgazx9Dj0l0z6irgt8R3KXKET
@@ -1 +1 @@
1
- {"token":"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtY3AtcmVnaXN0cnkiLCJleHAiOjE3NzE3ODU4MTAsIm5iZiI6MTc3MTc4NTUxMCwiaWF0IjoxNzcxNzg1NTEwLCJhdXRoX21ldGhvZCI6ImdpdGh1Yi1hdCIsImF1dGhfbWV0aG9kX3N1YiI6Im1sZW9jYSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJwdWJsaXNoIiwicmVzb3VyY2UiOiJpby5naXRodWIubWxlb2NhLyoifV19.DW2gbu6Lnoc43CqRsxlhrIy3CQaMxAlLykOfYP3lY08d0FHVmXhg9gUt6qt2G6ihn-G99tA6oOf8tGvxUteeBQ","expires_at":1771785810}
1
+ {"token":"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtY3AtcmVnaXN0cnkiLCJleHAiOjE3NzIwMDg1ODksIm5iZiI6MTc3MjAwODI4OSwiaWF0IjoxNzcyMDA4Mjg5LCJhdXRoX21ldGhvZCI6ImdpdGh1Yi1hdCIsImF1dGhfbWV0aG9kX3N1YiI6Im1sZW9jYSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJwdWJsaXNoIiwicmVzb3VyY2UiOiJpby5naXRodWIubWxlb2NhLyoifV19.VANcz6OV-uLS8b-UAAEu1aX4HRO4FcTO2c3Ub9r4_8dfRjh5dGQlHddmnOLUBBTf8UwWu0Ap2m4PCQSUvcvfAg","expires_at":1772008589}
package/cli/index.js CHANGED
@@ -82,7 +82,11 @@ const flags = {
82
82
  followSymlinks: !args.includes('--no-follow-symlinks'),
83
83
  // Diff-impact options
84
84
  base: args.find(a => a.startsWith('--base='))?.split('=')[1] || null,
85
- staged: args.includes('--staged')
85
+ staged: args.includes('--staged'),
86
+ // Regex search mode (default: ON; --no-regex to force plain text)
87
+ regex: args.includes('--no-regex') ? false : undefined,
88
+ // Stats: per-function line counts
89
+ functions: args.includes('--functions')
86
90
  };
87
91
 
88
92
  // Handle --file flag with space
@@ -101,7 +105,8 @@ const knownFlags = new Set([
101
105
  '--file', '--context', '--exclude', '--not', '--in',
102
106
  '--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
103
107
  '--default', '--top', '--no-follow-symlinks',
104
- '--base', '--staged'
108
+ '--base', '--staged',
109
+ '--regex', '--no-regex', '--functions'
105
110
  ]);
106
111
 
107
112
  // Handle help flag
@@ -905,7 +910,7 @@ function runProjectCommand(rootDir, command, arg) {
905
910
  if (arg.includes(',')) {
906
911
  const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
907
912
  for (let i = 0; i < fnNames.length; i++) {
908
- if (i > 0) console.log('');
913
+ if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
909
914
  extractFunctionFromProject(index, fnNames[i]);
910
915
  }
911
916
  } else {
@@ -1016,7 +1021,7 @@ function runProjectCommand(rootDir, command, arg) {
1016
1021
  case 'search': {
1017
1022
  requireArg(arg, 'Usage: ucn . search <term>');
1018
1023
  const searchExclude = flags.includeTests ? flags.exclude : addTestExclusions(flags.exclude);
1019
- const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context, caseSensitive: flags.caseSensitive, exclude: searchExclude, in: flags.in });
1024
+ const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context, caseSensitive: flags.caseSensitive, exclude: searchExclude, in: flags.in, regex: flags.regex });
1020
1025
  printOutput(searchResults,
1021
1026
  r => output.formatSearchJson(r, arg),
1022
1027
  r => output.formatSearch(r, arg)
@@ -1040,8 +1045,11 @@ function runProjectCommand(rootDir, command, arg) {
1040
1045
  }
1041
1046
 
1042
1047
  case 'stats': {
1043
- const stats = index.getStats();
1044
- printOutput(stats, output.formatStatsJson, output.formatStats);
1048
+ const stats = index.getStats({ functions: flags.functions });
1049
+ printOutput(stats,
1050
+ output.formatStatsJson,
1051
+ r => output.formatStats(r, { top: flags.top })
1052
+ );
1045
1053
  break;
1046
1054
  }
1047
1055
 
@@ -1506,7 +1514,13 @@ function findInGlobFiles(files, name) {
1506
1514
 
1507
1515
  function searchGlobFiles(files, term) {
1508
1516
  const results = [];
1509
- const regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1517
+ const useRegex = flags.regex !== false; // Default: regex ON
1518
+ let regex;
1519
+ if (useRegex) {
1520
+ try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
1521
+ } else {
1522
+ regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1523
+ }
1510
1524
 
1511
1525
  for (const file of files) {
1512
1526
  try {
@@ -1557,7 +1571,13 @@ function searchGlobFiles(files, term) {
1557
1571
  // ============================================================================
1558
1572
 
1559
1573
  function searchFile(filePath, lines, term) {
1560
- const regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1574
+ const useRegex = flags.regex !== false;
1575
+ let regex;
1576
+ if (useRegex) {
1577
+ try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
1578
+ } else {
1579
+ regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1580
+ }
1561
1581
  const matches = [];
1562
1582
 
1563
1583
  lines.forEach((line, idx) => {
@@ -1686,7 +1706,7 @@ FIND CODE
1686
1706
  find <name> Find symbol definitions (supports glob: find "handle*")
1687
1707
  usages <name> All usages grouped: definitions, calls, imports, references
1688
1708
  toc Table of contents (compact; --detailed lists all symbols)
1689
- search <term> Text search (--context=N, --exclude=, --in=)
1709
+ search <term> Text search (regex default, --context=N, --exclude=, --in=)
1690
1710
  tests <name> Find test files for a function
1691
1711
 
1692
1712
  ═══════════════════════════════════════════════════════════════════════════════
@@ -1719,7 +1739,7 @@ OTHER
1719
1739
  ═══════════════════════════════════════════════════════════════════════════════
1720
1740
  api Show exported/public symbols
1721
1741
  typedef <name> Find type definitions
1722
- stats Project statistics
1742
+ stats Project statistics (--functions for per-function line counts)
1723
1743
  stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
1724
1744
  example <name> Best usage example with context
1725
1745
 
@@ -1739,6 +1759,8 @@ Common Flags:
1739
1759
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1740
1760
  --include-uncertain Include ambiguous/uncertain matches
1741
1761
  --include-exported Include exported symbols in deadcode
1762
+ --no-regex Force plain text search (regex is default)
1763
+ --functions Show per-function line counts (stats command)
1742
1764
  --include-decorated Include decorated/annotated symbols in deadcode
1743
1765
  --exact Exact name match only (find)
1744
1766
  --calls-only Only show call/test-case matches (tests)
@@ -1898,6 +1920,8 @@ function parseInteractiveFlags(tokens) {
1898
1920
  base: tokens.find(a => a.startsWith('--base='))?.split('=')[1] || null,
1899
1921
  staged: tokens.includes('--staged'),
1900
1922
  maxLines: parseInt(tokens.find(a => a.startsWith('--max-lines='))?.split('=')[1] || '0') || null,
1923
+ regex: tokens.includes('--no-regex') ? false : undefined,
1924
+ functions: tokens.includes('--functions'),
1901
1925
  };
1902
1926
  }
1903
1927
 
@@ -2011,7 +2035,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2011
2035
  if (arg.includes(',')) {
2012
2036
  const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
2013
2037
  for (let i = 0; i < fnNames.length; i++) {
2014
- if (i > 0) console.log('');
2038
+ if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
2015
2039
  extractFunctionFromProject(index, fnNames[i], iflags);
2016
2040
  }
2017
2041
  } else {
@@ -2104,7 +2128,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2104
2128
  console.log('Usage: search <term>');
2105
2129
  return;
2106
2130
  }
2107
- const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context, exclude: iflags.exclude, in: iflags.in });
2131
+ const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context, exclude: iflags.exclude, in: iflags.in, regex: iflags.regex });
2108
2132
  console.log(output.formatSearch(results, arg));
2109
2133
  break;
2110
2134
  }
@@ -2136,8 +2160,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2136
2160
  }
2137
2161
 
2138
2162
  case 'stats': {
2139
- const stats = index.getStats();
2140
- console.log(output.formatStats(stats));
2163
+ const stats = index.getStats({ functions: iflags.functions });
2164
+ console.log(output.formatStats(stats, { top: iflags.top }));
2141
2165
  break;
2142
2166
  }
2143
2167
 
package/core/output.js CHANGED
@@ -1925,7 +1925,16 @@ function formatGraph(graph, options = {}) {
1925
1925
  */
1926
1926
  function formatSearch(results, term) {
1927
1927
  const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
1928
- if (totalMatches === 0) return `No matches found for "${term}"`;
1928
+ if (totalMatches === 0) {
1929
+ const meta = results.meta;
1930
+ if (meta) {
1931
+ const scope = meta.filesSkipped > 0
1932
+ ? `Searched ${meta.filesScanned} of ${meta.totalFiles} files (${meta.filesSkipped} excluded by filters).`
1933
+ : `Searched ${meta.filesScanned} files.`;
1934
+ return `No matches found for "${term}". ${scope}`;
1935
+ }
1936
+ return `No matches found for "${term}"`;
1937
+ }
1929
1938
 
1930
1939
  const lines = [];
1931
1940
  lines.push(`Found ${totalMatches} matches for "${term}" in ${results.length} files:`);
@@ -1969,7 +1978,7 @@ function formatFileExports(exports, filePath) {
1969
1978
  /**
1970
1979
  * Format stats command output
1971
1980
  */
1972
- function formatStats(stats) {
1981
+ function formatStats(stats, options = {}) {
1973
1982
  const lines = [];
1974
1983
  lines.push('PROJECT STATISTICS');
1975
1984
  lines.push('═'.repeat(60));
@@ -1988,6 +1997,19 @@ function formatStats(stats) {
1988
1997
  lines.push(` ${type}: ${count}`);
1989
1998
  }
1990
1999
 
2000
+ if (stats.functions) {
2001
+ const top = options.top || 30;
2002
+ const shown = stats.functions.slice(0, top);
2003
+ lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
2004
+ for (const fn of shown) {
2005
+ const loc = `${fn.file}:${fn.startLine}`;
2006
+ lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
2007
+ }
2008
+ if (stats.functions.length > top) {
2009
+ lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
2010
+ }
2011
+ }
2012
+
1991
2013
  return lines.join('\n');
1992
2014
  }
1993
2015
 
package/core/project.js CHANGED
@@ -4439,17 +4439,31 @@ class ProjectIndex {
4439
4439
  this._beginOp();
4440
4440
  try {
4441
4441
  const results = [];
4442
- // Escape the term to handle special regex characters
4442
+ let filesScanned = 0;
4443
+ let filesSkipped = 0;
4443
4444
  const regexFlags = options.caseSensitive ? 'g' : 'gi';
4444
- const regex = new RegExp(escapeRegExp(term), regexFlags);
4445
+ const useRegex = options.regex !== false; // Default: regex ON
4446
+ let regex;
4447
+ if (useRegex) {
4448
+ try {
4449
+ regex = new RegExp(term, regexFlags);
4450
+ } catch (e) {
4451
+ // Invalid regex — fall back to plain text
4452
+ regex = new RegExp(escapeRegExp(term), regexFlags);
4453
+ }
4454
+ } else {
4455
+ regex = new RegExp(escapeRegExp(term), regexFlags);
4456
+ }
4445
4457
 
4446
4458
  for (const [filePath, fileEntry] of this.files) {
4447
4459
  // Apply exclude/in filters
4448
4460
  if ((options.exclude && options.exclude.length > 0) || options.in) {
4449
4461
  if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) {
4462
+ filesSkipped++;
4450
4463
  continue;
4451
4464
  }
4452
4465
  }
4466
+ filesScanned++;
4453
4467
  try {
4454
4468
  const content = this._readFile(filePath);
4455
4469
  const lines = content.split('\n');
@@ -4462,7 +4476,7 @@ class ProjectIndex {
4462
4476
  try {
4463
4477
  const parser = getParser(language);
4464
4478
  const { findMatchesWithASTFilter } = require('../languages/utils');
4465
- const astMatches = findMatchesWithASTFilter(content, term, parser, { codeOnly: true });
4479
+ const astMatches = findMatchesWithASTFilter(content, term, parser, { codeOnly: true, regex: useRegex });
4466
4480
 
4467
4481
  for (const m of astMatches) {
4468
4482
  const match = {
@@ -4542,6 +4556,7 @@ class ProjectIndex {
4542
4556
  }
4543
4557
  }
4544
4558
 
4559
+ results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size };
4545
4560
  return results;
4546
4561
  } finally { this._endOp(); }
4547
4562
  }
@@ -4553,7 +4568,7 @@ class ProjectIndex {
4553
4568
  /**
4554
4569
  * Get project statistics
4555
4570
  */
4556
- getStats() {
4571
+ getStats(options = {}) {
4557
4572
  // Count total symbols (not just unique names)
4558
4573
  let totalSymbols = 0;
4559
4574
  for (const [name, symbols] of this.symbols) {
@@ -4588,6 +4603,27 @@ class ProjectIndex {
4588
4603
  }
4589
4604
  }
4590
4605
 
4606
+ // Per-function line counts for complexity audits
4607
+ if (options.functions) {
4608
+ const functions = [];
4609
+ for (const [name, symbols] of this.symbols) {
4610
+ for (const sym of symbols) {
4611
+ if (sym.type === 'function' || sym.params !== undefined) {
4612
+ const lineCount = sym.endLine - sym.startLine + 1;
4613
+ const relativePath = sym.relativePath || (sym.file ? path.relative(this.root, sym.file) : '');
4614
+ functions.push({
4615
+ name: sym.className ? `${sym.className}.${sym.name}` : sym.name,
4616
+ file: relativePath,
4617
+ startLine: sym.startLine,
4618
+ lines: lineCount
4619
+ });
4620
+ }
4621
+ }
4622
+ }
4623
+ functions.sort((a, b) => b.lines - a.lines);
4624
+ stats.functions = functions;
4625
+ }
4626
+
4591
4627
  return stats;
4592
4628
  }
4593
4629
 
@@ -417,9 +417,13 @@ function findMatchesWithASTFilter(content, term, parser, options = {}) {
417
417
  const lines = content.split('\n');
418
418
  const matches = [];
419
419
 
420
- // Escape special regex characters and create pattern
421
- const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
422
- const regex = new RegExp(escapedTerm, 'gi');
420
+ // Default: regex mode ON. Use raw pattern unless regex=false.
421
+ let regex;
422
+ if (options.regex !== false) {
423
+ try { regex = new RegExp(term, 'gi'); } catch (e) { regex = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); }
424
+ } else {
425
+ regex = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
426
+ }
423
427
 
424
428
  lines.forEach((line, idx) => {
425
429
  const lineNum = idx + 1;
package/mcp/server.js CHANGED
@@ -196,7 +196,7 @@ FINDING CODE:
196
196
  - find <name>: Locate definitions ranked by usage count. Supports glob patterns (e.g. find "handle*" or "_update*"). Use when you know the name but not the file.
197
197
  - usages <name>: See every usage organized by type: definitions, calls, imports, references. Complete picture of how something is used. Use code_only=true to skip comments/strings.
198
198
  - toc: Get a quick overview of a project you haven't seen before — file counts, line counts, function/class counts, entry points. Use detailed=true for full symbol listing.
199
- - search <term>: Plain text search (like grep, respects .gitignore). Supports context=N for surrounding lines, exclude/in for file filtering. Case-insensitive by default; set case_sensitive=true for exact case.
199
+ - search <term>: Text search (like grep, respects .gitignore). Supports regex by default (e.g. "\\d+" or "foo|bar"). Supports context=N for surrounding lines, exclude/in for file filtering. Case-insensitive by default; set case_sensitive=true for exact case. Invalid regex auto-falls back to plain text.
200
200
  - tests <name>: Find test files covering a function, test case names, and how it's called in tests. Use before modifying or to find test patterns to follow.
201
201
  - deadcode: Find dead code: functions/classes with zero callers. Use during cleanup to identify safely deletable code. Excludes exported, decorated, and test symbols by default — use include_exported/include_decorated/include_tests to expand.
202
202
 
@@ -221,7 +221,7 @@ OTHER:
221
221
  - typedef <name>: Find type definitions matching a name: interfaces, enums, structs, traits, type aliases. See field shapes, required methods, or enum values.
222
222
  - stacktrace: Parse a stack trace, show source context per frame. Requires stack param. Handles JS, Python, Go, Rust, Java formats.
223
223
  - api: Public API surface of project or file: all exported/public symbols with signatures. Use to understand what a library exposes. Pass file to scope to one file. Python needs __all__; use toc instead.
224
- - stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type.`;
224
+ - stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit).`;
225
225
 
226
226
  server.registerTool(
227
227
  'ucn',
@@ -256,7 +256,9 @@ server.registerTool(
256
256
  calls_only: z.boolean().optional().describe('Only direct calls and test-case matches (tests command)'),
257
257
  max_lines: z.number().optional().describe('Max source lines for class (large classes show summary by default)'),
258
258
  direction: z.enum(['imports', 'importers', 'both']).optional().describe('Graph direction: imports (what this file uses), importers (who uses this file), both (default: both)'),
259
- term: z.string().optional().describe('Search term (plain text, not regex)'),
259
+ term: z.string().optional().describe('Search term (regex by default; set regex=false to force plain text)'),
260
+ regex: z.boolean().optional().describe('Treat search term as a regex pattern (default: true). Set false to force plain text escaping.'),
261
+ functions: z.boolean().optional().describe('Include per-function line counts in stats output, sorted by size (complexity audit)'),
260
262
  add_param: z.string().optional().describe('Parameter name to add (plan command)'),
261
263
  remove_param: z.string().optional().describe('Parameter name to remove (plan command)'),
262
264
  rename_to: z.string().optional().describe('New function name (plan command)'),
@@ -276,7 +278,7 @@ server.registerTool(
276
278
  include_exported, include_decorated, calls_only, max_lines,
277
279
  direction, term, add_param, remove_param, rename_to,
278
280
  default_value, stack, item, range, base, staged,
279
- case_sensitive } = args;
281
+ case_sensitive, regex, functions } = args;
280
282
 
281
283
  try {
282
284
  switch (command) {
@@ -431,7 +433,8 @@ server.registerTool(
431
433
  context: ctxLines || 0,
432
434
  caseSensitive: case_sensitive || false,
433
435
  exclude: searchExclude,
434
- in: inPath || undefined
436
+ in: inPath || undefined,
437
+ regex: regex
435
438
  });
436
439
  return toolResult(output.formatSearch(result, term));
437
440
  }
@@ -495,7 +498,8 @@ server.registerTool(
495
498
  parts.push(note + output.formatFn(match, fnCode));
496
499
  }
497
500
 
498
- return toolResult(parts.join('\n\n'));
501
+ const separator = fnNames.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '\n\n';
502
+ return toolResult(parts.join(separator));
499
503
  }
500
504
 
501
505
  case 'class': {
@@ -805,8 +809,8 @@ server.registerTool(
805
809
 
806
810
  case 'stats': {
807
811
  const index = getIndex(project_dir);
808
- const stats = index.getStats();
809
- return toolResult(output.formatStats(stats));
812
+ const stats = index.getStats({ functions: functions || false });
813
+ return toolResult(output.formatStats(stats, { top: top || 30 }));
810
814
  }
811
815
 
812
816
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.11",
3
+ "version": "3.7.13",
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",