ucn 3.7.44 → 3.7.46

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.
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: ucn
3
- description: "Code relationship analyzer (callers, call trees, impact, dead code) via tree-sitter AST. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on JS/TS, Python, Go, Rust, Java, HTML. Skip for plain text search or codebases under 500 LOC."
3
+ description: "Code intelligence toolkit — extract functions, trace callers, analyze impact, detect dead code without reading whole files. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on JS/TS, Python, Go, Rust, Java, HTML. Skip for plain text search or codebases under 500 LOC."
4
4
  allowed-tools: Bash(ucn *), Bash(npx ucn *)
5
5
  argument-hint: "[command] [symbol-name] [--flags]"
6
6
  ---
7
7
 
8
8
  # UCN — Universal Code Navigator
9
9
 
10
- Understands code structure via tree-sitter ASTs: who calls what, what breaks if you change something, full call trees, dead code. Works on JS/TS, Python, Go, Rust, Java. Also parses HTML files (inline scripts and event handlers).
10
+ Extract functions, trace call chains, find callers, and detect dead code without reading entire files. Works on JS/TS, Python, Go, Rust, Java, and HTML (inline scripts and event handlers).
11
11
 
12
12
  ## When to Reach for UCN Instead of Grep/Read
13
13
 
14
- **Use UCN when your next action would be:**
14
+ **Use UCN when the next action would be:**
15
15
 
16
16
  - "Let me grep for all callers of this function" → `ucn impact <name>` — finds every call site, grouped by file, with args shown
17
17
  - "Let me read this 800-line file to find one function" → `ucn fn <name> --file=<hint>` — extracts just that function
@@ -164,7 +164,7 @@ ucn trace problematic_function --depth=2 # See what it calls
164
164
  ```bash
165
165
  ucn impact the_function # Who will break?
166
166
  ucn smart the_function # See it + its helpers
167
- # ... make your changes ...
167
+ # ... make changes ...
168
168
  ucn verify the_function # Did all call sites survive?
169
169
  ```
170
170
 
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # UCN Universal Code Navigator
1
+ # UCN - Universal Code Navigator
2
2
 
3
- AST-powered code intelligence from the terminal.
3
+ Code intelligence for AI agents and developers - understand, extract, and navigate code without reading whole files.
4
4
 
5
- UCN answers structural code questions instantly:
6
- - Who calls this function?
7
- - What breaks if I change this signature?
8
- - What changed in this diff, and who depends on it?
9
- - What code is safe to delete?
5
+ Precise answers to structural code questions:
6
+ - Who calls this function? → without grepping the whole project
7
+ - What breaks if I change this? → every call site, with arguments
8
+ - What does this function do? extracted with dependencies inline
9
+ - What code is safe to delete? → verified unused symbols
10
10
 
11
- Instead of reading full files, UCN gives precise, AST-verified answers.
11
+ One command replaces 3-4 grep+read cycles. Powered by tree-sitter.
12
12
 
13
13
  [![npm](https://img.shields.io/npm/v/ucn)](https://www.npmjs.com/package/ucn)
14
14
  [![license](https://img.shields.io/npm/l/ucn)](LICENSE)
@@ -20,22 +20,21 @@ Instead of reading full files, UCN gives precise, AST-verified answers.
20
20
  ```bash
21
21
  npm install -g ucn
22
22
 
23
+ ucn toc # project overview
24
+ ucn fn handleRequest # extract a function without reading the file
23
25
  ucn about handleRequest # full picture: definition, callers, callees, tests
24
26
  ucn impact handleRequest # all call sites with arguments
25
27
  ucn trace main --depth=3 # call tree, no file reads
26
28
  ucn deadcode # unused functions, AST-verified
27
- ucn fn handleRequest # extract a function without reading the file
28
- ucn toc # project overview
29
- ucn --interactive # REPL mode, index stays in memory
30
29
  ```
31
30
 
32
- Parses JS/TS, Python, Go, Rust, Java, and HTML with tree-sitter. Runs locally.
31
+ Supports JS/TS, Python, Go, Rust, Java, and HTML. Runs locally.
33
32
 
34
33
  ```
35
- Terminal AI Agents Agent Skills
36
- │ │
37
- CLI MCP Skill
38
- └────────────────────┼───────────────────────┘
34
+ Terminal AI Agents Agent Skills
35
+ │ │
36
+ CLI MCP Skill
37
+ └────────────────────┼────────────────────┘
39
38
 
40
39
  ┌──────┴──────┐
41
40
  │ UCN Engine │
@@ -48,7 +47,7 @@ Parses JS/TS, Python, Go, Rust, Java, and HTML with tree-sitter. Runs locally.
48
47
 
49
48
  ## Why UCN
50
49
 
51
- UCN uses tree-sitter to parse code into an AST, then builds a call graph and symbol table on top of it. Instead of matching text, it understands which functions call which, what depends on what, and what's unused. Everything runs locally.
50
+ AI agents waste tokens reading entire files to find one function, or grep for callers and miss half of them. UCN builds a structural index of the codebase - it knows which functions call which, what depends on what, and what's unused. One command gives what would take 3-4 file reads and greps.
52
51
 
53
52
  "What happens when `build()` runs?"
54
53
 
@@ -139,7 +138,7 @@ VS Code uses `.vscode/mcp.json`:
139
138
 
140
139
  </details>
141
140
 
142
- All 28 commands ship as a single MCP tool, under 2KB of schema in the agent's context.
141
+ All 28 commands ship as a single MCP tool - under 2KB of context.
143
142
 
144
143
  ### Agent Skill (no server needed)
145
144
 
@@ -294,13 +293,13 @@ ucn deadcode --exclude=test # what can be deleted?
294
293
 
295
294
  ## Limitations
296
295
 
297
- UCN is static AST analysis, not runtime instrumentation.
296
+ UCN analyzes code structure statically - it doesn't run code.
298
297
 
299
- - **5 languages + HTML** JS/TS, Python, Go, Rust, Java. Falls back to text search for others.
300
- - **Static analysis only** Can't follow `eval()`, `getattr()`, reflection, or other dynamic dispatch.
301
- - **Duck-typed methods** `obj.method()` in JS/TS/Python is marked "uncertain" when the receiver type is ambiguous. Go/Rust/Java resolve with high confidence.
302
- - **Single project scope** Follows imports within the project but not into `node_modules` or `site-packages`.
303
- - **First-query index time** A few seconds on large projects. Cached incrementally after that.
298
+ - **5 languages + HTML** - JS/TS, Python, Go, Rust, Java. Falls back to text search for others.
299
+ - **Static analysis only** - Can't follow `eval()`, `getattr()`, reflection, or other dynamic dispatch.
300
+ - **Duck-typed methods** - `obj.method()` in JS/TS/Python is marked "uncertain" when the receiver type is ambiguous. Go/Rust/Java resolve with high confidence.
301
+ - **Single project scope** - Follows imports within the project but not into `node_modules` or `site-packages`.
302
+ - **First-query index time** - A few seconds on large projects. Cached incrementally after that.
304
303
 
305
304
  ---
306
305
 
package/core/execute.js CHANGED
@@ -99,6 +99,24 @@ function checkFileError(result, file) {
99
99
  return null;
100
100
  }
101
101
 
102
+ /**
103
+ * Validate that className filter actually matches a definition.
104
+ * Returns error string if className is invalid, null if OK.
105
+ */
106
+ function validateClassName(index, name, className) {
107
+ if (!className) return null;
108
+ const allDefs = index.symbols.get(name);
109
+ if (!allDefs || allDefs.length === 0) return null; // no defs at all — let the command handle "not found"
110
+ const matching = allDefs.filter(d => d.className === className);
111
+ if (matching.length > 0) return null; // className matched
112
+ // className specified but no definitions match
113
+ const available = [...new Set(allDefs.filter(d => d.className).map(d => d.className))];
114
+ if (available.length > 0) {
115
+ return `Symbol "${name}" not found in class "${className}". Available in: ${available.join(', ')}.`;
116
+ }
117
+ return `Symbol "${name}" is not a method of any class. Defined in: ${allDefs[0].relativePath}:${allDefs[0].startLine}.`;
118
+ }
119
+
102
120
  /** Parse a number param (handles string from CLI, number from MCP). */
103
121
  function num(val, fallback) {
104
122
  if (val == null) return fallback;
@@ -162,6 +180,8 @@ const HANDLERS = {
162
180
  const err = requireName(p.name);
163
181
  if (err) return { ok: false, error: err };
164
182
  applyClassMethodSyntax(p);
183
+ const classErr = validateClassName(index, p.name, p.className);
184
+ if (classErr) return { ok: false, error: classErr };
165
185
  const result = index.context(p.name, {
166
186
  includeMethods: p.includeMethods,
167
187
  includeUncertain: p.includeUncertain || false,
@@ -177,6 +197,8 @@ const HANDLERS = {
177
197
  const err = requireName(p.name);
178
198
  if (err) return { ok: false, error: err };
179
199
  applyClassMethodSyntax(p);
200
+ const classErr = validateClassName(index, p.name, p.className);
201
+ if (classErr) return { ok: false, error: classErr };
180
202
  const result = index.impact(p.name, {
181
203
  file: p.file,
182
204
  className: p.className,
@@ -291,6 +313,7 @@ const HANDLERS = {
291
313
  topLevel: p.topLevel,
292
314
  all: p.all,
293
315
  top: num(p.top, undefined),
316
+ file: p.file,
294
317
  });
295
318
  return { ok: true, result };
296
319
  },
@@ -562,6 +585,8 @@ const HANDLERS = {
562
585
  const err = requireName(p.name);
563
586
  if (err) return { ok: false, error: err };
564
587
  applyClassMethodSyntax(p);
588
+ const classErr = validateClassName(index, p.name, p.className);
589
+ if (classErr) return { ok: false, error: classErr };
565
590
  const result = index.verify(p.name, { file: p.file, className: p.className });
566
591
  return { ok: true, result };
567
592
  },
@@ -570,6 +595,8 @@ const HANDLERS = {
570
595
  const err = requireName(p.name);
571
596
  if (err) return { ok: false, error: err };
572
597
  applyClassMethodSyntax(p);
598
+ const classErr = validateClassName(index, p.name, p.className);
599
+ if (classErr) return { ok: false, error: classErr };
573
600
  if (!p.addParam && !p.removeParam && !p.renameTo) {
574
601
  return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
575
602
  }
package/core/project.js CHANGED
@@ -919,6 +919,21 @@ class ProjectIndex {
919
919
  }
920
920
  }
921
921
 
922
+ // For methods (symbols with className), objects can be passed as parameters
923
+ // to files with no import relationship to the definition file.
924
+ // Expand to all project files that mention the name (fast text pre-check).
925
+ if (symbol.className) {
926
+ for (const filePath of this.files.keys()) {
927
+ if (relevantFiles.has(filePath)) continue;
928
+ try {
929
+ const content = this._readFile(filePath);
930
+ if (content.includes(name)) {
931
+ relevantFiles.add(filePath);
932
+ }
933
+ } catch (e) { /* skip unreadable */ }
934
+ }
935
+ }
936
+
922
937
  let calls = 0;
923
938
  let definitions = 0;
924
939
  let imports = 0;
@@ -3393,7 +3408,38 @@ class ProjectIndex {
3393
3408
  let totalDynamic = 0;
3394
3409
  let totalTests = 0;
3395
3410
 
3411
+ // When file= is specified, scope to matching files only
3412
+ let fileFilter = null;
3413
+ if (options.file) {
3414
+ const resolved = this.findFile(options.file);
3415
+ if (resolved) {
3416
+ fileFilter = new Set([resolved]);
3417
+ } else {
3418
+ // Try substring match for partial paths
3419
+ const matching = [];
3420
+ for (const fp of this.files.keys()) {
3421
+ const rp = path.relative(this.root, fp);
3422
+ if (rp.includes(options.file) || fp.includes(options.file)) {
3423
+ matching.push(fp);
3424
+ }
3425
+ }
3426
+ if (matching.length > 0) {
3427
+ fileFilter = new Set(matching);
3428
+ } else {
3429
+ return {
3430
+ meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
3431
+ totals: { files: 0, lines: 0, functions: 0, classes: 0, state: 0, testFiles: 0 },
3432
+ summary: { topFunctionFiles: [], topLineFiles: [], entryFiles: [] },
3433
+ files: [],
3434
+ hiddenFiles: 0,
3435
+ error: `File not found in project: ${options.file}`
3436
+ };
3437
+ }
3438
+ }
3439
+ }
3440
+
3396
3441
  for (const [filePath, fileEntry] of this.files) {
3442
+ if (fileFilter && !fileFilter.has(filePath)) continue;
3397
3443
  let functions = fileEntry.symbols.filter(s =>
3398
3444
  s.type === 'function' || s.type === 'method' || s.type === 'static' ||
3399
3445
  s.type === 'constructor' || s.type === 'public' || s.type === 'abstract'
package/core/verify.js CHANGED
@@ -482,7 +482,23 @@ function plan(index, name, options = {}) {
482
482
  name: options.addParam,
483
483
  ...(options.defaultValue && { default: options.defaultValue })
484
484
  };
485
- newParams.push(newParam);
485
+
486
+ // When adding a required param (no default), insert before the first
487
+ // optional param to avoid producing invalid signatures in Python/TS
488
+ // (required params must precede optional ones).
489
+ if (!options.defaultValue) {
490
+ const firstOptIdx = newParams.findIndex(p => p.optional || p.default !== undefined);
491
+ if (firstOptIdx !== -1) {
492
+ // Also skip self/cls/&self/&mut self at position 0
493
+ const insertIdx = Math.max(firstOptIdx,
494
+ (newParams.length > 0 && ['self', 'cls', '&self', '&mut self'].includes(newParams[0].name)) ? 1 : 0);
495
+ newParams.splice(insertIdx, 0, newParam);
496
+ } else {
497
+ newParams.push(newParam);
498
+ }
499
+ } else {
500
+ newParams.push(newParam);
501
+ }
486
502
 
487
503
  // Generate new signature
488
504
  const paramsList = newParams.map(p => {
package/mcp/server.js CHANGED
@@ -158,7 +158,7 @@ function requireName(name) {
158
158
  // CONSOLIDATED TOOL REGISTRATION
159
159
  // ============================================================================
160
160
 
161
- const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies across JavaScript/TypeScript, Python, Go, Rust, Java, and HTML (inline scripts and event handlers). Use instead of grep/read for code relationships.
161
+ const TOOL_DESCRIPTION = `Code intelligence toolkit for AI agents. Extract specific functions, trace call chains, find all callers, and detect dead code without reading entire files or scanning full projects. Use instead of grep/read for code relationships. Supports JavaScript/TypeScript, Python, Go, Rust, Java, and HTML.
162
162
 
163
163
  TOP 5 (covers 90% of tasks): about, impact, trace, find, deadcode
164
164
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.7.44",
3
+ "version": "3.7.46",
4
4
  "mcpName": "io.github.mleoca/ucn",
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.",
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",
7
7
  "bin": {
8
8
  "ucn": "cli/index.js",
@@ -16,28 +16,29 @@
16
16
  "mcp",
17
17
  "mcp-server",
18
18
  "model-context-protocol",
19
+ "ai-agent",
20
+ "ai-coding",
21
+ "code-intelligence",
19
22
  "code-navigation",
20
23
  "code-analysis",
21
- "static-analysis",
24
+ "code-extraction",
22
25
  "call-graph",
23
26
  "callers",
24
27
  "impact-analysis",
25
28
  "dead-code",
26
29
  "deadcode",
27
- "ast",
28
- "tree-sitter",
29
- "parser",
30
- "skill",
31
30
  "agent-skill",
31
+ "skill",
32
32
  "cli",
33
- "ai-agent",
33
+ "tree-sitter",
34
+ "ast",
35
+ "static-analysis",
34
36
  "javascript",
35
37
  "typescript",
36
38
  "python",
37
39
  "go",
38
40
  "rust",
39
- "java",
40
- "html"
41
+ "java"
41
42
  ],
42
43
  "author": "Constantin-Mihail Leoca (https://github.com/mleoca)",
43
44
  "repository": {