ucn 3.7.10 → 3.7.12

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.
@@ -63,10 +63,11 @@ Shows the entire pipeline — what `generate_report` calls, what those functions
63
63
 
64
64
  ### 4. `fn` / `class` — Extract without reading the whole file
65
65
 
66
- Pull one function or class out of a large file. Saves hundreds of lines of context window.
66
+ Pull one or more functions out of a large file. Supports comma-separated names for bulk extraction.
67
67
 
68
68
  ```bash
69
69
  ucn fn handle_request --file=api # --file disambiguates when name exists in multiple files
70
+ ucn fn parse,format,validate # Extract multiple functions in one call
70
71
  ucn class MarketDataFetcher
71
72
  ```
72
73
 
@@ -89,6 +90,10 @@ ucn deadcode --exclude=test # Skip test files (most useful)
89
90
  | Understanding a file's role in the project | `ucn imports <file>` | What it depends on |
90
91
  | Understanding who depends on a file | `ucn exporters <file>` | Which files import it |
91
92
  | Quick project overview | `ucn toc` | Every file with function/class counts and line counts |
93
+ | Find by glob pattern | `ucn find "handle*"` | Locate definitions matching a glob (supports * and ?) |
94
+ | Text search with context | `ucn search term --context=3` | Like grep -C 3, shows surrounding lines |
95
+ | Regex search | `ucn search '\d+' --regex` | Search with regex patterns (alternation, character classes, etc.) |
96
+ | Text search filtered | `ucn search term --exclude=test` | Search only in matching files |
92
97
  | Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
93
98
  | Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
94
99
  | Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
@@ -122,6 +127,8 @@ ucn [target] <command> [name] [--flags]
122
127
  | `--staged` | Analyze staged changes (diff-impact) |
123
128
  | `--no-cache` | Force re-index after editing files |
124
129
  | `--context=N` | Lines of surrounding context in `usages`/`search` output |
130
+ | `--regex` | Use search term as a regex pattern |
131
+ | `--functions` | Show per-function line counts in `stats` (complexity audit) |
125
132
 
126
133
  ## Workflow Integration
127
134
 
@@ -0,0 +1 @@
1
+ ghu_Jo1ldIUK4d6zLMSctdSV2Jx8UkoXa50x6epI
@@ -0,0 +1 @@
1
+ {"token":"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtY3AtcmVnaXN0cnkiLCJleHAiOjE3NzE5NjQ2MjMsIm5iZiI6MTc3MTk2NDMyMywiaWF0IjoxNzcxOTY0MzIzLCJhdXRoX21ldGhvZCI6ImdpdGh1Yi1hdCIsImF1dGhfbWV0aG9kX3N1YiI6Im1sZW9jYSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJwdWJsaXNoIiwicmVzb3VyY2UiOiJpby5naXRodWIubWxlb2NhLyoifV19.weq7KLw5cAAB7qZpxtU45meX0nLUU1vTiHlzFB5Ecf9OTFtKM63-JSwYBXrr410pdt-7CgW6pV6aGruoIpiIBw","expires_at":1771964623}
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
87
+ regex: args.includes('--regex'),
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', '--functions'
105
110
  ]);
106
111
 
107
112
  // Handle help flag
@@ -901,7 +906,16 @@ function runProjectCommand(rootDir, command, arg) {
901
906
 
902
907
  case 'fn': {
903
908
  requireArg(arg, 'Usage: ucn . fn <name>');
904
- extractFunctionFromProject(index, arg);
909
+ // Support comma-separated names for bulk extraction
910
+ if (arg.includes(',')) {
911
+ const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
912
+ for (let i = 0; i < fnNames.length; i++) {
913
+ if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
914
+ extractFunctionFromProject(index, fnNames[i]);
915
+ }
916
+ } else {
917
+ extractFunctionFromProject(index, arg);
918
+ }
905
919
  break;
906
920
  }
907
921
 
@@ -1006,7 +1020,8 @@ function runProjectCommand(rootDir, command, arg) {
1006
1020
 
1007
1021
  case 'search': {
1008
1022
  requireArg(arg, 'Usage: ucn . search <term>');
1009
- const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context, caseSensitive: flags.caseSensitive });
1023
+ const searchExclude = flags.includeTests ? flags.exclude : addTestExclusions(flags.exclude);
1024
+ const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context, caseSensitive: flags.caseSensitive, exclude: searchExclude, in: flags.in, regex: flags.regex });
1010
1025
  printOutput(searchResults,
1011
1026
  r => output.formatSearchJson(r, arg),
1012
1027
  r => output.formatSearch(r, arg)
@@ -1030,8 +1045,11 @@ function runProjectCommand(rootDir, command, arg) {
1030
1045
  }
1031
1046
 
1032
1047
  case 'stats': {
1033
- const stats = index.getStats();
1034
- 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
+ );
1035
1053
  break;
1036
1054
  }
1037
1055
 
@@ -1496,7 +1514,7 @@ function findInGlobFiles(files, name) {
1496
1514
 
1497
1515
  function searchGlobFiles(files, term) {
1498
1516
  const results = [];
1499
- const regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1517
+ const regex = flags.regex ? new RegExp(term, flags.caseSensitive ? '' : 'i') : new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1500
1518
 
1501
1519
  for (const file of files) {
1502
1520
  try {
@@ -1547,7 +1565,7 @@ function searchGlobFiles(files, term) {
1547
1565
  // ============================================================================
1548
1566
 
1549
1567
  function searchFile(filePath, lines, term) {
1550
- const regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1568
+ const regex = flags.regex ? new RegExp(term, flags.caseSensitive ? '' : 'i') : new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1551
1569
  const matches = [];
1552
1570
 
1553
1571
  lines.forEach((line, idx) => {
@@ -1673,16 +1691,16 @@ UNDERSTAND CODE (UCN's strength - semantic analysis)
1673
1691
  ═══════════════════════════════════════════════════════════════════════════════
1674
1692
  FIND CODE
1675
1693
  ═══════════════════════════════════════════════════════════════════════════════
1676
- find <name> Find symbol definitions (top 5 by usage count)
1694
+ find <name> Find symbol definitions (supports glob: find "handle*")
1677
1695
  usages <name> All usages grouped: definitions, calls, imports, references
1678
1696
  toc Table of contents (compact; --detailed lists all symbols)
1679
- search <term> Text search (for simple patterns, consider grep instead)
1697
+ search <term> Text search (--context=N, --exclude=, --in=, --regex)
1680
1698
  tests <name> Find test files for a function
1681
1699
 
1682
1700
  ═══════════════════════════════════════════════════════════════════════════════
1683
1701
  EXTRACT CODE
1684
1702
  ═══════════════════════════════════════════════════════════════════════════════
1685
- fn <name> Extract function (--file to disambiguate)
1703
+ fn <name>[,n2,...] Extract function(s) (comma-separated for bulk, --file)
1686
1704
  class <name> Extract class
1687
1705
  lines <range> Extract line range (e.g., lines 50-100)
1688
1706
  expand <N> Show code for item N from context output
@@ -1709,7 +1727,7 @@ OTHER
1709
1727
  ═══════════════════════════════════════════════════════════════════════════════
1710
1728
  api Show exported/public symbols
1711
1729
  typedef <name> Find type definitions
1712
- stats Project statistics
1730
+ stats Project statistics (--functions for per-function line counts)
1713
1731
  stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
1714
1732
  example <name> Best usage example with context
1715
1733
 
@@ -1729,6 +1747,8 @@ Common Flags:
1729
1747
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1730
1748
  --include-uncertain Include ambiguous/uncertain matches
1731
1749
  --include-exported Include exported symbols in deadcode
1750
+ --regex Use search term as a regex pattern
1751
+ --functions Show per-function line counts (stats command)
1732
1752
  --include-decorated Include decorated/annotated symbols in deadcode
1733
1753
  --exact Exact name match only (find)
1734
1754
  --calls-only Only show call/test-case matches (tests)
@@ -1789,7 +1809,7 @@ function runInteractive(rootDir) {
1789
1809
  console.log(`
1790
1810
  Commands:
1791
1811
  toc Project overview (--detailed)
1792
- find <name> Find symbol (--exact, --file=)
1812
+ find <name> Find symbol (--exact, glob: "handle*")
1793
1813
  about <name> Everything about a symbol
1794
1814
  usages <name> All usages grouped by type
1795
1815
  context <name> Callers + callees
@@ -1799,7 +1819,7 @@ Commands:
1799
1819
  trace <name> Call tree (--depth=N)
1800
1820
  example <name> Best usage example
1801
1821
  related <name> Sibling functions
1802
- fn <name> Extract function code (--file=)
1822
+ fn <name>[,n2,...] Extract function(s) (--file=)
1803
1823
  class <name> Extract class code (--file=)
1804
1824
  lines <range> Extract lines (--file= required)
1805
1825
  graph <file> File dependency tree (--direction=, --depth=)
@@ -1807,7 +1827,7 @@ Commands:
1807
1827
  imports <file> What file imports
1808
1828
  exporters <file> Who imports file
1809
1829
  tests <name> Find tests (--calls-only)
1810
- search <term> Text search (--code-only, --case-sensitive)
1830
+ search <term> Text search (--context=N, --exclude=, --in=)
1811
1831
  typedef <name> Find type definitions
1812
1832
  deadcode Find unused functions/classes
1813
1833
  verify <name> Check call sites match signature
@@ -1888,6 +1908,8 @@ function parseInteractiveFlags(tokens) {
1888
1908
  base: tokens.find(a => a.startsWith('--base='))?.split('=')[1] || null,
1889
1909
  staged: tokens.includes('--staged'),
1890
1910
  maxLines: parseInt(tokens.find(a => a.startsWith('--max-lines='))?.split('=')[1] || '0') || null,
1911
+ regex: tokens.includes('--regex'),
1912
+ functions: tokens.includes('--functions'),
1891
1913
  };
1892
1914
  }
1893
1915
 
@@ -1994,10 +2016,19 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
1994
2016
 
1995
2017
  case 'fn': {
1996
2018
  if (!arg) {
1997
- console.log('Usage: fn <name> [--file=<pattern>]');
2019
+ console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]');
1998
2020
  return;
1999
2021
  }
2000
- extractFunctionFromProject(index, arg, iflags);
2022
+ // Support comma-separated names for bulk extraction
2023
+ if (arg.includes(',')) {
2024
+ const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
2025
+ for (let i = 0; i < fnNames.length; i++) {
2026
+ if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
2027
+ extractFunctionFromProject(index, fnNames[i], iflags);
2028
+ }
2029
+ } else {
2030
+ extractFunctionFromProject(index, arg, iflags);
2031
+ }
2001
2032
  break;
2002
2033
  }
2003
2034
 
@@ -2085,7 +2116,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2085
2116
  console.log('Usage: search <term>');
2086
2117
  return;
2087
2118
  }
2088
- const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context });
2119
+ const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context, exclude: iflags.exclude, in: iflags.in, regex: iflags.regex });
2089
2120
  console.log(output.formatSearch(results, arg));
2090
2121
  break;
2091
2122
  }
@@ -2117,8 +2148,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2117
2148
  }
2118
2149
 
2119
2150
  case 'stats': {
2120
- const stats = index.getStats();
2121
- console.log(output.formatStats(stats));
2151
+ const stats = index.getStats({ functions: iflags.functions });
2152
+ console.log(output.formatStats(stats, { top: iflags.top }));
2122
2153
  break;
2123
2154
  }
2124
2155
 
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
@@ -687,6 +687,22 @@ class ProjectIndex {
687
687
  }
688
688
 
689
689
  find(name, options = {}) {
690
+ // Glob pattern matching (e.g., _update*, handle*Request, get?ata)
691
+ const isGlob = name.includes('*') || name.includes('?');
692
+ if (isGlob && !options.exact) {
693
+ const globRegex = new RegExp('^' + name.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
694
+ const matches = [];
695
+ for (const [symName, symbols] of this.symbols) {
696
+ if (globRegex.test(symName)) {
697
+ for (const sym of symbols) {
698
+ matches.push({ ...sym, _fuzzyScore: 800 });
699
+ }
700
+ }
701
+ }
702
+ matches.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
703
+ return this._applyFindFilters(matches, options);
704
+ }
705
+
690
706
  const matches = this.symbols.get(name) || [];
691
707
 
692
708
  if (matches.length === 0 && !options.exact) {
@@ -705,7 +721,13 @@ class ProjectIndex {
705
721
  matches.push(...candidates);
706
722
  }
707
723
 
708
- // Apply filters
724
+ return this._applyFindFilters(matches, options);
725
+ }
726
+
727
+ /**
728
+ * Apply file/exclude/in filters and usage counts to find results
729
+ */
730
+ _applyFindFilters(matches, options) {
709
731
  let filtered = matches;
710
732
 
711
733
  // Filter by file pattern
@@ -4411,17 +4433,26 @@ class ProjectIndex {
4411
4433
  /**
4412
4434
  * Search for text across the project
4413
4435
  * @param {string} term - Search term
4414
- * @param {object} options - { codeOnly, context }
4436
+ * @param {object} options - { codeOnly, context, caseSensitive, exclude, in }
4415
4437
  */
4416
4438
  search(term, options = {}) {
4417
4439
  this._beginOp();
4418
4440
  try {
4419
4441
  const results = [];
4420
- // Escape the term to handle special regex characters
4442
+ let filesScanned = 0;
4443
+ let filesSkipped = 0;
4421
4444
  const regexFlags = options.caseSensitive ? 'g' : 'gi';
4422
- const regex = new RegExp(escapeRegExp(term), regexFlags);
4445
+ const regex = options.regex ? new RegExp(term, regexFlags) : new RegExp(escapeRegExp(term), regexFlags);
4423
4446
 
4424
4447
  for (const [filePath, fileEntry] of this.files) {
4448
+ // Apply exclude/in filters
4449
+ if ((options.exclude && options.exclude.length > 0) || options.in) {
4450
+ if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) {
4451
+ filesSkipped++;
4452
+ continue;
4453
+ }
4454
+ }
4455
+ filesScanned++;
4425
4456
  try {
4426
4457
  const content = this._readFile(filePath);
4427
4458
  const lines = content.split('\n');
@@ -4434,7 +4465,7 @@ class ProjectIndex {
4434
4465
  try {
4435
4466
  const parser = getParser(language);
4436
4467
  const { findMatchesWithASTFilter } = require('../languages/utils');
4437
- const astMatches = findMatchesWithASTFilter(content, term, parser, { codeOnly: true });
4468
+ const astMatches = findMatchesWithASTFilter(content, term, parser, { codeOnly: true, regex: options.regex });
4438
4469
 
4439
4470
  for (const m of astMatches) {
4440
4471
  const match = {
@@ -4514,6 +4545,7 @@ class ProjectIndex {
4514
4545
  }
4515
4546
  }
4516
4547
 
4548
+ results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size };
4517
4549
  return results;
4518
4550
  } finally { this._endOp(); }
4519
4551
  }
@@ -4525,7 +4557,7 @@ class ProjectIndex {
4525
4557
  /**
4526
4558
  * Get project statistics
4527
4559
  */
4528
- getStats() {
4560
+ getStats(options = {}) {
4529
4561
  // Count total symbols (not just unique names)
4530
4562
  let totalSymbols = 0;
4531
4563
  for (const [name, symbols] of this.symbols) {
@@ -4560,6 +4592,27 @@ class ProjectIndex {
4560
4592
  }
4561
4593
  }
4562
4594
 
4595
+ // Per-function line counts for complexity audits
4596
+ if (options.functions) {
4597
+ const functions = [];
4598
+ for (const [name, symbols] of this.symbols) {
4599
+ for (const sym of symbols) {
4600
+ if (sym.type === 'function' || sym.params !== undefined) {
4601
+ const lineCount = sym.endLine - sym.startLine + 1;
4602
+ const relativePath = sym.relativePath || (sym.file ? path.relative(this.root, sym.file) : '');
4603
+ functions.push({
4604
+ name: sym.className ? `${sym.className}.${sym.name}` : sym.name,
4605
+ file: relativePath,
4606
+ startLine: sym.startLine,
4607
+ lines: lineCount
4608
+ });
4609
+ }
4610
+ }
4611
+ }
4612
+ functions.sort((a, b) => b.lines - a.lines);
4613
+ stats.functions = functions;
4614
+ }
4615
+
4563
4616
  return stats;
4564
4617
  }
4565
4618
 
@@ -417,9 +417,8 @@ 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
+ // Create search pattern — use raw regex when regex mode is enabled
421
+ const regex = options.regex ? new RegExp(term, 'gi') : new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
423
422
 
424
423
  lines.forEach((line, idx) => {
425
424
  const lineNum = idx + 1;
package/mcp/server.js CHANGED
@@ -193,15 +193,15 @@ UNDERSTANDING CODE:
193
193
  - related <name>: Sibling functions: same file, similar names, or shared callers/callees. Find companions to update together (e.g., serialize when you're changing deserialize). Name-based, not semantic.
194
194
 
195
195
  FINDING CODE:
196
- - find <name>: Locate definitions ranked by usage count. Use when you know the name but not the file.
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). For TODOs, error messages, config keys. Search is case-insensitive by default; set case_sensitive=true for exact case.
199
+ - search <term>: 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. Set regex=true to use the term as a regex pattern (e.g. "\\d+" or "foo|bar").
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
 
203
203
  EXTRACTING CODE (use instead of reading entire files):
204
- - fn <name>: Extract one function's source. Use file to disambiguate (e.g. file="parser" for parser.js).
204
+ - fn <name>: Extract one or more functions. Comma-separated for bulk extraction (e.g. "parse,format,validate"). Use file to disambiguate.
205
205
  - class <name>: Extract a class/struct/interface with all its methods. Handles all supported types: JS/TS, Python, Go, Rust, Java. Large classes (>200 lines) show summary; use max_lines for truncated source.
206
206
  - lines: Extract specific lines (e.g. range="10-20" or just "15"). Requires file and range. Use when you know the exact line range you need.
207
207
  - expand <item>: Drill into a numbered item from the last context result (requires running context first in the same session). Context returns numbered callers/callees — use this to see their full source code.
@@ -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',
@@ -237,7 +237,7 @@ server.registerTool(
237
237
  'api', 'stats', 'diff_impact', 'stacktrace'
238
238
  ]),
239
239
  project_dir: z.string().describe('Absolute or relative path to the project root directory'),
240
- name: z.string().optional().describe('Symbol name to analyze (function, class, method, etc.)'),
240
+ 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
241
  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")'),
242
242
  exclude: z.string().optional().describe('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
243
243
  include_tests: z.boolean().optional().describe('Include test files in results (excluded by default)'),
@@ -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 (plain text by default; set regex=true to use as regex pattern)'),
260
+ regex: z.boolean().optional().describe('Treat search term as a regex pattern (default: false, plain text)'),
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) {
@@ -425,10 +427,14 @@ server.registerTool(
425
427
  return toolError('Search term is required.');
426
428
  }
427
429
  const index = getIndex(project_dir);
430
+ const searchExclude = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
428
431
  const result = index.search(term, {
429
432
  codeOnly: code_only || false,
430
433
  context: ctxLines || 0,
431
- caseSensitive: case_sensitive || false
434
+ caseSensitive: case_sensitive || false,
435
+ exclude: searchExclude,
436
+ in: inPath || undefined,
437
+ regex: regex || false
432
438
  });
433
439
  return toolResult(output.formatSearch(result, term));
434
440
  }
@@ -465,26 +471,35 @@ server.registerTool(
465
471
  const err = requireName(name);
466
472
  if (err) return err;
467
473
  const index = getIndex(project_dir);
468
- const matches = index.find(name, { file }).filter(m => m.type === 'function' || m.params !== undefined);
469
474
 
470
- if (matches.length === 0) {
471
- return toolResult(`Function "${name}" not found.`);
472
- }
475
+ // Support comma-separated names for bulk extraction
476
+ const fnNames = name.includes(',') ? name.split(',').map(n => n.trim()).filter(Boolean) : [name];
477
+ const parts = [];
473
478
 
474
- const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
475
- // Validate file is within project root
476
- const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
477
- if (typeof fnPathCheck !== 'string') return fnPathCheck;
478
- const code = fs.readFileSync(match.file, 'utf-8');
479
- const codeLines = code.split('\n');
480
- const fnCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
479
+ for (const fnName of fnNames) {
480
+ const matches = index.find(fnName, { file }).filter(m => m.type === 'function' || m.params !== undefined);
481
481
 
482
- let note = '';
483
- if (matches.length > 1 && !file) {
484
- note = `Note: Found ${matches.length} definitions for "${name}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n\n`;
482
+ if (matches.length === 0) {
483
+ parts.push(`Function "${fnName}" not found.`);
484
+ continue;
485
+ }
486
+
487
+ const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
488
+ const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
489
+ if (typeof fnPathCheck !== 'string') return fnPathCheck;
490
+ const code = fs.readFileSync(match.file, 'utf-8');
491
+ const codeLines = code.split('\n');
492
+ const fnCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
493
+
494
+ let note = '';
495
+ if (matches.length > 1 && !file) {
496
+ note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n`;
497
+ }
498
+ parts.push(note + output.formatFn(match, fnCode));
485
499
  }
486
500
 
487
- return toolResult(note + output.formatFn(match, fnCode));
501
+ const separator = fnNames.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '\n\n';
502
+ return toolResult(parts.join(separator));
488
503
  }
489
504
 
490
505
  case 'class': {
@@ -794,8 +809,8 @@ server.registerTool(
794
809
 
795
810
  case 'stats': {
796
811
  const index = getIndex(project_dir);
797
- const stats = index.getStats();
798
- return toolResult(output.formatStats(stats));
812
+ const stats = index.getStats({ functions: functions || false });
813
+ return toolResult(output.formatStats(stats, { top: top || 30 }));
799
814
  }
800
815
 
801
816
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.10",
3
+ "version": "3.7.12",
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",