ucn 3.8.5 → 3.8.7
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 +5 -5
- package/core/entrypoints.js +78 -1
- package/core/execute.js +27 -10
- package/core/output.js +16 -4
- package/core/project.js +59 -23
- package/core/registry.js +1 -0
- package/languages/go.js +102 -0
- package/mcp/server.js +143 -120
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -74,7 +74,7 @@ function parseFlags(tokens) {
|
|
|
74
74
|
file: getValueFlag('--file'),
|
|
75
75
|
exclude: parseExclude(),
|
|
76
76
|
in: getValueFlag('--in'),
|
|
77
|
-
includeTests: tokens.includes('--include-tests'),
|
|
77
|
+
includeTests: tokens.includes('--include-tests') ? true : undefined,
|
|
78
78
|
includeExported: tokens.includes('--include-exported'),
|
|
79
79
|
includeDecorated: tokens.includes('--include-decorated'),
|
|
80
80
|
includeUncertain: tokens.includes('--include-uncertain'),
|
|
@@ -715,7 +715,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
715
715
|
case 'affectedTests': {
|
|
716
716
|
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...flags });
|
|
717
717
|
if (!ok) fail(error);
|
|
718
|
-
printOutput(result, output.formatAffectedTestsJson, output.formatAffectedTests);
|
|
718
|
+
printOutput(result, output.formatAffectedTestsJson, r => output.formatAffectedTests(r, { all: flags.all }));
|
|
719
719
|
break;
|
|
720
720
|
}
|
|
721
721
|
|
|
@@ -782,7 +782,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
782
782
|
case 'diffImpact': {
|
|
783
783
|
const { ok, result, error } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file });
|
|
784
784
|
if (!ok) fail(error);
|
|
785
|
-
printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
|
|
785
|
+
printOutput(result, output.formatDiffImpactJson, r => output.formatDiffImpact(r, { all: flags.all }));
|
|
786
786
|
break;
|
|
787
787
|
}
|
|
788
788
|
|
|
@@ -1504,7 +1504,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1504
1504
|
case 'affectedTests': {
|
|
1505
1505
|
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...iflags });
|
|
1506
1506
|
if (!ok) { console.log(error); return; }
|
|
1507
|
-
console.log(output.formatAffectedTests(result));
|
|
1507
|
+
console.log(output.formatAffectedTests(result, { all: iflags.all }));
|
|
1508
1508
|
break;
|
|
1509
1509
|
}
|
|
1510
1510
|
|
|
@@ -1536,7 +1536,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1536
1536
|
case 'diffImpact': {
|
|
1537
1537
|
const { ok, result, error } = execute(index, 'diffImpact', iflags);
|
|
1538
1538
|
if (!ok) { console.log(error); return; }
|
|
1539
|
-
console.log(output.formatDiffImpact(result));
|
|
1539
|
+
console.log(output.formatDiffImpact(result, { all: iflags.all }));
|
|
1540
1540
|
break;
|
|
1541
1541
|
}
|
|
1542
1542
|
|
package/core/entrypoints.js
CHANGED
|
@@ -174,6 +174,55 @@ const FRAMEWORK_PATTERNS = [
|
|
|
174
174
|
pattern: /^tokio::main$/,
|
|
175
175
|
},
|
|
176
176
|
|
|
177
|
+
// ── Go Runtime Entry Points ─────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
// Go main function (program entry)
|
|
180
|
+
{
|
|
181
|
+
id: 'go-main',
|
|
182
|
+
languages: new Set(['go']),
|
|
183
|
+
type: 'runtime',
|
|
184
|
+
framework: 'go',
|
|
185
|
+
detection: 'namePattern',
|
|
186
|
+
pattern: /^main$/,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Go init functions (package initialization, called by runtime)
|
|
190
|
+
{
|
|
191
|
+
id: 'go-init',
|
|
192
|
+
languages: new Set(['go']),
|
|
193
|
+
type: 'runtime',
|
|
194
|
+
framework: 'go',
|
|
195
|
+
detection: 'namePattern',
|
|
196
|
+
pattern: /^init$/,
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// Go test functions (called by go test)
|
|
200
|
+
{
|
|
201
|
+
id: 'go-test',
|
|
202
|
+
languages: new Set(['go']),
|
|
203
|
+
type: 'test',
|
|
204
|
+
framework: 'go',
|
|
205
|
+
detection: 'namePattern',
|
|
206
|
+
pattern: /^(Test|Benchmark|Example|Fuzz)[A-Z_]/,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// ── Go Framework Patterns ─────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
// Cobra CLI framework — RunE, Run, PreRunE etc. assigned to cobra.Command struct fields
|
|
212
|
+
// Detected via call-pattern: cobra.Command composite literals with function value fields
|
|
213
|
+
{
|
|
214
|
+
id: 'cobra-command',
|
|
215
|
+
languages: new Set(['go']),
|
|
216
|
+
type: 'cli',
|
|
217
|
+
framework: 'cobra',
|
|
218
|
+
detection: 'callPattern',
|
|
219
|
+
receiverPattern: /^cobra$/i,
|
|
220
|
+
methodPattern: /^Command$/,
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Go goroutine launch — go func() or go handler()
|
|
224
|
+
// (detected separately in namePattern since it's a language feature)
|
|
225
|
+
|
|
177
226
|
// ── Catch-all fallbacks ─────────────────────────────────────────────
|
|
178
227
|
|
|
179
228
|
// Python: any decorator with '.' (attribute access) — framework registration heuristic
|
|
@@ -308,12 +357,16 @@ function detectEntrypoints(index, options = {}) {
|
|
|
308
357
|
const results = [];
|
|
309
358
|
const seen = new Set(); // file:line:name dedup key
|
|
310
359
|
|
|
311
|
-
//
|
|
360
|
+
// Collect name-based patterns for efficient matching
|
|
361
|
+
const namePatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'namePattern');
|
|
362
|
+
|
|
363
|
+
// 1. Scan all symbols for decorator/modifier/name-based patterns
|
|
312
364
|
for (const [name, symbols] of index.symbols) {
|
|
313
365
|
for (const symbol of symbols) {
|
|
314
366
|
const fileEntry = index.files.get(symbol.file);
|
|
315
367
|
if (!fileEntry) continue;
|
|
316
368
|
|
|
369
|
+
// Check decorator/modifier-based patterns
|
|
317
370
|
const match = matchDecoratorOrModifier(symbol, fileEntry.language);
|
|
318
371
|
if (match) {
|
|
319
372
|
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
@@ -331,6 +384,30 @@ function detectEntrypoints(index, options = {}) {
|
|
|
331
384
|
evidence: [match.matchedOn],
|
|
332
385
|
confidence: 0.95,
|
|
333
386
|
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check name-based patterns (main, init, TestXxx, etc.)
|
|
391
|
+
for (const np of namePatterns) {
|
|
392
|
+
if (!np.languages.has(fileEntry.language)) continue;
|
|
393
|
+
if (np.pattern.test(name)) {
|
|
394
|
+
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
395
|
+
if (seen.has(key)) continue;
|
|
396
|
+
seen.add(key);
|
|
397
|
+
|
|
398
|
+
results.push({
|
|
399
|
+
name,
|
|
400
|
+
file: symbol.relativePath || symbol.file,
|
|
401
|
+
absoluteFile: symbol.file,
|
|
402
|
+
line: symbol.startLine,
|
|
403
|
+
type: np.type,
|
|
404
|
+
framework: np.framework,
|
|
405
|
+
patternId: np.id,
|
|
406
|
+
evidence: [`${name}() convention`],
|
|
407
|
+
confidence: 1.0,
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
334
411
|
}
|
|
335
412
|
}
|
|
336
413
|
}
|
package/core/execute.js
CHANGED
|
@@ -149,6 +149,23 @@ function checkFilePatternMatch(index, filePattern) {
|
|
|
149
149
|
for (const [, fileEntry] of index.files) {
|
|
150
150
|
if (fileEntry.relativePath.includes(filePattern)) return null;
|
|
151
151
|
}
|
|
152
|
+
// Suggest similar directories/files to help user refine
|
|
153
|
+
const patternLower = filePattern.toLowerCase();
|
|
154
|
+
const basename = filePattern.split('/').pop().toLowerCase();
|
|
155
|
+
const suggestions = new Set();
|
|
156
|
+
for (const [, fileEntry] of index.files) {
|
|
157
|
+
const rp = fileEntry.relativePath.toLowerCase();
|
|
158
|
+
// Check if any path component contains the last segment of the pattern
|
|
159
|
+
if (basename && rp.includes(basename)) {
|
|
160
|
+
// Extract the directory containing the match
|
|
161
|
+
const dir = fileEntry.relativePath.split('/').slice(0, -1).join('/');
|
|
162
|
+
if (dir) suggestions.add(dir);
|
|
163
|
+
if (suggestions.size >= 5) break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (suggestions.size > 0) {
|
|
167
|
+
return `No files matched pattern '${filePattern}'. Similar paths:\n${[...suggestions].map(s => ' ' + s).join('\n')}`;
|
|
168
|
+
}
|
|
152
169
|
return `No files matched pattern '${filePattern}'.`;
|
|
153
170
|
}
|
|
154
171
|
|
|
@@ -187,17 +204,16 @@ const HANDLERS = {
|
|
|
187
204
|
if (!result) {
|
|
188
205
|
// Give better error if file/className filter is the problem
|
|
189
206
|
if (p.file || p.className) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
includeMethods: p.includeMethods,
|
|
194
|
-
includeUncertain: p.includeUncertain || false,
|
|
195
|
-
exclude: toExcludeArray(p.exclude),
|
|
196
|
-
});
|
|
197
|
-
if (unfiltered && unfiltered.found !== false && unfiltered.symbol) {
|
|
198
|
-
const loc = `${unfiltered.symbol.file}:${unfiltered.symbol.startLine}`;
|
|
207
|
+
// Show ALL definitions so user can pick the right file= filter
|
|
208
|
+
const allDefs = index.symbols.get(p.name) || [];
|
|
209
|
+
if (allDefs.length > 0) {
|
|
199
210
|
const filterDesc = p.className ? `class "${p.className}"` : `file "${p.file}"`;
|
|
200
|
-
|
|
211
|
+
const locations = allDefs
|
|
212
|
+
.slice(0, 10)
|
|
213
|
+
.map(d => ` ${d.relativePath}:${d.startLine}${d.className ? ` (${d.className})` : ''}`)
|
|
214
|
+
.join('\n');
|
|
215
|
+
const more = allDefs.length > 10 ? `\n ... and ${allDefs.length - 10} more` : '';
|
|
216
|
+
return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found ${allDefs.length} definition(s) elsewhere:\n${locations}${more}\nUse file= with a path fragment from the list above to disambiguate.` };
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
@@ -422,6 +438,7 @@ const HANDLERS = {
|
|
|
422
438
|
top: num(p.top, undefined),
|
|
423
439
|
file: p.file,
|
|
424
440
|
exclude: p.exclude,
|
|
441
|
+
in: p.in,
|
|
425
442
|
});
|
|
426
443
|
// Apply limit to detailed toc entries (symbols are in f.symbols.functions/classes arrays)
|
|
427
444
|
const limit = num(p.limit, undefined);
|
package/core/output.js
CHANGED
|
@@ -1084,7 +1084,7 @@ function formatReverseTraceJson(result) {
|
|
|
1084
1084
|
/**
|
|
1085
1085
|
* Format affected-tests command output - text
|
|
1086
1086
|
*/
|
|
1087
|
-
function formatAffectedTests(result) {
|
|
1087
|
+
function formatAffectedTests(result, options = {}) {
|
|
1088
1088
|
if (!result) return 'Function not found.';
|
|
1089
1089
|
|
|
1090
1090
|
const lines = [];
|
|
@@ -1099,9 +1099,12 @@ function formatAffectedTests(result) {
|
|
|
1099
1099
|
if (result.testFiles.length === 0) {
|
|
1100
1100
|
lines.push('No test files found for any affected function.');
|
|
1101
1101
|
} else {
|
|
1102
|
+
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
1103
|
+
const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
|
|
1104
|
+
const truncatedFiles = result.testFiles.length - displayFiles.length;
|
|
1102
1105
|
lines.push(`Test files to run (${summary.totalTestFiles}):`);
|
|
1103
1106
|
lines.push('');
|
|
1104
|
-
for (const tf of
|
|
1107
|
+
for (const tf of displayFiles) {
|
|
1105
1108
|
lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
|
|
1106
1109
|
// Show up to 5 key matches per file
|
|
1107
1110
|
const keyMatches = tf.matches
|
|
@@ -1111,6 +1114,9 @@ function formatAffectedTests(result) {
|
|
|
1111
1114
|
lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
|
|
1112
1115
|
}
|
|
1113
1116
|
}
|
|
1117
|
+
if (truncatedFiles > 0) {
|
|
1118
|
+
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
1119
|
+
}
|
|
1114
1120
|
}
|
|
1115
1121
|
|
|
1116
1122
|
if (result.uncovered.length > 0) {
|
|
@@ -2542,10 +2548,11 @@ function formatStats(stats, options = {}) {
|
|
|
2542
2548
|
// DIFF IMPACT
|
|
2543
2549
|
// ============================================================================
|
|
2544
2550
|
|
|
2545
|
-
function formatDiffImpact(result) {
|
|
2551
|
+
function formatDiffImpact(result, options = {}) {
|
|
2546
2552
|
if (!result) return 'No diff data.';
|
|
2547
2553
|
|
|
2548
2554
|
const lines = [];
|
|
2555
|
+
const MAX_CALLERS_PER_FN = options.all ? Infinity : 30;
|
|
2549
2556
|
|
|
2550
2557
|
lines.push(`Diff Impact Analysis (vs ${result.base})`);
|
|
2551
2558
|
lines.push('═'.repeat(60));
|
|
@@ -2574,12 +2581,17 @@ function formatDiffImpact(result) {
|
|
|
2574
2581
|
}
|
|
2575
2582
|
|
|
2576
2583
|
if (fn.callers.length > 0) {
|
|
2584
|
+
const displayCallers = fn.callers.slice(0, MAX_CALLERS_PER_FN);
|
|
2585
|
+
const truncated = fn.callers.length - displayCallers.length;
|
|
2577
2586
|
lines.push(` Callers (${fn.callers.length}):`);
|
|
2578
|
-
for (const c of
|
|
2587
|
+
for (const c of displayCallers) {
|
|
2579
2588
|
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
2580
2589
|
lines.push(` ${c.relativePath}:${c.line} ${caller}`);
|
|
2581
2590
|
lines.push(` ${c.content}`);
|
|
2582
2591
|
}
|
|
2592
|
+
if (truncated > 0) {
|
|
2593
|
+
lines.push(` ... ${truncated} more callers (use file= to scope diff to specific files, or use impact with class_name= for type-filtered results)`);
|
|
2594
|
+
}
|
|
2583
2595
|
} else {
|
|
2584
2596
|
lines.push(' Callers: none found');
|
|
2585
2597
|
}
|
package/core/project.js
CHANGED
|
@@ -919,6 +919,10 @@ class ProjectIndex {
|
|
|
919
919
|
if (d.startLine && d.endLine && d.type === 'function') {
|
|
920
920
|
score += Math.min(d.endLine - d.startLine, 100);
|
|
921
921
|
}
|
|
922
|
+
// Prefer shallower paths (fewer directory levels = more central to project)
|
|
923
|
+
// Max bonus 50 for root-level files, decreasing with depth
|
|
924
|
+
const depth = (rp.match(/\//g) || []).length;
|
|
925
|
+
score += Math.max(0, 50 - depth * 10);
|
|
922
926
|
return { def: d, score };
|
|
923
927
|
});
|
|
924
928
|
|
|
@@ -969,10 +973,15 @@ class ProjectIndex {
|
|
|
969
973
|
// Build warnings
|
|
970
974
|
const warnings = [];
|
|
971
975
|
if (definitions.length > 1) {
|
|
976
|
+
const others = definitions.filter(d => d !== def);
|
|
977
|
+
const shown = others.slice(0, 5);
|
|
978
|
+
const extra = others.length - shown.length;
|
|
979
|
+
const alsoIn = shown.map(d => `${d.relativePath}:${d.startLine}`).join(', ');
|
|
980
|
+
const suffix = extra > 0 ? `, and ${extra} more` : '';
|
|
972
981
|
warnings.push({
|
|
973
982
|
type: 'ambiguous',
|
|
974
|
-
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Also in: ${
|
|
975
|
-
alternatives:
|
|
983
|
+
message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Also in: ${alsoIn}${suffix}. Use file= to disambiguate.`,
|
|
984
|
+
alternatives: others.map(d => ({
|
|
976
985
|
file: d.relativePath,
|
|
977
986
|
line: d.startLine
|
|
978
987
|
}))
|
|
@@ -3255,11 +3264,29 @@ class ProjectIndex {
|
|
|
3255
3264
|
// like .close(), .get() etc. where many types have the same method.
|
|
3256
3265
|
if (def.className) {
|
|
3257
3266
|
const targetClassName = def.className;
|
|
3267
|
+
// Pre-compute how many types share this method name
|
|
3268
|
+
const _impMethodDefs = this.symbols.get(name);
|
|
3269
|
+
const _impClassNames = new Set();
|
|
3270
|
+
if (_impMethodDefs) {
|
|
3271
|
+
for (const d of _impMethodDefs) {
|
|
3272
|
+
if (d.className) _impClassNames.add(d.className);
|
|
3273
|
+
else if (d.receiver) _impClassNames.add(d.receiver.replace(/^\*/, ''));
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3258
3276
|
callerResults = callerResults.filter(c => {
|
|
3259
3277
|
// Keep non-method calls and self/this/cls calls (already resolved by findCallers)
|
|
3260
3278
|
if (!c.isMethod) return true;
|
|
3261
3279
|
const r = c.receiver;
|
|
3262
|
-
if (
|
|
3280
|
+
if (r && ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
3281
|
+
// Use receiverType from findCallers when available (Go/Java/Rust type inference)
|
|
3282
|
+
if (c.receiverType) {
|
|
3283
|
+
return c.receiverType === targetClassName;
|
|
3284
|
+
}
|
|
3285
|
+
// No receiver (chained/complex expression): only include if method is
|
|
3286
|
+
// unique or rare across types — otherwise too many false positives
|
|
3287
|
+
if (!r) {
|
|
3288
|
+
return _impClassNames.size <= 1;
|
|
3289
|
+
}
|
|
3263
3290
|
// Check if receiver matches the target class name (case-insensitive camelCase convention)
|
|
3264
3291
|
if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
|
|
3265
3292
|
// Check if receiver is an instance of the target class using local variable type inference
|
|
@@ -3341,17 +3368,8 @@ class ProjectIndex {
|
|
|
3341
3368
|
}
|
|
3342
3369
|
// Unique method heuristic: if the called method exists on exactly one class/type
|
|
3343
3370
|
// and it matches the target, include the call (no other class could match)
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
const classNames = new Set();
|
|
3347
|
-
for (const d of methodDefs) {
|
|
3348
|
-
if (d.className) classNames.add(d.className);
|
|
3349
|
-
// Go/Rust: use receiver type as className equivalent
|
|
3350
|
-
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
3351
|
-
}
|
|
3352
|
-
if (classNames.size === 1 && classNames.has(targetClassName)) {
|
|
3353
|
-
return true;
|
|
3354
|
-
}
|
|
3371
|
+
if (_impClassNames.size === 1 && _impClassNames.has(targetClassName)) {
|
|
3372
|
+
return true;
|
|
3355
3373
|
}
|
|
3356
3374
|
// Type-scoped query but receiver type unknown — filter it out.
|
|
3357
3375
|
// Unknown receivers are likely unrelated.
|
|
@@ -4697,6 +4715,9 @@ class ProjectIndex {
|
|
|
4697
4715
|
if (options.exclude && options.exclude.length > 0) {
|
|
4698
4716
|
if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) continue;
|
|
4699
4717
|
}
|
|
4718
|
+
if (options.in) {
|
|
4719
|
+
if (!this.matchesFilters(fileEntry.relativePath, { in: options.in })) continue;
|
|
4720
|
+
}
|
|
4700
4721
|
let functions = fileEntry.symbols.filter(s =>
|
|
4701
4722
|
s.type === 'function' || s.type === 'method' || s.type === 'static' ||
|
|
4702
4723
|
s.type === 'constructor' || s.type === 'public' || s.type === 'abstract' ||
|
|
@@ -4789,6 +4810,7 @@ class ProjectIndex {
|
|
|
4789
4810
|
uncertain: 0,
|
|
4790
4811
|
projectLanguage: this._getPredominantLanguage(),
|
|
4791
4812
|
...(fileFilter && { filteredBy: options.file, matchedFiles: files.length }),
|
|
4813
|
+
...(options.in && { scopedTo: options.in }),
|
|
4792
4814
|
...(emptyFiles > 0 && fileFilter && { emptyFiles })
|
|
4793
4815
|
},
|
|
4794
4816
|
totals: {
|
|
@@ -5153,24 +5175,38 @@ class ProjectIndex {
|
|
|
5153
5175
|
const targetDef = targetDefs[0] || symbol;
|
|
5154
5176
|
if (targetDef.className && (lang === 'go' || lang === 'java' || lang === 'rust')) {
|
|
5155
5177
|
const targetClassName = targetDef.className;
|
|
5178
|
+
// Pre-compute how many types share this method name
|
|
5179
|
+
const methodDefs = this.symbols.get(symbol.name);
|
|
5180
|
+
const classNames = new Set();
|
|
5181
|
+
if (methodDefs) {
|
|
5182
|
+
for (const d of methodDefs) {
|
|
5183
|
+
if (d.className) classNames.add(d.className);
|
|
5184
|
+
else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
const isWidelyShared = classNames.size > 3;
|
|
5156
5188
|
callers = callers.filter(c => {
|
|
5157
5189
|
if (!c.isMethod) return true;
|
|
5158
5190
|
const r = c.receiver;
|
|
5159
|
-
if (
|
|
5191
|
+
if (r && ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
5192
|
+
// No receiver (chained/complex expression): only include if method is
|
|
5193
|
+
// unique or rare across types — otherwise too many false positives
|
|
5194
|
+
if (!r) {
|
|
5195
|
+
return classNames.size <= 1;
|
|
5196
|
+
}
|
|
5160
5197
|
// Use receiverType from findCallers when available
|
|
5161
5198
|
if (c.receiverType) {
|
|
5162
5199
|
return c.receiverType === targetClassName ||
|
|
5163
5200
|
c.receiverType === targetDef.receiver?.replace(/^\*/, '');
|
|
5164
5201
|
}
|
|
5165
5202
|
// Unique method heuristic: if the method exists on exactly one class/type, include
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
if (classNames.size === 1 && classNames.has(targetClassName)) return true;
|
|
5203
|
+
if (classNames.size === 1 && classNames.has(targetClassName)) return true;
|
|
5204
|
+
// For widely shared method names (Get, Set, Run, etc.), require same-package
|
|
5205
|
+
// evidence when receiver type is unknown
|
|
5206
|
+
if (isWidelyShared) {
|
|
5207
|
+
const callerFile = c.file || '';
|
|
5208
|
+
const targetDir = path.dirname(change.filePath);
|
|
5209
|
+
return path.dirname(callerFile) === targetDir;
|
|
5174
5210
|
}
|
|
5175
5211
|
// Unknown receiver + multiple classes with this method → filter out
|
|
5176
5212
|
return false;
|
package/core/registry.js
CHANGED
package/languages/go.js
CHANGED
|
@@ -889,6 +889,108 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
// Detect function-value assignments:
|
|
893
|
+
// Pattern 1: sched.SchedulePod = sched.schedulePod (field = method reference)
|
|
894
|
+
// Pattern 2: sched.SchedulePod = schedulePod (field = function reference)
|
|
895
|
+
// Pattern 3: var handler = processEvent (short variable = function reference)
|
|
896
|
+
// The RHS is a function reference (not a call — no parentheses)
|
|
897
|
+
if (node.type === 'assignment_statement' || node.type === 'short_var_declaration') {
|
|
898
|
+
// Skip blank identifier assignments: _ = x (used to suppress unused warnings)
|
|
899
|
+
const left = node.childForFieldName('left');
|
|
900
|
+
const isBlankAssign = left && left.text.trim() === '_';
|
|
901
|
+
const right = isBlankAssign ? null : node.childForFieldName('right');
|
|
902
|
+
if (right) {
|
|
903
|
+
// Walk through the expression list (could be multiple assignments)
|
|
904
|
+
const rhsNodes = right.type === 'expression_list' ? right.namedChildren : [right];
|
|
905
|
+
for (const rhs of rhsNodes) {
|
|
906
|
+
// selector_expression: sched.schedulePod
|
|
907
|
+
if (rhs.type === 'selector_expression') {
|
|
908
|
+
const fieldNode = rhs.childForFieldName('field');
|
|
909
|
+
const operandNode = rhs.childForFieldName('operand');
|
|
910
|
+
if (fieldNode && operandNode) {
|
|
911
|
+
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
912
|
+
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
913
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
914
|
+
calls.push({
|
|
915
|
+
name: fieldNode.text,
|
|
916
|
+
line: rhs.startPosition.row + 1,
|
|
917
|
+
isMethod: true,
|
|
918
|
+
receiver,
|
|
919
|
+
...(receiverType && { receiverType }),
|
|
920
|
+
enclosingFunction,
|
|
921
|
+
isPotentialCallback: true,
|
|
922
|
+
uncertain: false
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Plain identifier: schedulePod (standalone function reference)
|
|
927
|
+
if (rhs.type === 'identifier') {
|
|
928
|
+
const name = rhs.text;
|
|
929
|
+
if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) && !importAliases.has(name) && /^[a-zA-Z]/.test(name)) {
|
|
930
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
931
|
+
calls.push({
|
|
932
|
+
name,
|
|
933
|
+
line: rhs.startPosition.row + 1,
|
|
934
|
+
isMethod: false,
|
|
935
|
+
isFunctionReference: true,
|
|
936
|
+
isPotentialCallback: true,
|
|
937
|
+
enclosingFunction,
|
|
938
|
+
uncertain: false
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Detect function references in composite literal fields:
|
|
947
|
+
// ResourceEventHandlerFuncs{AddFunc: addNodeToCache, UpdateFunc: updateNode}
|
|
948
|
+
// keyed_element → literal_element(key) ":" literal_element(value)
|
|
949
|
+
// Go wraps values in literal_element nodes — unwrap to get the actual expression
|
|
950
|
+
if (node.type === 'keyed_element') {
|
|
951
|
+
// The value (second child, unwrap literal_element if present)
|
|
952
|
+
let valueNode = node.namedChildCount >= 2 ? node.namedChild(node.namedChildCount - 1) : null;
|
|
953
|
+
if (valueNode && valueNode.type === 'literal_element') {
|
|
954
|
+
valueNode = valueNode.namedChildCount > 0 ? valueNode.namedChild(0) : null;
|
|
955
|
+
}
|
|
956
|
+
if (valueNode) {
|
|
957
|
+
if (valueNode.type === 'identifier') {
|
|
958
|
+
const name = valueNode.text;
|
|
959
|
+
if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) && !importAliases.has(name) && /^[a-zA-Z]/.test(name)) {
|
|
960
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
961
|
+
calls.push({
|
|
962
|
+
name,
|
|
963
|
+
line: valueNode.startPosition.row + 1,
|
|
964
|
+
isMethod: false,
|
|
965
|
+
isFunctionReference: true,
|
|
966
|
+
isPotentialCallback: true,
|
|
967
|
+
enclosingFunction,
|
|
968
|
+
uncertain: false
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (valueNode.type === 'selector_expression') {
|
|
973
|
+
const fieldNode = valueNode.childForFieldName('field');
|
|
974
|
+
const operandNode = valueNode.childForFieldName('operand');
|
|
975
|
+
if (fieldNode && operandNode) {
|
|
976
|
+
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
977
|
+
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
978
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
979
|
+
calls.push({
|
|
980
|
+
name: fieldNode.text,
|
|
981
|
+
line: valueNode.startPosition.row + 1,
|
|
982
|
+
isMethod: true,
|
|
983
|
+
receiver,
|
|
984
|
+
...(receiverType && { receiverType }),
|
|
985
|
+
enclosingFunction,
|
|
986
|
+
isPotentialCallback: true,
|
|
987
|
+
uncertain: false
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
892
994
|
return true;
|
|
893
995
|
}, {
|
|
894
996
|
onLeave: (node) => {
|
package/mcp/server.js
CHANGED
|
@@ -45,20 +45,18 @@ const indexCache = new Map(); // projectDir → { index, checkedAt }
|
|
|
45
45
|
const MAX_CACHE_SIZE = 10;
|
|
46
46
|
const expandCacheInstance = new ExpandCache();
|
|
47
47
|
|
|
48
|
-
function getIndex(projectDir) {
|
|
48
|
+
function getIndex(projectDir, options) {
|
|
49
|
+
const maxFiles = options && options.maxFiles;
|
|
49
50
|
const absDir = path.resolve(projectDir);
|
|
50
51
|
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
|
|
51
52
|
throw new Error(`Project directory not found: ${absDir}`);
|
|
52
53
|
}
|
|
53
54
|
const root = findProjectRoot(absDir);
|
|
54
55
|
const cached = indexCache.get(root);
|
|
55
|
-
const STALE_CHECK_INTERVAL_MS = 2000;
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return cached.index; // Recently verified fresh
|
|
61
|
-
}
|
|
57
|
+
// Always check staleness — MCP is used in iterative agent loops where
|
|
58
|
+
// files change between requests, so a throttle causes stale results.
|
|
59
|
+
if (cached && !maxFiles) {
|
|
62
60
|
if (!cached.index.isCacheStale()) {
|
|
63
61
|
cached.checkedAt = Date.now();
|
|
64
62
|
return cached.index;
|
|
@@ -67,12 +65,15 @@ function getIndex(projectDir) {
|
|
|
67
65
|
|
|
68
66
|
// Build new index (or rebuild stale one)
|
|
69
67
|
const index = new ProjectIndex(root);
|
|
68
|
+
const buildOpts = { quiet: true, forceRebuild: false };
|
|
69
|
+
if (maxFiles) buildOpts.maxFiles = maxFiles;
|
|
70
70
|
const loaded = index.loadCache();
|
|
71
|
-
if (loaded && !index.isCacheStale()) {
|
|
72
|
-
// Disk cache is fresh
|
|
71
|
+
if (loaded && !maxFiles && !index.isCacheStale()) {
|
|
72
|
+
// Disk cache is fresh (skip when maxFiles is set — cached index may have different file count)
|
|
73
73
|
} else {
|
|
74
|
-
|
|
75
|
-
index.
|
|
74
|
+
buildOpts.forceRebuild = !!loaded;
|
|
75
|
+
index.build(null, buildOpts);
|
|
76
|
+
if (!maxFiles) index.saveCache(); // Don't pollute disk cache with partial indexes
|
|
76
77
|
// Clear expand cache entries for this project — stale after rebuild
|
|
77
78
|
expandCacheInstance.clearForRoot(root);
|
|
78
79
|
}
|
|
@@ -93,7 +94,10 @@ function getIndex(projectDir) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
// Don't cache partial indexes (maxFiles) — they'd serve wrong results for full queries
|
|
98
|
+
if (!maxFiles) {
|
|
99
|
+
indexCache.set(root, { index, checkedAt: Date.now() });
|
|
100
|
+
}
|
|
97
101
|
return index;
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -110,16 +114,29 @@ const server = new McpServer({
|
|
|
110
114
|
// TOOL HELPERS
|
|
111
115
|
// ============================================================================
|
|
112
116
|
|
|
113
|
-
const
|
|
117
|
+
const DEFAULT_OUTPUT_CHARS = 30000; // ~7.5K tokens — safe default for AI context
|
|
118
|
+
const MAX_OUTPUT_CHARS = 100000; // hard ceiling even with max_chars override
|
|
114
119
|
|
|
115
|
-
function toolResult(text) {
|
|
120
|
+
function toolResult(text, command, maxChars) {
|
|
116
121
|
if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
const limit = Math.min(maxChars || DEFAULT_OUTPUT_CHARS, MAX_OUTPUT_CHARS);
|
|
123
|
+
if (text.length > limit) {
|
|
124
|
+
const fullSize = text.length;
|
|
125
|
+
const fullTokens = Math.round(fullSize / 4);
|
|
126
|
+
const truncated = text.substring(0, limit);
|
|
119
127
|
// Cut at last newline to avoid breaking mid-line
|
|
120
128
|
const lastNewline = truncated.lastIndexOf('\n');
|
|
121
|
-
const cleanCut = lastNewline >
|
|
122
|
-
|
|
129
|
+
const cleanCut = lastNewline > limit * 0.8 ? truncated.substring(0, lastNewline) : truncated;
|
|
130
|
+
// Command-specific narrowing hints
|
|
131
|
+
let narrow = 'Use file=/in=/exclude= to narrow scope.';
|
|
132
|
+
if (command === 'toc') {
|
|
133
|
+
narrow = 'Use in= to scope to a subdirectory, or detailed=false for compact view.';
|
|
134
|
+
} else if (command === 'diff_impact') {
|
|
135
|
+
narrow = 'Use file= to scope to specific files/directories.';
|
|
136
|
+
} else if (command === 'affected_tests') {
|
|
137
|
+
narrow = 'Use file= to scope, exclude= to skip patterns.';
|
|
138
|
+
}
|
|
139
|
+
return { content: [{ type: 'text', text: cleanCut + `\n\n... OUTPUT TRUNCATED: showing ${limit} of ${fullSize} chars. Full output would be ~${fullTokens} tokens. ${narrow} Or use all=true to see everything (warning: ~${fullTokens} tokens).` }] };
|
|
123
140
|
}
|
|
124
141
|
return { content: [{ type: 'text', text }] };
|
|
125
142
|
}
|
|
@@ -268,6 +285,7 @@ server.registerTool(
|
|
|
268
285
|
class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
|
|
269
286
|
limit: z.number().optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed.'),
|
|
270
287
|
max_files: z.number().optional().describe('Max files to index (default: 10000). Use for very large codebases.'),
|
|
288
|
+
max_chars: z.number().optional().describe('Max output chars before truncation (default: 30000 ~7.5K tokens, max: 100000 ~25K tokens). Use all=true to bypass all caps, or set this for fine-grained control. Truncation message shows full size.'),
|
|
271
289
|
// Structural search flags (search command)
|
|
272
290
|
type: z.string().optional().describe('Symbol type filter for structural search: function, class, call, method, type. Triggers index-based search.'),
|
|
273
291
|
param: z.string().optional().describe('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
|
|
@@ -287,6 +305,11 @@ server.registerTool(
|
|
|
287
305
|
// This eliminates per-case param selection and prevents CLI/MCP drift.
|
|
288
306
|
const { command: _c, project_dir: _p, ...rawParams } = args;
|
|
289
307
|
const ep = normalizeParams(rawParams);
|
|
308
|
+
// all=true bypasses both formatter caps AND char truncation (parity with CLI --all)
|
|
309
|
+
const maxChars = ep.all ? MAX_OUTPUT_CHARS : ep.maxChars;
|
|
310
|
+
|
|
311
|
+
// Wrap toolResult to auto-inject maxChars from this request
|
|
312
|
+
const tr = (text, cmd) => toolResult(text, cmd, maxChars);
|
|
290
313
|
|
|
291
314
|
try {
|
|
292
315
|
switch (command) {
|
|
@@ -298,10 +321,10 @@ server.registerTool(
|
|
|
298
321
|
// ── Commands using shared executor ─────────────────────────
|
|
299
322
|
|
|
300
323
|
case 'about': {
|
|
301
|
-
const index = getIndex(project_dir);
|
|
324
|
+
const index = getIndex(project_dir, ep);
|
|
302
325
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
303
|
-
if (!ok) return
|
|
304
|
-
return
|
|
326
|
+
if (!ok) return tr(error); // soft error — won't kill sibling calls
|
|
327
|
+
return tr(output.formatAbout(result, {
|
|
305
328
|
allHint: 'Repeat with all=true to show all.',
|
|
306
329
|
methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.',
|
|
307
330
|
showConfidence: ep.showConfidence,
|
|
@@ -309,73 +332,73 @@ server.registerTool(
|
|
|
309
332
|
}
|
|
310
333
|
|
|
311
334
|
case 'context': {
|
|
312
|
-
const index = getIndex(project_dir);
|
|
335
|
+
const index = getIndex(project_dir, ep);
|
|
313
336
|
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
314
|
-
if (!ok) return
|
|
337
|
+
if (!ok) return tr(error); // context uses soft error (not toolError)
|
|
315
338
|
const { text, expandable } = output.formatContext(ctx, {
|
|
316
339
|
expandHint: 'Use expand command with item number to see code for any item.',
|
|
317
340
|
showConfidence: ep.showConfidence,
|
|
318
341
|
});
|
|
319
342
|
expandCacheInstance.save(index.root, ep.name, ep.file, expandable);
|
|
320
|
-
return
|
|
343
|
+
return tr(text);
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
case 'impact': {
|
|
324
|
-
const index = getIndex(project_dir);
|
|
347
|
+
const index = getIndex(project_dir, ep);
|
|
325
348
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
326
|
-
if (!ok) return
|
|
327
|
-
return
|
|
349
|
+
if (!ok) return tr(error); // soft error
|
|
350
|
+
return tr(output.formatImpact(result));
|
|
328
351
|
}
|
|
329
352
|
|
|
330
353
|
case 'blast': {
|
|
331
|
-
const index = getIndex(project_dir);
|
|
354
|
+
const index = getIndex(project_dir, ep);
|
|
332
355
|
const { ok, result, error } = execute(index, 'blast', ep);
|
|
333
|
-
if (!ok) return
|
|
334
|
-
return
|
|
356
|
+
if (!ok) return tr(error); // soft error
|
|
357
|
+
return tr(output.formatBlast(result, {
|
|
335
358
|
allHint: 'Set depth to expand all children.',
|
|
336
359
|
}));
|
|
337
360
|
}
|
|
338
361
|
|
|
339
362
|
case 'smart': {
|
|
340
|
-
const index = getIndex(project_dir);
|
|
363
|
+
const index = getIndex(project_dir, ep);
|
|
341
364
|
const { ok, result, error } = execute(index, 'smart', ep);
|
|
342
|
-
if (!ok) return
|
|
343
|
-
return
|
|
365
|
+
if (!ok) return tr(error); // soft error
|
|
366
|
+
return tr(output.formatSmart(result));
|
|
344
367
|
}
|
|
345
368
|
|
|
346
369
|
case 'trace': {
|
|
347
|
-
const index = getIndex(project_dir);
|
|
370
|
+
const index = getIndex(project_dir, ep);
|
|
348
371
|
const { ok, result, error } = execute(index, 'trace', ep);
|
|
349
|
-
if (!ok) return
|
|
350
|
-
return
|
|
372
|
+
if (!ok) return tr(error); // soft error
|
|
373
|
+
return tr(output.formatTrace(result, {
|
|
351
374
|
allHint: 'Set depth to expand all children.',
|
|
352
375
|
methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
|
|
353
376
|
}));
|
|
354
377
|
}
|
|
355
378
|
|
|
356
379
|
case 'reverse_trace': {
|
|
357
|
-
const index = getIndex(project_dir);
|
|
380
|
+
const index = getIndex(project_dir, ep);
|
|
358
381
|
const { ok, result, error } = execute(index, 'reverseTrace', ep);
|
|
359
|
-
if (!ok) return
|
|
360
|
-
return
|
|
382
|
+
if (!ok) return tr(error);
|
|
383
|
+
return tr(output.formatReverseTrace(result, {
|
|
361
384
|
allHint: 'Set depth to expand all children.',
|
|
362
385
|
}));
|
|
363
386
|
}
|
|
364
387
|
|
|
365
388
|
case 'example': {
|
|
366
|
-
const index = getIndex(project_dir);
|
|
389
|
+
const index = getIndex(project_dir, ep);
|
|
367
390
|
const { ok, result, error } = execute(index, 'example', ep);
|
|
368
|
-
if (!ok) return
|
|
369
|
-
if (!result) return
|
|
370
|
-
return
|
|
391
|
+
if (!ok) return tr(error);
|
|
392
|
+
if (!result) return tr(`No usage examples found for "${ep.name}".`);
|
|
393
|
+
return tr(output.formatExample(result, ep.name));
|
|
371
394
|
}
|
|
372
395
|
|
|
373
396
|
case 'related': {
|
|
374
|
-
const index = getIndex(project_dir);
|
|
397
|
+
const index = getIndex(project_dir, ep);
|
|
375
398
|
const { ok, result, error } = execute(index, 'related', ep);
|
|
376
|
-
if (!ok) return
|
|
377
|
-
if (!result) return
|
|
378
|
-
return
|
|
399
|
+
if (!ok) return tr(error);
|
|
400
|
+
if (!result) return tr(`Symbol "${ep.name}" not found.`);
|
|
401
|
+
return tr(output.formatRelated(result, {
|
|
379
402
|
all: ep.all || false, top: ep.top,
|
|
380
403
|
allHint: 'Repeat with all=true to show all.'
|
|
381
404
|
}));
|
|
@@ -384,62 +407,62 @@ server.registerTool(
|
|
|
384
407
|
// ── Finding Code ────────────────────────────────────────────
|
|
385
408
|
|
|
386
409
|
case 'find': {
|
|
387
|
-
const index = getIndex(project_dir);
|
|
410
|
+
const index = getIndex(project_dir, ep);
|
|
388
411
|
const { ok, result, error, note } = execute(index, 'find', ep);
|
|
389
|
-
if (!ok) return
|
|
412
|
+
if (!ok) return tr(error); // soft error
|
|
390
413
|
let text = output.formatFind(result, ep.name, ep.top);
|
|
391
414
|
if (note) text += '\n\n' + note;
|
|
392
|
-
return
|
|
415
|
+
return tr(text);
|
|
393
416
|
}
|
|
394
417
|
|
|
395
418
|
case 'usages': {
|
|
396
|
-
const index = getIndex(project_dir);
|
|
419
|
+
const index = getIndex(project_dir, ep);
|
|
397
420
|
const { ok, result, error, note } = execute(index, 'usages', ep);
|
|
398
|
-
if (!ok) return
|
|
421
|
+
if (!ok) return tr(error); // soft error
|
|
399
422
|
let text = output.formatUsages(result, ep.name);
|
|
400
423
|
if (note) text += '\n\n' + note;
|
|
401
|
-
return
|
|
424
|
+
return tr(text);
|
|
402
425
|
}
|
|
403
426
|
|
|
404
427
|
case 'toc': {
|
|
405
|
-
const index = getIndex(project_dir);
|
|
428
|
+
const index = getIndex(project_dir, ep);
|
|
406
429
|
const { ok, result, error, note } = execute(index, 'toc', ep);
|
|
407
|
-
if (!ok) return
|
|
430
|
+
if (!ok) return tr(error); // soft error
|
|
408
431
|
let text = output.formatToc(result, {
|
|
409
432
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
410
433
|
});
|
|
411
434
|
if (note) text += '\n\n' + note;
|
|
412
|
-
return
|
|
435
|
+
return tr(text, 'toc');
|
|
413
436
|
}
|
|
414
437
|
|
|
415
438
|
case 'search': {
|
|
416
|
-
const index = getIndex(project_dir);
|
|
439
|
+
const index = getIndex(project_dir, ep);
|
|
417
440
|
const { ok, result, error, structural } = execute(index, 'search', ep);
|
|
418
|
-
if (!ok) return
|
|
441
|
+
if (!ok) return tr(error); // soft error
|
|
419
442
|
if (structural) {
|
|
420
|
-
return
|
|
443
|
+
return tr(output.formatStructuralSearch(result));
|
|
421
444
|
}
|
|
422
|
-
return
|
|
445
|
+
return tr(output.formatSearch(result, ep.term));
|
|
423
446
|
}
|
|
424
447
|
|
|
425
448
|
case 'tests': {
|
|
426
|
-
const index = getIndex(project_dir);
|
|
449
|
+
const index = getIndex(project_dir, ep);
|
|
427
450
|
const { ok, result, error } = execute(index, 'tests', ep);
|
|
428
|
-
if (!ok) return
|
|
429
|
-
return
|
|
451
|
+
if (!ok) return tr(error); // soft error
|
|
452
|
+
return tr(output.formatTests(result, ep.name));
|
|
430
453
|
}
|
|
431
454
|
|
|
432
455
|
case 'affected_tests': {
|
|
433
|
-
const index = getIndex(project_dir);
|
|
456
|
+
const index = getIndex(project_dir, ep);
|
|
434
457
|
const { ok, result, error } = execute(index, 'affectedTests', ep);
|
|
435
|
-
if (!ok) return
|
|
436
|
-
return
|
|
458
|
+
if (!ok) return tr(error);
|
|
459
|
+
return tr(output.formatAffectedTests(result, { all: ep.all }), 'affected_tests');
|
|
437
460
|
}
|
|
438
461
|
|
|
439
462
|
case 'deadcode': {
|
|
440
|
-
const index = getIndex(project_dir);
|
|
463
|
+
const index = getIndex(project_dir, ep);
|
|
441
464
|
const { ok, result, error, note } = execute(index, 'deadcode', ep);
|
|
442
|
-
if (!ok) return
|
|
465
|
+
if (!ok) return tr(error); // soft error
|
|
443
466
|
const dcNote = note;
|
|
444
467
|
let dcText = output.formatDeadcode(result, {
|
|
445
468
|
top: ep.top || 0,
|
|
@@ -447,44 +470,44 @@ server.registerTool(
|
|
|
447
470
|
exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined
|
|
448
471
|
});
|
|
449
472
|
if (dcNote) dcText += '\n\n' + dcNote;
|
|
450
|
-
return
|
|
473
|
+
return tr(dcText);
|
|
451
474
|
}
|
|
452
475
|
|
|
453
476
|
case 'entrypoints': {
|
|
454
|
-
const index = getIndex(project_dir);
|
|
477
|
+
const index = getIndex(project_dir, ep);
|
|
455
478
|
const { ok, result, error } = execute(index, 'entrypoints', ep);
|
|
456
|
-
if (!ok) return
|
|
457
|
-
return
|
|
479
|
+
if (!ok) return tr(error);
|
|
480
|
+
return tr(output.formatEntrypoints(result));
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
// ── File Dependencies ───────────────────────────────────────
|
|
461
484
|
|
|
462
485
|
case 'imports': {
|
|
463
|
-
const index = getIndex(project_dir);
|
|
486
|
+
const index = getIndex(project_dir, ep);
|
|
464
487
|
const { ok, result, error } = execute(index, 'imports', ep);
|
|
465
|
-
if (!ok) return
|
|
466
|
-
return
|
|
488
|
+
if (!ok) return tr(error); // soft error
|
|
489
|
+
return tr(output.formatImports(result, ep.file));
|
|
467
490
|
}
|
|
468
491
|
|
|
469
492
|
case 'exporters': {
|
|
470
|
-
const index = getIndex(project_dir);
|
|
493
|
+
const index = getIndex(project_dir, ep);
|
|
471
494
|
const { ok, result, error } = execute(index, 'exporters', ep);
|
|
472
|
-
if (!ok) return
|
|
473
|
-
return
|
|
495
|
+
if (!ok) return tr(error); // soft error
|
|
496
|
+
return tr(output.formatExporters(result, ep.file));
|
|
474
497
|
}
|
|
475
498
|
|
|
476
499
|
case 'file_exports': {
|
|
477
|
-
const index = getIndex(project_dir);
|
|
500
|
+
const index = getIndex(project_dir, ep);
|
|
478
501
|
const { ok, result, error } = execute(index, 'fileExports', ep);
|
|
479
|
-
if (!ok) return
|
|
480
|
-
return
|
|
502
|
+
if (!ok) return tr(error); // soft error
|
|
503
|
+
return tr(output.formatFileExports(result, ep.file));
|
|
481
504
|
}
|
|
482
505
|
|
|
483
506
|
case 'graph': {
|
|
484
|
-
const index = getIndex(project_dir);
|
|
507
|
+
const index = getIndex(project_dir, ep);
|
|
485
508
|
const { ok, result, error } = execute(index, 'graph', ep);
|
|
486
|
-
if (!ok) return
|
|
487
|
-
return
|
|
509
|
+
if (!ok) return tr(error); // soft error
|
|
510
|
+
return tr(output.formatGraph(result, {
|
|
488
511
|
showAll: ep.all || ep.depth !== undefined,
|
|
489
512
|
maxDepth: ep.depth ?? 2, file: ep.file,
|
|
490
513
|
depthHint: 'Set depth parameter for deeper graph.',
|
|
@@ -493,65 +516,65 @@ server.registerTool(
|
|
|
493
516
|
}
|
|
494
517
|
|
|
495
518
|
case 'circular_deps': {
|
|
496
|
-
const index = getIndex(project_dir);
|
|
519
|
+
const index = getIndex(project_dir, ep);
|
|
497
520
|
const { ok, result, error } = execute(index, 'circularDeps', ep);
|
|
498
|
-
if (!ok) return
|
|
499
|
-
return
|
|
521
|
+
if (!ok) return tr(error);
|
|
522
|
+
return tr(output.formatCircularDeps(result));
|
|
500
523
|
}
|
|
501
524
|
|
|
502
525
|
// ── Refactoring ─────────────────────────────────────────────
|
|
503
526
|
|
|
504
527
|
case 'verify': {
|
|
505
|
-
const index = getIndex(project_dir);
|
|
528
|
+
const index = getIndex(project_dir, ep);
|
|
506
529
|
const { ok, result, error } = execute(index, 'verify', ep);
|
|
507
|
-
if (!ok) return
|
|
508
|
-
return
|
|
530
|
+
if (!ok) return tr(error); // soft error
|
|
531
|
+
return tr(output.formatVerify(result));
|
|
509
532
|
}
|
|
510
533
|
|
|
511
534
|
case 'plan': {
|
|
512
|
-
const index = getIndex(project_dir);
|
|
535
|
+
const index = getIndex(project_dir, ep);
|
|
513
536
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
514
|
-
if (!ok) return
|
|
515
|
-
return
|
|
537
|
+
if (!ok) return tr(error); // soft error
|
|
538
|
+
return tr(output.formatPlan(result));
|
|
516
539
|
}
|
|
517
540
|
|
|
518
541
|
case 'diff_impact': {
|
|
519
|
-
const index = getIndex(project_dir);
|
|
542
|
+
const index = getIndex(project_dir, ep);
|
|
520
543
|
const { ok, result, error } = execute(index, 'diffImpact', ep);
|
|
521
|
-
if (!ok) return
|
|
522
|
-
return
|
|
544
|
+
if (!ok) return tr(error); // soft error — e.g. "not a git repo"
|
|
545
|
+
return tr(output.formatDiffImpact(result, { all: ep.all }), 'diff_impact');
|
|
523
546
|
}
|
|
524
547
|
|
|
525
548
|
// ── Other ───────────────────────────────────────────────────
|
|
526
549
|
|
|
527
550
|
case 'typedef': {
|
|
528
|
-
const index = getIndex(project_dir);
|
|
551
|
+
const index = getIndex(project_dir, ep);
|
|
529
552
|
const { ok, result, error } = execute(index, 'typedef', ep);
|
|
530
|
-
if (!ok) return
|
|
531
|
-
return
|
|
553
|
+
if (!ok) return tr(error); // soft error
|
|
554
|
+
return tr(output.formatTypedef(result, ep.name));
|
|
532
555
|
}
|
|
533
556
|
|
|
534
557
|
case 'stacktrace': {
|
|
535
|
-
const index = getIndex(project_dir);
|
|
558
|
+
const index = getIndex(project_dir, ep);
|
|
536
559
|
const { ok, result, error } = execute(index, 'stacktrace', ep);
|
|
537
|
-
if (!ok) return
|
|
538
|
-
return
|
|
560
|
+
if (!ok) return tr(error); // soft error
|
|
561
|
+
return tr(output.formatStackTrace(result));
|
|
539
562
|
}
|
|
540
563
|
|
|
541
564
|
case 'api': {
|
|
542
|
-
const index = getIndex(project_dir);
|
|
565
|
+
const index = getIndex(project_dir, ep);
|
|
543
566
|
const { ok, result, error, note } = execute(index, 'api', ep);
|
|
544
|
-
if (!ok) return
|
|
567
|
+
if (!ok) return tr(error); // soft error
|
|
545
568
|
let apiText = output.formatApi(result, ep.file || '.');
|
|
546
569
|
if (note) apiText += '\n\n' + note;
|
|
547
|
-
return
|
|
570
|
+
return tr(apiText);
|
|
548
571
|
}
|
|
549
572
|
|
|
550
573
|
case 'stats': {
|
|
551
|
-
const index = getIndex(project_dir);
|
|
574
|
+
const index = getIndex(project_dir, ep);
|
|
552
575
|
const { ok, result, error } = execute(index, 'stats', ep);
|
|
553
|
-
if (!ok) return
|
|
554
|
-
return
|
|
576
|
+
if (!ok) return tr(error); // soft error
|
|
577
|
+
return tr(output.formatStats(result, { top: ep.top || 0 }));
|
|
555
578
|
}
|
|
556
579
|
|
|
557
580
|
// ── Extracting Code (via execute) ────────────────────────────
|
|
@@ -559,16 +582,16 @@ server.registerTool(
|
|
|
559
582
|
case 'fn': {
|
|
560
583
|
const err = requireName(ep.name);
|
|
561
584
|
if (err) return err;
|
|
562
|
-
const index = getIndex(project_dir);
|
|
585
|
+
const index = getIndex(project_dir, ep);
|
|
563
586
|
const { ok, result, error } = execute(index, 'fn', ep);
|
|
564
|
-
if (!ok) return
|
|
587
|
+
if (!ok) return tr(error); // soft error
|
|
565
588
|
// MCP path security: validate all result files are within project root
|
|
566
589
|
for (const entry of result.entries) {
|
|
567
590
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
568
591
|
if (typeof check !== 'string') return check;
|
|
569
592
|
}
|
|
570
593
|
const notes = result.notes.length ? result.notes.map(n => 'Note: ' + n).join('\n') + '\n\n' : '';
|
|
571
|
-
return
|
|
594
|
+
return tr(notes + output.formatFnResult(result));
|
|
572
595
|
}
|
|
573
596
|
|
|
574
597
|
case 'class': {
|
|
@@ -577,41 +600,41 @@ server.registerTool(
|
|
|
577
600
|
if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
|
|
578
601
|
return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
|
|
579
602
|
}
|
|
580
|
-
const index = getIndex(project_dir);
|
|
603
|
+
const index = getIndex(project_dir, ep);
|
|
581
604
|
const { ok, result, error } = execute(index, 'class', ep);
|
|
582
|
-
if (!ok) return
|
|
605
|
+
if (!ok) return tr(error); // soft error (class not found)
|
|
583
606
|
// MCP path security: validate all result files are within project root
|
|
584
607
|
for (const entry of result.entries) {
|
|
585
608
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
586
609
|
if (typeof check !== 'string') return check;
|
|
587
610
|
}
|
|
588
611
|
const notes = result.notes.length ? result.notes.map(n => 'Note: ' + n).join('\n') + '\n\n' : '';
|
|
589
|
-
return
|
|
612
|
+
return tr(notes + output.formatClassResult(result));
|
|
590
613
|
}
|
|
591
614
|
|
|
592
615
|
case 'lines': {
|
|
593
|
-
const index = getIndex(project_dir);
|
|
616
|
+
const index = getIndex(project_dir, ep);
|
|
594
617
|
const { ok, result, error } = execute(index, 'lines', ep);
|
|
595
|
-
if (!ok) return
|
|
618
|
+
if (!ok) return tr(error); // soft error
|
|
596
619
|
// MCP path security: validate file is within project root
|
|
597
620
|
const check = resolveAndValidatePath(index, result.relativePath);
|
|
598
621
|
if (typeof check !== 'string') return check;
|
|
599
|
-
return
|
|
622
|
+
return tr(output.formatLines(result));
|
|
600
623
|
}
|
|
601
624
|
|
|
602
625
|
case 'expand': {
|
|
603
626
|
if (ep.item === undefined || ep.item === null) {
|
|
604
627
|
return toolError('Item number is required (e.g. item=1).');
|
|
605
628
|
}
|
|
606
|
-
const index = getIndex(project_dir);
|
|
629
|
+
const index = getIndex(project_dir, ep);
|
|
607
630
|
const lookup = expandCacheInstance.lookup(index.root, ep.item);
|
|
608
631
|
const { ok, result, error } = execute(index, 'expand', {
|
|
609
632
|
match: lookup.match, itemNum: ep.item,
|
|
610
633
|
itemCount: lookup.itemCount, symbolName: lookup.symbolName,
|
|
611
634
|
validateRoot: true
|
|
612
635
|
});
|
|
613
|
-
if (!ok) return
|
|
614
|
-
return
|
|
636
|
+
if (!ok) return tr(error); // soft error
|
|
637
|
+
return tr(result.text);
|
|
615
638
|
}
|
|
616
639
|
|
|
617
640
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.7",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|