ucn 3.8.10 → 3.8.12
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/.github/workflows/ci.yml +33 -0
- package/.github/workflows/publish.yml +67 -0
- package/README.md +5 -2
- package/core/project.js +30 -9
- package/languages/go.js +249 -216
- package/languages/java.js +303 -250
- package/languages/javascript.js +463 -412
- package/languages/python.js +189 -148
- package/languages/rust.js +394 -337
- package/languages/utils.js +89 -10
- package/mcp/server.js +65 -49
- package/package.json +1 -1
- package/.claude/scheduled_tasks.lock +0 -1
package/languages/utils.js
CHANGED
|
@@ -26,12 +26,13 @@ function traverseTree(node, callback, options) {
|
|
|
26
26
|
* @param {string} code - Original source code
|
|
27
27
|
* @returns {{ startLine: number, endLine: number, indent: number }}
|
|
28
28
|
*/
|
|
29
|
-
function nodeToLocation(node,
|
|
29
|
+
function nodeToLocation(node, codeOrLines) {
|
|
30
30
|
const startLine = node.startPosition.row + 1; // tree-sitter is 0-indexed
|
|
31
31
|
const endLine = node.endPosition.row + 1;
|
|
32
32
|
|
|
33
33
|
// Calculate indent from start of line
|
|
34
|
-
|
|
34
|
+
// Accept pre-split lines array to avoid repeated code.split('\n')
|
|
35
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
35
36
|
const firstLine = lines[node.startPosition.row] || '';
|
|
36
37
|
const indentMatch = firstLine.match(/^(\s*)/);
|
|
37
38
|
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
@@ -235,8 +236,8 @@ function parseJavaParam(param, info) {
|
|
|
235
236
|
* @param {number} startLine - 1-indexed line number of the function/class
|
|
236
237
|
* @returns {string|null} First line of docstring or null
|
|
237
238
|
*/
|
|
238
|
-
function extractJSDocstring(
|
|
239
|
-
const lines =
|
|
239
|
+
function extractJSDocstring(codeOrLines, startLine) {
|
|
240
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
240
241
|
const lineIndex = startLine - 1;
|
|
241
242
|
if (lineIndex <= 0) return null;
|
|
242
243
|
|
|
@@ -280,8 +281,8 @@ function extractJSDocstring(code, startLine) {
|
|
|
280
281
|
* @param {number} defLine - 1-indexed line number of the def/class (not decorator)
|
|
281
282
|
* @returns {string|null} First line of docstring or null
|
|
282
283
|
*/
|
|
283
|
-
function extractPythonDocstring(
|
|
284
|
-
const lines =
|
|
284
|
+
function extractPythonDocstring(codeOrLines, defLine) {
|
|
285
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
285
286
|
// Python docstring is INSIDE the function, on lines after the def:
|
|
286
287
|
let i = defLine; // Start after the def line (defLine is 1-indexed)
|
|
287
288
|
// Skip to find the first non-empty line inside the function
|
|
@@ -316,8 +317,8 @@ function extractPythonDocstring(code, defLine) {
|
|
|
316
317
|
* @param {number} startLine - 1-indexed line number of the function
|
|
317
318
|
* @returns {string|null} First line of doc comment or null
|
|
318
319
|
*/
|
|
319
|
-
function extractGoDocstring(
|
|
320
|
-
const lines =
|
|
320
|
+
function extractGoDocstring(codeOrLines, startLine) {
|
|
321
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
321
322
|
const lineIndex = startLine - 1;
|
|
322
323
|
if (lineIndex <= 0) return null;
|
|
323
324
|
|
|
@@ -349,8 +350,8 @@ function extractGoDocstring(code, startLine) {
|
|
|
349
350
|
* @param {number} startLine - 1-indexed line number of the item
|
|
350
351
|
* @returns {string|null} First line of doc comment or null
|
|
351
352
|
*/
|
|
352
|
-
function extractRustDocstring(
|
|
353
|
-
const lines =
|
|
353
|
+
function extractRustDocstring(codeOrLines, startLine) {
|
|
354
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
354
355
|
const lineIndex = startLine - 1;
|
|
355
356
|
if (lineIndex <= 0) return null;
|
|
356
357
|
|
|
@@ -515,8 +516,86 @@ function findMatchesWithASTFilter(content, term, parser, options = {}) {
|
|
|
515
516
|
return matches;
|
|
516
517
|
}
|
|
517
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Single-entry cache for flat node lists.
|
|
521
|
+
* During indexFile(), the same tree is traversed 5+ times (findFunctions,
|
|
522
|
+
* findClasses, findStateObjects, findImports, findExports). Building a flat
|
|
523
|
+
* list once and iterating it for each pass eliminates repeated recursive
|
|
524
|
+
* traversal overhead (namedChild object creation, function call overhead).
|
|
525
|
+
*/
|
|
526
|
+
let _cachedRootNode = null;
|
|
527
|
+
let _cachedNodeList = null;
|
|
528
|
+
let _cachedSubtreeEnds = null;
|
|
529
|
+
|
|
530
|
+
function _buildNodeList(rootNode) {
|
|
531
|
+
const nodes = [];
|
|
532
|
+
const subtreeEnds = [];
|
|
533
|
+
const stack = [rootNode];
|
|
534
|
+
// Iterative DFS with subtreeEnd tracking
|
|
535
|
+
// We use a post-processing step to fill subtreeEnds
|
|
536
|
+
function collect(node) {
|
|
537
|
+
const idx = nodes.length;
|
|
538
|
+
nodes.push(node);
|
|
539
|
+
subtreeEnds.push(0);
|
|
540
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
541
|
+
collect(node.namedChild(i));
|
|
542
|
+
}
|
|
543
|
+
subtreeEnds[idx] = nodes.length;
|
|
544
|
+
}
|
|
545
|
+
collect(rootNode);
|
|
546
|
+
return { nodes, subtreeEnds };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get or build a cached flat node list for the given tree.
|
|
551
|
+
* Returns { nodes: SyntaxNode[], subtreeEnds: number[] }.
|
|
552
|
+
* subtreeEnds[i] is the index past the last descendant of nodes[i],
|
|
553
|
+
* enabling O(1) subtree skipping (for 'return false' semantics).
|
|
554
|
+
*/
|
|
555
|
+
function getCachedNodeList(rootNode) {
|
|
556
|
+
if (rootNode === _cachedRootNode && _cachedNodeList) {
|
|
557
|
+
return { nodes: _cachedNodeList, subtreeEnds: _cachedSubtreeEnds };
|
|
558
|
+
}
|
|
559
|
+
const { nodes, subtreeEnds } = _buildNodeList(rootNode);
|
|
560
|
+
_cachedRootNode = rootNode;
|
|
561
|
+
_cachedNodeList = nodes;
|
|
562
|
+
_cachedSubtreeEnds = subtreeEnds;
|
|
563
|
+
return { nodes, subtreeEnds };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Traverse a tree-sitter AST using a cached flat node list.
|
|
568
|
+
* Semantically equivalent to traverseTree() but ~3x faster when the same
|
|
569
|
+
* tree is traversed multiple times (which happens 5+ times per file during build).
|
|
570
|
+
* Supports 'return false' to skip a node's entire subtree.
|
|
571
|
+
*
|
|
572
|
+
* NOTE: Does not support onLeave callbacks. Use traverseTree() for those.
|
|
573
|
+
*/
|
|
574
|
+
function traverseTreeCached(rootNode, callback) {
|
|
575
|
+
const { nodes, subtreeEnds } = getCachedNodeList(rootNode);
|
|
576
|
+
for (let i = 0; i < nodes.length; ) {
|
|
577
|
+
if (callback(nodes[i]) === false) {
|
|
578
|
+
i = subtreeEnds[i];
|
|
579
|
+
} else {
|
|
580
|
+
i++;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Clear the cached node list (call when the tree changes).
|
|
587
|
+
*/
|
|
588
|
+
function clearNodeListCache() {
|
|
589
|
+
_cachedRootNode = null;
|
|
590
|
+
_cachedNodeList = null;
|
|
591
|
+
_cachedSubtreeEnds = null;
|
|
592
|
+
}
|
|
593
|
+
|
|
518
594
|
module.exports = {
|
|
519
595
|
traverseTree,
|
|
596
|
+
traverseTreeCached,
|
|
597
|
+
getCachedNodeList,
|
|
598
|
+
clearNodeListCache,
|
|
520
599
|
nodeToLocation,
|
|
521
600
|
extractParams,
|
|
522
601
|
parseStructuredParams,
|
package/mcp/server.js
CHANGED
|
@@ -114,12 +114,17 @@ const server = new McpServer({
|
|
|
114
114
|
// TOOL HELPERS
|
|
115
115
|
// ============================================================================
|
|
116
116
|
|
|
117
|
-
const DEFAULT_OUTPUT_CHARS = 10000; // ~2.5K tokens —
|
|
117
|
+
const DEFAULT_OUTPUT_CHARS = 10000; // ~2.5K tokens — targeted commands (about, context, smart, etc.)
|
|
118
|
+
const BROAD_OUTPUT_CHARS = 3000; // ~750 tokens — broad commands where truncated listings are useless
|
|
118
119
|
const MAX_OUTPUT_CHARS = 100000; // hard ceiling even with max_chars override
|
|
119
120
|
|
|
121
|
+
// Broad commands: output is project-wide, truncation means you need a filter, not more text
|
|
122
|
+
const BROAD_COMMANDS = new Set(['toc', 'entrypoints', 'diff_impact', 'affected_tests', 'deadcode', 'usages']);
|
|
123
|
+
|
|
120
124
|
function toolResult(text, command, maxChars) {
|
|
121
125
|
if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
|
|
122
|
-
const
|
|
126
|
+
const defaultLimit = BROAD_COMMANDS.has(command) ? BROAD_OUTPUT_CHARS : DEFAULT_OUTPUT_CHARS;
|
|
127
|
+
const limit = Math.min(maxChars || defaultLimit, MAX_OUTPUT_CHARS);
|
|
123
128
|
if (text.length > limit) {
|
|
124
129
|
const fullSize = text.length;
|
|
125
130
|
const fullTokens = Math.round(fullSize / 4);
|
|
@@ -128,14 +133,15 @@ function toolResult(text, command, maxChars) {
|
|
|
128
133
|
const lastNewline = truncated.lastIndexOf('\n');
|
|
129
134
|
const cleanCut = lastNewline > limit * 0.8 ? truncated.substring(0, lastNewline) : truncated;
|
|
130
135
|
// Command-specific narrowing hints
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
136
|
+
const hints = {
|
|
137
|
+
toc: 'Use in= to scope to a subdirectory, or detailed=false for compact view.',
|
|
138
|
+
entrypoints: 'Use framework= to filter by framework, exclude= to skip patterns.',
|
|
139
|
+
diff_impact: 'Use file= to scope to specific files/directories.',
|
|
140
|
+
affected_tests: 'Use file= to scope, exclude= to skip patterns.',
|
|
141
|
+
deadcode: 'Use file= to scope, exclude= to skip patterns.',
|
|
142
|
+
usages: 'Use file= to scope to specific files.',
|
|
143
|
+
};
|
|
144
|
+
const narrow = hints[command] || 'Use file=/in=/exclude= to narrow scope.';
|
|
139
145
|
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).` }] };
|
|
140
146
|
}
|
|
141
147
|
return { content: [{ type: 'text', text }] };
|
|
@@ -285,7 +291,7 @@ server.registerTool(
|
|
|
285
291
|
class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
|
|
286
292
|
limit: z.number().optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed.'),
|
|
287
293
|
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 (
|
|
294
|
+
max_chars: z.number().optional().describe('Max output chars before truncation. Targeted commands (about, context, smart, etc.): 10K default. Broad commands (toc, entrypoints, deadcode, etc.): 3K default. Max: 100K. Use all=true to bypass all caps.'),
|
|
289
295
|
// Structural search flags (search command)
|
|
290
296
|
type: z.string().optional().describe('Symbol type filter for structural search: function, class, call, method, type. Triggers index-based search.'),
|
|
291
297
|
param: z.string().optional().describe('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
|
|
@@ -308,9 +314,10 @@ server.registerTool(
|
|
|
308
314
|
// all=true bypasses both formatter caps AND char truncation (parity with CLI --all)
|
|
309
315
|
const maxChars = ep.all ? MAX_OUTPUT_CHARS : ep.maxChars;
|
|
310
316
|
|
|
311
|
-
// Wrap toolResult to auto-inject maxChars from this request
|
|
312
|
-
const tr = (text
|
|
317
|
+
// Wrap toolResult to auto-inject command + maxChars from this request
|
|
318
|
+
const tr = (text) => toolResult(text, command, maxChars);
|
|
313
319
|
|
|
320
|
+
let index = null; // Track for post-command cache save
|
|
314
321
|
try {
|
|
315
322
|
switch (command) {
|
|
316
323
|
|
|
@@ -321,7 +328,7 @@ server.registerTool(
|
|
|
321
328
|
// ── Commands using shared executor ─────────────────────────
|
|
322
329
|
|
|
323
330
|
case 'about': {
|
|
324
|
-
|
|
331
|
+
index = getIndex(project_dir, ep);
|
|
325
332
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
326
333
|
if (!ok) return tr(error); // soft error — won't kill sibling calls
|
|
327
334
|
return tr(output.formatAbout(result, {
|
|
@@ -332,7 +339,7 @@ server.registerTool(
|
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
case 'context': {
|
|
335
|
-
|
|
342
|
+
index = getIndex(project_dir, ep);
|
|
336
343
|
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
337
344
|
if (!ok) return tr(error); // context uses soft error (not toolError)
|
|
338
345
|
const { text, expandable } = output.formatContext(ctx, {
|
|
@@ -344,14 +351,14 @@ server.registerTool(
|
|
|
344
351
|
}
|
|
345
352
|
|
|
346
353
|
case 'impact': {
|
|
347
|
-
|
|
354
|
+
index = getIndex(project_dir, ep);
|
|
348
355
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
349
356
|
if (!ok) return tr(error); // soft error
|
|
350
357
|
return tr(output.formatImpact(result));
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
case 'blast': {
|
|
354
|
-
|
|
361
|
+
index = getIndex(project_dir, ep);
|
|
355
362
|
const { ok, result, error } = execute(index, 'blast', ep);
|
|
356
363
|
if (!ok) return tr(error); // soft error
|
|
357
364
|
return tr(output.formatBlast(result, {
|
|
@@ -360,14 +367,14 @@ server.registerTool(
|
|
|
360
367
|
}
|
|
361
368
|
|
|
362
369
|
case 'smart': {
|
|
363
|
-
|
|
370
|
+
index = getIndex(project_dir, ep);
|
|
364
371
|
const { ok, result, error } = execute(index, 'smart', ep);
|
|
365
372
|
if (!ok) return tr(error); // soft error
|
|
366
373
|
return tr(output.formatSmart(result));
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
case 'trace': {
|
|
370
|
-
|
|
377
|
+
index = getIndex(project_dir, ep);
|
|
371
378
|
const { ok, result, error } = execute(index, 'trace', ep);
|
|
372
379
|
if (!ok) return tr(error); // soft error
|
|
373
380
|
return tr(output.formatTrace(result, {
|
|
@@ -377,7 +384,7 @@ server.registerTool(
|
|
|
377
384
|
}
|
|
378
385
|
|
|
379
386
|
case 'reverse_trace': {
|
|
380
|
-
|
|
387
|
+
index = getIndex(project_dir, ep);
|
|
381
388
|
const { ok, result, error } = execute(index, 'reverseTrace', ep);
|
|
382
389
|
if (!ok) return tr(error);
|
|
383
390
|
return tr(output.formatReverseTrace(result, {
|
|
@@ -386,7 +393,7 @@ server.registerTool(
|
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
case 'example': {
|
|
389
|
-
|
|
396
|
+
index = getIndex(project_dir, ep);
|
|
390
397
|
const { ok, result, error } = execute(index, 'example', ep);
|
|
391
398
|
if (!ok) return tr(error);
|
|
392
399
|
if (!result) return tr(`No usage examples found for "${ep.name}".`);
|
|
@@ -394,7 +401,7 @@ server.registerTool(
|
|
|
394
401
|
}
|
|
395
402
|
|
|
396
403
|
case 'related': {
|
|
397
|
-
|
|
404
|
+
index = getIndex(project_dir, ep);
|
|
398
405
|
const { ok, result, error } = execute(index, 'related', ep);
|
|
399
406
|
if (!ok) return tr(error);
|
|
400
407
|
if (!result) return tr(`Symbol "${ep.name}" not found.`);
|
|
@@ -407,7 +414,7 @@ server.registerTool(
|
|
|
407
414
|
// ── Finding Code ────────────────────────────────────────────
|
|
408
415
|
|
|
409
416
|
case 'find': {
|
|
410
|
-
|
|
417
|
+
index = getIndex(project_dir, ep);
|
|
411
418
|
const { ok, result, error, note } = execute(index, 'find', ep);
|
|
412
419
|
if (!ok) return tr(error); // soft error
|
|
413
420
|
let text = output.formatFind(result, ep.name, ep.top);
|
|
@@ -416,7 +423,7 @@ server.registerTool(
|
|
|
416
423
|
}
|
|
417
424
|
|
|
418
425
|
case 'usages': {
|
|
419
|
-
|
|
426
|
+
index = getIndex(project_dir, ep);
|
|
420
427
|
const { ok, result, error, note } = execute(index, 'usages', ep);
|
|
421
428
|
if (!ok) return tr(error); // soft error
|
|
422
429
|
let text = output.formatUsages(result, ep.name);
|
|
@@ -425,18 +432,18 @@ server.registerTool(
|
|
|
425
432
|
}
|
|
426
433
|
|
|
427
434
|
case 'toc': {
|
|
428
|
-
|
|
435
|
+
index = getIndex(project_dir, ep);
|
|
429
436
|
const { ok, result, error, note } = execute(index, 'toc', ep);
|
|
430
437
|
if (!ok) return tr(error); // soft error
|
|
431
438
|
let text = output.formatToc(result, {
|
|
432
439
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
433
440
|
});
|
|
434
441
|
if (note) text += '\n\n' + note;
|
|
435
|
-
return tr(text
|
|
442
|
+
return tr(text);
|
|
436
443
|
}
|
|
437
444
|
|
|
438
445
|
case 'search': {
|
|
439
|
-
|
|
446
|
+
index = getIndex(project_dir, ep);
|
|
440
447
|
const { ok, result, error, structural } = execute(index, 'search', ep);
|
|
441
448
|
if (!ok) return tr(error); // soft error
|
|
442
449
|
if (structural) {
|
|
@@ -446,21 +453,21 @@ server.registerTool(
|
|
|
446
453
|
}
|
|
447
454
|
|
|
448
455
|
case 'tests': {
|
|
449
|
-
|
|
456
|
+
index = getIndex(project_dir, ep);
|
|
450
457
|
const { ok, result, error } = execute(index, 'tests', ep);
|
|
451
458
|
if (!ok) return tr(error); // soft error
|
|
452
459
|
return tr(output.formatTests(result, ep.name));
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
case 'affected_tests': {
|
|
456
|
-
|
|
463
|
+
index = getIndex(project_dir, ep);
|
|
457
464
|
const { ok, result, error } = execute(index, 'affectedTests', ep);
|
|
458
465
|
if (!ok) return tr(error);
|
|
459
|
-
return tr(output.formatAffectedTests(result, { all: ep.all })
|
|
466
|
+
return tr(output.formatAffectedTests(result, { all: ep.all }));
|
|
460
467
|
}
|
|
461
468
|
|
|
462
469
|
case 'deadcode': {
|
|
463
|
-
|
|
470
|
+
index = getIndex(project_dir, ep);
|
|
464
471
|
const { ok, result, error, note } = execute(index, 'deadcode', ep);
|
|
465
472
|
if (!ok) return tr(error); // soft error
|
|
466
473
|
const dcNote = note;
|
|
@@ -474,7 +481,7 @@ server.registerTool(
|
|
|
474
481
|
}
|
|
475
482
|
|
|
476
483
|
case 'entrypoints': {
|
|
477
|
-
|
|
484
|
+
index = getIndex(project_dir, ep);
|
|
478
485
|
const { ok, result, error } = execute(index, 'entrypoints', ep);
|
|
479
486
|
if (!ok) return tr(error);
|
|
480
487
|
return tr(output.formatEntrypoints(result));
|
|
@@ -483,28 +490,28 @@ server.registerTool(
|
|
|
483
490
|
// ── File Dependencies ───────────────────────────────────────
|
|
484
491
|
|
|
485
492
|
case 'imports': {
|
|
486
|
-
|
|
493
|
+
index = getIndex(project_dir, ep);
|
|
487
494
|
const { ok, result, error } = execute(index, 'imports', ep);
|
|
488
495
|
if (!ok) return tr(error); // soft error
|
|
489
496
|
return tr(output.formatImports(result, ep.file));
|
|
490
497
|
}
|
|
491
498
|
|
|
492
499
|
case 'exporters': {
|
|
493
|
-
|
|
500
|
+
index = getIndex(project_dir, ep);
|
|
494
501
|
const { ok, result, error } = execute(index, 'exporters', ep);
|
|
495
502
|
if (!ok) return tr(error); // soft error
|
|
496
503
|
return tr(output.formatExporters(result, ep.file));
|
|
497
504
|
}
|
|
498
505
|
|
|
499
506
|
case 'file_exports': {
|
|
500
|
-
|
|
507
|
+
index = getIndex(project_dir, ep);
|
|
501
508
|
const { ok, result, error } = execute(index, 'fileExports', ep);
|
|
502
509
|
if (!ok) return tr(error); // soft error
|
|
503
510
|
return tr(output.formatFileExports(result, ep.file));
|
|
504
511
|
}
|
|
505
512
|
|
|
506
513
|
case 'graph': {
|
|
507
|
-
|
|
514
|
+
index = getIndex(project_dir, ep);
|
|
508
515
|
const { ok, result, error } = execute(index, 'graph', ep);
|
|
509
516
|
if (!ok) return tr(error); // soft error
|
|
510
517
|
return tr(output.formatGraph(result, {
|
|
@@ -516,7 +523,7 @@ server.registerTool(
|
|
|
516
523
|
}
|
|
517
524
|
|
|
518
525
|
case 'circular_deps': {
|
|
519
|
-
|
|
526
|
+
index = getIndex(project_dir, ep);
|
|
520
527
|
const { ok, result, error } = execute(index, 'circularDeps', ep);
|
|
521
528
|
if (!ok) return tr(error);
|
|
522
529
|
return tr(output.formatCircularDeps(result));
|
|
@@ -525,44 +532,44 @@ server.registerTool(
|
|
|
525
532
|
// ── Refactoring ─────────────────────────────────────────────
|
|
526
533
|
|
|
527
534
|
case 'verify': {
|
|
528
|
-
|
|
535
|
+
index = getIndex(project_dir, ep);
|
|
529
536
|
const { ok, result, error } = execute(index, 'verify', ep);
|
|
530
537
|
if (!ok) return tr(error); // soft error
|
|
531
538
|
return tr(output.formatVerify(result));
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
case 'plan': {
|
|
535
|
-
|
|
542
|
+
index = getIndex(project_dir, ep);
|
|
536
543
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
537
544
|
if (!ok) return tr(error); // soft error
|
|
538
545
|
return tr(output.formatPlan(result));
|
|
539
546
|
}
|
|
540
547
|
|
|
541
548
|
case 'diff_impact': {
|
|
542
|
-
|
|
549
|
+
index = getIndex(project_dir, ep);
|
|
543
550
|
const { ok, result, error } = execute(index, 'diffImpact', ep);
|
|
544
551
|
if (!ok) return tr(error); // soft error — e.g. "not a git repo"
|
|
545
|
-
return tr(output.formatDiffImpact(result, { all: ep.all })
|
|
552
|
+
return tr(output.formatDiffImpact(result, { all: ep.all }));
|
|
546
553
|
}
|
|
547
554
|
|
|
548
555
|
// ── Other ───────────────────────────────────────────────────
|
|
549
556
|
|
|
550
557
|
case 'typedef': {
|
|
551
|
-
|
|
558
|
+
index = getIndex(project_dir, ep);
|
|
552
559
|
const { ok, result, error } = execute(index, 'typedef', ep);
|
|
553
560
|
if (!ok) return tr(error); // soft error
|
|
554
561
|
return tr(output.formatTypedef(result, ep.name));
|
|
555
562
|
}
|
|
556
563
|
|
|
557
564
|
case 'stacktrace': {
|
|
558
|
-
|
|
565
|
+
index = getIndex(project_dir, ep);
|
|
559
566
|
const { ok, result, error } = execute(index, 'stacktrace', ep);
|
|
560
567
|
if (!ok) return tr(error); // soft error
|
|
561
568
|
return tr(output.formatStackTrace(result));
|
|
562
569
|
}
|
|
563
570
|
|
|
564
571
|
case 'api': {
|
|
565
|
-
|
|
572
|
+
index = getIndex(project_dir, ep);
|
|
566
573
|
const { ok, result, error, note } = execute(index, 'api', ep);
|
|
567
574
|
if (!ok) return tr(error); // soft error
|
|
568
575
|
let apiText = output.formatApi(result, ep.file || '.');
|
|
@@ -571,7 +578,7 @@ server.registerTool(
|
|
|
571
578
|
}
|
|
572
579
|
|
|
573
580
|
case 'stats': {
|
|
574
|
-
|
|
581
|
+
index = getIndex(project_dir, ep);
|
|
575
582
|
const { ok, result, error } = execute(index, 'stats', ep);
|
|
576
583
|
if (!ok) return tr(error); // soft error
|
|
577
584
|
return tr(output.formatStats(result, { top: ep.top || 0 }));
|
|
@@ -582,7 +589,7 @@ server.registerTool(
|
|
|
582
589
|
case 'fn': {
|
|
583
590
|
const err = requireName(ep.name);
|
|
584
591
|
if (err) return err;
|
|
585
|
-
|
|
592
|
+
index = getIndex(project_dir, ep);
|
|
586
593
|
const { ok, result, error } = execute(index, 'fn', ep);
|
|
587
594
|
if (!ok) return tr(error); // soft error
|
|
588
595
|
// MCP path security: validate all result files are within project root
|
|
@@ -600,7 +607,7 @@ server.registerTool(
|
|
|
600
607
|
if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
|
|
601
608
|
return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
|
|
602
609
|
}
|
|
603
|
-
|
|
610
|
+
index = getIndex(project_dir, ep);
|
|
604
611
|
const { ok, result, error } = execute(index, 'class', ep);
|
|
605
612
|
if (!ok) return tr(error); // soft error (class not found)
|
|
606
613
|
// MCP path security: validate all result files are within project root
|
|
@@ -613,7 +620,7 @@ server.registerTool(
|
|
|
613
620
|
}
|
|
614
621
|
|
|
615
622
|
case 'lines': {
|
|
616
|
-
|
|
623
|
+
index = getIndex(project_dir, ep);
|
|
617
624
|
const { ok, result, error } = execute(index, 'lines', ep);
|
|
618
625
|
if (!ok) return tr(error); // soft error
|
|
619
626
|
// MCP path security: validate file is within project root
|
|
@@ -626,7 +633,7 @@ server.registerTool(
|
|
|
626
633
|
if (ep.item === undefined || ep.item === null) {
|
|
627
634
|
return toolError('Item number is required (e.g. item=1).');
|
|
628
635
|
}
|
|
629
|
-
|
|
636
|
+
index = getIndex(project_dir, ep);
|
|
630
637
|
const lookup = expandCacheInstance.lookup(index.root, ep.item);
|
|
631
638
|
const { ok, result, error } = execute(index, 'expand', {
|
|
632
639
|
match: lookup.match, itemNum: ep.item,
|
|
@@ -642,6 +649,15 @@ server.registerTool(
|
|
|
642
649
|
}
|
|
643
650
|
} catch (e) {
|
|
644
651
|
return toolError(e.message);
|
|
652
|
+
} finally {
|
|
653
|
+
// Persist calls cache after command execution.
|
|
654
|
+
// getIndex() only saves after build (when callsCache is empty).
|
|
655
|
+
// Commands like context/about/impact populate callsCache lazily,
|
|
656
|
+
// so we save here to avoid re-parsing all files on every MCP session.
|
|
657
|
+
if (index && index.callsCacheDirty) {
|
|
658
|
+
try { index.saveCache(); } catch (_) { /* best-effort */ }
|
|
659
|
+
index.callsCacheDirty = false;
|
|
660
|
+
}
|
|
645
661
|
}
|
|
646
662
|
}
|
|
647
663
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.12",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"sessionId":"5e2a28ce-7f65-4369-a922-8d1d902f9d8f","pid":18086,"acquiredAt":1773363188494}
|