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 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
 
@@ -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
- // 1. Scan all symbols for decorator/modifier-based patterns
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
- const unfiltered = index.about(p.name, {
191
- withTypes: p.withTypes || false,
192
- all: false,
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
- return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found in: ${loc}. Use the correct --file or Class.method syntax.` };
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 result.testFiles) {
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 fn.callers) {
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: ${definitions.filter(d => d !== def).map(d => `${d.relativePath}:${d.startLine}`).join(', ')}. Specify a file to disambiguate.`,
975
- alternatives: definitions.filter(d => d !== def).map(d => ({
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 (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
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
- const methodDefs = this.symbols.get(name);
3345
- if (methodDefs) {
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 (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
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
- const methodDefs = this.symbols.get(symbol.name);
5167
- if (methodDefs) {
5168
- const classNames = new Set();
5169
- for (const d of methodDefs) {
5170
- if (d.className) classNames.add(d.className);
5171
- else if (d.receiver) classNames.add(d.receiver.replace(/^\*/, ''));
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
@@ -83,6 +83,7 @@ const PARAM_MAP = {
83
83
  default_value: 'defaultValue',
84
84
  top_level: 'topLevel',
85
85
  max_files: 'maxFiles',
86
+ max_chars: 'maxChars',
86
87
  };
87
88
 
88
89
  // ============================================================================
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
- // Throttle staleness checks isCacheStale() re-globs and stats all files
58
- if (cached) {
59
- if (Date.now() - cached.checkedAt < STALE_CHECK_INTERVAL_MS) {
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
- index.build(null, { quiet: true, forceRebuild: loaded });
75
- index.saveCache();
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
- indexCache.set(root, { index, checkedAt: Date.now() });
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 MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
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
- if (text.length > MAX_OUTPUT_CHARS) {
118
- const truncated = text.substring(0, MAX_OUTPUT_CHARS);
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 > MAX_OUTPUT_CHARS * 0.8 ? truncated.substring(0, lastNewline) : truncated;
122
- return { content: [{ type: 'text', text: cleanCut + '\n\n... (output truncated — refine query or use file/in/exclude parameters to narrow scope)' }] };
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 toolResult(error); // soft error — won't kill sibling calls
304
- return toolResult(output.formatAbout(result, {
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 toolResult(error); // context uses soft error (not toolError)
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 toolResult(text);
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 toolResult(error); // soft error
327
- return toolResult(output.formatImpact(result));
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 toolResult(error); // soft error
334
- return toolResult(output.formatBlast(result, {
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 toolResult(error); // soft error
343
- return toolResult(output.formatSmart(result));
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 toolResult(error); // soft error
350
- return toolResult(output.formatTrace(result, {
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 toolResult(error);
360
- return toolResult(output.formatReverseTrace(result, {
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 toolResult(error);
369
- if (!result) return toolResult(`No usage examples found for "${ep.name}".`);
370
- return toolResult(output.formatExample(result, ep.name));
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 toolResult(error);
377
- if (!result) return toolResult(`Symbol "${ep.name}" not found.`);
378
- return toolResult(output.formatRelated(result, {
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
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 toolResult(text);
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 toolResult(error); // soft error
441
+ if (!ok) return tr(error); // soft error
419
442
  if (structural) {
420
- return toolResult(output.formatStructuralSearch(result));
443
+ return tr(output.formatStructuralSearch(result));
421
444
  }
422
- return toolResult(output.formatSearch(result, ep.term));
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 toolResult(error); // soft error
429
- return toolResult(output.formatTests(result, ep.name));
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 toolResult(error);
436
- return toolResult(output.formatAffectedTests(result));
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 toolResult(error); // soft error
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 toolResult(dcText);
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 toolResult(error);
457
- return toolResult(output.formatEntrypoints(result));
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 toolResult(error); // soft error
466
- return toolResult(output.formatImports(result, ep.file));
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 toolResult(error); // soft error
473
- return toolResult(output.formatExporters(result, ep.file));
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 toolResult(error); // soft error
480
- return toolResult(output.formatFileExports(result, ep.file));
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 toolResult(error); // soft error
487
- return toolResult(output.formatGraph(result, {
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 toolResult(error);
499
- return toolResult(output.formatCircularDeps(result));
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 toolResult(error); // soft error
508
- return toolResult(output.formatVerify(result));
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 toolResult(error); // soft error
515
- return toolResult(output.formatPlan(result));
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 toolResult(error); // soft error — e.g. "not a git repo"
522
- return toolResult(output.formatDiffImpact(result));
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 toolResult(error); // soft error
531
- return toolResult(output.formatTypedef(result, ep.name));
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 toolResult(error); // soft error
538
- return toolResult(output.formatStackTrace(result));
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 toolResult(error); // soft error
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 toolResult(apiText);
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 toolResult(error); // soft error
554
- return toolResult(output.formatStats(result, { top: ep.top || 0 }));
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 toolResult(error); // soft error
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 toolResult(notes + output.formatFnResult(result));
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 toolResult(error); // soft error (class not found)
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 toolResult(notes + output.formatClassResult(result));
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 toolResult(error); // soft error
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 toolResult(output.formatLines(result));
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 toolResult(error); // soft error
614
- return toolResult(result.text);
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.5",
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",