ucn 3.8.5 → 3.8.6

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/cli/index.js CHANGED
@@ -74,7 +74,7 @@ function parseFlags(tokens) {
74
74
  file: getValueFlag('--file'),
75
75
  exclude: parseExclude(),
76
76
  in: getValueFlag('--in'),
77
- includeTests: tokens.includes('--include-tests'),
77
+ includeTests: tokens.includes('--include-tests') ? true : undefined,
78
78
  includeExported: tokens.includes('--include-exported'),
79
79
  includeDecorated: tokens.includes('--include-decorated'),
80
80
  includeUncertain: tokens.includes('--include-uncertain'),
package/mcp/server.js CHANGED
@@ -45,20 +45,18 @@ const indexCache = new Map(); // projectDir → { index, checkedAt }
45
45
  const MAX_CACHE_SIZE = 10;
46
46
  const expandCacheInstance = new ExpandCache();
47
47
 
48
- function getIndex(projectDir) {
48
+ function getIndex(projectDir, options) {
49
+ const maxFiles = options && options.maxFiles;
49
50
  const absDir = path.resolve(projectDir);
50
51
  if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
51
52
  throw new Error(`Project directory not found: ${absDir}`);
52
53
  }
53
54
  const root = findProjectRoot(absDir);
54
55
  const cached = indexCache.get(root);
55
- const STALE_CHECK_INTERVAL_MS = 2000;
56
56
 
57
- // Throttle staleness checks isCacheStale() re-globs and stats all files
58
- if (cached) {
59
- if (Date.now() - cached.checkedAt < STALE_CHECK_INTERVAL_MS) {
60
- return cached.index; // Recently verified fresh
61
- }
57
+ // Always check staleness — MCP is used in iterative agent loops where
58
+ // files change between requests, so a throttle causes stale results.
59
+ if (cached && !maxFiles) {
62
60
  if (!cached.index.isCacheStale()) {
63
61
  cached.checkedAt = Date.now();
64
62
  return cached.index;
@@ -67,12 +65,15 @@ function getIndex(projectDir) {
67
65
 
68
66
  // Build new index (or rebuild stale one)
69
67
  const index = new ProjectIndex(root);
68
+ const buildOpts = { quiet: true, forceRebuild: false };
69
+ if (maxFiles) buildOpts.maxFiles = maxFiles;
70
70
  const loaded = index.loadCache();
71
- if (loaded && !index.isCacheStale()) {
72
- // Disk cache is fresh
71
+ if (loaded && !maxFiles && !index.isCacheStale()) {
72
+ // Disk cache is fresh (skip when maxFiles is set — cached index may have different file count)
73
73
  } else {
74
- index.build(null, { quiet: true, forceRebuild: loaded });
75
- index.saveCache();
74
+ buildOpts.forceRebuild = !!loaded;
75
+ index.build(null, buildOpts);
76
+ if (!maxFiles) index.saveCache(); // Don't pollute disk cache with partial indexes
76
77
  // Clear expand cache entries for this project — stale after rebuild
77
78
  expandCacheInstance.clearForRoot(root);
78
79
  }
@@ -93,7 +94,10 @@ function getIndex(projectDir) {
93
94
  }
94
95
  }
95
96
 
96
- indexCache.set(root, { index, checkedAt: Date.now() });
97
+ // Don't cache partial indexes (maxFiles) — they'd serve wrong results for full queries
98
+ if (!maxFiles) {
99
+ indexCache.set(root, { index, checkedAt: Date.now() });
100
+ }
97
101
  return index;
98
102
  }
99
103
 
@@ -298,7 +302,7 @@ server.registerTool(
298
302
  // ── Commands using shared executor ─────────────────────────
299
303
 
300
304
  case 'about': {
301
- const index = getIndex(project_dir);
305
+ const index = getIndex(project_dir, ep);
302
306
  const { ok, result, error } = execute(index, 'about', ep);
303
307
  if (!ok) return toolResult(error); // soft error — won't kill sibling calls
304
308
  return toolResult(output.formatAbout(result, {
@@ -309,7 +313,7 @@ server.registerTool(
309
313
  }
310
314
 
311
315
  case 'context': {
312
- const index = getIndex(project_dir);
316
+ const index = getIndex(project_dir, ep);
313
317
  const { ok, result: ctx, error } = execute(index, 'context', ep);
314
318
  if (!ok) return toolResult(error); // context uses soft error (not toolError)
315
319
  const { text, expandable } = output.formatContext(ctx, {
@@ -321,14 +325,14 @@ server.registerTool(
321
325
  }
322
326
 
323
327
  case 'impact': {
324
- const index = getIndex(project_dir);
328
+ const index = getIndex(project_dir, ep);
325
329
  const { ok, result, error } = execute(index, 'impact', ep);
326
330
  if (!ok) return toolResult(error); // soft error
327
331
  return toolResult(output.formatImpact(result));
328
332
  }
329
333
 
330
334
  case 'blast': {
331
- const index = getIndex(project_dir);
335
+ const index = getIndex(project_dir, ep);
332
336
  const { ok, result, error } = execute(index, 'blast', ep);
333
337
  if (!ok) return toolResult(error); // soft error
334
338
  return toolResult(output.formatBlast(result, {
@@ -337,14 +341,14 @@ server.registerTool(
337
341
  }
338
342
 
339
343
  case 'smart': {
340
- const index = getIndex(project_dir);
344
+ const index = getIndex(project_dir, ep);
341
345
  const { ok, result, error } = execute(index, 'smart', ep);
342
346
  if (!ok) return toolResult(error); // soft error
343
347
  return toolResult(output.formatSmart(result));
344
348
  }
345
349
 
346
350
  case 'trace': {
347
- const index = getIndex(project_dir);
351
+ const index = getIndex(project_dir, ep);
348
352
  const { ok, result, error } = execute(index, 'trace', ep);
349
353
  if (!ok) return toolResult(error); // soft error
350
354
  return toolResult(output.formatTrace(result, {
@@ -354,7 +358,7 @@ server.registerTool(
354
358
  }
355
359
 
356
360
  case 'reverse_trace': {
357
- const index = getIndex(project_dir);
361
+ const index = getIndex(project_dir, ep);
358
362
  const { ok, result, error } = execute(index, 'reverseTrace', ep);
359
363
  if (!ok) return toolResult(error);
360
364
  return toolResult(output.formatReverseTrace(result, {
@@ -363,7 +367,7 @@ server.registerTool(
363
367
  }
364
368
 
365
369
  case 'example': {
366
- const index = getIndex(project_dir);
370
+ const index = getIndex(project_dir, ep);
367
371
  const { ok, result, error } = execute(index, 'example', ep);
368
372
  if (!ok) return toolResult(error);
369
373
  if (!result) return toolResult(`No usage examples found for "${ep.name}".`);
@@ -371,7 +375,7 @@ server.registerTool(
371
375
  }
372
376
 
373
377
  case 'related': {
374
- const index = getIndex(project_dir);
378
+ const index = getIndex(project_dir, ep);
375
379
  const { ok, result, error } = execute(index, 'related', ep);
376
380
  if (!ok) return toolResult(error);
377
381
  if (!result) return toolResult(`Symbol "${ep.name}" not found.`);
@@ -384,7 +388,7 @@ server.registerTool(
384
388
  // ── Finding Code ────────────────────────────────────────────
385
389
 
386
390
  case 'find': {
387
- const index = getIndex(project_dir);
391
+ const index = getIndex(project_dir, ep);
388
392
  const { ok, result, error, note } = execute(index, 'find', ep);
389
393
  if (!ok) return toolResult(error); // soft error
390
394
  let text = output.formatFind(result, ep.name, ep.top);
@@ -393,7 +397,7 @@ server.registerTool(
393
397
  }
394
398
 
395
399
  case 'usages': {
396
- const index = getIndex(project_dir);
400
+ const index = getIndex(project_dir, ep);
397
401
  const { ok, result, error, note } = execute(index, 'usages', ep);
398
402
  if (!ok) return toolResult(error); // soft error
399
403
  let text = output.formatUsages(result, ep.name);
@@ -402,7 +406,7 @@ server.registerTool(
402
406
  }
403
407
 
404
408
  case 'toc': {
405
- const index = getIndex(project_dir);
409
+ const index = getIndex(project_dir, ep);
406
410
  const { ok, result, error, note } = execute(index, 'toc', ep);
407
411
  if (!ok) return toolResult(error); // soft error
408
412
  let text = output.formatToc(result, {
@@ -413,7 +417,7 @@ server.registerTool(
413
417
  }
414
418
 
415
419
  case 'search': {
416
- const index = getIndex(project_dir);
420
+ const index = getIndex(project_dir, ep);
417
421
  const { ok, result, error, structural } = execute(index, 'search', ep);
418
422
  if (!ok) return toolResult(error); // soft error
419
423
  if (structural) {
@@ -423,21 +427,21 @@ server.registerTool(
423
427
  }
424
428
 
425
429
  case 'tests': {
426
- const index = getIndex(project_dir);
430
+ const index = getIndex(project_dir, ep);
427
431
  const { ok, result, error } = execute(index, 'tests', ep);
428
432
  if (!ok) return toolResult(error); // soft error
429
433
  return toolResult(output.formatTests(result, ep.name));
430
434
  }
431
435
 
432
436
  case 'affected_tests': {
433
- const index = getIndex(project_dir);
437
+ const index = getIndex(project_dir, ep);
434
438
  const { ok, result, error } = execute(index, 'affectedTests', ep);
435
439
  if (!ok) return toolResult(error);
436
440
  return toolResult(output.formatAffectedTests(result));
437
441
  }
438
442
 
439
443
  case 'deadcode': {
440
- const index = getIndex(project_dir);
444
+ const index = getIndex(project_dir, ep);
441
445
  const { ok, result, error, note } = execute(index, 'deadcode', ep);
442
446
  if (!ok) return toolResult(error); // soft error
443
447
  const dcNote = note;
@@ -451,7 +455,7 @@ server.registerTool(
451
455
  }
452
456
 
453
457
  case 'entrypoints': {
454
- const index = getIndex(project_dir);
458
+ const index = getIndex(project_dir, ep);
455
459
  const { ok, result, error } = execute(index, 'entrypoints', ep);
456
460
  if (!ok) return toolResult(error);
457
461
  return toolResult(output.formatEntrypoints(result));
@@ -460,28 +464,28 @@ server.registerTool(
460
464
  // ── File Dependencies ───────────────────────────────────────
461
465
 
462
466
  case 'imports': {
463
- const index = getIndex(project_dir);
467
+ const index = getIndex(project_dir, ep);
464
468
  const { ok, result, error } = execute(index, 'imports', ep);
465
469
  if (!ok) return toolResult(error); // soft error
466
470
  return toolResult(output.formatImports(result, ep.file));
467
471
  }
468
472
 
469
473
  case 'exporters': {
470
- const index = getIndex(project_dir);
474
+ const index = getIndex(project_dir, ep);
471
475
  const { ok, result, error } = execute(index, 'exporters', ep);
472
476
  if (!ok) return toolResult(error); // soft error
473
477
  return toolResult(output.formatExporters(result, ep.file));
474
478
  }
475
479
 
476
480
  case 'file_exports': {
477
- const index = getIndex(project_dir);
481
+ const index = getIndex(project_dir, ep);
478
482
  const { ok, result, error } = execute(index, 'fileExports', ep);
479
483
  if (!ok) return toolResult(error); // soft error
480
484
  return toolResult(output.formatFileExports(result, ep.file));
481
485
  }
482
486
 
483
487
  case 'graph': {
484
- const index = getIndex(project_dir);
488
+ const index = getIndex(project_dir, ep);
485
489
  const { ok, result, error } = execute(index, 'graph', ep);
486
490
  if (!ok) return toolResult(error); // soft error
487
491
  return toolResult(output.formatGraph(result, {
@@ -493,7 +497,7 @@ server.registerTool(
493
497
  }
494
498
 
495
499
  case 'circular_deps': {
496
- const index = getIndex(project_dir);
500
+ const index = getIndex(project_dir, ep);
497
501
  const { ok, result, error } = execute(index, 'circularDeps', ep);
498
502
  if (!ok) return toolResult(error);
499
503
  return toolResult(output.formatCircularDeps(result));
@@ -502,21 +506,21 @@ server.registerTool(
502
506
  // ── Refactoring ─────────────────────────────────────────────
503
507
 
504
508
  case 'verify': {
505
- const index = getIndex(project_dir);
509
+ const index = getIndex(project_dir, ep);
506
510
  const { ok, result, error } = execute(index, 'verify', ep);
507
511
  if (!ok) return toolResult(error); // soft error
508
512
  return toolResult(output.formatVerify(result));
509
513
  }
510
514
 
511
515
  case 'plan': {
512
- const index = getIndex(project_dir);
516
+ const index = getIndex(project_dir, ep);
513
517
  const { ok, result, error } = execute(index, 'plan', ep);
514
518
  if (!ok) return toolResult(error); // soft error
515
519
  return toolResult(output.formatPlan(result));
516
520
  }
517
521
 
518
522
  case 'diff_impact': {
519
- const index = getIndex(project_dir);
523
+ const index = getIndex(project_dir, ep);
520
524
  const { ok, result, error } = execute(index, 'diffImpact', ep);
521
525
  if (!ok) return toolResult(error); // soft error — e.g. "not a git repo"
522
526
  return toolResult(output.formatDiffImpact(result));
@@ -525,21 +529,21 @@ server.registerTool(
525
529
  // ── Other ───────────────────────────────────────────────────
526
530
 
527
531
  case 'typedef': {
528
- const index = getIndex(project_dir);
532
+ const index = getIndex(project_dir, ep);
529
533
  const { ok, result, error } = execute(index, 'typedef', ep);
530
534
  if (!ok) return toolResult(error); // soft error
531
535
  return toolResult(output.formatTypedef(result, ep.name));
532
536
  }
533
537
 
534
538
  case 'stacktrace': {
535
- const index = getIndex(project_dir);
539
+ const index = getIndex(project_dir, ep);
536
540
  const { ok, result, error } = execute(index, 'stacktrace', ep);
537
541
  if (!ok) return toolResult(error); // soft error
538
542
  return toolResult(output.formatStackTrace(result));
539
543
  }
540
544
 
541
545
  case 'api': {
542
- const index = getIndex(project_dir);
546
+ const index = getIndex(project_dir, ep);
543
547
  const { ok, result, error, note } = execute(index, 'api', ep);
544
548
  if (!ok) return toolResult(error); // soft error
545
549
  let apiText = output.formatApi(result, ep.file || '.');
@@ -548,7 +552,7 @@ server.registerTool(
548
552
  }
549
553
 
550
554
  case 'stats': {
551
- const index = getIndex(project_dir);
555
+ const index = getIndex(project_dir, ep);
552
556
  const { ok, result, error } = execute(index, 'stats', ep);
553
557
  if (!ok) return toolResult(error); // soft error
554
558
  return toolResult(output.formatStats(result, { top: ep.top || 0 }));
@@ -559,7 +563,7 @@ server.registerTool(
559
563
  case 'fn': {
560
564
  const err = requireName(ep.name);
561
565
  if (err) return err;
562
- const index = getIndex(project_dir);
566
+ const index = getIndex(project_dir, ep);
563
567
  const { ok, result, error } = execute(index, 'fn', ep);
564
568
  if (!ok) return toolResult(error); // soft error
565
569
  // MCP path security: validate all result files are within project root
@@ -577,7 +581,7 @@ server.registerTool(
577
581
  if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
578
582
  return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
579
583
  }
580
- const index = getIndex(project_dir);
584
+ const index = getIndex(project_dir, ep);
581
585
  const { ok, result, error } = execute(index, 'class', ep);
582
586
  if (!ok) return toolResult(error); // soft error (class not found)
583
587
  // MCP path security: validate all result files are within project root
@@ -590,7 +594,7 @@ server.registerTool(
590
594
  }
591
595
 
592
596
  case 'lines': {
593
- const index = getIndex(project_dir);
597
+ const index = getIndex(project_dir, ep);
594
598
  const { ok, result, error } = execute(index, 'lines', ep);
595
599
  if (!ok) return toolResult(error); // soft error
596
600
  // MCP path security: validate file is within project root
@@ -603,7 +607,7 @@ server.registerTool(
603
607
  if (ep.item === undefined || ep.item === null) {
604
608
  return toolError('Item number is required (e.g. item=1).');
605
609
  }
606
- const index = getIndex(project_dir);
610
+ const index = getIndex(project_dir, ep);
607
611
  const lookup = expandCacheInstance.lookup(index.root, ep.item);
608
612
  const { ok, result, error } = execute(index, 'expand', {
609
613
  match: lookup.match, itemNum: ep.item,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.8.5",
3
+ "version": "3.8.6",
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",