ucn 3.8.11 → 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 +43 -33
- 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
|
@@ -317,6 +317,7 @@ server.registerTool(
|
|
|
317
317
|
// Wrap toolResult to auto-inject command + maxChars from this request
|
|
318
318
|
const tr = (text) => toolResult(text, command, maxChars);
|
|
319
319
|
|
|
320
|
+
let index = null; // Track for post-command cache save
|
|
320
321
|
try {
|
|
321
322
|
switch (command) {
|
|
322
323
|
|
|
@@ -327,7 +328,7 @@ server.registerTool(
|
|
|
327
328
|
// ── Commands using shared executor ─────────────────────────
|
|
328
329
|
|
|
329
330
|
case 'about': {
|
|
330
|
-
|
|
331
|
+
index = getIndex(project_dir, ep);
|
|
331
332
|
const { ok, result, error } = execute(index, 'about', ep);
|
|
332
333
|
if (!ok) return tr(error); // soft error — won't kill sibling calls
|
|
333
334
|
return tr(output.formatAbout(result, {
|
|
@@ -338,7 +339,7 @@ server.registerTool(
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
case 'context': {
|
|
341
|
-
|
|
342
|
+
index = getIndex(project_dir, ep);
|
|
342
343
|
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
343
344
|
if (!ok) return tr(error); // context uses soft error (not toolError)
|
|
344
345
|
const { text, expandable } = output.formatContext(ctx, {
|
|
@@ -350,14 +351,14 @@ server.registerTool(
|
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
case 'impact': {
|
|
353
|
-
|
|
354
|
+
index = getIndex(project_dir, ep);
|
|
354
355
|
const { ok, result, error } = execute(index, 'impact', ep);
|
|
355
356
|
if (!ok) return tr(error); // soft error
|
|
356
357
|
return tr(output.formatImpact(result));
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
case 'blast': {
|
|
360
|
-
|
|
361
|
+
index = getIndex(project_dir, ep);
|
|
361
362
|
const { ok, result, error } = execute(index, 'blast', ep);
|
|
362
363
|
if (!ok) return tr(error); // soft error
|
|
363
364
|
return tr(output.formatBlast(result, {
|
|
@@ -366,14 +367,14 @@ server.registerTool(
|
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
case 'smart': {
|
|
369
|
-
|
|
370
|
+
index = getIndex(project_dir, ep);
|
|
370
371
|
const { ok, result, error } = execute(index, 'smart', ep);
|
|
371
372
|
if (!ok) return tr(error); // soft error
|
|
372
373
|
return tr(output.formatSmart(result));
|
|
373
374
|
}
|
|
374
375
|
|
|
375
376
|
case 'trace': {
|
|
376
|
-
|
|
377
|
+
index = getIndex(project_dir, ep);
|
|
377
378
|
const { ok, result, error } = execute(index, 'trace', ep);
|
|
378
379
|
if (!ok) return tr(error); // soft error
|
|
379
380
|
return tr(output.formatTrace(result, {
|
|
@@ -383,7 +384,7 @@ server.registerTool(
|
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
case 'reverse_trace': {
|
|
386
|
-
|
|
387
|
+
index = getIndex(project_dir, ep);
|
|
387
388
|
const { ok, result, error } = execute(index, 'reverseTrace', ep);
|
|
388
389
|
if (!ok) return tr(error);
|
|
389
390
|
return tr(output.formatReverseTrace(result, {
|
|
@@ -392,7 +393,7 @@ server.registerTool(
|
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
case 'example': {
|
|
395
|
-
|
|
396
|
+
index = getIndex(project_dir, ep);
|
|
396
397
|
const { ok, result, error } = execute(index, 'example', ep);
|
|
397
398
|
if (!ok) return tr(error);
|
|
398
399
|
if (!result) return tr(`No usage examples found for "${ep.name}".`);
|
|
@@ -400,7 +401,7 @@ server.registerTool(
|
|
|
400
401
|
}
|
|
401
402
|
|
|
402
403
|
case 'related': {
|
|
403
|
-
|
|
404
|
+
index = getIndex(project_dir, ep);
|
|
404
405
|
const { ok, result, error } = execute(index, 'related', ep);
|
|
405
406
|
if (!ok) return tr(error);
|
|
406
407
|
if (!result) return tr(`Symbol "${ep.name}" not found.`);
|
|
@@ -413,7 +414,7 @@ server.registerTool(
|
|
|
413
414
|
// ── Finding Code ────────────────────────────────────────────
|
|
414
415
|
|
|
415
416
|
case 'find': {
|
|
416
|
-
|
|
417
|
+
index = getIndex(project_dir, ep);
|
|
417
418
|
const { ok, result, error, note } = execute(index, 'find', ep);
|
|
418
419
|
if (!ok) return tr(error); // soft error
|
|
419
420
|
let text = output.formatFind(result, ep.name, ep.top);
|
|
@@ -422,7 +423,7 @@ server.registerTool(
|
|
|
422
423
|
}
|
|
423
424
|
|
|
424
425
|
case 'usages': {
|
|
425
|
-
|
|
426
|
+
index = getIndex(project_dir, ep);
|
|
426
427
|
const { ok, result, error, note } = execute(index, 'usages', ep);
|
|
427
428
|
if (!ok) return tr(error); // soft error
|
|
428
429
|
let text = output.formatUsages(result, ep.name);
|
|
@@ -431,7 +432,7 @@ server.registerTool(
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
case 'toc': {
|
|
434
|
-
|
|
435
|
+
index = getIndex(project_dir, ep);
|
|
435
436
|
const { ok, result, error, note } = execute(index, 'toc', ep);
|
|
436
437
|
if (!ok) return tr(error); // soft error
|
|
437
438
|
let text = output.formatToc(result, {
|
|
@@ -442,7 +443,7 @@ server.registerTool(
|
|
|
442
443
|
}
|
|
443
444
|
|
|
444
445
|
case 'search': {
|
|
445
|
-
|
|
446
|
+
index = getIndex(project_dir, ep);
|
|
446
447
|
const { ok, result, error, structural } = execute(index, 'search', ep);
|
|
447
448
|
if (!ok) return tr(error); // soft error
|
|
448
449
|
if (structural) {
|
|
@@ -452,21 +453,21 @@ server.registerTool(
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
case 'tests': {
|
|
455
|
-
|
|
456
|
+
index = getIndex(project_dir, ep);
|
|
456
457
|
const { ok, result, error } = execute(index, 'tests', ep);
|
|
457
458
|
if (!ok) return tr(error); // soft error
|
|
458
459
|
return tr(output.formatTests(result, ep.name));
|
|
459
460
|
}
|
|
460
461
|
|
|
461
462
|
case 'affected_tests': {
|
|
462
|
-
|
|
463
|
+
index = getIndex(project_dir, ep);
|
|
463
464
|
const { ok, result, error } = execute(index, 'affectedTests', ep);
|
|
464
465
|
if (!ok) return tr(error);
|
|
465
466
|
return tr(output.formatAffectedTests(result, { all: ep.all }));
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
case 'deadcode': {
|
|
469
|
-
|
|
470
|
+
index = getIndex(project_dir, ep);
|
|
470
471
|
const { ok, result, error, note } = execute(index, 'deadcode', ep);
|
|
471
472
|
if (!ok) return tr(error); // soft error
|
|
472
473
|
const dcNote = note;
|
|
@@ -480,7 +481,7 @@ server.registerTool(
|
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
case 'entrypoints': {
|
|
483
|
-
|
|
484
|
+
index = getIndex(project_dir, ep);
|
|
484
485
|
const { ok, result, error } = execute(index, 'entrypoints', ep);
|
|
485
486
|
if (!ok) return tr(error);
|
|
486
487
|
return tr(output.formatEntrypoints(result));
|
|
@@ -489,28 +490,28 @@ server.registerTool(
|
|
|
489
490
|
// ── File Dependencies ───────────────────────────────────────
|
|
490
491
|
|
|
491
492
|
case 'imports': {
|
|
492
|
-
|
|
493
|
+
index = getIndex(project_dir, ep);
|
|
493
494
|
const { ok, result, error } = execute(index, 'imports', ep);
|
|
494
495
|
if (!ok) return tr(error); // soft error
|
|
495
496
|
return tr(output.formatImports(result, ep.file));
|
|
496
497
|
}
|
|
497
498
|
|
|
498
499
|
case 'exporters': {
|
|
499
|
-
|
|
500
|
+
index = getIndex(project_dir, ep);
|
|
500
501
|
const { ok, result, error } = execute(index, 'exporters', ep);
|
|
501
502
|
if (!ok) return tr(error); // soft error
|
|
502
503
|
return tr(output.formatExporters(result, ep.file));
|
|
503
504
|
}
|
|
504
505
|
|
|
505
506
|
case 'file_exports': {
|
|
506
|
-
|
|
507
|
+
index = getIndex(project_dir, ep);
|
|
507
508
|
const { ok, result, error } = execute(index, 'fileExports', ep);
|
|
508
509
|
if (!ok) return tr(error); // soft error
|
|
509
510
|
return tr(output.formatFileExports(result, ep.file));
|
|
510
511
|
}
|
|
511
512
|
|
|
512
513
|
case 'graph': {
|
|
513
|
-
|
|
514
|
+
index = getIndex(project_dir, ep);
|
|
514
515
|
const { ok, result, error } = execute(index, 'graph', ep);
|
|
515
516
|
if (!ok) return tr(error); // soft error
|
|
516
517
|
return tr(output.formatGraph(result, {
|
|
@@ -522,7 +523,7 @@ server.registerTool(
|
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
case 'circular_deps': {
|
|
525
|
-
|
|
526
|
+
index = getIndex(project_dir, ep);
|
|
526
527
|
const { ok, result, error } = execute(index, 'circularDeps', ep);
|
|
527
528
|
if (!ok) return tr(error);
|
|
528
529
|
return tr(output.formatCircularDeps(result));
|
|
@@ -531,21 +532,21 @@ server.registerTool(
|
|
|
531
532
|
// ── Refactoring ─────────────────────────────────────────────
|
|
532
533
|
|
|
533
534
|
case 'verify': {
|
|
534
|
-
|
|
535
|
+
index = getIndex(project_dir, ep);
|
|
535
536
|
const { ok, result, error } = execute(index, 'verify', ep);
|
|
536
537
|
if (!ok) return tr(error); // soft error
|
|
537
538
|
return tr(output.formatVerify(result));
|
|
538
539
|
}
|
|
539
540
|
|
|
540
541
|
case 'plan': {
|
|
541
|
-
|
|
542
|
+
index = getIndex(project_dir, ep);
|
|
542
543
|
const { ok, result, error } = execute(index, 'plan', ep);
|
|
543
544
|
if (!ok) return tr(error); // soft error
|
|
544
545
|
return tr(output.formatPlan(result));
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
case 'diff_impact': {
|
|
548
|
-
|
|
549
|
+
index = getIndex(project_dir, ep);
|
|
549
550
|
const { ok, result, error } = execute(index, 'diffImpact', ep);
|
|
550
551
|
if (!ok) return tr(error); // soft error — e.g. "not a git repo"
|
|
551
552
|
return tr(output.formatDiffImpact(result, { all: ep.all }));
|
|
@@ -554,21 +555,21 @@ server.registerTool(
|
|
|
554
555
|
// ── Other ───────────────────────────────────────────────────
|
|
555
556
|
|
|
556
557
|
case 'typedef': {
|
|
557
|
-
|
|
558
|
+
index = getIndex(project_dir, ep);
|
|
558
559
|
const { ok, result, error } = execute(index, 'typedef', ep);
|
|
559
560
|
if (!ok) return tr(error); // soft error
|
|
560
561
|
return tr(output.formatTypedef(result, ep.name));
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
case 'stacktrace': {
|
|
564
|
-
|
|
565
|
+
index = getIndex(project_dir, ep);
|
|
565
566
|
const { ok, result, error } = execute(index, 'stacktrace', ep);
|
|
566
567
|
if (!ok) return tr(error); // soft error
|
|
567
568
|
return tr(output.formatStackTrace(result));
|
|
568
569
|
}
|
|
569
570
|
|
|
570
571
|
case 'api': {
|
|
571
|
-
|
|
572
|
+
index = getIndex(project_dir, ep);
|
|
572
573
|
const { ok, result, error, note } = execute(index, 'api', ep);
|
|
573
574
|
if (!ok) return tr(error); // soft error
|
|
574
575
|
let apiText = output.formatApi(result, ep.file || '.');
|
|
@@ -577,7 +578,7 @@ server.registerTool(
|
|
|
577
578
|
}
|
|
578
579
|
|
|
579
580
|
case 'stats': {
|
|
580
|
-
|
|
581
|
+
index = getIndex(project_dir, ep);
|
|
581
582
|
const { ok, result, error } = execute(index, 'stats', ep);
|
|
582
583
|
if (!ok) return tr(error); // soft error
|
|
583
584
|
return tr(output.formatStats(result, { top: ep.top || 0 }));
|
|
@@ -588,7 +589,7 @@ server.registerTool(
|
|
|
588
589
|
case 'fn': {
|
|
589
590
|
const err = requireName(ep.name);
|
|
590
591
|
if (err) return err;
|
|
591
|
-
|
|
592
|
+
index = getIndex(project_dir, ep);
|
|
592
593
|
const { ok, result, error } = execute(index, 'fn', ep);
|
|
593
594
|
if (!ok) return tr(error); // soft error
|
|
594
595
|
// MCP path security: validate all result files are within project root
|
|
@@ -606,7 +607,7 @@ server.registerTool(
|
|
|
606
607
|
if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
|
|
607
608
|
return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
|
|
608
609
|
}
|
|
609
|
-
|
|
610
|
+
index = getIndex(project_dir, ep);
|
|
610
611
|
const { ok, result, error } = execute(index, 'class', ep);
|
|
611
612
|
if (!ok) return tr(error); // soft error (class not found)
|
|
612
613
|
// MCP path security: validate all result files are within project root
|
|
@@ -619,7 +620,7 @@ server.registerTool(
|
|
|
619
620
|
}
|
|
620
621
|
|
|
621
622
|
case 'lines': {
|
|
622
|
-
|
|
623
|
+
index = getIndex(project_dir, ep);
|
|
623
624
|
const { ok, result, error } = execute(index, 'lines', ep);
|
|
624
625
|
if (!ok) return tr(error); // soft error
|
|
625
626
|
// MCP path security: validate file is within project root
|
|
@@ -632,7 +633,7 @@ server.registerTool(
|
|
|
632
633
|
if (ep.item === undefined || ep.item === null) {
|
|
633
634
|
return toolError('Item number is required (e.g. item=1).');
|
|
634
635
|
}
|
|
635
|
-
|
|
636
|
+
index = getIndex(project_dir, ep);
|
|
636
637
|
const lookup = expandCacheInstance.lookup(index.root, ep.item);
|
|
637
638
|
const { ok, result, error } = execute(index, 'expand', {
|
|
638
639
|
match: lookup.match, itemNum: ep.item,
|
|
@@ -648,6 +649,15 @@ server.registerTool(
|
|
|
648
649
|
}
|
|
649
650
|
} catch (e) {
|
|
650
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
|
+
}
|
|
651
661
|
}
|
|
652
662
|
}
|
|
653
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}
|