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.
- package/.claude/skills/ucn/SKILL.md +8 -1
- package/.mcpregistry_github_token +1 -0
- package/.mcpregistry_registry_token +1 -0
- package/cli/index.js +51 -20
- package/core/output.js +24 -2
- package/core/project.js +59 -6
- package/languages/utils.js +2 -3
- package/mcp/server.js +40 -25
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 (
|
|
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 (
|
|
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>
|
|
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,
|
|
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>
|
|
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 (--
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/languages/utils.js
CHANGED
|
@@ -417,9 +417,8 @@ function findMatchesWithASTFilter(content, term, parser, options = {}) {
|
|
|
417
417
|
const lines = content.split('\n');
|
|
418
418
|
const matches = [];
|
|
419
419
|
|
|
420
|
-
//
|
|
421
|
-
const
|
|
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>:
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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
|
|
475
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|