ucn 3.7.16 → 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/.claude/skills/ucn/SKILL.md +17 -0
- package/cli/index.js +76 -21
- package/mcp/server.js +41 -11
- package/package.json +2 -2
|
@@ -99,6 +99,12 @@ ucn deadcode --exclude=test # Skip test files (most useful)
|
|
|
99
99
|
| Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
|
|
100
100
|
| File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Setting `--depth=N` expands all children. Can be noisy — use depth=1 for large projects. For function-level flow, use `trace` instead |
|
|
101
101
|
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
|
|
102
|
+
| Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
|
|
103
|
+
| Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
|
|
104
|
+
| See a project's public API | `ucn api` or `ucn api --file=<file>` | All exported/public symbols with signatures |
|
|
105
|
+
| Drill into context results | `ucn expand <N>` | Show source code for item N from a previous `context` call |
|
|
106
|
+
| Best usage example of a function | `ucn example <name>` | Finds and scores the best call site with surrounding context |
|
|
107
|
+
| Debug a stack trace | `ucn stacktrace --stack="<trace>"` | Parses stack frames and shows source context per frame |
|
|
102
108
|
|
|
103
109
|
## Command Format
|
|
104
110
|
|
|
@@ -126,9 +132,20 @@ ucn [target] <command> [name] [--flags]
|
|
|
126
132
|
| `--base=<ref>` | Git ref for diff-impact (default: HEAD) |
|
|
127
133
|
| `--staged` | Analyze staged changes (diff-impact) |
|
|
128
134
|
| `--no-cache` | Force re-index after editing files |
|
|
135
|
+
| `--clear-cache` | Delete cached index entirely before running |
|
|
129
136
|
| `--context=N` | Lines of surrounding context in `usages`/`search` output |
|
|
130
137
|
| `--no-regex` | Force plain text search (regex is default) |
|
|
131
138
|
| `--functions` | Show per-function line counts in `stats` (complexity audit) |
|
|
139
|
+
| `--json` | Machine-readable JSON output (wrapped in `{meta, data}`) |
|
|
140
|
+
| `--code-only` | Exclude matches in comments and strings (`search`/`usages`) |
|
|
141
|
+
| `--with-types` | Include related type definitions in `smart`/`about` output |
|
|
142
|
+
| `--detailed` | Show full symbol listing per file in `toc` |
|
|
143
|
+
| `--top=N` | Limit result count (default: 10 for most commands) |
|
|
144
|
+
| `--case-sensitive` | Case-sensitive text search (default: case-insensitive) |
|
|
145
|
+
| `--exact` | Exact name match only in `find`/`typedef` (no substring) |
|
|
146
|
+
| `--include-uncertain` | Include ambiguous/uncertain matches in `context`/`smart`/`about` |
|
|
147
|
+
| `--include-exported` | Include exported symbols in `deadcode` results |
|
|
148
|
+
| `--include-decorated` | Include decorated/annotated symbols in `deadcode` results |
|
|
132
149
|
|
|
133
150
|
## Workflow Integration
|
|
134
151
|
|
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
|
@@ -268,7 +268,9 @@ server.registerTool(
|
|
|
268
268
|
range: z.string().optional().describe('Line range to extract, e.g. "10-20" or "15" (lines command)'),
|
|
269
269
|
base: z.string().optional().describe('Git ref to diff against (default: HEAD). E.g. "HEAD~3", "main", a commit SHA'),
|
|
270
270
|
staged: z.boolean().optional().describe('Analyze staged changes (diff_impact command)'),
|
|
271
|
-
case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)')
|
|
271
|
+
case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)'),
|
|
272
|
+
all: z.boolean().optional().describe('Show all results (expand truncated sections). Applies to about, toc, related, trace, and others.'),
|
|
273
|
+
top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)')
|
|
272
274
|
})
|
|
273
275
|
},
|
|
274
276
|
async (args) => {
|
|
@@ -278,7 +280,7 @@ server.registerTool(
|
|
|
278
280
|
include_exported, include_decorated, calls_only, max_lines,
|
|
279
281
|
direction, term, add_param, remove_param, rename_to,
|
|
280
282
|
default_value, stack, item, range, base, staged,
|
|
281
|
-
case_sensitive, regex, functions } = args;
|
|
283
|
+
case_sensitive, regex, functions, all, top_level } = args;
|
|
282
284
|
|
|
283
285
|
try {
|
|
284
286
|
switch (command) {
|
|
@@ -291,9 +293,9 @@ server.registerTool(
|
|
|
291
293
|
const err = requireName(name);
|
|
292
294
|
if (err) return err;
|
|
293
295
|
const index = getIndex(project_dir);
|
|
294
|
-
const result = index.about(name, { file, exclude: parseExclude(exclude), withTypes: with_types || false, includeMethods: include_methods ?? undefined, maxCallers: top, maxCallees: top });
|
|
296
|
+
const result = index.about(name, { file, exclude: parseExclude(exclude), withTypes: with_types || false, includeMethods: include_methods ?? undefined, includeUncertain: include_uncertain || false, all: all || false, maxCallers: top, maxCallees: top });
|
|
295
297
|
return toolResult(output.formatAbout(result, {
|
|
296
|
-
allHint: 'Repeat with
|
|
298
|
+
allHint: 'Repeat with all=true to show all.',
|
|
297
299
|
methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.'
|
|
298
300
|
}));
|
|
299
301
|
}
|
|
@@ -378,12 +380,12 @@ server.registerTool(
|
|
|
378
380
|
const err = requireName(name);
|
|
379
381
|
if (err) return err;
|
|
380
382
|
const index = getIndex(project_dir);
|
|
381
|
-
const result = index.related(name, { file, top, all:
|
|
383
|
+
const result = index.related(name, { file, top, all: all || false });
|
|
382
384
|
if (!result) return toolResult(`Symbol "${name}" not found.`);
|
|
383
385
|
return toolResult(output.formatRelated(result, {
|
|
384
|
-
showAll:
|
|
386
|
+
showAll: all || false,
|
|
385
387
|
top,
|
|
386
|
-
allHint: 'Repeat with
|
|
388
|
+
allHint: 'Repeat with all=true to show all.'
|
|
387
389
|
}));
|
|
388
390
|
}
|
|
389
391
|
|
|
@@ -416,7 +418,7 @@ server.registerTool(
|
|
|
416
418
|
|
|
417
419
|
case 'toc': {
|
|
418
420
|
const index = getIndex(project_dir);
|
|
419
|
-
const toc = index.getToc({ detailed: detailed || false, top });
|
|
421
|
+
const toc = index.getToc({ detailed: detailed || false, topLevel: top_level || false, all: all || false, top });
|
|
420
422
|
return toolResult(output.formatToc(toc, {
|
|
421
423
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
422
424
|
}));
|
|
@@ -484,6 +486,19 @@ server.registerTool(
|
|
|
484
486
|
continue;
|
|
485
487
|
}
|
|
486
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
|
+
|
|
487
502
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
488
503
|
const fnPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
489
504
|
if (typeof fnPathCheck !== 'string') return fnPathCheck;
|
|
@@ -493,7 +508,7 @@ server.registerTool(
|
|
|
493
508
|
|
|
494
509
|
let note = '';
|
|
495
510
|
if (matches.length > 1 && !file) {
|
|
496
|
-
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`;
|
|
497
512
|
}
|
|
498
513
|
parts.push(note + output.formatFn(match, fnCode));
|
|
499
514
|
}
|
|
@@ -514,6 +529,20 @@ server.registerTool(
|
|
|
514
529
|
return toolResult(`Class "${name}" not found.`);
|
|
515
530
|
}
|
|
516
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
|
+
|
|
517
546
|
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
518
547
|
// Validate file is within project root
|
|
519
548
|
const clsPathCheck = resolveAndValidatePath(index, match.relativePath || path.relative(index.root, match.file));
|
|
@@ -728,7 +757,8 @@ server.registerTool(
|
|
|
728
757
|
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
729
758
|
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
730
759
|
return toolResult(output.formatGraph(result, {
|
|
731
|
-
showAll: depth !== undefined,
|
|
760
|
+
showAll: all || depth !== undefined,
|
|
761
|
+
maxDepth: depth ?? 2,
|
|
732
762
|
file,
|
|
733
763
|
depthHint: 'Set depth parameter for deeper graph.',
|
|
734
764
|
allHint: 'Set depth to expand all children.'
|
|
@@ -810,7 +840,7 @@ server.registerTool(
|
|
|
810
840
|
case 'stats': {
|
|
811
841
|
const index = getIndex(project_dir);
|
|
812
842
|
const stats = index.getStats({ functions: functions || false });
|
|
813
|
-
return toolResult(output.formatStats(stats, { top: top ||
|
|
843
|
+
return toolResult(output.formatStats(stats, { top: top || 0 }));
|
|
814
844
|
}
|
|
815
845
|
|
|
816
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",
|