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.
- package/.claude/skills/ucn/SKILL.md +3 -0
- package/.mcpregistry_github_token +1 -1
- package/.mcpregistry_registry_token +1 -1
- package/cli/index.js +38 -14
- package/core/output.js +24 -2
- package/core/project.js +40 -4
- package/languages/utils.js +7 -3
- package/mcp/server.js +12 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
1
|
+
ghu_OKOjTMtrqEazgazx9Dj0l0z6irgt8R3KXKET
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"token":"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.
|
|
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,
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
4442
|
+
let filesScanned = 0;
|
|
4443
|
+
let filesSkipped = 0;
|
|
4443
4444
|
const regexFlags = options.caseSensitive ? 'g' : 'gi';
|
|
4444
|
-
const
|
|
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
|
|
package/languages/utils.js
CHANGED
|
@@ -417,9 +417,13 @@ function findMatchesWithASTFilter(content, term, parser, options = {}) {
|
|
|
417
417
|
const lines = content.split('\n');
|
|
418
418
|
const matches = [];
|
|
419
419
|
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
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>:
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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",
|