ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CANONICAL_COMMANDS } = require('./registry');
|
|
4
|
+
|
|
5
|
+
const NAME_COMMANDS = new Set(['show', 'find', 'usages', 'trace', 'tests', 'plan']);
|
|
6
|
+
|
|
7
|
+
function parseSourceTarget(arg, params) {
|
|
8
|
+
if (params.range) return { ...params, file: params.file, range: params.range };
|
|
9
|
+
const value = String(arg || '').trim();
|
|
10
|
+
const twoPart = value.match(/^(.+?)\s+(\d+(?:-\d+)?)$/);
|
|
11
|
+
if (twoPart) return { ...params, file: params.file || twoPart[1], range: twoPart[2] };
|
|
12
|
+
// A single-colon file:range target is a line request. Stable symbol
|
|
13
|
+
// handles with a name contain a second colon and stay symbol targets.
|
|
14
|
+
const inline = value.match(/^([^:]+(?:\/[^:]*)?):(\d+(?:-\d+)?)$/);
|
|
15
|
+
if (inline) return { ...params, file: params.file || inline[1], range: inline[2] };
|
|
16
|
+
return { ...params, name: arg };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Build normalized execute() params from the shared CLI-style positional arg. */
|
|
20
|
+
function buildPublicParams(command, arg, params = {}) {
|
|
21
|
+
const clean = { ...params };
|
|
22
|
+
if (clean.top === 0 && params.topRaw == null) delete clean.top;
|
|
23
|
+
if (clean.maxLines == null) delete clean.maxLines;
|
|
24
|
+
// Surface-only values never belong in execute params.
|
|
25
|
+
for (const key of ['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'interactive',
|
|
26
|
+
'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'maxChars', 'maxCharsRaw',
|
|
27
|
+
'depthRaw', 'contextRaw', 'workersRaw',
|
|
28
|
+
'_fileFromFileMode']) delete clean[key];
|
|
29
|
+
|
|
30
|
+
if (NAME_COMMANDS.has(command)) return { ...clean, name: arg || clean.name };
|
|
31
|
+
switch (command) {
|
|
32
|
+
case 'search': return { ...clean, term: arg || clean.term };
|
|
33
|
+
case 'source': return parseSourceTarget(arg || clean.name, clean);
|
|
34
|
+
case 'impact': return { ...clean, ...(arg ? { name: arg } : {}) };
|
|
35
|
+
case 'deps': return { ...clean, file: arg || clean.file };
|
|
36
|
+
case 'api': return { ...clean, file: arg || clean.file };
|
|
37
|
+
case 'check': return { ...clean, ...(arg ? { name: arg } : {}) };
|
|
38
|
+
case 'stacktrace': return { ...clean, stack: clean.stack || arg };
|
|
39
|
+
default: return clean;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPublicCommand(command) {
|
|
44
|
+
return CANONICAL_COMMANDS.includes(command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget };
|
package/core/registry.js
CHANGED
|
@@ -11,56 +11,81 @@
|
|
|
11
11
|
// CANONICAL COMMANDS
|
|
12
12
|
// ============================================================================
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// Public v5 commands using camelCase canonical IDs.
|
|
15
|
+
//
|
|
16
|
+
// The engine still exposes narrower internal operations (about/context/blast,
|
|
17
|
+
// etc.) through execute.js so their well-tested analysis primitives can be
|
|
18
|
+
// composed. CLI and MCP are derived exclusively from this list: the public
|
|
19
|
+
// surface is intentionally small and has no legacy command aliases.
|
|
16
20
|
const CANONICAL_COMMANDS = [
|
|
17
|
-
//
|
|
18
|
-
'
|
|
19
|
-
//
|
|
20
|
-
'
|
|
21
|
-
//
|
|
22
|
-
'
|
|
23
|
-
//
|
|
24
|
-
'
|
|
25
|
-
// Refactoring
|
|
26
|
-
'verify', 'plan', 'diffImpact', 'check',
|
|
27
|
-
// Other
|
|
28
|
-
'typedef', 'stacktrace', 'api', 'stats', 'doctor', 'auditAsync', 'orient',
|
|
21
|
+
// Understand and navigate
|
|
22
|
+
'show', 'find', 'usages', 'search', 'source', 'trace',
|
|
23
|
+
// Change and validation
|
|
24
|
+
'impact', 'tests', 'check', 'plan',
|
|
25
|
+
// Repository and architecture
|
|
26
|
+
'repo', 'deps', 'api', 'entrypoints', 'endpoints',
|
|
27
|
+
// Focused audits / runtime evidence
|
|
28
|
+
'deadcode', 'auditAsync', 'stacktrace',
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'who
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
'
|
|
31
|
+
// Directive guidance for retired v4 commands. These names remain invalid —
|
|
32
|
+
// this is not a compatibility execution layer — but agents receive the exact
|
|
33
|
+
// v5 command that answers the old intent instead of a dead end.
|
|
34
|
+
const V4_COMMAND_MIGRATIONS = {
|
|
35
|
+
about: { purpose: 'For a symbol summary', cli: 'ucn show <name>', mcp: 'command "show" with name' },
|
|
36
|
+
context: { purpose: 'For callers and callees', cli: 'ucn show <name>', mcp: 'command "show" with name' },
|
|
37
|
+
smart: { purpose: 'For source plus callees', cli: 'ucn show <name> --sections=source,callees', mcp: 'command "show" with name, sections="source,callees"' },
|
|
38
|
+
related: { purpose: 'For related symbols', cli: 'ucn show <name> --sections=related', mcp: 'command "show" with name, sections="related"' },
|
|
39
|
+
example: { purpose: 'For a call-site example', cli: 'ucn show <name> --sections=example', mcp: 'command "show" with name, sections="example"' },
|
|
40
|
+
brief: { purpose: 'For a compact symbol summary', cli: 'ucn show <name> --compact', mcp: 'command "show" with name, compact=true' },
|
|
41
|
+
callers: { purpose: 'For who calls a symbol', cli: 'ucn show <name> --sections=callers', mcp: 'command "show" with name, sections="callers"' },
|
|
42
|
+
callees: { purpose: 'For what a symbol calls', cli: 'ucn show <name> --sections=callees', mcp: 'command "show" with name, sections="callees"' },
|
|
43
|
+
typedef: { purpose: 'For a type definition', cli: 'ucn find <name> --type=type', mcp: 'command "find" with name, type="type"' },
|
|
44
|
+
blast: { purpose: 'For the transitive caller tree', cli: 'ucn trace <name> --direction=callers', mcp: 'command "trace" with name, direction="callers"' },
|
|
45
|
+
reverseTrace: { purpose: 'For entry-point paths', cli: 'ucn trace <name> --direction=callers --to=entrypoints', mcp: 'command "trace" with name, direction="callers", to="entrypoints"' },
|
|
46
|
+
toc: { purpose: 'For the per-file symbol listing', cli: 'ucn repo --sections=files', mcp: 'command "repo" with sections="files"' },
|
|
47
|
+
stats: { purpose: 'For project statistics', cli: 'ucn repo --sections=stats', mcp: 'command "repo" with sections="stats"' },
|
|
48
|
+
doctor: { purpose: 'For index health and trust', cli: 'ucn repo --sections=health --deep', mcp: 'command "repo" with sections="health", deep=true' },
|
|
49
|
+
orient: { purpose: 'For repository orientation', cli: 'ucn repo', mcp: 'command "repo"' },
|
|
50
|
+
affectedTests: { purpose: 'For transitively affected tests', cli: 'ucn tests <name> --depth=<n>', mcp: 'command "tests" with name, depth=<n>' },
|
|
51
|
+
fn: { purpose: 'To extract a function', cli: 'ucn source <name>', mcp: 'command "source" with name' },
|
|
52
|
+
class: { purpose: 'To extract a class', cli: 'ucn source <name>', mcp: 'command "source" with name' },
|
|
53
|
+
lines: { purpose: 'To extract a line range', cli: 'ucn source <file> --range=<start-end>', mcp: 'command "source" with name=<file>, range="<start-end>"' },
|
|
54
|
+
expand: { purpose: 'To expand a listed item', cli: 'ucn source <handle>', mcp: 'command "source" with name=<handle>' },
|
|
55
|
+
imports: { purpose: 'For what a file imports', cli: 'ucn deps <file> --direction=imports', mcp: 'command "deps" with file, direction="imports"' },
|
|
56
|
+
exporters: { purpose: 'For who imports a file', cli: 'ucn deps <file> --direction=importers', mcp: 'command "deps" with file, direction="importers"' },
|
|
57
|
+
graph: { purpose: 'For a dependency graph', cli: 'ucn deps <file> --depth=<n>', mcp: 'command "deps" with file, depth=<n>' },
|
|
58
|
+
circularDeps: { purpose: 'For circular dependencies', cli: 'ucn deps --cycles', mcp: 'command "deps" with cycles=true' },
|
|
59
|
+
fileExports: { purpose: 'For a file public surface', cli: 'ucn api <file>', mcp: 'command "api" with file' },
|
|
60
|
+
verify: { purpose: 'To validate call sites', cli: 'ucn check <name>', mcp: 'command "check" with name' },
|
|
61
|
+
diffImpact: { purpose: 'For Git-diff impact', cli: 'ucn impact --base <ref>', mcp: 'command "impact" with base' },
|
|
62
|
+
stack: { purpose: 'To analyze a stack trace', cli: 'ucn stacktrace "<paste>"', mcp: 'command "stacktrace" with stack' },
|
|
53
63
|
};
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'reverse_trace': 'reverseTrace',
|
|
61
|
-
'circular_deps': 'circularDeps',
|
|
62
|
-
'audit_async': 'auditAsync',
|
|
65
|
+
const V4_SPELLING_ALIASES = {
|
|
66
|
+
rtrace: 'reverseTrace', affected: 'affectedTests',
|
|
67
|
+
circular: 'circularDeps', cycles: 'circularDeps',
|
|
68
|
+
'what-exports': 'fileExports', 'what-imports': 'imports',
|
|
69
|
+
'who-imports': 'exporters',
|
|
63
70
|
};
|
|
71
|
+
const V4_MIGRATION_LOOKUP = new Map();
|
|
72
|
+
{
|
|
73
|
+
const normalize = value => String(value).toLowerCase().replace(/[-_]/g, '');
|
|
74
|
+
for (const [name, entry] of Object.entries(V4_COMMAND_MIGRATIONS)) {
|
|
75
|
+
V4_MIGRATION_LOOKUP.set(normalize(name), entry);
|
|
76
|
+
}
|
|
77
|
+
for (const [alias, name] of Object.entries(V4_SPELLING_ALIASES)) {
|
|
78
|
+
V4_MIGRATION_LOOKUP.set(normalize(alias), V4_COMMAND_MIGRATIONS[name]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function v4MigrationHint(name, surface = 'cli') {
|
|
83
|
+
if (!name || resolveCommand(String(name), surface)) return null;
|
|
84
|
+
const entry = V4_MIGRATION_LOOKUP.get(
|
|
85
|
+
String(name).toLowerCase().replace(/[-_]/g, ''));
|
|
86
|
+
if (!entry) return null;
|
|
87
|
+
return `${entry.purpose}, use: ${surface === 'mcp' ? entry.mcp : entry.cli}.`;
|
|
88
|
+
}
|
|
64
89
|
|
|
65
90
|
// ============================================================================
|
|
66
91
|
// PARAM NORMALIZATION (snake_case → camelCase)
|
|
@@ -71,7 +96,6 @@ const PARAM_MAP = {
|
|
|
71
96
|
include_tests: 'includeTests',
|
|
72
97
|
exclude_tests: 'excludeTests',
|
|
73
98
|
include_methods: 'includeMethods',
|
|
74
|
-
include_uncertain: 'includeUncertain',
|
|
75
99
|
with_types: 'withTypes',
|
|
76
100
|
code_only: 'codeOnly',
|
|
77
101
|
case_sensitive: 'caseSensitive',
|
|
@@ -79,7 +103,6 @@ const PARAM_MAP = {
|
|
|
79
103
|
include_decorated: 'includeDecorated',
|
|
80
104
|
min_confidence: 'minConfidence',
|
|
81
105
|
show_confidence: 'showConfidence',
|
|
82
|
-
hide_confidence: 'hideConfidence',
|
|
83
106
|
calls_only: 'callsOnly',
|
|
84
107
|
class_name: 'className',
|
|
85
108
|
max_lines: 'maxLines',
|
|
@@ -96,6 +119,7 @@ const PARAM_MAP = {
|
|
|
96
119
|
client_only: 'clientOnly',
|
|
97
120
|
hide_uncertain: 'hideUncertain',
|
|
98
121
|
expand_unverified: 'expandUnverified',
|
|
122
|
+
with_source: 'withSource',
|
|
99
123
|
};
|
|
100
124
|
|
|
101
125
|
// ============================================================================
|
|
@@ -106,78 +130,39 @@ const PARAM_MAP = {
|
|
|
106
130
|
// MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
|
|
107
131
|
// file* = file is the command subject (required), not a filter pattern.
|
|
108
132
|
const FLAG_APPLICABILITY = {
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// unverified display cap.
|
|
116
|
-
context: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'all'],
|
|
117
|
-
impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'top', 'unreachableOnly', 'compact'],
|
|
118
|
-
// trace/blast/reverseTrace/affectedTests run the tiered tree contract:
|
|
119
|
-
// includeUncertain is an implied no-op (unverified edges are always
|
|
120
|
-
// visible — frontier/possible band); expandUnverified follows unverified
|
|
121
|
-
// CALLER edges, marking downstream nodes chainUnverified (blast/
|
|
122
|
-
// reverseTrace only — surface trace is down-direction, where unresolved
|
|
123
|
-
// callees have no definition to expand into).
|
|
124
|
-
blast: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
|
|
125
|
-
reverseTrace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
|
|
126
|
-
smart: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
|
|
127
|
-
trace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
|
|
128
|
-
example: ['name', 'file', 'className', 'line', 'diverse', 'top', 'includeTests'],
|
|
129
|
-
related: ['name', 'file', 'className', 'line', 'top', 'all'],
|
|
130
|
-
brief: ['name', 'file', 'className', 'line', 'git'],
|
|
131
|
-
// Finding code
|
|
132
|
-
find: ['name', 'file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all', 'depth', 'compact'],
|
|
133
|
-
usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact'],
|
|
134
|
-
toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
|
|
133
|
+
// Understand one symbol. `sections` is a comma-separated projection:
|
|
134
|
+
// summary, callers, callees, source, dependencies, tests, types, example,
|
|
135
|
+
// related. Caller-bearing projections always preserve ACCOUNT/CONTRACT.
|
|
136
|
+
show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse'],
|
|
137
|
+
find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource'],
|
|
138
|
+
usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all'],
|
|
135
139
|
search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines'],
|
|
141
|
+
trace: ['name', 'file', 'exclude', 'className', 'line', 'direction', 'to', 'includeMethods', 'depth', 'all', 'expandUnverified'],
|
|
142
|
+
impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all'],
|
|
143
|
+
tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
|
|
144
|
+
deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
|
|
145
|
+
api: ['file', 'in', 'limit'],
|
|
146
|
+
check: ['name', 'file', 'className', 'line', 'includeMethods', 'base', 'staged', 'limit'],
|
|
147
|
+
plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
|
|
148
|
+
repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
|
|
138
149
|
deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
|
|
139
150
|
entrypoints: ['file', 'exclude', 'includeTests', 'excludeTests', 'limit', 'type', 'framework'],
|
|
140
151
|
endpoints: ['file', 'exclude', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
|
|
141
|
-
// Extracting code
|
|
142
|
-
fn: ['name', 'file', 'className', 'line', 'all'],
|
|
143
|
-
class: ['name', 'file', 'line', 'all', 'maxLines'],
|
|
144
|
-
lines: ['file', 'range'],
|
|
145
|
-
expand: ['item'],
|
|
146
|
-
// File dependencies
|
|
147
|
-
imports: ['file'],
|
|
148
|
-
exporters: ['file'],
|
|
149
|
-
fileExports: ['file'],
|
|
150
|
-
graph: ['file', 'depth', 'direction', 'all'],
|
|
151
|
-
circularDeps: ['file', 'exclude'],
|
|
152
|
-
// Refactoring
|
|
153
|
-
// verify runs the tiered caller contract (v4): includeMethods/
|
|
154
|
-
// includeUncertain are implied no-ops (unverified sites always visible in
|
|
155
|
-
// their own band); kept in the matrix so legacy invocations don't warn.
|
|
156
|
-
verify: ['name', 'file', 'className', 'line', 'includeMethods', 'includeUncertain'],
|
|
157
|
-
plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
|
|
158
|
-
diffImpact: ['file', 'limit', 'base', 'staged', 'all'],
|
|
159
|
-
check: ['file', 'base', 'staged', 'limit'],
|
|
160
|
-
// Other
|
|
161
|
-
typedef: ['name', 'file', 'className', 'exact'],
|
|
162
152
|
stacktrace: ['stack'],
|
|
163
|
-
api: ['file', 'limit'],
|
|
164
|
-
stats: ['functions', 'hot', 'top'],
|
|
165
|
-
doctor: ['file', 'in', 'deep'],
|
|
166
|
-
orient: ['top'],
|
|
167
153
|
auditAsync: ['file', 'exclude', 'limit'],
|
|
168
154
|
};
|
|
169
155
|
|
|
170
156
|
// Commands whose output is project-wide — truncation means you need a filter, not more text.
|
|
171
157
|
// Used by MCP server for tighter default output limits.
|
|
172
158
|
const BROAD_COMMANDS = new Set([
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
'doctor', 'check', 'auditAsync', 'orient',
|
|
159
|
+
'repo', 'entrypoints', 'endpoints', 'tests', 'deadcode', 'usages',
|
|
160
|
+
'deps', 'check', 'auditAsync',
|
|
176
161
|
]);
|
|
177
162
|
|
|
178
163
|
// Commands that can operate on a single file without a project index.
|
|
179
164
|
// Used by CLI to decide whether to build a file-local or project-wide index.
|
|
180
|
-
const FILE_LOCAL_COMMANDS = new Set(['
|
|
165
|
+
const FILE_LOCAL_COMMANDS = new Set(['find', 'usages', 'search', 'source', 'api']);
|
|
181
166
|
|
|
182
167
|
// ============================================================================
|
|
183
168
|
// HELPERS
|
|
@@ -186,16 +171,60 @@ const FILE_LOCAL_COMMANDS = new Set(['toc', 'fn', 'class', 'find', 'usages', 'se
|
|
|
186
171
|
/**
|
|
187
172
|
* Resolve a surface-specific command name to its canonical ID.
|
|
188
173
|
*
|
|
189
|
-
* @param {string} name - Command name as used by the surface (e.g. '
|
|
190
|
-
* @param {'cli'|'mcp'} [surface='cli'] -
|
|
174
|
+
* @param {string} name - Command name as used by the surface (e.g. 'audit-async', 'audit_async')
|
|
175
|
+
* @param {'cli'|'mcp'} [surface='cli'] - Surface spelling to resolve
|
|
191
176
|
* @returns {string|null} Canonical command ID, or null if unknown
|
|
192
177
|
*/
|
|
193
178
|
function resolveCommand(name, surface) {
|
|
194
179
|
if (CANONICAL_COMMANDS.includes(name)) return name;
|
|
195
180
|
if (surface === 'mcp') {
|
|
196
|
-
return
|
|
181
|
+
return CANONICAL_COMMANDS.find(cmd => toMcpName(cmd) === name) || null;
|
|
197
182
|
}
|
|
198
|
-
return
|
|
183
|
+
return CANONICAL_COMMANDS.find(cmd => toCliName(cmd) === name) || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function editDistance(left, right) {
|
|
187
|
+
const a = String(left || '').toLowerCase();
|
|
188
|
+
const b = String(right || '').toLowerCase();
|
|
189
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
190
|
+
for (let i = 1; i <= a.length; i++) {
|
|
191
|
+
let diagonal = row[0];
|
|
192
|
+
row[0] = i;
|
|
193
|
+
for (let j = 1; j <= b.length; j++) {
|
|
194
|
+
const above = row[j];
|
|
195
|
+
row[j] = Math.min(
|
|
196
|
+
row[j] + 1,
|
|
197
|
+
row[j - 1] + 1,
|
|
198
|
+
diagonal + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
199
|
+
);
|
|
200
|
+
diagonal = above;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return row[b.length];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Return a single high-confidence correction for a misspelled public command.
|
|
208
|
+
* The threshold is deliberately strict because the CLI's first positional can
|
|
209
|
+
* also be a project path; an unrelated missing path must not become a command.
|
|
210
|
+
*/
|
|
211
|
+
function suggestCommand(name, surface = 'cli') {
|
|
212
|
+
if (!name) return null;
|
|
213
|
+
const spellings = CANONICAL_COMMANDS.map(command => ({
|
|
214
|
+
command,
|
|
215
|
+
spelling: surface === 'mcp' ? toMcpName(command) : toCliName(command),
|
|
216
|
+
}));
|
|
217
|
+
const ranked = spellings
|
|
218
|
+
.map(row => ({ ...row, distance: editDistance(name, row.spelling) }))
|
|
219
|
+
// Code-unit tiebreak (rule 11) — registry stays require-free, so the
|
|
220
|
+
// comparison is inlined instead of importing shared.codeUnitCompare.
|
|
221
|
+
.sort((a, b) => a.distance - b.distance ||
|
|
222
|
+
(a.spelling < b.spelling ? -1 : a.spelling > b.spelling ? 1 : 0));
|
|
223
|
+
const best = ranked[0];
|
|
224
|
+
const threshold = String(name).length <= 4 ? 2 : Math.min(3, Math.floor(String(name).length / 3));
|
|
225
|
+
if (!best || best.distance > threshold) return null;
|
|
226
|
+
if (ranked[1] && ranked[1].distance === best.distance) return null;
|
|
227
|
+
return best.spelling;
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
/**
|
|
@@ -215,8 +244,7 @@ function normalizeParams(params) {
|
|
|
215
244
|
// ============================================================================
|
|
216
245
|
|
|
217
246
|
/**
|
|
218
|
-
* Generate the CLI
|
|
219
|
-
* Includes hyphenated forms and legacy aliases.
|
|
247
|
+
* Generate the exact CLI command set using hyphenated surface spelling.
|
|
220
248
|
*/
|
|
221
249
|
function getCliCommandSet() {
|
|
222
250
|
const set = new Set();
|
|
@@ -227,11 +255,6 @@ function getCliCommandSet() {
|
|
|
227
255
|
set.add(hyphenated);
|
|
228
256
|
}
|
|
229
257
|
|
|
230
|
-
// Add legacy aliases
|
|
231
|
-
for (const alias of Object.keys(CLI_ALIASES)) {
|
|
232
|
-
set.add(alias);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
258
|
return set;
|
|
236
259
|
}
|
|
237
260
|
|
|
@@ -260,6 +283,67 @@ function toCliName(canonical) {
|
|
|
260
283
|
return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
261
284
|
}
|
|
262
285
|
|
|
286
|
+
function formatSurfaceMessage(message, surface = 'cli') {
|
|
287
|
+
let rendered = String(message || '');
|
|
288
|
+
for (const [snake, camel] of Object.entries(PARAM_MAP)) {
|
|
289
|
+
const spellings = [snake, camel];
|
|
290
|
+
const target = surface === 'mcp'
|
|
291
|
+
? snake
|
|
292
|
+
: `--${snake.replace(/_/g, '-')}`;
|
|
293
|
+
for (const spelling of spellings) {
|
|
294
|
+
rendered = rendered.replace(
|
|
295
|
+
new RegExp(`(?<![A-Za-z0-9_-])${spelling}(?![A-Za-z0-9_-])`, 'g'),
|
|
296
|
+
target,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const knownParams = new Set(Object.values(FLAG_APPLICABILITY).flat());
|
|
301
|
+
const booleanParams = new Set([
|
|
302
|
+
'includeTests', 'excludeTests', 'includeMethods', 'withTypes',
|
|
303
|
+
'codeOnly', 'caseSensitive', 'includeExported', 'includeDecorated',
|
|
304
|
+
'showConfidence', 'callsOnly', 'topLevel', 'followSymlinks',
|
|
305
|
+
'unreachableOnly', 'serverOnly', 'clientOnly', 'hideUncertain',
|
|
306
|
+
'expandUnverified', 'withSource', 'all', 'compact', 'exact',
|
|
307
|
+
'regex', 'exported', 'unused', 'staged', 'detailed', 'functions',
|
|
308
|
+
'hot', 'deep', 'cycles', 'bridge', 'unmatched', 'diverse', 'git',
|
|
309
|
+
]);
|
|
310
|
+
if (surface === 'mcp') {
|
|
311
|
+
rendered = rendered.replace(/--([a-z][a-z0-9-]*)(?:=([^\s,.)]+))?/g,
|
|
312
|
+
(whole, flag, rawValue) => {
|
|
313
|
+
const camel = flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
314
|
+
if (!knownParams.has(camel) && !['maxChars', 'maxFiles'].includes(camel)) {
|
|
315
|
+
return whole;
|
|
316
|
+
}
|
|
317
|
+
const snake = REVERSE_PARAM_MAP[camel] || camel;
|
|
318
|
+
if (rawValue != null) {
|
|
319
|
+
const value = rawValue === 'N' ? '<n>' : rawValue;
|
|
320
|
+
return `${snake}=${value}`;
|
|
321
|
+
}
|
|
322
|
+
return booleanParams.has(camel) ? `${snake}=true` : snake;
|
|
323
|
+
});
|
|
324
|
+
rendered = rendered
|
|
325
|
+
.replace(/\b(line|top|limit|depth|max_lines|max_files|max_chars)=(?=\s|[,.]|$)/g, '$1=<n>')
|
|
326
|
+
.replace(/\b(file|in|exclude)=(?=\s|[,.]|$)/g, '$1=<path>')
|
|
327
|
+
.replace(/\bclass_name=(?=\s|[,.]|$)/g, 'class_name=<name>');
|
|
328
|
+
} else {
|
|
329
|
+
// Single-word parameters are intentionally absent from PARAM_MAP.
|
|
330
|
+
// Translate them only in parameter syntax (`file=`, not prose "file").
|
|
331
|
+
for (const param of knownParams) {
|
|
332
|
+
if (REVERSE_PARAM_MAP[param] || Object.values(PARAM_MAP).includes(param)) continue;
|
|
333
|
+
rendered = rendered.replace(
|
|
334
|
+
new RegExp(`(?<![A-Za-z0-9_-])${param}(?=\\s*=)`, 'g'),
|
|
335
|
+
`--${param.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
rendered = rendered.replace(/(--[a-z][a-z0-9-]*)=true\b/g, '$1');
|
|
339
|
+
rendered = rendered
|
|
340
|
+
.replace(/--(line|top|limit|depth|max-lines|max-files|max-chars)=(?=\s|[,.]|$)/g, '--$1=N')
|
|
341
|
+
.replace(/--(file|in|exclude)=(?=\s|[,.]|$)/g, '--$1=<path>')
|
|
342
|
+
.replace(/--class-name=(?=\s|[,.]|$)/g, '--class-name=<name>');
|
|
343
|
+
}
|
|
344
|
+
return rendered;
|
|
345
|
+
}
|
|
346
|
+
|
|
263
347
|
/**
|
|
264
348
|
* Build a reverse map: camelCase → snake_case from PARAM_MAP.
|
|
265
349
|
* Flags not in PARAM_MAP are already snake_case-safe (single words).
|
|
@@ -274,10 +358,51 @@ function buildReverseParamMap() {
|
|
|
274
358
|
|
|
275
359
|
const REVERSE_PARAM_MAP = buildReverseParamMap();
|
|
276
360
|
|
|
361
|
+
const CLI_GLOBAL_FLAGS = Object.freeze([
|
|
362
|
+
'--help', '-h', '--version', '-v', '--mcp',
|
|
363
|
+
'--json', '--verbose', '--no-quiet', '--quiet',
|
|
364
|
+
'--interactive', '-i',
|
|
365
|
+
'--no-cache', '--clear-cache', '--no-follow-symlinks',
|
|
366
|
+
'--max-files', '--max-chars', '--workers',
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
const CLI_PARAM_FLAG_OVERRIDES = Object.freeze({
|
|
370
|
+
exclude: ['--exclude', '--not'],
|
|
371
|
+
includeMethods: ['--include-methods', '--no-include-methods'],
|
|
372
|
+
regex: ['--regex', '--no-regex'],
|
|
373
|
+
showConfidence: ['--show-confidence', '--hide-confidence', '--no-confidence'],
|
|
374
|
+
hideUncertain: ['--hide-uncertain', '--no-uncertain'],
|
|
375
|
+
compact: ['--compact', '--no-compact'],
|
|
376
|
+
defaultValue: ['--default-value', '--default'],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
function cliFlagsForParam(param) {
|
|
380
|
+
if (CLI_PARAM_FLAG_OVERRIDES[param]) {
|
|
381
|
+
return CLI_PARAM_FLAG_OVERRIDES[param];
|
|
382
|
+
}
|
|
383
|
+
const snake = REVERSE_PARAM_MAP[param] || param;
|
|
384
|
+
return [`--${snake.replace(/_/g, '-')}`];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getCliFlagsForCommand(command) {
|
|
388
|
+
const canonical = resolveCommand(command, 'cli') || command;
|
|
389
|
+
return [...new Set((FLAG_APPLICABILITY[canonical] || [])
|
|
390
|
+
.filter(param => !['name', 'term'].includes(param))
|
|
391
|
+
.flatMap(cliFlagsForParam))];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getCliAcceptedFlags() {
|
|
395
|
+
const accepted = new Set(CLI_GLOBAL_FLAGS);
|
|
396
|
+
for (const command of CANONICAL_COMMANDS) {
|
|
397
|
+
for (const flag of getCliFlagsForCommand(command)) accepted.add(flag);
|
|
398
|
+
}
|
|
399
|
+
return accepted;
|
|
400
|
+
}
|
|
401
|
+
|
|
277
402
|
/**
|
|
278
403
|
* Generate per-command parameter listing for the MCP tool description.
|
|
279
404
|
* Maps camelCase flags back to snake_case for MCP clients.
|
|
280
|
-
* One line per command: `
|
|
405
|
+
* One line per command: `show: file, exclude, class_name, ...`
|
|
281
406
|
*/
|
|
282
407
|
function generateMcpParamSection() {
|
|
283
408
|
const lines = ['', 'ACCEPTED FLAGS PER COMMAND (max_chars, max_files, follow_symlinks always accepted; flags not listed below are ignored):'];
|
|
@@ -293,18 +418,23 @@ function generateMcpParamSection() {
|
|
|
293
418
|
|
|
294
419
|
module.exports = {
|
|
295
420
|
CANONICAL_COMMANDS,
|
|
296
|
-
|
|
297
|
-
|
|
421
|
+
V4_COMMAND_MIGRATIONS,
|
|
422
|
+
v4MigrationHint,
|
|
298
423
|
PARAM_MAP,
|
|
299
424
|
REVERSE_PARAM_MAP,
|
|
300
425
|
FLAG_APPLICABILITY,
|
|
301
426
|
BROAD_COMMANDS,
|
|
302
427
|
FILE_LOCAL_COMMANDS,
|
|
303
428
|
resolveCommand,
|
|
429
|
+
suggestCommand,
|
|
304
430
|
normalizeParams,
|
|
305
431
|
getCliCommandSet,
|
|
306
432
|
getMcpCommandEnum,
|
|
307
433
|
toMcpName,
|
|
308
434
|
toCliName,
|
|
435
|
+
formatSurfaceMessage,
|
|
436
|
+
cliFlagsForParam,
|
|
437
|
+
getCliFlagsForCommand,
|
|
438
|
+
getCliAcceptedFlags,
|
|
309
439
|
generateMcpParamSection,
|
|
310
440
|
};
|