ucn 3.8.23 → 3.8.26
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 +127 -12
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1095 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -52
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/languages/utils.js
CHANGED
|
@@ -46,10 +46,14 @@ function nodeToLocation(node, codeOrLines) {
|
|
|
46
46
|
* @returns {string}
|
|
47
47
|
*/
|
|
48
48
|
function extractParams(paramsNode) {
|
|
49
|
+
// Distinguish "we have no node" (genuinely unknown) from "node is empty".
|
|
50
|
+
// Returning '...' for empty parens caused signatures like `main(...)` for
|
|
51
|
+
// functions that actually take zero arguments. Empty → '' so callers can
|
|
52
|
+
// render `main()` cleanly.
|
|
49
53
|
if (!paramsNode) return '...';
|
|
50
54
|
const text = paramsNode.text;
|
|
51
|
-
|
|
52
|
-
return
|
|
55
|
+
const stripped = text.replace(/^\(|\)$/g, '').trim();
|
|
56
|
+
return stripped; // '' for empty params, '...' only when paramsNode missing
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -159,8 +163,10 @@ function parsePythonParam(param, info) {
|
|
|
159
163
|
} else if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
|
|
160
164
|
const nameNode = param.childForFieldName('name');
|
|
161
165
|
const valueNode = param.childForFieldName('value');
|
|
166
|
+
const typeNode = param.childForFieldName('type');
|
|
162
167
|
if (nameNode) info.name = nameNode.text;
|
|
163
168
|
if (valueNode) info.default = valueNode.text;
|
|
169
|
+
if (typeNode) info.type = typeNode.text;
|
|
164
170
|
info.optional = true;
|
|
165
171
|
} else if (param.type === 'list_splat_pattern' || param.type === 'dictionary_splat_pattern') {
|
|
166
172
|
info.name = param.text;
|
|
@@ -390,6 +396,126 @@ function extractJavaDocstring(code, startLine) {
|
|
|
390
396
|
return extractJSDocstring(code, startLine);
|
|
391
397
|
}
|
|
392
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Build a paramTypes map from a structured-params array.
|
|
401
|
+
* Skips entries without a `type`. Preserves the structured order via plain object.
|
|
402
|
+
* @param {Array<{name: string, type?: string}>} paramsStructured
|
|
403
|
+
* @returns {Object<string,string>|null} map { paramName: typeString } or null if empty
|
|
404
|
+
*/
|
|
405
|
+
function paramTypesFromStructured(paramsStructured) {
|
|
406
|
+
if (!Array.isArray(paramsStructured) || paramsStructured.length === 0) return null;
|
|
407
|
+
const map = {};
|
|
408
|
+
let any = false;
|
|
409
|
+
for (const p of paramsStructured) {
|
|
410
|
+
if (p && p.name && p.type) {
|
|
411
|
+
map[p.name] = p.type;
|
|
412
|
+
any = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return any ? map : null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Parse @param and @returns/@return tags from a JSDoc block above the given line.
|
|
420
|
+
* Returns { paramTypes, returnType } where paramTypes is { name: type } and
|
|
421
|
+
* returnType is a string, both possibly omitted if not present.
|
|
422
|
+
*
|
|
423
|
+
* Tag forms supported:
|
|
424
|
+
* @param {Type} name - typed param
|
|
425
|
+
* @param {Type} name - desc - typed param with description
|
|
426
|
+
* @returns {Type} desc - return type (also @return)
|
|
427
|
+
* Untyped @param tags are ignored.
|
|
428
|
+
*
|
|
429
|
+
* @param {string|string[]} codeOrLines
|
|
430
|
+
* @param {number} startLine - 1-indexed line of the function/method declaration
|
|
431
|
+
* @returns {{ paramTypes?: Object, returnType?: string }}
|
|
432
|
+
*/
|
|
433
|
+
function parseJSDocTags(codeOrLines, startLine) {
|
|
434
|
+
const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
|
|
435
|
+
const lineIndex = startLine - 1;
|
|
436
|
+
if (lineIndex <= 0) return {};
|
|
437
|
+
|
|
438
|
+
// Walk up past blank lines and decorators to find a JSDoc end line `*/`
|
|
439
|
+
let i = lineIndex - 1;
|
|
440
|
+
while (i >= 0 && (lines[i].trim() === '' || lines[i].trim().startsWith('@'))) {
|
|
441
|
+
i--;
|
|
442
|
+
}
|
|
443
|
+
if (i < 0) return {};
|
|
444
|
+
if (!lines[i].trim().endsWith('*/')) return {};
|
|
445
|
+
|
|
446
|
+
const docEnd = i;
|
|
447
|
+
while (i >= 0 && !lines[i].includes('/**')) {
|
|
448
|
+
i--;
|
|
449
|
+
}
|
|
450
|
+
if (i < 0) return {};
|
|
451
|
+
|
|
452
|
+
// Collect block text lines, stripping leading `*` and surrounding whitespace
|
|
453
|
+
const blockLines = [];
|
|
454
|
+
for (let j = i; j <= docEnd; j++) {
|
|
455
|
+
const line = lines[j]
|
|
456
|
+
.replace(/^\s*\/\*\*\s?/, '')
|
|
457
|
+
.replace(/\s*\*\/\s*$/, '')
|
|
458
|
+
.replace(/^\s*\*\s?/, '');
|
|
459
|
+
blockLines.push(line);
|
|
460
|
+
}
|
|
461
|
+
const block = blockLines.join('\n');
|
|
462
|
+
|
|
463
|
+
// @param {Type} name — capture balanced braces (no nested braces in JSDoc types in practice)
|
|
464
|
+
const paramTypes = {};
|
|
465
|
+
let any = false;
|
|
466
|
+
const paramRegex = /@param\s+\{([^}]+)\}\s+([A-Za-z_$][\w$]*)/g;
|
|
467
|
+
let m;
|
|
468
|
+
while ((m = paramRegex.exec(block)) !== null) {
|
|
469
|
+
const type = m[1].trim();
|
|
470
|
+
const name = m[2];
|
|
471
|
+
// Strip optional brackets if author wrote @param {Type} [name]; here we already excluded that
|
|
472
|
+
// but also handle @param {Type} [name=default] form by scanning a separate regex
|
|
473
|
+
paramTypes[name] = type;
|
|
474
|
+
any = true;
|
|
475
|
+
}
|
|
476
|
+
// Optional-bracket form: @param {Type} [name] or [name=default]
|
|
477
|
+
const paramOptRegex = /@param\s+\{([^}]+)\}\s+\[([A-Za-z_$][\w$]*)(?:\s*=[^\]]*)?\]/g;
|
|
478
|
+
while ((m = paramOptRegex.exec(block)) !== null) {
|
|
479
|
+
const type = m[1].trim();
|
|
480
|
+
const name = m[2];
|
|
481
|
+
if (!paramTypes[name]) paramTypes[name] = type;
|
|
482
|
+
any = true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// @returns {Type} or @return {Type}
|
|
486
|
+
const retMatch = block.match(/@returns?\s+\{([^}]+)\}/);
|
|
487
|
+
const result = {};
|
|
488
|
+
if (any) result.paramTypes = paramTypes;
|
|
489
|
+
if (retMatch) result.returnType = retMatch[1].trim();
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Compute the final paramTypes/returnType for a function symbol.
|
|
495
|
+
* Native AST types take precedence; JSDoc fills gaps.
|
|
496
|
+
* @param {Array} paramsStructured - parser output
|
|
497
|
+
* @param {string|null} nativeReturnType - return type extracted from AST (TS/Py)
|
|
498
|
+
* @param {string|string[]} codeOrLines - source for JSDoc lookup
|
|
499
|
+
* @param {number} startLine - function start line
|
|
500
|
+
* @param {boolean} useJSDoc - whether to consult JSDoc (true for JS/TS, false for Py)
|
|
501
|
+
* @returns {{ paramTypes?: Object, returnType?: string }}
|
|
502
|
+
*/
|
|
503
|
+
function buildTypeAnnotations(paramsStructured, nativeReturnType, codeOrLines, startLine, useJSDoc) {
|
|
504
|
+
const native = paramTypesFromStructured(paramsStructured);
|
|
505
|
+
let jsdoc = {};
|
|
506
|
+
if (useJSDoc) jsdoc = parseJSDocTags(codeOrLines, startLine);
|
|
507
|
+
|
|
508
|
+
const out = {};
|
|
509
|
+
// Merge: JSDoc first, native overrides
|
|
510
|
+
if (jsdoc.paramTypes || native) {
|
|
511
|
+
const merged = { ...(jsdoc.paramTypes || {}), ...(native || {}) };
|
|
512
|
+
if (Object.keys(merged).length > 0) out.paramTypes = merged;
|
|
513
|
+
}
|
|
514
|
+
const rt = nativeReturnType || jsdoc.returnType;
|
|
515
|
+
if (rt) out.returnType = rt;
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
393
519
|
/**
|
|
394
520
|
* Get the token type at a specific position using AST
|
|
395
521
|
* @param {object} rootNode - Tree-sitter root node
|
|
@@ -591,6 +717,186 @@ function clearNodeListCache() {
|
|
|
591
717
|
_cachedSubtreeEnds = null;
|
|
592
718
|
}
|
|
593
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Extract a string value from a tree-sitter argument node, returning
|
|
722
|
+
* `{ value, interp }` where:
|
|
723
|
+
* - value is the literal portion (with quotes stripped)
|
|
724
|
+
* - interp is true when the argument is interpolated (template literal,
|
|
725
|
+
* f-string, format!() macro, fmt.Sprintf), in which case `value` is the
|
|
726
|
+
* literal *prefix* before the first interpolation, suffixed with '*'.
|
|
727
|
+
*
|
|
728
|
+
* Returns null when the node isn't a string-like value.
|
|
729
|
+
*
|
|
730
|
+
* Used by route extraction: server `app.get('/users/:id')` → '/users/:id';
|
|
731
|
+
* client `fetch(\`/users/${id}\`)` → '/users/*' (interp).
|
|
732
|
+
*
|
|
733
|
+
* @param {object} node - Tree-sitter node (any language)
|
|
734
|
+
* @returns {{ value: string, interp: boolean }|null}
|
|
735
|
+
*/
|
|
736
|
+
function extractStringArg(node) {
|
|
737
|
+
if (!node) return null;
|
|
738
|
+
const t = node.type;
|
|
739
|
+
|
|
740
|
+
// JS/TS string literals
|
|
741
|
+
if (t === 'string') {
|
|
742
|
+
const text = node.text;
|
|
743
|
+
return { value: stripQuotes(text), interp: false };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// JS/TS template literal: `/users/${id}` or plain `/users/all`
|
|
747
|
+
if (t === 'template_string' || t === 'template_literal') {
|
|
748
|
+
return parseTemplateLiteral(node);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Python string
|
|
752
|
+
if (t === 'string') {
|
|
753
|
+
return { value: stripQuotes(node.text), interp: false };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Python concatenated strings: 'a' 'b' → 'ab'
|
|
757
|
+
if (t === 'concatenated_string') {
|
|
758
|
+
let acc = '';
|
|
759
|
+
let interp = false;
|
|
760
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
761
|
+
const child = node.namedChild(i);
|
|
762
|
+
const r = extractStringArg(child);
|
|
763
|
+
if (r) {
|
|
764
|
+
acc += r.value;
|
|
765
|
+
if (r.interp) { interp = true; break; }
|
|
766
|
+
} else {
|
|
767
|
+
interp = true;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return { value: acc, interp };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Go interpreted/raw string
|
|
775
|
+
if (t === 'interpreted_string_literal' || t === 'raw_string_literal') {
|
|
776
|
+
return { value: stripQuotes(node.text), interp: false };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Java string literal / text block
|
|
780
|
+
if (t === 'string_literal' || t === 'text_block') {
|
|
781
|
+
return { value: stripQuotes(node.text), interp: false };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Rust string literal: "..." or raw r"..." / r#"..."#
|
|
785
|
+
if (t === 'string_literal') {
|
|
786
|
+
return { value: stripRustString(node.text), interp: false };
|
|
787
|
+
}
|
|
788
|
+
if (t === 'raw_string_literal') {
|
|
789
|
+
return { value: stripRustString(node.text), interp: false };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Rust macro_invocation: format!("/users/{}", id), format!("/path") → extract first string arg
|
|
793
|
+
if (t === 'macro_invocation') {
|
|
794
|
+
const argsNode = node.childForFieldName('arguments');
|
|
795
|
+
if (!argsNode) return null;
|
|
796
|
+
// Find first string-like child of token_tree
|
|
797
|
+
const first = findFirstStringInRustMacro(argsNode);
|
|
798
|
+
if (first == null) return null;
|
|
799
|
+
// Detect interpolation by presence of `{}` or `{...}` placeholders or extra args
|
|
800
|
+
const hasPlaceholder = /\{[^}]*\}/.test(first);
|
|
801
|
+
// Truncate at first placeholder
|
|
802
|
+
const m = first.match(/^([^{]*)/);
|
|
803
|
+
return { value: (m ? m[1] : first), interp: hasPlaceholder };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** Strip surrounding quotes (' " `) from a literal, leaving the inner content. */
|
|
810
|
+
function stripQuotes(text) {
|
|
811
|
+
if (typeof text !== 'string' || text.length < 2) return text;
|
|
812
|
+
const first = text[0];
|
|
813
|
+
const last = text[text.length - 1];
|
|
814
|
+
if ((first === '"' || first === "'" || first === '`') && first === last) {
|
|
815
|
+
return text.slice(1, -1);
|
|
816
|
+
}
|
|
817
|
+
return text;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Strip Rust string literal quoting: "..." or r"..." or r#"..."# etc. */
|
|
821
|
+
function stripRustString(text) {
|
|
822
|
+
if (typeof text !== 'string') return text;
|
|
823
|
+
// r#"..."# (any number of #)
|
|
824
|
+
const rawHash = text.match(/^r(#+)"([\s\S]*)"\1$/);
|
|
825
|
+
if (rawHash) return rawHash[2];
|
|
826
|
+
// r"..."
|
|
827
|
+
const raw = text.match(/^r"([\s\S]*)"$/);
|
|
828
|
+
if (raw) return raw[1];
|
|
829
|
+
// "..."
|
|
830
|
+
if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
|
|
831
|
+
return text;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Parse a JS template literal AST node and return literal prefix + interp flag.
|
|
836
|
+
* `/users/all` → { value: '/users/all', interp: false }
|
|
837
|
+
* `/users/${id}` → { value: '/users/*', interp: true }
|
|
838
|
+
*/
|
|
839
|
+
function parseTemplateLiteral(node) {
|
|
840
|
+
let prefix = '';
|
|
841
|
+
let interp = false;
|
|
842
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
843
|
+
const child = node.namedChild(i);
|
|
844
|
+
// template_substitution = ${...}
|
|
845
|
+
if (child.type === 'template_substitution') {
|
|
846
|
+
interp = true;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
// string_fragment / template_chars: literal segment
|
|
850
|
+
if (child.type === 'string_fragment' || child.type === 'template_chars') {
|
|
851
|
+
prefix += child.text;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (interp && !prefix.endsWith('*')) prefix += '*';
|
|
855
|
+
return { value: prefix, interp };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Find the first quoted string inside a Rust macro_invocation token_tree. */
|
|
859
|
+
function findFirstStringInRustMacro(tokenTree) {
|
|
860
|
+
if (!tokenTree) return null;
|
|
861
|
+
for (let i = 0; i < tokenTree.namedChildCount; i++) {
|
|
862
|
+
const child = tokenTree.namedChild(i);
|
|
863
|
+
if (child.type === 'string_literal' || child.type === 'raw_string_literal') {
|
|
864
|
+
return stripRustString(child.text);
|
|
865
|
+
}
|
|
866
|
+
// Recurse into nested token_trees (rare)
|
|
867
|
+
if (child.namedChildCount > 0) {
|
|
868
|
+
const r = findFirstStringInRustMacro(child);
|
|
869
|
+
if (r != null) return r;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Fallback: scan raw text for first quoted string
|
|
873
|
+
const m = tokenTree.text.match(/"([^"\\]|\\.)*"/);
|
|
874
|
+
if (m) {
|
|
875
|
+
const raw = m[0];
|
|
876
|
+
return raw.slice(1, -1);
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Extract path/method from a fmt.Sprintf("...", args) call inside Go.
|
|
883
|
+
* Returns the literal prefix before the first %v/%s/%d etc. with '*' suffix.
|
|
884
|
+
*/
|
|
885
|
+
function extractSprintfPrefix(callNode) {
|
|
886
|
+
// call_expression with function = selector_expression "fmt.Sprintf"
|
|
887
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
888
|
+
if (!argsNode) return null;
|
|
889
|
+
const first = argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : null;
|
|
890
|
+
if (!first) return null;
|
|
891
|
+
const r = extractStringArg(first);
|
|
892
|
+
if (!r) return null;
|
|
893
|
+
// Find first format directive %x and truncate
|
|
894
|
+
const m = r.value.match(/^([^%]*)/);
|
|
895
|
+
const literal = m ? m[1] : r.value;
|
|
896
|
+
const hasFmt = /%/.test(r.value);
|
|
897
|
+
return { value: hasFmt ? (literal + '*') : literal, interp: hasFmt };
|
|
898
|
+
}
|
|
899
|
+
|
|
594
900
|
module.exports = {
|
|
595
901
|
traverseTree,
|
|
596
902
|
traverseTreeCached,
|
|
@@ -604,7 +910,13 @@ module.exports = {
|
|
|
604
910
|
extractGoDocstring,
|
|
605
911
|
extractRustDocstring,
|
|
606
912
|
extractJavaDocstring,
|
|
913
|
+
paramTypesFromStructured,
|
|
914
|
+
parseJSDocTags,
|
|
915
|
+
buildTypeAnnotations,
|
|
607
916
|
getTokenTypeAtPosition,
|
|
608
917
|
isMatchInCommentOrString,
|
|
609
|
-
findMatchesWithASTFilter
|
|
918
|
+
findMatchesWithASTFilter,
|
|
919
|
+
extractStringArg,
|
|
920
|
+
stripQuotes,
|
|
921
|
+
extractSprintfPrefix,
|
|
610
922
|
};
|
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, BROAD_COMMANDS: BROAD_CANONICAL, toMcpName, FLAG_APPLICABILITY, REVERSE_PARAM_MAP, generateMcpParamSection } = require('../core/registry');
|
|
36
|
+
const { getMcpCommandEnum, normalizeParams, BROAD_COMMANDS: BROAD_CANONICAL, toMcpName, FLAG_APPLICABILITY, REVERSE_PARAM_MAP, generateMcpParamSection, resolveCommand } = require('../core/registry');
|
|
37
37
|
const { execute } = require('../core/execute');
|
|
38
38
|
const { ExpandCache } = require('../core/expand-cache');
|
|
39
39
|
|
|
@@ -139,6 +139,7 @@ function toolResult(text, command, maxChars, suffixNote) {
|
|
|
139
139
|
const hints = {
|
|
140
140
|
toc: 'Use in= to scope to a subdirectory, or detailed=false for compact view.',
|
|
141
141
|
entrypoints: 'Use framework= to filter by framework, exclude= to skip patterns.',
|
|
142
|
+
endpoints: 'Use prefix= to filter by URL prefix, method= to filter by HTTP method, server_only/client_only to halve output.',
|
|
142
143
|
diff_impact: 'Use file= to scope to specific files/directories.',
|
|
143
144
|
affected_tests: 'Use file= to scope, exclude= to skip patterns.',
|
|
144
145
|
deadcode: 'Use file= to scope, exclude= to skip patterns.',
|
|
@@ -197,15 +198,16 @@ QUICK GUIDE — choosing the right command:
|
|
|
197
198
|
Commands:
|
|
198
199
|
|
|
199
200
|
UNDERSTANDING CODE:
|
|
200
|
-
- about <name>: Definition, source, callers, callees, and tests — everything in one call. Replaces 3-4 grep+read cycles. Your first stop for any function or class.
|
|
201
|
+
- about <name>: Definition, source, callers, callees, and tests — everything in one call. Replaces 3-4 grep+read cycles. Your first stop for any function or class. Pass git=true for last-modified, author, and recent-changes (last 30d).
|
|
201
202
|
- context <name>: Who calls it and what does it call, without source code. Results are numbered for use with expand. For classes/structs, shows all methods instead.
|
|
202
203
|
- impact <name>: Every call site with actual arguments passed, grouped by file. Essential before changing a function signature — shows exactly what breaks.
|
|
203
204
|
- blast <name>: Transitive blast radius — callers of callers. Shows the full chain of functions affected if you change something. Like impact but recursive. Use depth (default: 3) to control how far up the chain to walk.
|
|
204
205
|
- smart <name>: Get a function's source with all called functions expanded inline (not constants/variables). Use to understand or modify a function and its dependencies in one read.
|
|
205
206
|
- trace <name>: Call tree from a function downward. Use to understand "what happens when X runs" — maps which modules a pipeline touches without reading files. Set depth (default: 3); setting depth expands all children.
|
|
206
|
-
- example <name>: Best real-world usage example. Automatically scores call sites by quality and returns the top one with context. Use to understand expected calling patterns.
|
|
207
|
+
- example <name>: Best real-world usage example. Automatically scores call sites by quality and returns the top one with context. Use to understand expected calling patterns. Set diverse=true to cluster call sites by argument shape and return one representative per cluster (pair with top=N, default 3).
|
|
207
208
|
- reverse_trace <name>: Upward call chain to entry points — who calls this, who calls those callers, etc. Use to find all paths that lead to a function. Set depth (default: 5) to control how far up. Complement to trace (which goes downward).
|
|
208
209
|
- related <name>: Sibling functions: same file, similar names, or shared callers/callees. Find companions to update together (e.g., serialize when you're changing deserialize). Name-based, not semantic.
|
|
210
|
+
- brief <name>: Compact summary of a function: typed signature, first sentence of docstring, side-effect classification (fs/network/process/global_mutation), complexity (branches, depth, lines). Cheaper than about; more useful than fn when you don't need the body. Pass git=true for last-modified info.
|
|
209
211
|
|
|
210
212
|
FINDING CODE:
|
|
211
213
|
- find <name>: Locate definitions ranked by usage count. Supports glob patterns (e.g. find "handle*" or "_update*"). Use when you know the name but not the file.
|
|
@@ -216,6 +218,7 @@ FINDING CODE:
|
|
|
216
218
|
- affected_tests <name>: Which tests to run after changing a function. Combines blast (transitive callers) with test detection. Shows test files, coverage %, and uncovered functions. Use depth= to control depth.
|
|
217
219
|
- deadcode: Find dead code: functions/classes with zero callers. Use during cleanup to identify safely deletable code. Excludes exported, decorated, and test symbols by default — use include_exported/include_decorated/include_tests to expand.
|
|
218
220
|
- entrypoints: Detect framework entry points: routes, handlers, DI providers, tasks. Auto-detects Express, Flask, Spring, Gin, Actix, and more. Use framework= to filter by specific framework.
|
|
221
|
+
- endpoints: HTTP API surface — list server routes (Express/Fastify/Koa/NestJS/Flask/FastAPI/Spring/JAX-RS/Gin/Echo/Chi/Fiber/axum/actix/Next.js) and client requests (fetch/axios/requests/httpx/http/restTemplate/webClient/reqwest). Use bridge=true to match clients to servers across language boundaries; method=/prefix= to filter; server_only/client_only to halve output.
|
|
219
222
|
|
|
220
223
|
EXTRACTING CODE (use instead of reading entire files):
|
|
221
224
|
- fn <name>: Extract one or more functions. Comma-separated for bulk extraction (e.g. "parse,format,validate"). Use file to disambiguate.
|
|
@@ -234,12 +237,17 @@ REFACTORING:
|
|
|
234
237
|
- verify <name>: Check all call sites match function signature (argument count). Run before adding/removing parameters to catch breakage early.
|
|
235
238
|
- plan <name>: Preview refactoring: before/after signatures and call sites needing updates. Use add_param (with optional default_value), remove_param, or rename_to. Pair with verify.
|
|
236
239
|
- diff_impact: Which functions changed in git diff and who calls them. Use to understand impact of recent changes before committing or reviewing. Use base, staged, or file params to scope.
|
|
240
|
+
- check: Pre-commit lint of pending changes against the index. Composes diff_impact + verify + affected_tests; flags ADDED functions with zero callers (ORPHAN), BROKEN_IMPORT, signature drift across call sites, and recommends which tests to run. Use base= to compare against a branch, staged=true for staged changes only.
|
|
241
|
+
|
|
242
|
+
DIAGNOSTICS:
|
|
243
|
+
- doctor: Index health/coverage report — file/symbol counts, language breakdown, dynamic-import / eval / reflection blind spots, parse failures, and a verdict (HIGH/MEDIUM/LOW trust). Use deep=true to also sample resolution coverage and bucket edges by confidence. Use in= to scope to a subtree.
|
|
237
244
|
|
|
238
245
|
OTHER:
|
|
239
246
|
- typedef <name>: Find type definitions matching a name: interfaces, enums, structs, traits, type aliases. See field shapes, required methods, or enum values.
|
|
240
247
|
- stacktrace: Parse a stack trace, show source context per frame. Requires stack param. Handles JS, Python, Go, Rust, Java formats.
|
|
241
248
|
- api: Public API surface of project or file: all exported/public symbols with signatures. Use to understand what a library exposes. Pass file to scope to one file. Python needs __all__; use toc instead.
|
|
242
|
-
- stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit)
|
|
249
|
+
- stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit). Set hot=true with top=N for the most-called functions (project orientation primitive).
|
|
250
|
+
- audit_async: Find async calls inside async functions that are likely missing await (probable bugs). JS/TS/Python only. Filter with file/exclude/limit.` + generateMcpParamSection();
|
|
243
251
|
|
|
244
252
|
server.registerTool(
|
|
245
253
|
'ucn',
|
|
@@ -252,42 +260,50 @@ server.registerTool(
|
|
|
252
260
|
file: z.string().optional().describe('File path (imports/exporters/graph/file_exports/lines/api/diff_impact) or filter pattern for disambiguation (e.g. "parser", "src/core")'),
|
|
253
261
|
exclude: z.string().optional().describe('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
|
|
254
262
|
include_tests: z.boolean().optional().describe('Include test files in results (excluded by default)'),
|
|
263
|
+
exclude_tests: z.boolean().optional().describe('Exclude test files from results. Used by entrypoints (where tests are included by default).'),
|
|
255
264
|
include_methods: z.boolean().optional().describe('Include obj.method() calls (default: true for about/trace)'),
|
|
256
265
|
include_uncertain: z.boolean().optional().describe('Include uncertain/ambiguous matches'),
|
|
257
|
-
min_confidence: z.number().optional().describe('Minimum confidence threshold (0.0-1.0) to filter caller/callee edges'),
|
|
266
|
+
min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold (0.0-1.0) to filter caller/callee edges'),
|
|
258
267
|
show_confidence: z.boolean().optional().describe('Show confidence scores per edge (default: true). Set false to hide.'),
|
|
268
|
+
hide_confidence: z.boolean().optional().describe('Hide confidence scores per edge (alias of show_confidence=false).'),
|
|
269
|
+
unreachable_only: z.boolean().optional().describe('Show only callers/callees that are unreachable from any detected entry point (about, context, impact).'),
|
|
259
270
|
with_types: z.boolean().optional().describe('Include related type definitions in output'),
|
|
260
271
|
detailed: z.boolean().optional().describe('Show full symbol listing per file'),
|
|
261
272
|
exact: z.boolean().optional().describe('Exact name match only (no substring matching)'),
|
|
262
273
|
in: z.string().optional().describe('Only search in this directory path (e.g. "src/core")'),
|
|
263
|
-
top: z.number().optional().describe('Max results to show (default: 10)'),
|
|
264
|
-
depth: z.number().optional().describe('Max depth (default: 3 for trace, 2 for graph); expands all children'),
|
|
274
|
+
top: z.number().int().positive().max(10000).optional().describe('Max results to show (default: 10). Must be a positive integer.'),
|
|
275
|
+
depth: z.number().int().nonnegative().max(100).optional().describe('Max depth (default: 3 for trace, 2 for graph); expands all children. Non-negative integer.'),
|
|
265
276
|
code_only: z.boolean().optional().describe('Exclude matches in comments and strings'),
|
|
266
|
-
context: z.number().optional().describe('Lines of context around each match'),
|
|
277
|
+
context: z.number().int().nonnegative().max(1000).optional().describe('Lines of context around each match. Non-negative integer.'),
|
|
267
278
|
include_exported: z.boolean().optional().describe('Include exported symbols in deadcode results'),
|
|
268
279
|
include_decorated: z.boolean().optional().describe('Include decorated/annotated symbols in deadcode results'),
|
|
269
280
|
calls_only: z.boolean().optional().describe('Only direct calls and test-case matches (tests command)'),
|
|
270
|
-
max_lines: z.number().optional().describe('Max source lines for class (large classes show summary by default)'),
|
|
281
|
+
max_lines: z.number().int().positive().max(1000000).optional().describe('Max source lines for class (large classes show summary by default). Must be a positive integer.'),
|
|
271
282
|
direction: z.enum(['imports', 'importers', 'both']).optional().describe('Graph direction: imports (what this file uses), importers (who uses this file), both (default: both)'),
|
|
272
283
|
term: z.string().optional().describe('Search term (regex by default; set regex=false to force plain text)'),
|
|
273
284
|
regex: z.boolean().optional().describe('Treat search term as a regex pattern (default: true). Set false to force plain text escaping.'),
|
|
274
285
|
functions: z.boolean().optional().describe('Include per-function line counts in stats output, sorted by size (complexity audit)'),
|
|
286
|
+
hot: z.boolean().optional().describe('Include top N most-called functions in stats output (orientation primitive). Pair with top=N (default 10).'),
|
|
287
|
+
diverse: z.boolean().optional().describe('For example: cluster call sites by argument shape and return one representative per cluster. Pair with top=N (default 3).'),
|
|
288
|
+
git: z.boolean().optional().describe('Attach git enrichment (last modified, author, recent change count last 30d) to about/brief output. Returns gracefully when not a git repo.'),
|
|
275
289
|
add_param: z.string().optional().describe('Parameter name to add (plan command)'),
|
|
276
290
|
remove_param: z.string().optional().describe('Parameter name to remove (plan command)'),
|
|
277
291
|
rename_to: z.string().optional().describe('New function name (plan command)'),
|
|
278
292
|
default_value: z.string().optional().describe('Default value for added parameter (plan command)'),
|
|
279
293
|
stack: z.string().optional().describe('The stack trace text to parse (stacktrace command)'),
|
|
280
|
-
item: z.number().optional().describe('Item number from context output to expand (e.g. 1, 2, 3)'),
|
|
294
|
+
item: z.number().int().positive().max(1000000).optional().describe('Item number from context output to expand (e.g. 1, 2, 3). Must be a positive integer.'),
|
|
281
295
|
range: z.string().optional().describe('Line range to extract, e.g. "10-20" or "15" (lines command)'),
|
|
282
296
|
base: z.string().optional().describe('Git ref to diff against (default: HEAD). E.g. "HEAD~3", "main", a commit SHA'),
|
|
283
297
|
staged: z.boolean().optional().describe('Analyze staged changes (diff_impact command)'),
|
|
298
|
+
deep: z.boolean().optional().describe('Run a deeper analysis (doctor: sample resolution coverage)'),
|
|
299
|
+
compact: z.boolean().optional().describe('Compact one-line-per-item output for about/context (saves tokens)'),
|
|
284
300
|
case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)'),
|
|
285
301
|
all: z.boolean().optional().describe('Show all results (expand truncated sections). Applies to about, toc, related, trace, and others.'),
|
|
286
302
|
top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)'),
|
|
287
303
|
class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
|
|
288
|
-
limit: z.number().optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed.'),
|
|
289
|
-
max_files: z.number().optional().describe('Max files to index (default: 10000). Use for very large codebases.'),
|
|
290
|
-
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.'),
|
|
304
|
+
limit: z.number().int().positive().max(1000000).optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed. Must be a positive integer.'),
|
|
305
|
+
max_files: z.number().int().positive().max(10000000).optional().describe('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.'),
|
|
306
|
+
max_chars: z.number().int().positive().max(10000000).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.'),
|
|
291
307
|
// Structural search flags (search command)
|
|
292
308
|
type: z.string().optional().describe('Symbol type filter for structural search: function, class, call, method, type. Triggers index-based search.'),
|
|
293
309
|
param: z.string().optional().describe('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
|
|
@@ -297,7 +313,15 @@ server.registerTool(
|
|
|
297
313
|
exported: z.boolean().optional().describe('Only exported/public symbols (structural search).'),
|
|
298
314
|
unused: z.boolean().optional().describe('Only symbols with zero callers (structural search).'),
|
|
299
315
|
framework: z.string().optional().describe('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
|
|
300
|
-
follow_symlinks: z.boolean().optional().describe('Follow symlinks during file discovery (default: true)')
|
|
316
|
+
follow_symlinks: z.boolean().optional().describe('Follow symlinks during file discovery (default: true)'),
|
|
317
|
+
// endpoints command
|
|
318
|
+
bridge: z.boolean().optional().describe('Match server routes to client requests (endpoints command).'),
|
|
319
|
+
server_only: z.boolean().optional().describe('Only list server routes (endpoints command).'),
|
|
320
|
+
client_only: z.boolean().optional().describe('Only list client requests (endpoints command).'),
|
|
321
|
+
unmatched: z.boolean().optional().describe('Only show unmatched routes/requests (endpoints command).'),
|
|
322
|
+
method: z.string().optional().describe('Filter by HTTP method (e.g. "GET", "POST") — endpoints command.'),
|
|
323
|
+
prefix: z.string().optional().describe('Filter routes/requests by path prefix (endpoints command).'),
|
|
324
|
+
hide_uncertain: z.boolean().optional().describe('Hide uncertain (interpolated-path) bridges (endpoints command).')
|
|
301
325
|
|
|
302
326
|
})
|
|
303
327
|
},
|
|
@@ -309,10 +333,20 @@ server.registerTool(
|
|
|
309
333
|
const { command: _c, project_dir: _p, ...rawParams } = args;
|
|
310
334
|
const ep = normalizeParams(rawParams);
|
|
311
335
|
|
|
336
|
+
// Translate hide_confidence → showConfidence:false (canonical inverse).
|
|
337
|
+
if (ep.hideConfidence === true && ep.showConfidence === undefined) {
|
|
338
|
+
ep.showConfidence = false;
|
|
339
|
+
}
|
|
340
|
+
delete ep.hideConfidence;
|
|
341
|
+
|
|
312
342
|
// Strip params not applicable to this command (prevents silent no-ops).
|
|
313
343
|
// Global/core params are always allowed — only optional flags are filtered.
|
|
344
|
+
// FLAG_APPLICABILITY is keyed by canonical (camelCase) names, but `command`
|
|
345
|
+
// is the MCP (snake_case) name — resolve to canonical first to avoid
|
|
346
|
+
// silently skipping multi-word commands (circular_deps, diff_impact, etc.).
|
|
314
347
|
const strippedParams = [];
|
|
315
|
-
const
|
|
348
|
+
const canonicalCommand = resolveCommand(command, 'mcp') || command;
|
|
349
|
+
const applicable = FLAG_APPLICABILITY[canonicalCommand];
|
|
316
350
|
if (applicable) {
|
|
317
351
|
// Truly global options — apply to all commands (build/display control).
|
|
318
352
|
// Command-specific params (name, term, stack, range, etc.) are in FLAG_APPLICABILITY.
|
|
@@ -450,6 +484,27 @@ server.registerTool(
|
|
|
450
484
|
return tr(relText);
|
|
451
485
|
}
|
|
452
486
|
|
|
487
|
+
case 'brief': {
|
|
488
|
+
index = getIndex(project_dir, ep);
|
|
489
|
+
const { ok, result, error } = execute(index, 'brief', ep);
|
|
490
|
+
if (!ok) return te(error);
|
|
491
|
+
return tr(output.formatBrief(result));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
case 'doctor': {
|
|
495
|
+
index = getIndex(project_dir, ep);
|
|
496
|
+
const { ok, result, error } = execute(index, 'doctor', ep);
|
|
497
|
+
if (!ok) return te(error);
|
|
498
|
+
return tr(output.formatDoctor(result));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
case 'check': {
|
|
502
|
+
index = getIndex(project_dir, ep);
|
|
503
|
+
const { ok, result, error } = execute(index, 'check', ep);
|
|
504
|
+
if (!ok) return te(error);
|
|
505
|
+
return tr(output.formatCheck(result));
|
|
506
|
+
}
|
|
507
|
+
|
|
453
508
|
// ── Finding Code ────────────────────────────────────────────
|
|
454
509
|
|
|
455
510
|
case 'find': {
|
|
@@ -536,6 +591,15 @@ server.registerTool(
|
|
|
536
591
|
return tr(epText);
|
|
537
592
|
}
|
|
538
593
|
|
|
594
|
+
case 'endpoints': {
|
|
595
|
+
index = getIndex(project_dir, ep);
|
|
596
|
+
const { ok, result, error, note } = execute(index, 'endpoints', ep);
|
|
597
|
+
if (!ok) return te(error);
|
|
598
|
+
let endText = output.formatEndpoints(result, { bridge: result._bridge, unmatched: result._unmatched });
|
|
599
|
+
if (note) endText += '\n\n' + note;
|
|
600
|
+
return tr(endText);
|
|
601
|
+
}
|
|
602
|
+
|
|
539
603
|
// ── File Dependencies ───────────────────────────────────────
|
|
540
604
|
|
|
541
605
|
case 'imports': {
|
|
@@ -637,6 +701,15 @@ server.registerTool(
|
|
|
637
701
|
return tr(statsText);
|
|
638
702
|
}
|
|
639
703
|
|
|
704
|
+
case 'audit_async': {
|
|
705
|
+
index = getIndex(project_dir, ep);
|
|
706
|
+
const { ok, result, error, note } = execute(index, 'auditAsync', ep);
|
|
707
|
+
if (!ok) return te(error);
|
|
708
|
+
let text = output.formatAuditAsync(result);
|
|
709
|
+
if (note) text += '\n\n' + note;
|
|
710
|
+
return tr(text);
|
|
711
|
+
}
|
|
712
|
+
|
|
640
713
|
// ── Extracting Code (via execute) ────────────────────────────
|
|
641
714
|
|
|
642
715
|
case 'fn': {
|
|
@@ -700,7 +773,9 @@ server.registerTool(
|
|
|
700
773
|
// getIndex() only saves after build (when callsCache is empty).
|
|
701
774
|
// Commands like context/about/impact populate callsCache lazily,
|
|
702
775
|
// so we save here to avoid re-parsing all files on every MCP session.
|
|
703
|
-
|
|
776
|
+
// MED-1: also persist when reachability was computed in-process so
|
|
777
|
+
// long-lived MCP servers carry the BFS result forward to disk.
|
|
778
|
+
if (index && (index.callsCacheDirty || index.reachabilityDirty)) {
|
|
704
779
|
try { index.saveCache(); } catch (_) { /* best-effort */ }
|
|
705
780
|
index.callsCacheDirty = false;
|
|
706
781
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.26",
|
|
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",
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
"impact-analysis",
|
|
29
29
|
"dead-code",
|
|
30
30
|
"deadcode",
|
|
31
|
+
"endpoints",
|
|
32
|
+
"polyglot",
|
|
33
|
+
"audit",
|
|
34
|
+
"async",
|
|
35
|
+
"api-bridge",
|
|
36
|
+
"http",
|
|
37
|
+
"route",
|
|
38
|
+
"fetch",
|
|
31
39
|
"agent-skill",
|
|
32
40
|
"skill",
|
|
33
41
|
"cli",
|