ucn 3.8.17 → 3.8.19
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 +2 -2
- package/cli/index.js +286 -205
- package/core/analysis.js +6 -2
- package/core/execute.js +17 -0
- package/core/output/analysis.js +1 -1
- package/core/project.js +24 -17
- package/core/registry.js +37 -4
- package/core/search.js +487 -74
- package/core/tracing.js +251 -39
- package/languages/javascript.js +19 -2
- package/languages/rust.js +40 -0
- package/mcp/server.js +75 -44
- package/package.json +1 -1
package/core/execute.js
CHANGED
|
@@ -96,6 +96,7 @@ function buildCallerOptions(p) {
|
|
|
96
96
|
className: p.className,
|
|
97
97
|
includeMethods: p.includeMethods,
|
|
98
98
|
includeUncertain: p.includeUncertain || false,
|
|
99
|
+
includeTests: p.includeTests,
|
|
99
100
|
exclude: toExcludeArray(p.exclude),
|
|
100
101
|
minConfidence: num(p.minConfidence, 0),
|
|
101
102
|
};
|
|
@@ -592,11 +593,27 @@ const HANDLERS = {
|
|
|
592
593
|
const err = requireName(p.name);
|
|
593
594
|
if (err) return { ok: false, error: err };
|
|
594
595
|
applyClassMethodSyntax(p);
|
|
596
|
+
if (p.file) {
|
|
597
|
+
const fileErr = checkFilePatternMatch(index, p.file);
|
|
598
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
599
|
+
// Validate that the symbol exists in the target file
|
|
600
|
+
const defs = index.find(p.name, { exact: true, file: p.file, className: p.className });
|
|
601
|
+
if (defs.length === 0) {
|
|
602
|
+
const allDefs = index.find(p.name, { exact: true });
|
|
603
|
+
if (allDefs.length > 0) {
|
|
604
|
+
const files = allDefs.map(d => d.relativePath).join(', ');
|
|
605
|
+
return { ok: false, error: `Symbol "${p.name}" not found in files matching "${p.file}". Defined in: ${files}` };
|
|
606
|
+
}
|
|
607
|
+
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
595
610
|
const classErr = validateClassName(index, p.name, p.className);
|
|
596
611
|
if (classErr) return { ok: false, error: classErr };
|
|
597
612
|
const result = index.tests(p.name, {
|
|
598
613
|
callsOnly: p.callsOnly || false,
|
|
599
614
|
className: p.className,
|
|
615
|
+
file: p.file,
|
|
616
|
+
exclude: toExcludeArray(p.exclude),
|
|
600
617
|
});
|
|
601
618
|
return { ok: true, result };
|
|
602
619
|
},
|
package/core/output/analysis.js
CHANGED
|
@@ -80,7 +80,7 @@ function formatContextJson(context) {
|
|
|
80
80
|
function formatContext(ctx, options = {}) {
|
|
81
81
|
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
82
82
|
|
|
83
|
-
const expandHint = options.expandHint
|
|
83
|
+
const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
|
|
84
84
|
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
|
|
85
85
|
|
|
86
86
|
const lines = [];
|
package/core/project.js
CHANGED
|
@@ -158,28 +158,35 @@ class ProjectIndex {
|
|
|
158
158
|
const startTime = Date.now();
|
|
159
159
|
const quiet = options.quiet !== false;
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
// Accept pre-expanded file array (glob mode) or a pattern string
|
|
162
|
+
let files;
|
|
163
|
+
if (Array.isArray(pattern)) {
|
|
164
|
+
files = pattern;
|
|
165
|
+
} else {
|
|
166
|
+
if (!pattern) {
|
|
167
|
+
pattern = detectProjectPattern(this.root);
|
|
168
|
+
}
|
|
164
169
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const globOpts = {
|
|
171
|
+
root: this.root,
|
|
172
|
+
maxFiles: options.maxFiles || this.config.maxFiles || 50000,
|
|
173
|
+
followSymlinks: options.followSymlinks
|
|
174
|
+
};
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
// Merge .gitignore and .ucn.json exclude into file discovery
|
|
177
|
+
const gitignorePatterns = parseGitignore(this.root);
|
|
178
|
+
const configExclude = this.config.exclude || [];
|
|
179
|
+
if (gitignorePatterns.length > 0 || configExclude.length > 0) {
|
|
180
|
+
globOpts.ignores = [...DEFAULT_IGNORES, ...gitignorePatterns, ...configExclude];
|
|
181
|
+
}
|
|
177
182
|
|
|
178
|
-
|
|
183
|
+
files = expandGlob(pattern, globOpts);
|
|
184
|
+
}
|
|
179
185
|
|
|
180
186
|
// Track if files were truncated by maxFiles limit
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
const maxFiles = options.maxFiles || this.config.maxFiles || 50000;
|
|
188
|
+
if (!Array.isArray(pattern) && files.length >= maxFiles) {
|
|
189
|
+
this.truncated = { indexed: files.length, maxFiles };
|
|
183
190
|
} else {
|
|
184
191
|
this.truncated = null;
|
|
185
192
|
}
|
package/core/registry.js
CHANGED
|
@@ -92,11 +92,11 @@ const PARAM_MAP = {
|
|
|
92
92
|
// ============================================================================
|
|
93
93
|
|
|
94
94
|
// Per-command list of accepted flag names (camelCase). Source of truth for help text,
|
|
95
|
-
// MCP
|
|
95
|
+
// MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
|
|
96
96
|
// file* = file is the command subject (required), not a filter pattern.
|
|
97
97
|
const FLAG_APPLICABILITY = {
|
|
98
98
|
// Understanding code
|
|
99
|
-
about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
|
|
99
|
+
about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
|
|
100
100
|
context: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
|
|
101
101
|
impact: ['file', 'exclude', 'className', 'top'],
|
|
102
102
|
blast: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
|
|
@@ -110,7 +110,7 @@ const FLAG_APPLICABILITY = {
|
|
|
110
110
|
usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
|
|
111
111
|
toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
|
|
112
112
|
search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
|
|
113
|
-
tests: ['className', 'callsOnly'],
|
|
113
|
+
tests: ['file', 'exclude', 'className', 'callsOnly'],
|
|
114
114
|
affectedTests:['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
|
|
115
115
|
deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
|
|
116
116
|
entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
|
|
@@ -123,7 +123,7 @@ const FLAG_APPLICABILITY = {
|
|
|
123
123
|
imports: ['file'],
|
|
124
124
|
exporters: ['file'],
|
|
125
125
|
fileExports: ['file'],
|
|
126
|
-
graph: ['file', 'depth', 'direction'],
|
|
126
|
+
graph: ['file', 'depth', 'direction', 'all'],
|
|
127
127
|
circularDeps: ['file', 'exclude'],
|
|
128
128
|
// Refactoring
|
|
129
129
|
verify: ['file', 'className'],
|
|
@@ -224,11 +224,43 @@ function toCliName(canonical) {
|
|
|
224
224
|
return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Build a reverse map: camelCase → snake_case from PARAM_MAP.
|
|
229
|
+
* Flags not in PARAM_MAP are already snake_case-safe (single words).
|
|
230
|
+
*/
|
|
231
|
+
function buildReverseParamMap() {
|
|
232
|
+
const rev = {};
|
|
233
|
+
for (const [snake, camel] of Object.entries(PARAM_MAP)) {
|
|
234
|
+
rev[camel] = snake;
|
|
235
|
+
}
|
|
236
|
+
return rev;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const REVERSE_PARAM_MAP = buildReverseParamMap();
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate per-command parameter listing for the MCP tool description.
|
|
243
|
+
* Maps camelCase flags back to snake_case for MCP clients.
|
|
244
|
+
* One line per command: `about: file, exclude, class_name, ...`
|
|
245
|
+
*/
|
|
246
|
+
function generateMcpParamSection() {
|
|
247
|
+
const lines = ['', 'ACCEPTED FLAGS PER COMMAND (name, term, stack, range, base, staged, max_chars always accepted; flags not listed below are ignored):'];
|
|
248
|
+
for (const cmd of CANONICAL_COMMANDS) {
|
|
249
|
+
const flags = FLAG_APPLICABILITY[cmd];
|
|
250
|
+
if (!flags || flags.length === 0) continue;
|
|
251
|
+
const mcpCmd = toMcpName(cmd);
|
|
252
|
+
const mcpFlags = flags.map(f => REVERSE_PARAM_MAP[f] || f);
|
|
253
|
+
lines.push(` ${mcpCmd}: ${mcpFlags.join(', ')}`);
|
|
254
|
+
}
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
227
258
|
module.exports = {
|
|
228
259
|
CANONICAL_COMMANDS,
|
|
229
260
|
CLI_ALIASES,
|
|
230
261
|
MCP_ALIASES,
|
|
231
262
|
PARAM_MAP,
|
|
263
|
+
REVERSE_PARAM_MAP,
|
|
232
264
|
FLAG_APPLICABILITY,
|
|
233
265
|
BROAD_COMMANDS,
|
|
234
266
|
resolveCommand,
|
|
@@ -237,4 +269,5 @@ module.exports = {
|
|
|
237
269
|
getMcpCommandEnum,
|
|
238
270
|
toMcpName,
|
|
239
271
|
toCliName,
|
|
272
|
+
generateMcpParamSection,
|
|
240
273
|
};
|