ucn 3.0.0 → 3.1.1

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.

Potentially problematic release.


This version of ucn might be problematic. Click here for more details.

package/README.md CHANGED
@@ -38,13 +38,8 @@ JavaScript, TypeScript, Python, Go, Rust, Java
38
38
 
39
39
  ## Install
40
40
 
41
- Not published to npm yet. Install from source:
42
-
43
41
  ```bash
44
- git clone https://github.com/mleoca/ucn.git
45
- cd ucn
46
- npm install
47
- npm link # makes 'ucn' available globally
42
+ npm install -g ucn
48
43
  ```
49
44
 
50
45
  ### Claude Code (optional)
@@ -53,6 +48,12 @@ To use UCN as a skill in Claude Code:
53
48
 
54
49
  ```bash
55
50
  mkdir -p ~/.claude/skills
51
+
52
+ # If installed via npm:
53
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.claude/skills/
54
+
55
+ # If cloned from git:
56
+ git clone https://github.com/mleoca/ucn.git
56
57
  cp -r ucn/.claude/skills/ucn ~/.claude/skills/
57
58
  ```
58
59
 
@@ -62,6 +63,12 @@ To use UCN as a skill in OpenAI Codex:
62
63
 
63
64
  ```bash
64
65
  mkdir -p ~/.agents/skills
66
+
67
+ # If installed via npm:
68
+ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
69
+
70
+ # If cloned from git:
71
+ git clone https://github.com/mleoca/ucn.git
65
72
  cp -r ucn/.claude/skills/ucn ~/.agents/skills/
66
73
  ```
67
74
 
package/cli/index.js CHANGED
@@ -1550,6 +1550,66 @@ function printBestExample(index, name) {
1550
1550
  }
1551
1551
 
1552
1552
  function printContext(ctx, options = {}) {
1553
+ // Handle struct/interface types differently
1554
+ if (ctx.type && ['struct', 'interface', 'type'].includes(ctx.type)) {
1555
+ console.log(`Context for ${ctx.type} ${ctx.name}:`);
1556
+ console.log('═'.repeat(60));
1557
+
1558
+ // Display warnings if any
1559
+ if (ctx.warnings && ctx.warnings.length > 0) {
1560
+ console.log('\n⚠️ WARNINGS:');
1561
+ for (const w of ctx.warnings) {
1562
+ console.log(` ${w.message}`);
1563
+ }
1564
+ }
1565
+
1566
+ const expandable = [];
1567
+ let itemNum = 1;
1568
+
1569
+ // Show methods for structs/interfaces
1570
+ const methods = ctx.methods || [];
1571
+ console.log(`\nMETHODS (${methods.length}):`);
1572
+ for (const m of methods) {
1573
+ const receiver = m.receiver ? `(${m.receiver}) ` : '';
1574
+ const params = m.params || '...';
1575
+ const returnType = m.returnType ? `: ${m.returnType}` : '';
1576
+ console.log(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
1577
+ console.log(` ${m.file}:${m.line}`);
1578
+ expandable.push({
1579
+ num: itemNum++,
1580
+ type: 'method',
1581
+ name: m.name,
1582
+ relativePath: m.file,
1583
+ startLine: m.line
1584
+ });
1585
+ }
1586
+
1587
+ // Show callers (type references/usages)
1588
+ const callers = ctx.callers || [];
1589
+ console.log(`\nUSAGES (${callers.length}):`);
1590
+ for (const c of callers) {
1591
+ const callerName = c.callerName || '(module level)';
1592
+ const displayName = c.callerName ? ` [${callerName}]` : '';
1593
+ console.log(` [${itemNum}] ${c.relativePath}:${c.line}${displayName}`);
1594
+ expandable.push({
1595
+ num: itemNum++,
1596
+ type: 'usage',
1597
+ name: callerName,
1598
+ file: c.callerFile || c.file,
1599
+ relativePath: c.relativePath,
1600
+ line: c.line,
1601
+ startLine: c.callerStartLine || c.line,
1602
+ endLine: c.callerEndLine || c.line
1603
+ });
1604
+ console.log(` ${c.content.trim()}`);
1605
+ }
1606
+
1607
+ console.log(`\nUse "ucn . expand <N>" to see code for item N`);
1608
+ lastContextExpandable = expandable;
1609
+ return;
1610
+ }
1611
+
1612
+ // Standard function/method context
1553
1613
  console.log(`Context for ${ctx.function}:`);
1554
1614
  console.log('═'.repeat(60));
1555
1615
 
@@ -1565,8 +1625,9 @@ function printContext(ctx, options = {}) {
1565
1625
  const expandable = [];
1566
1626
  let itemNum = 1;
1567
1627
 
1568
- console.log(`\nCALLERS (${ctx.callers.length}):`);
1569
- for (const c of ctx.callers) {
1628
+ const callers = ctx.callers || [];
1629
+ console.log(`\nCALLERS (${callers.length}):`);
1630
+ for (const c of callers) {
1570
1631
  // All callers are numbered for expand command
1571
1632
  const callerName = c.callerName || '(module level)';
1572
1633
  const displayName = c.callerName ? ` [${callerName}]` : '';
@@ -1584,8 +1645,9 @@ function printContext(ctx, options = {}) {
1584
1645
  console.log(` ${c.content.trim()}`);
1585
1646
  }
1586
1647
 
1587
- console.log(`\nCALLEES (${ctx.callees.length}):`);
1588
- for (const c of ctx.callees) {
1648
+ const callees = ctx.callees || [];
1649
+ console.log(`\nCALLEES (${callees.length}):`);
1650
+ for (const c of callees) {
1589
1651
  const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
1590
1652
  console.log(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
1591
1653
  expandable.push({
package/core/imports.js CHANGED
@@ -431,6 +431,12 @@ function resolveImport(importPath, fromFile, config = {}) {
431
431
  }
432
432
  }
433
433
 
434
+ // Check Go module imports
435
+ if (config.language === 'go') {
436
+ const resolved = resolveGoImport(importPath, fromFile, config.root);
437
+ if (resolved) return resolved;
438
+ }
439
+
434
440
  return null; // External package
435
441
  }
436
442
 
@@ -439,6 +445,83 @@ function resolveImport(importPath, fromFile, config = {}) {
439
445
  return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
440
446
  }
441
447
 
448
+ // Cache for Go module paths
449
+ const goModuleCache = new Map();
450
+
451
+ /**
452
+ * Find and parse go.mod to get the module path
453
+ * @param {string} startDir - Directory to start searching from
454
+ * @returns {{modulePath: string, root: string}|null}
455
+ */
456
+ function findGoModule(startDir) {
457
+ // Check cache first
458
+ if (goModuleCache.has(startDir)) {
459
+ return goModuleCache.get(startDir);
460
+ }
461
+
462
+ let dir = startDir;
463
+ while (dir !== path.dirname(dir)) {
464
+ const goModPath = path.join(dir, 'go.mod');
465
+ if (fs.existsSync(goModPath)) {
466
+ try {
467
+ const content = fs.readFileSync(goModPath, 'utf-8');
468
+ // Parse module line: module github.com/user/project
469
+ const match = content.match(/^module\s+(\S+)/m);
470
+ if (match) {
471
+ const result = { modulePath: match[1], root: dir };
472
+ goModuleCache.set(startDir, result);
473
+ return result;
474
+ }
475
+ } catch (e) {
476
+ // Ignore read errors
477
+ }
478
+ }
479
+ dir = path.dirname(dir);
480
+ }
481
+
482
+ goModuleCache.set(startDir, null);
483
+ return null;
484
+ }
485
+
486
+ /**
487
+ * Resolve Go package import to local files
488
+ * @param {string} importPath - Go import path (e.g., "github.com/user/proj/pkg/util")
489
+ * @param {string} fromFile - File containing the import
490
+ * @param {string} projectRoot - Project root directory
491
+ * @returns {string|null} - Directory path containing the package, or null if external
492
+ */
493
+ function resolveGoImport(importPath, fromFile, projectRoot) {
494
+ const goMod = findGoModule(path.dirname(fromFile));
495
+ if (!goMod) return null;
496
+
497
+ const { modulePath, root } = goMod;
498
+
499
+ // Check if the import is within this module
500
+ if (importPath.startsWith(modulePath)) {
501
+ // Convert module path to relative path
502
+ // e.g., "github.com/user/proj/pkg/util" -> "pkg/util"
503
+ const relativePath = importPath.slice(modulePath.length).replace(/^\//, '');
504
+ const pkgDir = path.join(root, relativePath);
505
+
506
+ // Go imports are directories, find a .go file in the directory
507
+ if (fs.existsSync(pkgDir) && fs.statSync(pkgDir).isDirectory()) {
508
+ // Return the first .go file in the directory (not _test.go)
509
+ try {
510
+ const files = fs.readdirSync(pkgDir);
511
+ for (const file of files) {
512
+ if (file.endsWith('.go') && !file.endsWith('_test.go')) {
513
+ return path.join(pkgDir, file);
514
+ }
515
+ }
516
+ } catch (e) {
517
+ // Ignore read errors
518
+ }
519
+ }
520
+ }
521
+
522
+ return null;
523
+ }
524
+
442
525
  /**
443
526
  * Try to resolve a path with various extensions
444
527
  */
package/core/output.js CHANGED
@@ -307,25 +307,59 @@ function formatUsagesJson(usages, name) {
307
307
  * Format context (callers + callees) as JSON
308
308
  */
309
309
  function formatContextJson(context) {
310
+ // Handle struct/interface types differently
311
+ if (context.type && ['struct', 'interface', 'type'].includes(context.type)) {
312
+ const callers = context.callers || [];
313
+ const methods = context.methods || [];
314
+ return JSON.stringify({
315
+ type: context.type,
316
+ name: context.name,
317
+ file: context.file,
318
+ startLine: context.startLine,
319
+ endLine: context.endLine,
320
+ methodCount: methods.length,
321
+ usageCount: callers.length,
322
+ methods: methods.map(m => ({
323
+ name: m.name,
324
+ file: m.file,
325
+ line: m.line,
326
+ params: m.params,
327
+ returnType: m.returnType,
328
+ receiver: m.receiver
329
+ })),
330
+ usages: callers.map(c => ({
331
+ file: c.relativePath || c.file,
332
+ line: c.line,
333
+ expression: c.content,
334
+ callerName: c.callerName
335
+ })),
336
+ ...(context.warnings && { warnings: context.warnings })
337
+ }, null, 2);
338
+ }
339
+
340
+ // Standard function/method context
341
+ const callers = context.callers || [];
342
+ const callees = context.callees || [];
310
343
  return JSON.stringify({
311
344
  function: context.function,
312
345
  file: context.file,
313
- callerCount: context.callers.length,
314
- calleeCount: context.callees.length,
315
- callers: context.callers.map(c => ({
346
+ callerCount: callers.length,
347
+ calleeCount: callees.length,
348
+ callers: callers.map(c => ({
316
349
  file: c.relativePath || c.file,
317
350
  line: c.line,
318
351
  expression: c.content, // FULL expression
319
352
  callerName: c.callerName
320
353
  })),
321
- callees: context.callees.map(c => ({
354
+ callees: callees.map(c => ({
322
355
  name: c.name,
323
356
  type: c.type,
324
357
  file: c.relativePath || c.file,
325
358
  line: c.startLine,
326
359
  params: c.params, // FULL params
327
360
  weight: c.weight || 'normal' // Dependency weight: core, setup, utility
328
- }))
361
+ })),
362
+ ...(context.warnings && { warnings: context.warnings })
329
363
  }, null, 2);
330
364
  }
331
365
 
package/core/project.js CHANGED
@@ -166,7 +166,11 @@ class ProjectIndex {
166
166
  ...(item.extends && { extends: item.extends }),
167
167
  ...(item.implements && { implements: item.implements }),
168
168
  ...(item.indent !== undefined && { indent: item.indent }),
169
- ...(item.isNested && { isNested: item.isNested })
169
+ ...(item.isNested && { isNested: item.isNested }),
170
+ ...(item.isMethod && { isMethod: item.isMethod }),
171
+ ...(item.receiver && { receiver: item.receiver }),
172
+ ...(item.className && { className: item.className }),
173
+ ...(item.memberType && { memberType: item.memberType })
170
174
  };
171
175
  fileEntry.symbols.push(symbol);
172
176
 
@@ -649,6 +653,55 @@ class ProjectIndex {
649
653
  return usages;
650
654
  }
651
655
 
656
+ /**
657
+ * Find methods that belong to a class/struct/type
658
+ * Works for:
659
+ * - Go: methods with receiver field (e.g., receiver: "*TypeName")
660
+ * - Python/Java: methods with className field
661
+ * - Rust: impl methods with receiver field
662
+ * @param {string} typeName - The class/struct/interface name
663
+ * @returns {Array} Methods belonging to this type
664
+ */
665
+ findMethodsForType(typeName) {
666
+ const methods = [];
667
+ // Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
668
+ const baseTypeName = typeName.replace(/^\*/, '');
669
+
670
+ for (const [name, symbols] of this.symbols) {
671
+ for (const symbol of symbols) {
672
+ // Skip non-method types (fields, properties, etc.)
673
+ if (symbol.type === 'field' || symbol.type === 'property') {
674
+ continue;
675
+ }
676
+
677
+ // Check Go/Rust-style receiver (e.g., func (r *Router) Method())
678
+ if (symbol.isMethod && symbol.receiver) {
679
+ const receiverBase = symbol.receiver.replace(/^\*/, '');
680
+ if (receiverBase === baseTypeName) {
681
+ methods.push(symbol);
682
+ continue;
683
+ }
684
+ }
685
+
686
+ // Check Python/Java/JS-style className (class members)
687
+ // Must be a method type, not just any symbol with className
688
+ if (symbol.className === baseTypeName &&
689
+ (symbol.isMethod || symbol.type === 'method' || symbol.type === 'constructor')) {
690
+ methods.push(symbol);
691
+ continue;
692
+ }
693
+ }
694
+ }
695
+
696
+ // Sort by file then line
697
+ methods.sort((a, b) => {
698
+ if (a.relativePath !== b.relativePath) return a.relativePath.localeCompare(b.relativePath);
699
+ return a.startLine - b.startLine;
700
+ });
701
+
702
+ return methods;
703
+ }
704
+
652
705
  /**
653
706
  * Get context for a symbol (callers + callees)
654
707
  */
@@ -658,7 +711,53 @@ class ProjectIndex {
658
711
  return { function: name, file: null, callers: [], callees: [] };
659
712
  }
660
713
 
661
- const def = definitions[0]; // Use first definition
714
+ // Prefer class/struct/interface definitions over functions/methods/constructors
715
+ // This ensures context('ClassName') finds the class, not a constructor with same name
716
+ const typeOrder = ['class', 'struct', 'interface', 'type', 'impl'];
717
+ let def = definitions[0];
718
+ for (const d of definitions) {
719
+ if (typeOrder.includes(d.type)) {
720
+ def = d;
721
+ break;
722
+ }
723
+ }
724
+
725
+ // Special handling for class/struct/interface types
726
+ if (['class', 'struct', 'interface', 'type'].includes(def.type)) {
727
+ const methods = this.findMethodsForType(name);
728
+
729
+ const result = {
730
+ type: def.type,
731
+ name: name,
732
+ file: def.relativePath,
733
+ startLine: def.startLine,
734
+ endLine: def.endLine,
735
+ methods: methods.map(m => ({
736
+ name: m.name,
737
+ file: m.relativePath,
738
+ line: m.startLine,
739
+ params: m.params,
740
+ returnType: m.returnType,
741
+ receiver: m.receiver
742
+ })),
743
+ // Also include places where the type is used in function parameters/returns
744
+ callers: this.findCallers(name, { includeMethods: options.includeMethods })
745
+ };
746
+
747
+ if (definitions.length > 1) {
748
+ result.warnings = [{
749
+ type: 'ambiguous',
750
+ message: `Found ${definitions.length} definitions for "${name}". Using ${def.relativePath}:${def.startLine}. Use --file to disambiguate.`,
751
+ alternatives: definitions.slice(1).map(d => ({
752
+ file: d.relativePath,
753
+ line: d.startLine
754
+ }))
755
+ }];
756
+ }
757
+
758
+ return result;
759
+ }
760
+
662
761
  const callers = this.findCallers(name, { includeMethods: options.includeMethods });
663
762
  const callees = this.findCallees(def, { includeMethods: options.includeMethods });
664
763
 
@@ -787,8 +886,9 @@ class ProjectIndex {
787
886
  if (call.isMethod) {
788
887
  // Always skip this/self/cls calls (internal state access, not function calls)
789
888
  if (['this', 'self', 'cls'].includes(call.receiver)) continue;
790
- // Skip other method calls unless explicitly requested
791
- if (!options.includeMethods) continue;
889
+ // Go doesn't use this/self/cls - always include Go method calls
890
+ // For other languages, skip method calls unless explicitly requested
891
+ if (fileEntry.language !== 'go' && !options.includeMethods) continue;
792
892
  }
793
893
 
794
894
  // Skip definition lines
@@ -1594,7 +1694,14 @@ class ProjectIndex {
1594
1694
  for (const [name, symbols] of this.symbols) {
1595
1695
  for (const symbol of symbols) {
1596
1696
  // Skip non-function/class types
1597
- if (!['function', 'class', 'method'].includes(symbol.type)) {
1697
+ // Include various method types from different languages:
1698
+ // - function: standalone functions
1699
+ // - class, struct, interface: type definitions (skip them in deadcode)
1700
+ // - method: class methods
1701
+ // - static, public, abstract: Java method modifiers used as types
1702
+ // - constructor: constructors
1703
+ const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
1704
+ if (!callableTypes.includes(symbol.type)) {
1598
1705
  continue;
1599
1706
  }
1600
1707
 
@@ -1605,11 +1712,33 @@ class ProjectIndex {
1605
1712
 
1606
1713
  // Check if exported
1607
1714
  const fileEntry = this.files.get(symbol.file);
1715
+ const lang = fileEntry?.language;
1716
+ const mods = symbol.modifiers || [];
1717
+
1718
+ // Language-specific entry points (called by runtime, no AST-visible callers)
1719
+ // Go: main() and init() are called by runtime
1720
+ const isGoEntryPoint = lang === 'go' && (name === 'main' || name === 'init');
1721
+
1722
+ // Java: public static void main(String[] args) is the entry point
1723
+ const isJavaEntryPoint = lang === 'java' && name === 'main' &&
1724
+ mods.includes('public') && mods.includes('static');
1725
+
1726
+ // Python: Magic/dunder methods are called by the interpreter, not user code
1727
+ const isPythonMagicMethod = lang === 'python' && /^__\w+__$/.test(name);
1728
+
1729
+ // Rust: main() is entry point, #[test] functions are called by test runner
1730
+ const isRustEntryPoint = lang === 'rust' &&
1731
+ (name === 'main' || mods.includes('test'));
1732
+
1733
+ const isEntryPoint = isGoEntryPoint || isJavaEntryPoint ||
1734
+ isPythonMagicMethod || isRustEntryPoint;
1735
+
1608
1736
  const isExported = fileEntry && (
1609
1737
  fileEntry.exports.includes(name) ||
1610
- (symbol.modifiers && symbol.modifiers.includes('export')) ||
1611
- (symbol.modifiers && symbol.modifiers.includes('public')) ||
1612
- (fileEntry.language === 'go' && /^[A-Z]/.test(name))
1738
+ mods.includes('export') ||
1739
+ mods.includes('public') ||
1740
+ (lang === 'go' && /^[A-Z]/.test(name)) ||
1741
+ isEntryPoint
1613
1742
  );
1614
1743
 
1615
1744
  // Skip exported unless requested
@@ -3130,7 +3259,7 @@ class ProjectIndex {
3130
3259
  }
3131
3260
 
3132
3261
  const cacheData = {
3133
- version: 2, // Bump version for new cache format
3262
+ version: 4, // v4: className, memberType, isMethod for all languages
3134
3263
  root: this.root,
3135
3264
  buildTime: this.buildTime,
3136
3265
  timestamp: Date.now(),
@@ -3163,8 +3292,10 @@ class ProjectIndex {
3163
3292
  try {
3164
3293
  const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
3165
3294
 
3166
- // Check version compatibility (support v1 and v2)
3167
- if (cacheData.version !== 1 && cacheData.version !== 2) {
3295
+ // Check version compatibility
3296
+ // v4 adds className, memberType, isMethod for all languages
3297
+ // Only accept exactly version 4 (or future versions handled explicitly)
3298
+ if (cacheData.version !== 4) {
3168
3299
  return false;
3169
3300
  }
3170
3301
 
package/languages/java.js CHANGED
@@ -111,6 +111,12 @@ function findFunctions(code, parser) {
111
111
  if (processedRanges.has(rangeKey)) return true;
112
112
  processedRanges.add(rangeKey);
113
113
 
114
+ // Skip methods inside a class body (they're extracted as class members)
115
+ let parent = node.parent;
116
+ if (parent && parent.type === 'class_body') {
117
+ return true; // Skip - this is a class method
118
+ }
119
+
114
120
  const nameNode = node.childForFieldName('name');
115
121
  const paramsNode = node.childForFieldName('parameters');
116
122
 
@@ -393,6 +399,7 @@ function extractClassMembers(classNode, code) {
393
399
  endLine,
394
400
  memberType,
395
401
  modifiers,
402
+ isMethod: true, // Mark as method for context() lookups
396
403
  ...(returnType && { returnType }),
397
404
  ...(docstring && { docstring })
398
405
  });
@@ -417,6 +424,7 @@ function extractClassMembers(classNode, code) {
417
424
  endLine,
418
425
  memberType: 'constructor',
419
426
  modifiers,
427
+ isMethod: true, // Mark as method for context() lookups
420
428
  ...(docstring && { docstring })
421
429
  });
422
430
  }
@@ -531,6 +531,7 @@ function extractClassMembers(classNode, code) {
531
531
  memberType,
532
532
  isAsync,
533
533
  isGenerator: isGen,
534
+ isMethod: true, // Mark as method for context() lookups
534
535
  ...(returnType && { returnType }),
535
536
  ...(docstring && { docstring })
536
537
  });
@@ -557,6 +558,7 @@ function extractClassMembers(classNode, code) {
557
558
  endLine,
558
559
  memberType: name.startsWith('#') ? 'private' : 'field',
559
560
  isArrow: true,
561
+ isMethod: true, // Arrow fields are callable like methods
560
562
  ...(returnType && { returnType })
561
563
  });
562
564
  } else {
@@ -565,6 +567,7 @@ function extractClassMembers(classNode, code) {
565
567
  startLine,
566
568
  endLine,
567
569
  memberType: name.startsWith('#') ? 'private field' : 'field'
570
+ // Not a method - regular field
568
571
  });
569
572
  }
570
573
  }
@@ -73,6 +73,20 @@ function findFunctions(code, parser) {
73
73
  if (processedRanges.has(rangeKey)) return true;
74
74
  processedRanges.add(rangeKey);
75
75
 
76
+ // Skip functions that are inside a class (they're extracted as class members)
77
+ let parent = node.parent;
78
+ // Handle decorated_definition wrapper
79
+ if (parent && parent.type === 'decorated_definition') {
80
+ parent = parent.parent;
81
+ }
82
+ // Check if parent is a class body (block inside class_definition)
83
+ if (parent && parent.type === 'block') {
84
+ const grandparent = parent.parent;
85
+ if (grandparent && grandparent.type === 'class_definition') {
86
+ return true; // Skip - this is a class method
87
+ }
88
+ }
89
+
76
90
  const nameNode = node.childForFieldName('name');
77
91
  const paramsNode = node.childForFieldName('parameters');
78
92
 
@@ -282,6 +296,7 @@ function extractClassMembers(classNode, code) {
282
296
  endLine,
283
297
  memberType,
284
298
  isAsync,
299
+ isMethod: true, // Mark as method for context() lookups
285
300
  ...(returnType && { returnType }),
286
301
  ...(docstring && { docstring }),
287
302
  ...(memberDecorators.length > 0 && { decorators: memberDecorators })