ucn 3.7.17 → 3.7.18
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/cli/index.js +76 -21
- package/mcp/server.js +30 -2
- package/package.json +2 -2
package/cli/index.js
CHANGED
|
@@ -86,7 +86,9 @@ const flags = {
|
|
|
86
86
|
// Regex search mode (default: ON; --no-regex to force plain text)
|
|
87
87
|
regex: args.includes('--no-regex') ? false : undefined,
|
|
88
88
|
// Stats: per-function line counts
|
|
89
|
-
functions: args.includes('--functions')
|
|
89
|
+
functions: args.includes('--functions'),
|
|
90
|
+
// Class: max lines to show (0 = no limit)
|
|
91
|
+
maxLines: parseInt(args.find(a => a.startsWith('--max-lines='))?.split('=')[1] || '0') || null
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
// Handle --file flag with space
|
|
@@ -106,7 +108,8 @@ const knownFlags = new Set([
|
|
|
106
108
|
'--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
|
|
107
109
|
'--default', '--top', '--no-follow-symlinks',
|
|
108
110
|
'--base', '--staged',
|
|
109
|
-
'--regex', '--no-regex', '--functions'
|
|
111
|
+
'--regex', '--no-regex', '--functions',
|
|
112
|
+
'--max-lines'
|
|
110
113
|
]);
|
|
111
114
|
|
|
112
115
|
// Handle help flag
|
|
@@ -841,7 +844,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
841
844
|
|
|
842
845
|
case 'about': {
|
|
843
846
|
requireArg(arg, 'Usage: ucn . about <name>');
|
|
844
|
-
const aboutResult = index.about(arg, { withTypes: flags.withTypes, file: flags.file, all: flags.all, includeMethods: flags.includeMethods, includeUncertain: flags.includeUncertain, exclude: flags.exclude });
|
|
847
|
+
const aboutResult = index.about(arg, { withTypes: flags.withTypes, file: flags.file, all: flags.all, includeMethods: flags.includeMethods, includeUncertain: flags.includeUncertain, exclude: flags.exclude, maxCallers: flags.top || undefined, maxCallees: flags.top || undefined });
|
|
845
848
|
printOutput(aboutResult,
|
|
846
849
|
output.formatAboutJson,
|
|
847
850
|
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
|
|
@@ -899,8 +902,8 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
899
902
|
|
|
900
903
|
case 'related': {
|
|
901
904
|
requireArg(arg, 'Usage: ucn . related <name>');
|
|
902
|
-
const relatedResult = index.related(arg, { file: flags.file, all: flags.all });
|
|
903
|
-
printOutput(relatedResult, output.formatRelatedJson, r => output.formatRelated(r, { showAll: flags.all }));
|
|
905
|
+
const relatedResult = index.related(arg, { file: flags.file, top: flags.top, all: flags.all });
|
|
906
|
+
printOutput(relatedResult, output.formatRelatedJson, r => output.formatRelated(r, { showAll: flags.all, top: flags.top }));
|
|
904
907
|
break;
|
|
905
908
|
}
|
|
906
909
|
|
|
@@ -997,7 +1000,11 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
997
1000
|
});
|
|
998
1001
|
printOutput(deadcodeResults,
|
|
999
1002
|
output.formatDeadcodeJson,
|
|
1000
|
-
r => output.formatDeadcode(r, {
|
|
1003
|
+
r => output.formatDeadcode(r, {
|
|
1004
|
+
top: flags.top,
|
|
1005
|
+
decoratedHint: !flags.includeDecorated && deadcodeResults.excludedDecorated > 0 ? `${deadcodeResults.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
1006
|
+
exportedHint: !flags.includeExported && deadcodeResults.excludedExported > 0 ? `${deadcodeResults.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
|
|
1007
|
+
})
|
|
1001
1008
|
);
|
|
1002
1009
|
break;
|
|
1003
1010
|
}
|
|
@@ -1162,6 +1169,45 @@ function extractClassFromProject(index, name, overrideFlags) {
|
|
|
1162
1169
|
// Use index data directly instead of re-parsing the file
|
|
1163
1170
|
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1164
1171
|
const codeLines = code.split('\n');
|
|
1172
|
+
const classLineCount = match.endLine - match.startLine + 1;
|
|
1173
|
+
|
|
1174
|
+
// Large class summary (>200 lines) when no --max-lines specified
|
|
1175
|
+
if (classLineCount > 200 && !f.maxLines) {
|
|
1176
|
+
if (f.json) {
|
|
1177
|
+
const extracted = codeLines.slice(match.startLine - 1, match.endLine);
|
|
1178
|
+
const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
|
|
1179
|
+
console.log(JSON.stringify({ ...match, code: clsCode }, null, 2));
|
|
1180
|
+
} else {
|
|
1181
|
+
const lines = [];
|
|
1182
|
+
lines.push(`${match.relativePath}:${match.startLine}`);
|
|
1183
|
+
lines.push(`${output.lineRange(match.startLine, match.endLine)} ${output.formatClassSignature(match)}`);
|
|
1184
|
+
lines.push('\u2500'.repeat(60));
|
|
1185
|
+
const methods = index.findMethodsForType(match.name);
|
|
1186
|
+
if (methods.length > 0) {
|
|
1187
|
+
lines.push(`\nMethods (${methods.length}):`);
|
|
1188
|
+
for (const m of methods) {
|
|
1189
|
+
lines.push(` ${output.formatFunctionSignature(m)} [line ${m.startLine}]`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
lines.push(`\nClass is ${classLineCount} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
|
|
1193
|
+
console.log(lines.join('\n'));
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Truncated source with --max-lines
|
|
1199
|
+
if (f.maxLines && classLineCount > f.maxLines) {
|
|
1200
|
+
const truncated = codeLines.slice(match.startLine - 1, match.startLine - 1 + f.maxLines);
|
|
1201
|
+
const truncatedCode = cleanHtmlScriptTags(truncated, detectLanguage(match.file)).join('\n');
|
|
1202
|
+
if (f.json) {
|
|
1203
|
+
console.log(JSON.stringify({ ...match, code: truncatedCode, truncated: true, totalLines: classLineCount }, null, 2));
|
|
1204
|
+
} else {
|
|
1205
|
+
console.log(output.formatClass(match, truncatedCode));
|
|
1206
|
+
console.log(`\n... showing ${f.maxLines} of ${classLineCount} lines`);
|
|
1207
|
+
}
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1165
1211
|
const extracted = codeLines.slice(match.startLine - 1, match.endLine);
|
|
1166
1212
|
const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
|
|
1167
1213
|
|
|
@@ -1902,6 +1948,7 @@ function parseInteractiveFlags(tokens) {
|
|
|
1902
1948
|
includeUncertain: tokens.includes('--include-uncertain'),
|
|
1903
1949
|
includeMethods: tokens.includes('--include-methods=false') ? false : tokens.includes('--include-methods') ? true : undefined,
|
|
1904
1950
|
detailed: tokens.includes('--detailed'),
|
|
1951
|
+
topLevel: tokens.includes('--top-level'),
|
|
1905
1952
|
all: tokens.includes('--all'),
|
|
1906
1953
|
exact: tokens.includes('--exact'),
|
|
1907
1954
|
callsOnly: tokens.includes('--calls-only'),
|
|
@@ -1928,9 +1975,10 @@ function parseInteractiveFlags(tokens) {
|
|
|
1928
1975
|
function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
1929
1976
|
switch (command) {
|
|
1930
1977
|
case 'toc': {
|
|
1931
|
-
const toc = index.getToc({ detailed: iflags.detailed });
|
|
1978
|
+
const toc = index.getToc({ detailed: iflags.detailed, topLevel: iflags.topLevel, all: iflags.all, top: iflags.top });
|
|
1932
1979
|
console.log(output.formatToc(toc, {
|
|
1933
|
-
detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol'
|
|
1980
|
+
detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol',
|
|
1981
|
+
uncertainHint: 'use --include-uncertain to include all'
|
|
1934
1982
|
}));
|
|
1935
1983
|
break;
|
|
1936
1984
|
}
|
|
@@ -1940,7 +1988,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
1940
1988
|
console.log('Usage: find <name>');
|
|
1941
1989
|
return;
|
|
1942
1990
|
}
|
|
1943
|
-
const
|
|
1991
|
+
const findExclude = iflags.includeTests ? iflags.exclude : addTestExclusions(iflags.exclude);
|
|
1992
|
+
const found = index.find(arg, { file: iflags.file, exact: iflags.exact, exclude: findExclude, in: iflags.in });
|
|
1944
1993
|
if (found.length === 0) {
|
|
1945
1994
|
console.log(`No symbols found for "${arg}"`);
|
|
1946
1995
|
} else {
|
|
@@ -1954,7 +2003,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
1954
2003
|
console.log('Usage: about <name>');
|
|
1955
2004
|
return;
|
|
1956
2005
|
}
|
|
1957
|
-
const aboutResult = index.about(arg, { includeMethods: iflags.includeMethods, includeUncertain: iflags.includeUncertain,
|
|
2006
|
+
const aboutResult = index.about(arg, { withTypes: iflags.withTypes, file: iflags.file, all: iflags.all, includeMethods: iflags.includeMethods, includeUncertain: iflags.includeUncertain, exclude: iflags.exclude, maxCallers: iflags.top || undefined, maxCallees: iflags.top || undefined });
|
|
1958
2007
|
console.log(output.formatAbout(aboutResult, { expand: iflags.expand, root: index.root, showAll: iflags.all }));
|
|
1959
2008
|
break;
|
|
1960
2009
|
}
|
|
@@ -1964,7 +2013,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
1964
2013
|
console.log('Usage: usages <name>');
|
|
1965
2014
|
return;
|
|
1966
2015
|
}
|
|
1967
|
-
const
|
|
2016
|
+
const usagesExclude = iflags.includeTests ? iflags.exclude : addTestExclusions(iflags.exclude);
|
|
2017
|
+
const usages = index.usages(arg, { codeOnly: iflags.codeOnly, context: iflags.context, exclude: usagesExclude, in: iflags.in });
|
|
1968
2018
|
console.log(output.formatUsages(usages, arg));
|
|
1969
2019
|
break;
|
|
1970
2020
|
}
|
|
@@ -1994,7 +2044,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
1994
2044
|
console.log('Usage: smart <name>');
|
|
1995
2045
|
return;
|
|
1996
2046
|
}
|
|
1997
|
-
const smart = index.smart(arg, { file: iflags.file,
|
|
2047
|
+
const smart = index.smart(arg, { file: iflags.file, withTypes: iflags.withTypes, includeMethods: iflags.includeMethods, includeUncertain: iflags.includeUncertain });
|
|
1998
2048
|
if (smart) {
|
|
1999
2049
|
console.log(output.formatSmart(smart, {
|
|
2000
2050
|
uncertainHint: 'use --include-uncertain to include all'
|
|
@@ -2010,7 +2060,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2010
2060
|
console.log('Usage: impact <name>');
|
|
2011
2061
|
return;
|
|
2012
2062
|
}
|
|
2013
|
-
const impactResult = index.impact(arg, { file: iflags.file });
|
|
2063
|
+
const impactResult = index.impact(arg, { file: iflags.file, exclude: iflags.exclude });
|
|
2014
2064
|
console.log(output.formatImpact(impactResult));
|
|
2015
2065
|
break;
|
|
2016
2066
|
}
|
|
@@ -2076,7 +2126,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2076
2126
|
const graphDepth = iflags.depth ? parseInt(iflags.depth) : 2;
|
|
2077
2127
|
const graphDirection = iflags.direction || 'both';
|
|
2078
2128
|
const graphResult = index.graph(arg, { direction: graphDirection, maxDepth: graphDepth });
|
|
2079
|
-
console.log(output.formatGraph(graphResult, { showAll: iflags.all, maxDepth: graphDepth }));
|
|
2129
|
+
console.log(output.formatGraph(graphResult, { showAll: iflags.all || !!iflags.depth, maxDepth: graphDepth, file: arg }));
|
|
2080
2130
|
break;
|
|
2081
2131
|
}
|
|
2082
2132
|
|
|
@@ -2128,7 +2178,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2128
2178
|
console.log('Usage: search <term>');
|
|
2129
2179
|
return;
|
|
2130
2180
|
}
|
|
2131
|
-
const
|
|
2181
|
+
const searchExclude = iflags.includeTests ? iflags.exclude : addTestExclusions(iflags.exclude);
|
|
2182
|
+
const results = index.search(arg, { codeOnly: iflags.codeOnly, caseSensitive: iflags.caseSensitive, context: iflags.context, exclude: searchExclude, in: iflags.in, regex: iflags.regex });
|
|
2132
2183
|
console.log(output.formatSearch(results, arg));
|
|
2133
2184
|
break;
|
|
2134
2185
|
}
|
|
@@ -2138,14 +2189,14 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2138
2189
|
console.log('Usage: typedef <name>');
|
|
2139
2190
|
return;
|
|
2140
2191
|
}
|
|
2141
|
-
const types = index.typedef(arg);
|
|
2192
|
+
const types = index.typedef(arg, { exact: iflags.exact });
|
|
2142
2193
|
console.log(output.formatTypedef(types, arg));
|
|
2143
2194
|
break;
|
|
2144
2195
|
}
|
|
2145
2196
|
|
|
2146
2197
|
case 'api': {
|
|
2147
|
-
const api = index.api();
|
|
2148
|
-
console.log(output.formatApi(api, '.'));
|
|
2198
|
+
const api = index.api(arg);
|
|
2199
|
+
console.log(output.formatApi(api, arg || '.'));
|
|
2149
2200
|
break;
|
|
2150
2201
|
}
|
|
2151
2202
|
|
|
@@ -2197,7 +2248,11 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2197
2248
|
exclude: iflags.exclude,
|
|
2198
2249
|
in: iflags.in
|
|
2199
2250
|
});
|
|
2200
|
-
console.log(output.formatDeadcode(deadResult, {
|
|
2251
|
+
console.log(output.formatDeadcode(deadResult, {
|
|
2252
|
+
top: iflags.top,
|
|
2253
|
+
decoratedHint: !iflags.includeDecorated && deadResult.excludedDecorated > 0 ? `${deadResult.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
2254
|
+
exportedHint: !iflags.includeExported && deadResult.excludedExported > 0 ? `${deadResult.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
|
|
2255
|
+
}));
|
|
2201
2256
|
break;
|
|
2202
2257
|
}
|
|
2203
2258
|
|
|
@@ -2206,8 +2261,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
2206
2261
|
console.log('Usage: related <name>');
|
|
2207
2262
|
return;
|
|
2208
2263
|
}
|
|
2209
|
-
const relResult = index.related(arg, { file: iflags.file, all: iflags.all });
|
|
2210
|
-
console.log(output.formatRelated(relResult, { showAll: iflags.all }));
|
|
2264
|
+
const relResult = index.related(arg, { file: iflags.file, top: iflags.top, all: iflags.all });
|
|
2265
|
+
console.log(output.formatRelated(relResult, { showAll: iflags.all, top: iflags.top }));
|
|
2211
2266
|
break;
|
|
2212
2267
|
}
|
|
2213
2268
|
|
package/mcp/server.js
CHANGED
|
@@ -486,6 +486,19 @@ server.registerTool(
|
|
|
486
486
|
continue;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
// Show all definitions when all=true and multiple matches
|
|
490
|
+
if (matches.length > 1 && !file && all) {
|
|
491
|
+
for (const m of matches) {
|
|
492
|
+
const mPathCheck = resolveAndValidatePath(index, m.relativePath || path.relative(index.root, m.file));
|
|
493
|
+
if (typeof mPathCheck !== 'string') return mPathCheck;
|
|
494
|
+
const mCode = fs.readFileSync(m.file, 'utf-8');
|
|
495
|
+
const mLines = mCode.split('\n');
|
|
496
|
+
const mFnCode = mLines.slice(m.startLine - 1, m.endLine).join('\n');
|
|
497
|
+
parts.push(output.formatFn(m, mFnCode));
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
489
502
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
490
503
|
const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
491
504
|
if (typeof fnPathCheck !== 'string') return fnPathCheck;
|
|
@@ -495,7 +508,7 @@ server.registerTool(
|
|
|
495
508
|
|
|
496
509
|
let note = '';
|
|
497
510
|
if (matches.length > 1 && !file) {
|
|
498
|
-
note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to
|
|
511
|
+
note = `Note: Found ${matches.length} definitions for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Use file parameter or all=true to show all.\n`;
|
|
499
512
|
}
|
|
500
513
|
parts.push(note + output.formatFn(match, fnCode));
|
|
501
514
|
}
|
|
@@ -516,6 +529,20 @@ server.registerTool(
|
|
|
516
529
|
return toolResult(`Class "${name}" not found.`);
|
|
517
530
|
}
|
|
518
531
|
|
|
532
|
+
// Show all definitions when all=true and multiple matches
|
|
533
|
+
if (matches.length > 1 && !file && all) {
|
|
534
|
+
const allParts = [];
|
|
535
|
+
for (const m of matches) {
|
|
536
|
+
const mPathCheck = resolveAndValidatePath(index, m.relativePath || path.relative(index.root, m.file));
|
|
537
|
+
if (typeof mPathCheck !== 'string') return mPathCheck;
|
|
538
|
+
const mCode = fs.readFileSync(m.file, 'utf-8');
|
|
539
|
+
const mLines = mCode.split('\n');
|
|
540
|
+
const clsCode = mLines.slice(m.startLine - 1, m.endLine).join('\n');
|
|
541
|
+
allParts.push(output.formatClass(m, clsCode));
|
|
542
|
+
}
|
|
543
|
+
return toolResult(allParts.join('\n\n'));
|
|
544
|
+
}
|
|
545
|
+
|
|
519
546
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
520
547
|
// Validate file is within project root
|
|
521
548
|
const clsPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
@@ -731,6 +758,7 @@ server.registerTool(
|
|
|
731
758
|
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
732
759
|
return toolResult(output.formatGraph(result, {
|
|
733
760
|
showAll: all || depth !== undefined,
|
|
761
|
+
maxDepth: depth ?? 2,
|
|
734
762
|
file,
|
|
735
763
|
depthHint: 'Set depth parameter for deeper graph.',
|
|
736
764
|
allHint: 'Set depth to expand all children.'
|
|
@@ -812,7 +840,7 @@ server.registerTool(
|
|
|
812
840
|
case 'stats': {
|
|
813
841
|
const index = getIndex(project_dir);
|
|
814
842
|
const stats = index.getStats({ functions: functions || false });
|
|
815
|
-
return toolResult(output.formatStats(stats, { top: top ||
|
|
843
|
+
return toolResult(output.formatStats(stats, { top: top || 0 }));
|
|
816
844
|
}
|
|
817
845
|
|
|
818
846
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.18",
|
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"ucn-mcp": "mcp/server.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/parser.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js"
|
|
12
|
+
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|