ucn 3.7.9 → 3.7.11

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.
Files changed (36) hide show
  1. package/.claude/skills/ucn/SKILL.md +5 -1
  2. package/.mcpregistry_github_token +1 -0
  3. package/.mcpregistry_registry_token +1 -0
  4. package/cli/index.js +30 -11
  5. package/core/project.js +30 -2
  6. package/mcp/server.js +31 -20
  7. package/package.json +2 -1
  8. package/test/accuracy.test.js +0 -1863
  9. package/test/fixtures/go/go.mod +0 -3
  10. package/test/fixtures/go/main.go +0 -257
  11. package/test/fixtures/go/service.go +0 -187
  12. package/test/fixtures/java/DataService.java +0 -279
  13. package/test/fixtures/java/Main.java +0 -287
  14. package/test/fixtures/java/Utils.java +0 -199
  15. package/test/fixtures/java/pom.xml +0 -6
  16. package/test/fixtures/javascript/main.js +0 -109
  17. package/test/fixtures/javascript/package.json +0 -1
  18. package/test/fixtures/javascript/service.js +0 -88
  19. package/test/fixtures/javascript/utils.js +0 -67
  20. package/test/fixtures/python/main.py +0 -198
  21. package/test/fixtures/python/pyproject.toml +0 -3
  22. package/test/fixtures/python/service.py +0 -166
  23. package/test/fixtures/python/utils.py +0 -118
  24. package/test/fixtures/rust/Cargo.toml +0 -3
  25. package/test/fixtures/rust/main.rs +0 -253
  26. package/test/fixtures/rust/service.rs +0 -210
  27. package/test/fixtures/rust/utils.rs +0 -154
  28. package/test/fixtures/typescript/main.ts +0 -154
  29. package/test/fixtures/typescript/package.json +0 -1
  30. package/test/fixtures/typescript/repository.ts +0 -149
  31. package/test/fixtures/typescript/types.ts +0 -114
  32. package/test/mcp-edge-cases.js +0 -634
  33. package/test/parser.test.js +0 -13634
  34. package/test/public-repos-bugs.json +0 -32
  35. package/test/public-repos-test.js +0 -477
  36. package/test/systematic-test.js +0 -619
@@ -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,9 @@ 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
+ | Text search filtered | `ucn search term --exclude=test` | Search only in matching files |
92
96
  | Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
93
97
  | Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
94
98
  | Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
@@ -0,0 +1 @@
1
+ ghu_gwrBjpCa3Gl8WuJxABQ8XOgRdbPfFf36TmIA
@@ -0,0 +1 @@
1
+ {"token":"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtY3AtcmVnaXN0cnkiLCJleHAiOjE3NzE3ODU4MTAsIm5iZiI6MTc3MTc4NTUxMCwiaWF0IjoxNzcxNzg1NTEwLCJhdXRoX21ldGhvZCI6ImdpdGh1Yi1hdCIsImF1dGhfbWV0aG9kX3N1YiI6Im1sZW9jYSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJwdWJsaXNoIiwicmVzb3VyY2UiOiJpby5naXRodWIubWxlb2NhLyoifV19.DW2gbu6Lnoc43CqRsxlhrIy3CQaMxAlLykOfYP3lY08d0FHVmXhg9gUt6qt2G6ihn-G99tA6oOf8tGvxUteeBQ","expires_at":1771785810}
package/cli/index.js CHANGED
@@ -901,7 +901,16 @@ function runProjectCommand(rootDir, command, arg) {
901
901
 
902
902
  case 'fn': {
903
903
  requireArg(arg, 'Usage: ucn . fn <name>');
904
- extractFunctionFromProject(index, arg);
904
+ // Support comma-separated names for bulk extraction
905
+ if (arg.includes(',')) {
906
+ const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
907
+ for (let i = 0; i < fnNames.length; i++) {
908
+ if (i > 0) console.log('');
909
+ extractFunctionFromProject(index, fnNames[i]);
910
+ }
911
+ } else {
912
+ extractFunctionFromProject(index, arg);
913
+ }
905
914
  break;
906
915
  }
907
916
 
@@ -1006,7 +1015,8 @@ function runProjectCommand(rootDir, command, arg) {
1006
1015
 
1007
1016
  case 'search': {
1008
1017
  requireArg(arg, 'Usage: ucn . search <term>');
1009
- const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context, caseSensitive: flags.caseSensitive });
1018
+ 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 });
1010
1020
  printOutput(searchResults,
1011
1021
  r => output.formatSearchJson(r, arg),
1012
1022
  r => output.formatSearch(r, arg)
@@ -1673,16 +1683,16 @@ UNDERSTAND CODE (UCN's strength - semantic analysis)
1673
1683
  ═══════════════════════════════════════════════════════════════════════════════
1674
1684
  FIND CODE
1675
1685
  ═══════════════════════════════════════════════════════════════════════════════
1676
- find <name> Find symbol definitions (top 5 by usage count)
1686
+ find <name> Find symbol definitions (supports glob: find "handle*")
1677
1687
  usages <name> All usages grouped: definitions, calls, imports, references
1678
1688
  toc Table of contents (compact; --detailed lists all symbols)
1679
- search <term> Text search (for simple patterns, consider grep instead)
1689
+ search <term> Text search (--context=N, --exclude=, --in=)
1680
1690
  tests <name> Find test files for a function
1681
1691
 
1682
1692
  ═══════════════════════════════════════════════════════════════════════════════
1683
1693
  EXTRACT CODE
1684
1694
  ═══════════════════════════════════════════════════════════════════════════════
1685
- fn <name> Extract function (--file to disambiguate)
1695
+ fn <name>[,n2,...] Extract function(s) (comma-separated for bulk, --file)
1686
1696
  class <name> Extract class
1687
1697
  lines <range> Extract line range (e.g., lines 50-100)
1688
1698
  expand <N> Show code for item N from context output
@@ -1789,7 +1799,7 @@ function runInteractive(rootDir) {
1789
1799
  console.log(`
1790
1800
  Commands:
1791
1801
  toc Project overview (--detailed)
1792
- find <name> Find symbol (--exact, --file=)
1802
+ find <name> Find symbol (--exact, glob: "handle*")
1793
1803
  about <name> Everything about a symbol
1794
1804
  usages <name> All usages grouped by type
1795
1805
  context <name> Callers + callees
@@ -1799,7 +1809,7 @@ Commands:
1799
1809
  trace <name> Call tree (--depth=N)
1800
1810
  example <name> Best usage example
1801
1811
  related <name> Sibling functions
1802
- fn <name> Extract function code (--file=)
1812
+ fn <name>[,n2,...] Extract function(s) (--file=)
1803
1813
  class <name> Extract class code (--file=)
1804
1814
  lines <range> Extract lines (--file= required)
1805
1815
  graph <file> File dependency tree (--direction=, --depth=)
@@ -1807,7 +1817,7 @@ Commands:
1807
1817
  imports <file> What file imports
1808
1818
  exporters <file> Who imports file
1809
1819
  tests <name> Find tests (--calls-only)
1810
- search <term> Text search (--code-only, --case-sensitive)
1820
+ search <term> Text search (--context=N, --exclude=, --in=)
1811
1821
  typedef <name> Find type definitions
1812
1822
  deadcode Find unused functions/classes
1813
1823
  verify <name> Check call sites match signature
@@ -1994,10 +2004,19 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
1994
2004
 
1995
2005
  case 'fn': {
1996
2006
  if (!arg) {
1997
- console.log('Usage: fn <name> [--file=<pattern>]');
2007
+ console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]');
1998
2008
  return;
1999
2009
  }
2000
- extractFunctionFromProject(index, arg, iflags);
2010
+ // Support comma-separated names for bulk extraction
2011
+ if (arg.includes(',')) {
2012
+ const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
2013
+ for (let i = 0; i < fnNames.length; i++) {
2014
+ if (i > 0) console.log('');
2015
+ extractFunctionFromProject(index, fnNames[i], iflags);
2016
+ }
2017
+ } else {
2018
+ extractFunctionFromProject(index, arg, iflags);
2019
+ }
2001
2020
  break;
2002
2021
  }
2003
2022
 
@@ -2085,7 +2104,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
2085
2104
  console.log('Usage: search <term>');
2086
2105
  return;
2087
2106
  }
2088
- const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context });
2107
+ const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context, exclude: iflags.exclude, in: iflags.in });
2089
2108
  console.log(output.formatSearch(results, arg));
2090
2109
  break;
2091
2110
  }
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,7 +4433,7 @@ 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();
@@ -4422,6 +4444,12 @@ class ProjectIndex {
4422
4444
  const regex = new RegExp(escapeRegExp(term), regexFlags);
4423
4445
 
4424
4446
  for (const [filePath, fileEntry] of this.files) {
4447
+ // Apply exclude/in filters
4448
+ if ((options.exclude && options.exclude.length > 0) || options.in) {
4449
+ if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) {
4450
+ continue;
4451
+ }
4452
+ }
4425
4453
  try {
4426
4454
  const content = this._readFile(filePath);
4427
4455
  const lines = content.split('\n');
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>: 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.
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.
@@ -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)'),
@@ -425,10 +425,13 @@ server.registerTool(
425
425
  return toolError('Search term is required.');
426
426
  }
427
427
  const index = getIndex(project_dir);
428
+ const searchExclude = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
428
429
  const result = index.search(term, {
429
430
  codeOnly: code_only || false,
430
431
  context: ctxLines || 0,
431
- caseSensitive: case_sensitive || false
432
+ caseSensitive: case_sensitive || false,
433
+ exclude: searchExclude,
434
+ in: inPath || undefined
432
435
  });
433
436
  return toolResult(output.formatSearch(result, term));
434
437
  }
@@ -465,26 +468,34 @@ server.registerTool(
465
468
  const err = requireName(name);
466
469
  if (err) return err;
467
470
  const index = getIndex(project_dir);
468
- const matches = index.find(name, { file }).filter(m => m.type === 'function' || m.params !== undefined);
469
471
 
470
- if (matches.length === 0) {
471
- return toolResult(`Function "${name}" not found.`);
472
- }
472
+ // Support comma-separated names for bulk extraction
473
+ const fnNames = name.includes(',') ? name.split(',').map(n => n.trim()).filter(Boolean) : [name];
474
+ const parts = [];
473
475
 
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');
476
+ for (const fnName of fnNames) {
477
+ const matches = index.find(fnName, { file }).filter(m => m.type === 'function' || m.params !== undefined);
481
478
 
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`;
479
+ if (matches.length === 0) {
480
+ parts.push(`Function "${fnName}" not found.`);
481
+ continue;
482
+ }
483
+
484
+ const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
485
+ const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
486
+ if (typeof fnPathCheck !== 'string') return fnPathCheck;
487
+ const code = fs.readFileSync(match.file, 'utf-8');
488
+ const codeLines = code.split('\n');
489
+ const fnCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
490
+
491
+ let note = '';
492
+ if (matches.length > 1 && !file) {
493
+ note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n`;
494
+ }
495
+ parts.push(note + output.formatFn(match, fnCode));
485
496
  }
486
497
 
487
- return toolResult(note + output.formatFn(match, fnCode));
498
+ return toolResult(parts.join('\n\n'));
488
499
  }
489
500
 
490
501
  case 'class': {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.9",
3
+ "version": "3.7.11",
4
+ "mcpName": "io.github.mleoca/ucn",
4
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.",
5
6
  "main": "index.js",
6
7
  "bin": {