ucn 3.8.12 → 3.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +3 -1
- package/.github/workflows/ci.yml +15 -3
- package/.github/workflows/publish.yml +20 -8
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- package/package.json +8 -3
package/mcp/server.js
CHANGED
|
@@ -33,7 +33,7 @@ try {
|
|
|
33
33
|
const { ProjectIndex } = require('../core/project');
|
|
34
34
|
const { findProjectRoot } = require('../core/discovery');
|
|
35
35
|
const output = require('../core/output');
|
|
36
|
-
const { getMcpCommandEnum, normalizeParams } = require('../core/registry');
|
|
36
|
+
const { getMcpCommandEnum, normalizeParams, BROAD_COMMANDS: BROAD_CANONICAL, toMcpName } = require('../core/registry');
|
|
37
37
|
const { execute } = require('../core/execute');
|
|
38
38
|
const { ExpandCache } = require('../core/expand-cache');
|
|
39
39
|
|
|
@@ -47,6 +47,7 @@ const expandCacheInstance = new ExpandCache();
|
|
|
47
47
|
|
|
48
48
|
function getIndex(projectDir, options) {
|
|
49
49
|
const maxFiles = options && options.maxFiles;
|
|
50
|
+
const followSymlinks = options && options.followSymlinks;
|
|
50
51
|
const absDir = path.resolve(projectDir);
|
|
51
52
|
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
|
|
52
53
|
throw new Error(`Project directory not found: ${absDir}`);
|
|
@@ -67,6 +68,7 @@ function getIndex(projectDir, options) {
|
|
|
67
68
|
const index = new ProjectIndex(root);
|
|
68
69
|
const buildOpts = { quiet: true, forceRebuild: false };
|
|
69
70
|
if (maxFiles) buildOpts.maxFiles = maxFiles;
|
|
71
|
+
if (followSymlinks === false) buildOpts.followSymlinks = false;
|
|
70
72
|
const loaded = index.loadCache();
|
|
71
73
|
if (loaded && !maxFiles && !index.isCacheStale()) {
|
|
72
74
|
// Disk cache is fresh (skip when maxFiles is set — cached index may have different file count)
|
|
@@ -118,8 +120,8 @@ const DEFAULT_OUTPUT_CHARS = 10000; // ~2.5K tokens — targeted commands (abou
|
|
|
118
120
|
const BROAD_OUTPUT_CHARS = 3000; // ~750 tokens — broad commands where truncated listings are useless
|
|
119
121
|
const MAX_OUTPUT_CHARS = 100000; // hard ceiling even with max_chars override
|
|
120
122
|
|
|
121
|
-
// Broad commands: output is project-wide, truncation means you need a filter
|
|
122
|
-
const BROAD_COMMANDS = new Set([
|
|
123
|
+
// Broad commands (derived from registry): output is project-wide, truncation means you need a filter
|
|
124
|
+
const BROAD_COMMANDS = new Set([...BROAD_CANONICAL].map(toMcpName));
|
|
123
125
|
|
|
124
126
|
function toolResult(text, command, maxChars) {
|
|
125
127
|
if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
|
|
@@ -300,7 +302,8 @@ server.registerTool(
|
|
|
300
302
|
decorator: z.string().optional().describe('Filter by decorator/annotation (structural search). E.g. "Route", "Test".'),
|
|
301
303
|
exported: z.boolean().optional().describe('Only exported/public symbols (structural search).'),
|
|
302
304
|
unused: z.boolean().optional().describe('Only symbols with zero callers (structural search).'),
|
|
303
|
-
framework: z.string().optional().describe('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.')
|
|
305
|
+
framework: z.string().optional().describe('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
|
|
306
|
+
follow_symlinks: z.boolean().optional().describe('Follow symlinks during file discovery (default: true)')
|
|
304
307
|
|
|
305
308
|
})
|
|
306
309
|
},
|
|
@@ -340,30 +343,36 @@ server.registerTool(
|
|
|
340
343
|
|
|
341
344
|
case 'context': {
|
|
342
345
|
index = getIndex(project_dir, ep);
|
|
343
|
-
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
346
|
+
const { ok, result: ctx, error, note } = execute(index, 'context', ep);
|
|
344
347
|
if (!ok) return tr(error); // context uses soft error (not toolError)
|
|
345
348
|
const { text, expandable } = output.formatContext(ctx, {
|
|
346
349
|
expandHint: 'Use expand command with item number to see code for any item.',
|
|
347
350
|
showConfidence: ep.showConfidence !== false,
|
|
348
351
|
});
|
|
349
352
|
expandCacheInstance.save(index.root, ep.name, ep.file, expandable);
|
|
350
|
-
|
|
353
|
+
let ctxText = text;
|
|
354
|
+
if (note) ctxText += '\n\n' + note;
|
|
355
|
+
return tr(ctxText);
|
|
351
356
|
}
|
|
352
357
|
|
|
353
358
|
case 'impact': {
|
|
354
359
|
index = getIndex(project_dir, ep);
|
|
355
|
-
const { ok, result, error } = execute(index, 'impact', ep);
|
|
360
|
+
const { ok, result, error, note } = execute(index, 'impact', ep);
|
|
356
361
|
if (!ok) return tr(error); // soft error
|
|
357
|
-
|
|
362
|
+
let impactText = output.formatImpact(result);
|
|
363
|
+
if (note) impactText += '\n\n' + note;
|
|
364
|
+
return tr(impactText);
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
case 'blast': {
|
|
361
368
|
index = getIndex(project_dir, ep);
|
|
362
|
-
const { ok, result, error } = execute(index, 'blast', ep);
|
|
369
|
+
const { ok, result, error, note } = execute(index, 'blast', ep);
|
|
363
370
|
if (!ok) return tr(error); // soft error
|
|
364
|
-
|
|
371
|
+
let blastText = output.formatBlast(result, {
|
|
365
372
|
allHint: 'Set depth to expand all children.',
|
|
366
|
-
})
|
|
373
|
+
});
|
|
374
|
+
if (note) blastText += '\n\n' + note;
|
|
375
|
+
return tr(blastText);
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
case 'smart': {
|
|
@@ -375,21 +384,25 @@ server.registerTool(
|
|
|
375
384
|
|
|
376
385
|
case 'trace': {
|
|
377
386
|
index = getIndex(project_dir, ep);
|
|
378
|
-
const { ok, result, error } = execute(index, 'trace', ep);
|
|
387
|
+
const { ok, result, error, note } = execute(index, 'trace', ep);
|
|
379
388
|
if (!ok) return tr(error); // soft error
|
|
380
|
-
|
|
389
|
+
let traceText = output.formatTrace(result, {
|
|
381
390
|
allHint: 'Set depth to expand all children.',
|
|
382
391
|
methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
|
|
383
|
-
})
|
|
392
|
+
});
|
|
393
|
+
if (note) traceText += '\n\n' + note;
|
|
394
|
+
return tr(traceText);
|
|
384
395
|
}
|
|
385
396
|
|
|
386
397
|
case 'reverse_trace': {
|
|
387
398
|
index = getIndex(project_dir, ep);
|
|
388
|
-
const { ok, result, error } = execute(index, 'reverseTrace', ep);
|
|
399
|
+
const { ok, result, error, note } = execute(index, 'reverseTrace', ep);
|
|
389
400
|
if (!ok) return tr(error);
|
|
390
|
-
|
|
401
|
+
let rtText = output.formatReverseTrace(result, {
|
|
391
402
|
allHint: 'Set depth to expand all children.',
|
|
392
|
-
})
|
|
403
|
+
});
|
|
404
|
+
if (note) rtText += '\n\n' + note;
|
|
405
|
+
return tr(rtText);
|
|
393
406
|
}
|
|
394
407
|
|
|
395
408
|
case 'example': {
|
|
@@ -402,13 +415,15 @@ server.registerTool(
|
|
|
402
415
|
|
|
403
416
|
case 'related': {
|
|
404
417
|
index = getIndex(project_dir, ep);
|
|
405
|
-
const { ok, result, error } = execute(index, 'related', ep);
|
|
418
|
+
const { ok, result, error, note } = execute(index, 'related', ep);
|
|
406
419
|
if (!ok) return tr(error);
|
|
407
420
|
if (!result) return tr(`Symbol "${ep.name}" not found.`);
|
|
408
|
-
|
|
421
|
+
let relText = output.formatRelated(result, {
|
|
409
422
|
all: ep.all || false, top: ep.top,
|
|
410
423
|
allHint: 'Repeat with all=true to show all.'
|
|
411
|
-
})
|
|
424
|
+
});
|
|
425
|
+
if (note) relText += '\n\n' + note;
|
|
426
|
+
return tr(relText);
|
|
412
427
|
}
|
|
413
428
|
|
|
414
429
|
// ── Finding Code ────────────────────────────────────────────
|
|
@@ -461,9 +476,11 @@ server.registerTool(
|
|
|
461
476
|
|
|
462
477
|
case 'affected_tests': {
|
|
463
478
|
index = getIndex(project_dir, ep);
|
|
464
|
-
const { ok, result, error } = execute(index, 'affectedTests', ep);
|
|
479
|
+
const { ok, result, error, note } = execute(index, 'affectedTests', ep);
|
|
465
480
|
if (!ok) return tr(error);
|
|
466
|
-
|
|
481
|
+
let atText = output.formatAffectedTests(result, { all: ep.all });
|
|
482
|
+
if (note) atText += '\n\n' + note;
|
|
483
|
+
return tr(atText);
|
|
467
484
|
}
|
|
468
485
|
|
|
469
486
|
case 'deadcode': {
|
|
@@ -590,15 +607,15 @@ server.registerTool(
|
|
|
590
607
|
const err = requireName(ep.name);
|
|
591
608
|
if (err) return err;
|
|
592
609
|
index = getIndex(project_dir, ep);
|
|
593
|
-
const { ok, result, error } = execute(index, 'fn', ep);
|
|
610
|
+
const { ok, result, error, note } = execute(index, 'fn', ep);
|
|
594
611
|
if (!ok) return tr(error); // soft error
|
|
595
612
|
// MCP path security: validate all result files are within project root
|
|
596
613
|
for (const entry of result.entries) {
|
|
597
614
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
598
615
|
if (typeof check !== 'string') return check;
|
|
599
616
|
}
|
|
600
|
-
const
|
|
601
|
-
return tr(
|
|
617
|
+
const fnText = (note ? note + '\n\n' : '') + output.formatFnResult(result);
|
|
618
|
+
return tr(fnText);
|
|
602
619
|
}
|
|
603
620
|
|
|
604
621
|
case 'class': {
|
|
@@ -608,15 +625,15 @@ server.registerTool(
|
|
|
608
625
|
return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
|
|
609
626
|
}
|
|
610
627
|
index = getIndex(project_dir, ep);
|
|
611
|
-
const { ok, result, error } = execute(index, 'class', ep);
|
|
628
|
+
const { ok, result, error, note } = execute(index, 'class', ep);
|
|
612
629
|
if (!ok) return tr(error); // soft error (class not found)
|
|
613
630
|
// MCP path security: validate all result files are within project root
|
|
614
631
|
for (const entry of result.entries) {
|
|
615
632
|
const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
|
|
616
633
|
if (typeof check !== 'string') return check;
|
|
617
634
|
}
|
|
618
|
-
const
|
|
619
|
-
return tr(
|
|
635
|
+
const classText = (note ? note + '\n\n' : '') + output.formatClassResult(result);
|
|
636
|
+
return tr(classText);
|
|
620
637
|
}
|
|
621
638
|
|
|
622
639
|
case 'lines': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.14",
|
|
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",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"ucn-mcp": "mcp/server.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/accuracy.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
|
|
13
|
-
"benchmark:agent": "node test/agent-understanding-benchmark.js"
|
|
12
|
+
"test": "node --test test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-cross.test.js test/regression-mcp.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/systematic-test.js test/mcp-edge-cases.js test/parity-test.js",
|
|
13
|
+
"benchmark:agent": "node test/agent-understanding-benchmark.js",
|
|
14
|
+
"lint": "eslint core/ cli/ mcp/ languages/"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"mcp",
|
|
@@ -60,5 +61,9 @@
|
|
|
60
61
|
"optionalDependencies": {
|
|
61
62
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
62
63
|
"zod": "^3.25.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@eslint/js": "^10.0.1",
|
|
67
|
+
"eslint": "^10.0.3"
|
|
63
68
|
}
|
|
64
69
|
}
|