ucn 3.7.18 → 3.7.20
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 +5 -0
- package/cli/index.js +332 -428
- package/core/execute.js +376 -0
- package/core/expand-cache.js +162 -0
- package/core/project.js +14 -6
- package/core/registry.js +166 -0
- package/languages/rust.js +10 -7
- package/languages/utils.js +2 -2
- package/mcp/server.js +153 -315
- package/package.json +1 -1
package/mcp/server.js
CHANGED
|
@@ -31,20 +31,20 @@ try {
|
|
|
31
31
|
// ============================================================================
|
|
32
32
|
|
|
33
33
|
const { ProjectIndex } = require('../core/project');
|
|
34
|
-
const { findProjectRoot
|
|
35
|
-
const { detectLanguage } = require('../core/parser');
|
|
34
|
+
const { findProjectRoot } = require('../core/discovery');
|
|
36
35
|
const output = require('../core/output');
|
|
37
|
-
const { pickBestDefinition
|
|
36
|
+
const { pickBestDefinition } = require('../core/shared');
|
|
37
|
+
const { getMcpCommandEnum, normalizeParams } = require('../core/registry');
|
|
38
|
+
const { execute } = require('../core/execute');
|
|
39
|
+
const { ExpandCache, renderExpandItem } = require('../core/expand-cache');
|
|
38
40
|
|
|
39
41
|
// ============================================================================
|
|
40
42
|
// INDEX CACHE
|
|
41
43
|
// ============================================================================
|
|
42
44
|
|
|
43
45
|
const indexCache = new Map(); // projectDir → { index, checkedAt }
|
|
44
|
-
const expandCache = new Map(); // projectDir:symbolName → { items, root, symbolName, usedAt }
|
|
45
|
-
const lastContextKey = new Map(); // projectRoot → expandCache key
|
|
46
46
|
const MAX_CACHE_SIZE = 10;
|
|
47
|
-
const
|
|
47
|
+
const expandCacheInstance = new ExpandCache();
|
|
48
48
|
|
|
49
49
|
function getIndex(projectDir) {
|
|
50
50
|
const absDir = path.resolve(projectDir);
|
|
@@ -68,11 +68,8 @@ function getIndex(projectDir) {
|
|
|
68
68
|
} else {
|
|
69
69
|
index.build(null, { quiet: true, forceRebuild: loaded });
|
|
70
70
|
index.saveCache();
|
|
71
|
-
// Clear
|
|
72
|
-
|
|
73
|
-
if (val.root === root) expandCache.delete(key);
|
|
74
|
-
}
|
|
75
|
-
lastContextKey.delete(root);
|
|
71
|
+
// Clear expand cache entries for this project — stale after rebuild
|
|
72
|
+
expandCacheInstance.clearForRoot(root);
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
// LRU eviction
|
|
@@ -87,11 +84,7 @@ function getIndex(projectDir) {
|
|
|
87
84
|
}
|
|
88
85
|
if (oldestKey) {
|
|
89
86
|
indexCache.delete(oldestKey);
|
|
90
|
-
|
|
91
|
-
for (const [key, val] of expandCache) {
|
|
92
|
-
if (val.root === oldestKey) expandCache.delete(key);
|
|
93
|
-
}
|
|
94
|
-
lastContextKey.delete(oldestKey);
|
|
87
|
+
expandCacheInstance.clearForRoot(oldestKey);
|
|
95
88
|
}
|
|
96
89
|
}
|
|
97
90
|
|
|
@@ -112,11 +105,6 @@ const server = new McpServer({
|
|
|
112
105
|
// TOOL HELPERS
|
|
113
106
|
// ============================================================================
|
|
114
107
|
|
|
115
|
-
function parseExclude(excludeStr) {
|
|
116
|
-
if (!excludeStr) return [];
|
|
117
|
-
return excludeStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
108
|
const MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
|
|
121
109
|
|
|
122
110
|
function toolResult(text) {
|
|
@@ -228,14 +216,7 @@ server.registerTool(
|
|
|
228
216
|
{
|
|
229
217
|
description: TOOL_DESCRIPTION,
|
|
230
218
|
inputSchema: z.object({
|
|
231
|
-
command: z.enum(
|
|
232
|
-
'about', 'context', 'impact', 'smart', 'trace',
|
|
233
|
-
'find', 'usages', 'fn', 'class', 'example',
|
|
234
|
-
'related', 'tests', 'verify', 'plan', 'typedef',
|
|
235
|
-
'expand', 'toc', 'search', 'deadcode',
|
|
236
|
-
'imports', 'exporters', 'file_exports', 'graph', 'lines',
|
|
237
|
-
'api', 'stats', 'diff_impact', 'stacktrace'
|
|
238
|
-
]),
|
|
219
|
+
command: z.enum(getMcpCommandEnum()),
|
|
239
220
|
project_dir: z.string().describe('Absolute or relative path to the project root directory'),
|
|
240
221
|
name: z.string().optional().describe('Symbol name to analyze. For fn: comma-separated for bulk (e.g. "parse,format"). For find: supports glob patterns (e.g. "handle*").'),
|
|
241
222
|
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")'),
|
|
@@ -289,11 +270,13 @@ server.registerTool(
|
|
|
289
270
|
// UNDERSTANDING CODE
|
|
290
271
|
// ==================================================================
|
|
291
272
|
|
|
273
|
+
// ── Commands using shared executor ─────────────────────────
|
|
274
|
+
|
|
292
275
|
case 'about': {
|
|
293
|
-
const err = requireName(name);
|
|
294
|
-
if (err) return err;
|
|
295
276
|
const index = getIndex(project_dir);
|
|
296
|
-
const
|
|
277
|
+
const ep = normalizeParams({ name, file, exclude, with_types, all, include_methods, include_uncertain, top });
|
|
278
|
+
const { ok, result, error } = execute(index, 'about', ep);
|
|
279
|
+
if (!ok) return toolError(error);
|
|
297
280
|
return toolResult(output.formatAbout(result, {
|
|
298
281
|
allHint: 'Repeat with all=true to show all.',
|
|
299
282
|
methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.'
|
|
@@ -301,66 +284,38 @@ server.registerTool(
|
|
|
301
284
|
}
|
|
302
285
|
|
|
303
286
|
case 'context': {
|
|
304
|
-
const err = requireName(name);
|
|
305
|
-
if (err) return err;
|
|
306
287
|
const index = getIndex(project_dir);
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
file,
|
|
311
|
-
exclude: parseExclude(exclude)
|
|
312
|
-
});
|
|
313
|
-
if (!ctx) return toolResult(`Symbol "${name}" not found.`);
|
|
288
|
+
const ep = normalizeParams({ name, file, exclude, include_methods, include_uncertain });
|
|
289
|
+
const { ok, result: ctx, error } = execute(index, 'context', ep);
|
|
290
|
+
if (!ok) return toolResult(error); // context uses soft error (not toolError)
|
|
314
291
|
const { text, expandable } = output.formatContext(ctx, {
|
|
315
292
|
expandHint: 'Use expand command with item number to see code for any item.'
|
|
316
293
|
});
|
|
317
|
-
|
|
318
|
-
const cacheKey = `${index.root}:${name}:${file || ''}`;
|
|
319
|
-
// LRU eviction for expandCache
|
|
320
|
-
if (expandCache.size >= MAX_EXPAND_CACHE_SIZE && !expandCache.has(cacheKey)) {
|
|
321
|
-
let oldestKey = null;
|
|
322
|
-
let oldestTime = Infinity;
|
|
323
|
-
for (const [key, val] of expandCache) {
|
|
324
|
-
if ((val.usedAt || 0) < oldestTime) {
|
|
325
|
-
oldestTime = val.usedAt || 0;
|
|
326
|
-
oldestKey = key;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (oldestKey) expandCache.delete(oldestKey);
|
|
330
|
-
}
|
|
331
|
-
expandCache.set(cacheKey, { items: expandable, root: index.root, symbolName: name, usedAt: Date.now() });
|
|
332
|
-
lastContextKey.set(index.root, cacheKey);
|
|
333
|
-
}
|
|
294
|
+
expandCacheInstance.save(index.root, name, file, expandable);
|
|
334
295
|
return toolResult(text);
|
|
335
296
|
}
|
|
336
297
|
|
|
337
298
|
case 'impact': {
|
|
338
|
-
const err = requireName(name);
|
|
339
|
-
if (err) return err;
|
|
340
299
|
const index = getIndex(project_dir);
|
|
341
|
-
const
|
|
300
|
+
const ep = normalizeParams({ name, file, exclude });
|
|
301
|
+
const { ok, result, error } = execute(index, 'impact', ep);
|
|
302
|
+
if (!ok) return toolError(error);
|
|
342
303
|
return toolResult(output.formatImpact(result));
|
|
343
304
|
}
|
|
344
305
|
|
|
345
306
|
case 'smart': {
|
|
346
|
-
const err = requireName(name);
|
|
347
|
-
if (err) return err;
|
|
348
307
|
const index = getIndex(project_dir);
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
includeMethods: include_methods,
|
|
353
|
-
includeUncertain: include_uncertain || false
|
|
354
|
-
});
|
|
355
|
-
if (!result) return toolResult(`Function "${name}" not found.`);
|
|
308
|
+
const ep = normalizeParams({ name, file, with_types, include_methods, include_uncertain });
|
|
309
|
+
const { ok, result, error } = execute(index, 'smart', ep);
|
|
310
|
+
if (!ok) return toolResult(error); // soft error
|
|
356
311
|
return toolResult(output.formatSmart(result));
|
|
357
312
|
}
|
|
358
313
|
|
|
359
314
|
case 'trace': {
|
|
360
|
-
const err = requireName(name);
|
|
361
|
-
if (err) return err;
|
|
362
315
|
const index = getIndex(project_dir);
|
|
363
|
-
const
|
|
316
|
+
const ep = normalizeParams({ name, file, depth, all, include_methods, include_uncertain });
|
|
317
|
+
const { ok, result, error } = execute(index, 'trace', ep);
|
|
318
|
+
if (!ok) return toolError(error);
|
|
364
319
|
return toolResult(output.formatTrace(result, {
|
|
365
320
|
allHint: 'Set depth to expand all children.',
|
|
366
321
|
methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
|
|
@@ -368,96 +323,73 @@ server.registerTool(
|
|
|
368
323
|
}
|
|
369
324
|
|
|
370
325
|
case 'example': {
|
|
371
|
-
const err = requireName(name);
|
|
372
|
-
if (err) return err;
|
|
373
326
|
const index = getIndex(project_dir);
|
|
374
|
-
const
|
|
375
|
-
if (!
|
|
376
|
-
return toolResult(
|
|
327
|
+
const { ok, result, error } = execute(index, 'example', { name });
|
|
328
|
+
if (!ok) return toolError(error);
|
|
329
|
+
if (!result) return toolResult(`No usage examples found for "${name}".`);
|
|
330
|
+
return toolResult(output.formatExample(result, name));
|
|
377
331
|
}
|
|
378
332
|
|
|
379
333
|
case 'related': {
|
|
380
|
-
const err = requireName(name);
|
|
381
|
-
if (err) return err;
|
|
382
334
|
const index = getIndex(project_dir);
|
|
383
|
-
const result = index
|
|
335
|
+
const { ok, result, error } = execute(index, 'related', { name, file, top, all });
|
|
336
|
+
if (!ok) return toolError(error);
|
|
384
337
|
if (!result) return toolResult(`Symbol "${name}" not found.`);
|
|
385
338
|
return toolResult(output.formatRelated(result, {
|
|
386
|
-
showAll: all || false,
|
|
387
|
-
top,
|
|
339
|
+
showAll: all || false, top,
|
|
388
340
|
allHint: 'Repeat with all=true to show all.'
|
|
389
341
|
}));
|
|
390
342
|
}
|
|
391
343
|
|
|
392
|
-
//
|
|
393
|
-
// FINDING CODE
|
|
394
|
-
// ==================================================================
|
|
344
|
+
// ── Finding Code ────────────────────────────────────────────
|
|
395
345
|
|
|
396
346
|
case 'find': {
|
|
397
|
-
const err = requireName(name);
|
|
398
|
-
if (err) return err;
|
|
399
347
|
const index = getIndex(project_dir);
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
348
|
+
const ep = normalizeParams({ name, file, exclude, include_tests, exact, in: inPath });
|
|
349
|
+
const { ok, result, error } = execute(index, 'find', ep);
|
|
350
|
+
if (!ok) return toolError(error);
|
|
351
|
+
return toolResult(output.formatFind(result, name, top));
|
|
403
352
|
}
|
|
404
353
|
|
|
405
354
|
case 'usages': {
|
|
406
|
-
const err = requireName(name);
|
|
407
|
-
if (err) return err;
|
|
408
355
|
const index = getIndex(project_dir);
|
|
409
|
-
const
|
|
410
|
-
const result = index
|
|
411
|
-
|
|
412
|
-
codeOnly: code_only || false,
|
|
413
|
-
context: ctxLines || 0,
|
|
414
|
-
in: inPath
|
|
415
|
-
});
|
|
356
|
+
const ep = normalizeParams({ name, exclude, include_tests, code_only, context: ctxLines, in: inPath });
|
|
357
|
+
const { ok, result, error } = execute(index, 'usages', ep);
|
|
358
|
+
if (!ok) return toolError(error);
|
|
416
359
|
return toolResult(output.formatUsages(result, name));
|
|
417
360
|
}
|
|
418
361
|
|
|
419
362
|
case 'toc': {
|
|
420
363
|
const index = getIndex(project_dir);
|
|
421
|
-
const
|
|
422
|
-
|
|
364
|
+
const ep = normalizeParams({ detailed, top_level, all, top });
|
|
365
|
+
const { ok, result, error } = execute(index, 'toc', ep);
|
|
366
|
+
if (!ok) return toolError(error);
|
|
367
|
+
return toolResult(output.formatToc(result, {
|
|
423
368
|
topHint: 'Set top=N or use detailed=false for compact view.'
|
|
424
369
|
}));
|
|
425
370
|
}
|
|
426
371
|
|
|
427
372
|
case 'search': {
|
|
428
|
-
if (!term || !term.trim()) {
|
|
429
|
-
return toolError('Search term is required.');
|
|
430
|
-
}
|
|
431
373
|
const index = getIndex(project_dir);
|
|
432
|
-
const
|
|
433
|
-
const result = index
|
|
434
|
-
|
|
435
|
-
context: ctxLines || 0,
|
|
436
|
-
caseSensitive: case_sensitive || false,
|
|
437
|
-
exclude: searchExclude,
|
|
438
|
-
in: inPath || undefined,
|
|
439
|
-
regex: regex
|
|
440
|
-
});
|
|
374
|
+
const ep = normalizeParams({ term, exclude, include_tests, code_only, context: ctxLines, case_sensitive, in: inPath, regex });
|
|
375
|
+
const { ok, result, error } = execute(index, 'search', ep);
|
|
376
|
+
if (!ok) return toolError(error);
|
|
441
377
|
return toolResult(output.formatSearch(result, term));
|
|
442
378
|
}
|
|
443
379
|
|
|
444
380
|
case 'tests': {
|
|
445
|
-
const err = requireName(name);
|
|
446
|
-
if (err) return err;
|
|
447
381
|
const index = getIndex(project_dir);
|
|
448
|
-
const
|
|
382
|
+
const ep = normalizeParams({ name, calls_only });
|
|
383
|
+
const { ok, result, error } = execute(index, 'tests', ep);
|
|
384
|
+
if (!ok) return toolError(error);
|
|
449
385
|
return toolResult(output.formatTests(result, name));
|
|
450
386
|
}
|
|
451
387
|
|
|
452
388
|
case 'deadcode': {
|
|
453
389
|
const index = getIndex(project_dir);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
includeExported: include_exported || false,
|
|
458
|
-
includeDecorated: include_decorated || false,
|
|
459
|
-
includeTests: include_tests || false
|
|
460
|
-
});
|
|
390
|
+
const ep = normalizeParams({ exclude, in: inPath, include_exported, include_decorated, include_tests });
|
|
391
|
+
const { ok, result, error } = execute(index, 'deadcode', ep);
|
|
392
|
+
if (!ok) return toolError(error);
|
|
461
393
|
return toolResult(output.formatDeadcode(result, {
|
|
462
394
|
top: top || 0,
|
|
463
395
|
decoratedHint: !include_decorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
|
|
@@ -465,9 +397,96 @@ server.registerTool(
|
|
|
465
397
|
}));
|
|
466
398
|
}
|
|
467
399
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
400
|
+
// ── File Dependencies ───────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
case 'imports': {
|
|
403
|
+
const index = getIndex(project_dir);
|
|
404
|
+
const { ok, result, error } = execute(index, 'imports', { file });
|
|
405
|
+
if (!ok) return toolError(error);
|
|
406
|
+
return toolResult(output.formatImports(result, file));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'exporters': {
|
|
410
|
+
const index = getIndex(project_dir);
|
|
411
|
+
const { ok, result, error } = execute(index, 'exporters', { file });
|
|
412
|
+
if (!ok) return toolError(error);
|
|
413
|
+
return toolResult(output.formatExporters(result, file));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'file_exports': {
|
|
417
|
+
const index = getIndex(project_dir);
|
|
418
|
+
const { ok, result, error } = execute(index, 'fileExports', { file });
|
|
419
|
+
if (!ok) return toolError(error);
|
|
420
|
+
return toolResult(output.formatFileExports(result, file));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'graph': {
|
|
424
|
+
const index = getIndex(project_dir);
|
|
425
|
+
const { ok, result, error } = execute(index, 'graph', { file, direction, depth, all });
|
|
426
|
+
if (!ok) return toolError(error);
|
|
427
|
+
return toolResult(output.formatGraph(result, {
|
|
428
|
+
showAll: all || depth !== undefined,
|
|
429
|
+
maxDepth: depth ?? 2, file,
|
|
430
|
+
depthHint: 'Set depth parameter for deeper graph.',
|
|
431
|
+
allHint: 'Set depth to expand all children.'
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Refactoring ─────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
case 'verify': {
|
|
438
|
+
const index = getIndex(project_dir);
|
|
439
|
+
const { ok, result, error } = execute(index, 'verify', { name, file });
|
|
440
|
+
if (!ok) return toolError(error);
|
|
441
|
+
return toolResult(output.formatVerify(result));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
case 'plan': {
|
|
445
|
+
const index = getIndex(project_dir);
|
|
446
|
+
const ep = normalizeParams({ name, add_param, remove_param, rename_to, default_value, file });
|
|
447
|
+
const { ok, result, error } = execute(index, 'plan', ep);
|
|
448
|
+
if (!ok) return toolError(error);
|
|
449
|
+
return toolResult(output.formatPlan(result));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
case 'diff_impact': {
|
|
453
|
+
const index = getIndex(project_dir);
|
|
454
|
+
const { ok, result, error } = execute(index, 'diffImpact', { base, staged, file });
|
|
455
|
+
if (!ok) return toolError(error);
|
|
456
|
+
return toolResult(output.formatDiffImpact(result));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Other ───────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
case 'typedef': {
|
|
462
|
+
const index = getIndex(project_dir);
|
|
463
|
+
const { ok, result, error } = execute(index, 'typedef', { name, exact });
|
|
464
|
+
if (!ok) return toolError(error);
|
|
465
|
+
return toolResult(output.formatTypedef(result, name));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case 'stacktrace': {
|
|
469
|
+
const index = getIndex(project_dir);
|
|
470
|
+
const { ok, result, error } = execute(index, 'stacktrace', { stack });
|
|
471
|
+
if (!ok) return toolError(error);
|
|
472
|
+
return toolResult(output.formatStackTrace(result));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
case 'api': {
|
|
476
|
+
const index = getIndex(project_dir);
|
|
477
|
+
const { ok, result, error } = execute(index, 'api', { file });
|
|
478
|
+
if (!ok) return toolError(error);
|
|
479
|
+
return toolResult(output.formatApi(result, file || '.'));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
case 'stats': {
|
|
483
|
+
const index = getIndex(project_dir);
|
|
484
|
+
const { ok, result, error } = execute(index, 'stats', { functions });
|
|
485
|
+
if (!ok) return toolError(error);
|
|
486
|
+
return toolResult(output.formatStats(result, { top: top || 0 }));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Extracting Code (adapter-specific) ──────────────────────
|
|
471
490
|
|
|
472
491
|
case 'fn': {
|
|
473
492
|
const err = requireName(name);
|
|
@@ -647,200 +666,19 @@ server.registerTool(
|
|
|
647
666
|
return toolError('Item number is required (e.g. item=1).');
|
|
648
667
|
}
|
|
649
668
|
const index = getIndex(project_dir);
|
|
650
|
-
|
|
651
|
-
const recentKey = lastContextKey.get(index.root);
|
|
652
|
-
const recentCache = recentKey ? expandCache.get(recentKey) : null;
|
|
653
|
-
|
|
654
|
-
let match = null;
|
|
655
|
-
let cachedItemCount = 0;
|
|
656
|
-
|
|
657
|
-
if (recentCache && recentCache.items) {
|
|
658
|
-
// Strict: only expand from the most recent context call
|
|
659
|
-
recentCache.usedAt = Date.now(); // LRU: refresh on access
|
|
660
|
-
cachedItemCount = recentCache.items.length;
|
|
661
|
-
match = recentCache.items.find(i => i.num === item);
|
|
662
|
-
} else {
|
|
663
|
-
// No recent context — fallback to any cached context for this project
|
|
664
|
-
for (const [key, cached] of expandCache) {
|
|
665
|
-
if (cached.root === index.root && cached.items) {
|
|
666
|
-
cached.usedAt = Date.now(); // LRU: refresh on access
|
|
667
|
-
cachedItemCount = Math.max(cachedItemCount, cached.items.length);
|
|
668
|
-
const found = cached.items.find(i => i.num === item);
|
|
669
|
-
if (found) { match = found; break; }
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
669
|
+
const { match, itemCount, symbolName } = expandCacheInstance.lookup(index.root, item);
|
|
673
670
|
|
|
674
|
-
if (!match &&
|
|
671
|
+
if (!match && itemCount === 0) {
|
|
675
672
|
return toolError('No expandable items found. Run context command first to get numbered items.');
|
|
676
673
|
}
|
|
677
674
|
if (!match) {
|
|
678
|
-
const scopeHint =
|
|
679
|
-
return toolError(`Item ${item} not found${scopeHint}. Available items: 1-${
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const filePath = match.file || (index.root && match.relativePath ? path.join(index.root, match.relativePath) : null);
|
|
683
|
-
if (!filePath || !fs.existsSync(filePath)) {
|
|
684
|
-
return toolError(`Cannot locate file for ${match.name}`);
|
|
685
|
-
}
|
|
686
|
-
// Validate file is within project root
|
|
687
|
-
try {
|
|
688
|
-
const realPath = fs.realpathSync(filePath);
|
|
689
|
-
const realRoot = fs.realpathSync(index.root);
|
|
690
|
-
if (realPath !== realRoot && !realPath.startsWith(realRoot + path.sep)) {
|
|
691
|
-
return toolError(`File is outside project root: ${match.name}`);
|
|
692
|
-
}
|
|
693
|
-
} catch (e) {
|
|
694
|
-
return toolError(`Cannot resolve file path for ${match.name}`);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
698
|
-
const fileLines = content.split('\n');
|
|
699
|
-
const startLine = match.startLine || match.line || 1;
|
|
700
|
-
const endLine = match.endLine || startLine + 20;
|
|
701
|
-
|
|
702
|
-
const lines = [];
|
|
703
|
-
lines.push(`[${match.num}] ${match.name} (${match.type})`);
|
|
704
|
-
lines.push(`${match.relativePath}:${startLine}-${endLine}`);
|
|
705
|
-
lines.push('\u2550'.repeat(60));
|
|
706
|
-
|
|
707
|
-
for (let i = startLine - 1; i < Math.min(endLine, fileLines.length); i++) {
|
|
708
|
-
lines.push(fileLines[i]);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return toolResult(lines.join('\n'));
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// ==================================================================
|
|
715
|
-
// FILE DEPENDENCIES
|
|
716
|
-
// ==================================================================
|
|
717
|
-
|
|
718
|
-
case 'imports': {
|
|
719
|
-
if (!file) {
|
|
720
|
-
return toolError('File parameter is required for imports command.');
|
|
721
|
-
}
|
|
722
|
-
const index = getIndex(project_dir);
|
|
723
|
-
const result = index.imports(file);
|
|
724
|
-
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
725
|
-
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
726
|
-
return toolResult(output.formatImports(result, file));
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
case 'exporters': {
|
|
730
|
-
if (!file) {
|
|
731
|
-
return toolError('File parameter is required for exporters command.');
|
|
732
|
-
}
|
|
733
|
-
const index = getIndex(project_dir);
|
|
734
|
-
const result = index.exporters(file);
|
|
735
|
-
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
736
|
-
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
737
|
-
return toolResult(output.formatExporters(result, file));
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
case 'file_exports': {
|
|
741
|
-
if (!file) {
|
|
742
|
-
return toolError('File parameter is required for file_exports command.');
|
|
743
|
-
}
|
|
744
|
-
const index = getIndex(project_dir);
|
|
745
|
-
const result = index.fileExports(file);
|
|
746
|
-
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
747
|
-
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
748
|
-
return toolResult(output.formatFileExports(result, file));
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
case 'graph': {
|
|
752
|
-
if (!file) {
|
|
753
|
-
return toolError('File parameter is required for graph command.');
|
|
754
|
-
}
|
|
755
|
-
const index = getIndex(project_dir);
|
|
756
|
-
const result = index.graph(file, { direction: direction || 'both', maxDepth: depth ?? 2 });
|
|
757
|
-
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
758
|
-
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
759
|
-
return toolResult(output.formatGraph(result, {
|
|
760
|
-
showAll: all || depth !== undefined,
|
|
761
|
-
maxDepth: depth ?? 2,
|
|
762
|
-
file,
|
|
763
|
-
depthHint: 'Set depth parameter for deeper graph.',
|
|
764
|
-
allHint: 'Set depth to expand all children.'
|
|
765
|
-
}));
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// ==================================================================
|
|
769
|
-
// REFACTORING
|
|
770
|
-
// ==================================================================
|
|
771
|
-
|
|
772
|
-
case 'verify': {
|
|
773
|
-
const err = requireName(name);
|
|
774
|
-
if (err) return err;
|
|
775
|
-
const index = getIndex(project_dir);
|
|
776
|
-
const result = index.verify(name, { file });
|
|
777
|
-
return toolResult(output.formatVerify(result));
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
case 'plan': {
|
|
781
|
-
const err = requireName(name);
|
|
782
|
-
if (err) return err;
|
|
783
|
-
if (!add_param && !remove_param && !rename_to) {
|
|
784
|
-
return toolError('Plan requires an operation: add_param, remove_param, or rename_to');
|
|
785
|
-
}
|
|
786
|
-
const index = getIndex(project_dir);
|
|
787
|
-
const result = index.plan(name, {
|
|
788
|
-
addParam: add_param,
|
|
789
|
-
removeParam: remove_param,
|
|
790
|
-
renameTo: rename_to,
|
|
791
|
-
defaultValue: default_value,
|
|
792
|
-
file
|
|
793
|
-
});
|
|
794
|
-
return toolResult(output.formatPlan(result));
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
case 'diff_impact': {
|
|
798
|
-
// Validate git ref format to prevent argument injection
|
|
799
|
-
if (base && !/^[a-zA-Z0-9._\-~\/^@{}:]+$/.test(base)) {
|
|
800
|
-
return toolError(`Invalid git ref format: ${base}`);
|
|
801
|
-
}
|
|
802
|
-
const index = getIndex(project_dir);
|
|
803
|
-
const result = index.diffImpact({
|
|
804
|
-
base: base || 'HEAD',
|
|
805
|
-
staged: staged || false,
|
|
806
|
-
file: file || undefined
|
|
807
|
-
});
|
|
808
|
-
return toolResult(output.formatDiffImpact(result));
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// ==================================================================
|
|
812
|
-
// OTHER
|
|
813
|
-
// ==================================================================
|
|
814
|
-
|
|
815
|
-
case 'typedef': {
|
|
816
|
-
const err = requireName(name);
|
|
817
|
-
if (err) return err;
|
|
818
|
-
const index = getIndex(project_dir);
|
|
819
|
-
const result = index.typedef(name, { exact: exact || false });
|
|
820
|
-
return toolResult(output.formatTypedef(result, name));
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
case 'stacktrace': {
|
|
824
|
-
if (!stack || !stack.trim()) {
|
|
825
|
-
return toolError('Stack trace text is required.');
|
|
675
|
+
const scopeHint = symbolName ? ` (from last context for "${symbolName}")` : '';
|
|
676
|
+
return toolError(`Item ${item} not found${scopeHint}. Available items: 1-${itemCount}`);
|
|
826
677
|
}
|
|
827
|
-
const index = getIndex(project_dir);
|
|
828
|
-
const result = index.parseStackTrace(stack);
|
|
829
|
-
return toolResult(output.formatStackTrace(result));
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
case 'api': {
|
|
833
|
-
const index = getIndex(project_dir);
|
|
834
|
-
const result = index.api(file || undefined);
|
|
835
|
-
if (result?.error === 'file-not-found') return toolError(`File not found in project: ${file}`);
|
|
836
|
-
if (result?.error === 'file-ambiguous') return toolError(`Ambiguous file "${file}". Candidates:\n${result.candidates.map(c => ' ' + c).join('\n')}`);
|
|
837
|
-
return toolResult(output.formatApi(result, file || '.'));
|
|
838
|
-
}
|
|
839
678
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return toolResult(output.formatStats(stats, { top: top || 0 }));
|
|
679
|
+
const rendered = renderExpandItem(match, index.root, { validateRoot: true });
|
|
680
|
+
if (!rendered.ok) return toolError(rendered.error);
|
|
681
|
+
return toolResult(rendered.text);
|
|
844
682
|
}
|
|
845
683
|
|
|
846
684
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.20",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Universal Code Navigator — AST-based call graph analysis for AI agents. Find callers, trace impact, detect dead code across JS/TS, Python, Go, Rust, Java, and HTML. CLI, MCP server, and agent skill.",
|
|
6
6
|
"main": "index.js",
|