ucn 3.8.4 → 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.
@@ -26,7 +26,7 @@ Extract functions, trace call chains, find callers, and detect dead code — wit
26
26
  - Language not supported (only JS/TS, Python, Go, Rust, Java, HTML)
27
27
  - Finding files by name — use glob
28
28
 
29
- ## The 5 Commands You'll Use Most
29
+ ## The Commands You'll Use Most
30
30
 
31
31
  ### 1. `about` — First stop for any investigation
32
32
 
@@ -157,6 +157,7 @@ ucn [target] <command> [name] [--flags]
157
157
 
158
158
  | Flag | When to use it |
159
159
  |------|---------------|
160
+ | `--class-name=X` | Scope to specific class (e.g., `--class-name=Repository` for method `save`) |
160
161
  | `--file=<pattern>` | Disambiguate when a name exists in multiple files (e.g., `--file=api`) |
161
162
  | `--exclude=test,mock` | Focus on production code only |
162
163
  | `--in=src/core` | Limit search to a subdirectory |
@@ -185,6 +186,10 @@ ucn [target] <command> [name] [--flags]
185
186
  | `--include-uncertain` | Include ambiguous/uncertain matches in `context`/`smart`/`about` |
186
187
  | `--show-confidence` | Show confidence scores (0.0–1.0) per caller/callee edge in `context`/`about` |
187
188
  | `--min-confidence=N` | Filter edges below confidence threshold (e.g., `--min-confidence=0.7` keeps only high-confidence edges) |
189
+ | `--calls-only` | Only show call/test-case matches in `tests` (skip file-level results) |
190
+ | `--add-param=<name>` | Add a parameter (`plan` command). Combine with `--default=<value>` |
191
+ | `--remove-param=<name>` | Remove a parameter (`plan` command) |
192
+ | `--rename-to=<name>` | Rename a function (`plan` command) |
188
193
  | `--include-exported` | Include exported symbols in `deadcode` results |
189
194
  | `--include-decorated` | Include decorated/annotated symbols in `deadcode` results |
190
195
  | `--framework=X` | Filter `entrypoints` by framework (e.g., `express`, `spring`, `celery`) |
package/README.md CHANGED
@@ -1,34 +1,13 @@
1
1
  # UCN - Universal Code Navigator
2
2
 
3
- Code intelligence for AI agents and developers - understand, extract, and navigate code without reading whole files.
3
+ See what code does before you touch it.
4
4
 
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
-
11
- One command replaces 3-4 grep+read cycles. Powered by tree-sitter.
5
+ Find symbols, trace callers, check impact, pick the right tests, extract code and spot what's dead - from the terminal.
12
6
 
13
7
  [![npm](https://img.shields.io/npm/v/ucn)](https://www.npmjs.com/package/ucn)
14
8
  [![license](https://img.shields.io/npm/l/ucn)](LICENSE)
15
9
 
16
- ---
17
-
18
- ## 60-Second Quickstart
19
-
20
- ```bash
21
- npm install -g ucn
22
-
23
- ucn toc # project overview
24
- ucn fn handleRequest # extract a function without reading the file
25
- ucn about handleRequest # full picture: definition, callers, callees, tests
26
- ucn impact handleRequest # all call sites with arguments
27
- ucn trace main --depth=3 # call tree, no file reads
28
- ucn deadcode # unused functions, AST-verified
29
- ```
30
-
31
- Supports JS/TS, Python, Go, Rust, Java, and HTML. Runs locally.
10
+ All commands, one engine, three surfaces:
32
11
 
33
12
  ```
34
13
  Terminal AI Agents Agent Skills
@@ -43,11 +22,27 @@ Supports JS/TS, Python, Go, Rust, Java, and HTML. Runs locally.
43
22
  └─────────────┘
44
23
  ```
45
24
 
25
+ Supports JavaScript, TypeScript, Python, Go, Rust, Java, and HTML inline scripts.
26
+
27
+ If you work with AI, add UCN as a [Skill or MCP](#ai-setup) and let the agent ask better code questions instead of reading whole files.
28
+ All commands ship as a single tool.
29
+
30
+ UCN is deliberately lightweight:
31
+
32
+ - **No background processes** - parses on demand, answers, exits
33
+ - **No language servers** - tree-sitter does the parsing, no compilation needed
34
+ - **MCP is optional** - only needed if you connect UCN to an AI agent, the CLI and Skill work on their own
35
+
46
36
  ---
47
37
 
48
- ## Why UCN
38
+ ```bash
39
+ npm install -g ucn
49
40
 
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.
41
+ ucn trace main --depth=3 # full execution flow
42
+ ucn about handleRequest # definition + callers + callees + tests
43
+ ucn impact handleRequest # every call site with arguments
44
+ ucn deadcode --exclude=test # unused code, AST-verified
45
+ ```
51
46
 
52
47
  "What happens when `build()` runs?"
53
48
 
@@ -55,47 +50,155 @@ AI agents waste tokens reading entire files to find one function, or grep for ca
55
50
  $ ucn trace build --depth=2
56
51
 
57
52
  build
58
- ├── detectProjectPattern (discovery.js:392) 1x
59
- │ ├── checkDir (discovery.js:396) 2x
60
- │ └── shouldIgnore (discovery.js:340) 1x
61
- ├── parseGitignore (discovery.js:123) 1x
62
- ├── expandGlob (discovery.js:183) 1x
63
- │ ├── parseGlobPattern (discovery.js:219) 1x
64
- │ ├── walkDir (discovery.js:276) 1x
65
- │ └── compareNames (discovery.js:162) 1x
66
- ├── indexFile (project.js:236) 1x
67
- │ ├── addSymbol (project.js:293) 4x
53
+ ├── detectProjectPattern (core/discovery.js:399) 1x
54
+ │ ├── checkDir (core/discovery.js:403) 2x
55
+ │ └── shouldIgnore (core/discovery.js:347) 1x
56
+ ├── parseGitignore (core/discovery.js:130) 1x
57
+ ├── expandGlob (core/discovery.js:190) 1x
58
+ │ ├── parseGlobPattern (core/discovery.js:226) 1x
59
+ │ ├── walkDir (core/discovery.js:283) 1x
60
+ │ └── compareNames (core/discovery.js:169) 1x
61
+ ├── indexFile (core/project.js:273) 1x
62
+ │ ├── addSymbol (core/project.js:343) 4x
68
63
  │ ├── detectLanguage (languages/index.js:157) 1x
69
- │ ├── parseFile (parser.js:93) 1x
70
- │ └── extractImports (imports.js:19) 1x
71
- ├── buildImportGraph (project.js:419) 1x
72
- └── buildInheritanceGraph (project.js:465) 1x
64
+ │ ├── parse (core/parser.js:69) 1x
65
+ │ └── extractImports (core/imports.js:19) 1x
66
+ ├── buildImportGraph (core/project.js:549) 1x
67
+ └── buildInheritanceGraph (core/project.js:627) 1x
73
68
  ```
74
69
 
75
- One command. No files opened. The full execution flow with every function located by file and line.
70
+ One command. No files opened. Every function located by file and line.
76
71
 
77
72
  ---
78
73
 
79
- ## What it does
74
+ ## Understand code you didn't write
75
+
76
+ `ucn about` gives you everything about a function in one shot - who calls it, what it calls, which tests cover it, and the source code.
77
+
78
+ ```
79
+ $ ucn about expandGlob
80
+
81
+ expandGlob (function)
82
+ ════════════════════════════════════════════════════════════
83
+ core/discovery.js:190-221
84
+ expandGlob (pattern, options = {})
85
+
86
+ CALLERS (3):
87
+ cli/index.js:859 [runGlobCommand]
88
+ const files = expandGlob(pattern);
89
+ core/cache.js:274 [isCacheStale]
90
+ const currentFiles = expandGlob(pattern, globOpts);
91
+ core/project.js:195 [build]
92
+ const files = expandGlob(pattern, globOpts);
93
+
94
+ CALLEES (3):
95
+ parseGlobPattern [utility] - core/discovery.js:226
96
+ walkDir [utility] - core/discovery.js:283
97
+ compareNames [utility] - core/discovery.js:169
98
+
99
+ TESTS: 6 matches in 2 file(s)
100
+ ```
101
+
102
+ Need to trace execution upward instead? `ucn reverse-trace fn` walks the caller chain back to entry points.
103
+
104
+ ## Change code without breaking things
105
+
106
+ Before touching a function, check if all existing call sites match its signature:
107
+
108
+ ```
109
+ $ ucn verify expandGlob
110
+
111
+ expandGlob (pattern, options = {})
112
+ Expected arguments: 1-2
113
+
114
+ STATUS: ✓ All calls valid (7 calls, 0 mismatches)
115
+ ```
116
+
117
+ Then preview the refactoring. UCN shows exactly what needs to change and where:
118
+
119
+ ```
120
+ $ ucn plan expandGlob --rename-to=expandGlobPattern
121
+
122
+ SIGNATURE CHANGE:
123
+ Before: expandGlob (pattern, options = {})
124
+ After: expandGlobPattern (pattern, options = {})
125
+
126
+ CHANGES NEEDED: 7 across 4 files
127
+
128
+ cli/index.js :859
129
+ const files = expandGlob(pattern);
130
+ → const files = expandGlobPattern(pattern);
131
+
132
+ core/cache.js :274
133
+ const currentFiles = expandGlob(pattern, globOpts);
134
+ → const currentFiles = expandGlobPattern(pattern, globOpts);
135
+
136
+ core/project.js :195
137
+ const files = expandGlob(pattern, globOpts);
138
+ → const files = expandGlobPattern(pattern, globOpts);
139
+ ```
140
+
141
+ Run `ucn diff-impact --staged` before committing to see what you changed and who calls it.
142
+
143
+ ## Find what to clean up
144
+
145
+ Which tests should you run after a change? `affected-tests` walks the blast radius and finds every test that touches the affected functions:
146
+
147
+ ```
148
+ $ ucn affected-tests expandGlob
149
+
150
+ 1 function changed → 18 functions affected (depth 3)
151
+ Summary: 18 affected → 12 test files, 11/18 functions covered (61%)
152
+
153
+ Uncovered (7): runGlobCommand, runProjectCommand, ...
154
+ ⚠ These affected functions have no test references
155
+ ```
156
+
157
+ ## Find unused code
158
+
159
+ ```
160
+ $ ucn deadcode --exclude=test
161
+
162
+ Dead code: 1 unused symbol(s)
163
+
164
+ core/discovery.js
165
+ [ 162- 166] legacyResolve (function)
166
+ ```
167
+
168
+ ## Extract without reading the whole file
169
+
170
+ ```
171
+ $ ucn fn compareNames
80
172
 
81
- | Task | Command | Output |
82
- |------|---------|--------|
83
- | Understand one symbol deeply | `ucn about expandGlob` | Definition, callers, callees, tests |
84
- | Who calls this and what do they pass? | `ucn impact shouldIgnore` | Call sites with argument context |
85
- | Map an execution path | `ucn trace expandGlob --depth=2` | Call tree |
86
- | Extract just one function | `ucn fn expandGlob` | Surgical snippet, no file read |
87
- | Check all call sites match signature | `ucn verify expandGlob` | Mismatch/uncertain call sites |
88
- | Review branch impact | `ucn diff-impact --base=main` | Changed functions + downstream callers |
89
- | Find deletable code | `ucn deadcode` | Unused symbols, AST-verified |
90
- | Get function + helpers inline | `ucn smart shouldIgnore` | Source with dependencies expanded |
173
+ core/discovery.js:169
174
+ [ 169- 177] compareNames(a, b)
175
+ ────────────────────────────────────────────────────────────
176
+ function compareNames(a, b) {
177
+ const aLower = a.toLowerCase();
178
+ const bLower = b.toLowerCase();
179
+ if (aLower < bLower) return -1;
180
+ if (aLower > bLower) return 1;
181
+ if (a < b) return -1;
182
+ if (a > b) return 1;
183
+ return 0;
184
+ }
185
+ ```
91
186
 
92
187
  ---
93
188
 
94
- ## Setup
189
+ ## Testing and reliability
190
+
191
+ - **Fast** - indexes its own ~25K-line codebase in under 100ms, cached after first run
192
+ - **Discipline** - every bug fix gets a regression test, test code is ~3x the source
193
+ - **Coverage** - every command, every supported language, every surface (CLI, MCP, interactive)
194
+ - **Systematic** - a harness exercises all command and flag combinations against real multi-language fixtures
195
+ - **Test types** - unit, integration, per-language regression, formatter, cache, MCP edge cases, architecture parity guards
95
196
 
96
- ### MCP Server (for AI agents)
197
+ ---
198
+
199
+ ## AI Setup
97
200
 
98
- One-line setup:
201
+ ### MCP
99
202
 
100
203
  ```bash
101
204
  # Claude Code
@@ -138,12 +241,8 @@ VS Code uses `.vscode/mcp.json`:
138
241
 
139
242
  </details>
140
243
 
141
- All commands ship as a single MCP tool - under 2KB of context.
142
-
143
244
  ### Agent Skill (no server needed)
144
245
 
145
- Drop-in for Claude Code or Codex CLI:
146
-
147
246
  ```bash
148
247
  # Claude Code
149
248
  mkdir -p ~/.claude/skills
@@ -156,156 +255,126 @@ cp -r "$(npm root -g)/ucn/.claude/skills/ucn" ~/.agents/skills/
156
255
 
157
256
  ---
158
257
 
159
- ## Examples
160
-
161
- **Extract a function** without reading the file:
162
-
163
- ```
164
- $ ucn fn expandGlob
165
-
166
- core/discovery.js:183
167
- [ 183- 214] expandGlob(pattern, options = {})
168
- ────────────────────────────────────────────────────────────
169
- function expandGlob(pattern, options = {}) {
170
- const root = path.resolve(options.root || process.cwd());
171
- const ignores = options.ignores || DEFAULT_IGNORES;
172
- ...
173
- return files.sort(compareNames);
174
- }
175
- ```
176
-
177
- **See callers and callees:**
178
-
179
- ```
180
- $ ucn context expandGlob
181
-
182
- CALLERS (7):
183
- [1] cli/index.js:785 [runGlobCommand]
184
- const files = expandGlob(pattern);
185
- [2] core/cache.js:149 [isCacheStale]
186
- const currentFiles = expandGlob(pattern, globOpts);
187
- [3] core/project.js:171 [build]
188
- const files = expandGlob(pattern, globOpts);
189
- ...
190
-
191
- CALLEES (3):
192
- [8] parseGlobPattern [utility] - core/discovery.js:219
193
- [9] walkDir [utility] - core/discovery.js:276
194
- [10] compareNames [utility] - core/discovery.js:162
195
- ```
196
-
197
- **See impact of recent edits:**
198
-
199
- ```
200
- $ ucn diff-impact --base=HEAD~1
201
-
202
- 3 modified, 1 new, 12 call sites across 4 files
203
-
204
- MODIFIED FUNCTIONS:
205
-
206
- processOrder
207
- src/orders/service.ts:45
208
- Lines added: 48-52, Lines deleted: 49
209
- Callers (3):
210
- src/api/checkout.ts:89 [handleCheckout]
211
- await processOrder(cart.items, req.user)
212
- src/workers/batch.ts:12 [batchProcess]
213
- processOrder(order.items, systemUser)
214
- src/jobs/daily.ts:88 [runDailyOrders]
215
- results.push(await processOrder(items, admin))
216
- ```
217
-
218
- **Trace a call tree:**
219
-
220
- ```
221
- $ ucn trace expandGlob --depth=2
222
-
223
- expandGlob
224
- ├── parseGlobPattern (core/discovery.js:219) [utility] 1x
225
- │ └── globToRegex (core/discovery.js:256) [utility] 1x
226
- ├── walkDir (core/discovery.js:276) [utility] 1x
227
- │ ├── compareNames (core/discovery.js:162) [utility] 1x
228
- │ ├── shouldIgnore (core/discovery.js:340) [utility] 1x
229
- └── walkDir (core/discovery.js:276) [utility] 1x (see above)
230
- └── compareNames (core/discovery.js:162) [utility] 1x (see above)
231
- ```
232
-
233
- **Find unused code:**
234
-
235
- ```
236
- $ ucn deadcode --exclude=test
237
-
238
- Dead code: 1 unused symbol(s)
239
-
240
- core/discovery.js
241
- [ 162- 166] legacyResolve (function)
242
- ```
243
-
244
- ---
245
-
246
- ## Workflows
247
-
248
- ```bash
249
- # Investigating a bug
250
- ucn about buggyFunction # understand it fully
251
- ucn trace buggyFunction --depth=2 # see what it calls
252
-
253
- # Before modifying code
254
- ucn impact theFunction # who will break?
255
- ucn smart theFunction # function + its helpers inline
256
- # ... make changes ...
257
- ucn verify theFunction # do all call sites still match?
258
-
259
- # Before committing
260
- ucn diff-impact --staged # what I changed + who calls it
261
-
262
- # Cleanup
263
- ucn deadcode --exclude=test # what can be deleted?
264
- ```
265
-
266
- ---
267
-
268
- ## All commands
269
-
270
- ```
271
- UNDERSTAND MODIFY SAFELY
272
- ───────────────────── ─────────────────────
273
- about full picture impact all call sites
274
- context callers + callees blast transitive impact
275
- smart function + helpers diff-impact git diff + callers
276
- trace call tree verify signature check
277
- reverse-trace callers → root plan refactor preview
278
-
279
- FIND & EXTRACT ARCHITECTURE
280
- ───────────────────── ─────────────────────
281
- find locate definitions imports file dependencies
282
- usages all occurrences exporters reverse dependencies
283
- fn extract function graph dependency tree
284
- class extract class circular-deps import cycles
285
- toc project overview related sibling functions
286
- deadcode unused code tests find test coverage
287
- search text search affected-tests tests for changes
288
- example best usage example stacktrace error trace context
289
- lines extract line range api public API surface
290
- expand drill into context typedef type definitions
291
- file-exports file's exports
292
- stats project stats
258
+ ## Full help
259
+
260
+ ```text
261
+ UCN - Universal Code Navigator
262
+
263
+ Supported: JavaScript, TypeScript, Python, Go, Rust, Java, HTML
264
+
265
+ Usage:
266
+ ucn [command] [args] Project mode (current directory)
267
+ ucn <file> [command] [args] Single file mode
268
+ ucn <dir> [command] [args] Project mode (specific directory)
269
+ ucn "pattern" [command] [args] Glob pattern mode
270
+ (Default output is text; add --json for machine-readable JSON)
271
+
272
+ ═══════════════════════════════════════════════════════════════════════════════
273
+ UNDERSTAND CODE
274
+ ═══════════════════════════════════════════════════════════════════════════════
275
+ about <name> Full picture (definition, callers, callees, tests, code)
276
+ context <name> Who calls this + what it calls (numbered for expand)
277
+ smart <name> Function + all dependencies inline
278
+ impact <name> What breaks if changed (call sites grouped by file)
279
+ blast <name> Transitive blast radius (callers of callers, --depth=N)
280
+ trace <name> Call tree visualization (--depth=N expands all children)
281
+ reverse-trace <name> Upward call chain to entry points (--depth=N, default 5)
282
+ related <name> Find similar functions (same file, shared deps)
283
+ example <name> Best usage example with context
284
+
285
+ ═══════════════════════════════════════════════════════════════════════════════
286
+ FIND CODE
287
+ ═══════════════════════════════════════════════════════════════════════════════
288
+ find <name> Find symbol definitions (supports glob: find "handle*")
289
+ usages <name> All usages grouped: definitions, calls, imports, references
290
+ toc Table of contents (compact; --detailed lists all symbols)
291
+ search <term> Text search (regex default, --context=N, --exclude=, --in=)
292
+ Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
293
+ tests <name> Find test files for a function
294
+ affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
295
+
296
+ ═══════════════════════════════════════════════════════════════════════════════
297
+ EXTRACT CODE
298
+ ═══════════════════════════════════════════════════════════════════════════════
299
+ fn <name>[,n2,...] Extract function(s) (comma-separated for bulk, --file)
300
+ class <name> Extract class
301
+ lines <range> Extract line range (e.g., lines 50-100)
302
+ expand <N> Show code for item N from context output
303
+
304
+ ═══════════════════════════════════════════════════════════════════════════════
305
+ FILE DEPENDENCIES
306
+ ═══════════════════════════════════════════════════════════════════════════════
307
+ imports <file> What does file import
308
+ exporters <file> Who imports this file
309
+ file-exports <file> What does file export
310
+ graph <file> Full dependency tree (--depth=N, --direction=imports|importers|both)
311
+ circular-deps Detect circular import chains (--file=, --exclude=)
312
+
313
+ ═══════════════════════════════════════════════════════════════════════════════
314
+ REFACTORING HELPERS
315
+ ═══════════════════════════════════════════════════════════════════════════════
316
+ plan <name> Preview refactoring (--add-param, --remove-param, --rename-to)
317
+ verify <name> Check all call sites match signature
318
+ diff-impact What changed in git diff and who calls it (--base, --staged)
319
+ deadcode Find unused functions/classes
320
+ entrypoints Detect framework entry points (routes, DI, tasks)
321
+
322
+ ═══════════════════════════════════════════════════════════════════════════════
323
+ OTHER
324
+ ═══════════════════════════════════════════════════════════════════════════════
325
+ api Show exported/public symbols
326
+ typedef <name> Find type definitions
327
+ stats Project statistics (--functions for per-function line counts)
328
+ stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
329
+
330
+ Common Flags:
331
+ --file <pattern> Filter by file path (e.g., --file=routes)
332
+ --exclude=a,b Exclude patterns (e.g., --exclude=test,mock)
333
+ --in=<path> Only in path (e.g., --in=src/core)
334
+ --depth=N Trace/graph depth (default: 3, also expands all children)
335
+ --direction=X Graph direction: imports, importers, or both (default: both)
336
+ --all Expand truncated sections (about, trace, graph, related)
337
+ --top=N Limit results (find, deadcode)
338
+ --limit=N Limit result count (find, usages, search, deadcode, api, toc)
339
+ --max-files=N Max files to index (large projects)
340
+ --context=N Lines of context around matches
341
+ --json Machine-readable output
342
+ --code-only Filter out comments and strings
343
+ --with-types Include type definitions
344
+ --include-tests Include test files
345
+ --class-name=X Scope to specific class (e.g., --class-name=Repository)
346
+ --include-methods Include method calls (obj.fn) in caller/callee analysis
347
+ --include-uncertain Include ambiguous/uncertain matches
348
+ --show-confidence Show confidence scores per caller/callee edge
349
+ --min-confidence=N Filter edges below confidence threshold (0.0-1.0)
350
+ --include-exported Include exported symbols in deadcode
351
+ --no-regex Force plain text search (regex is default)
352
+ --functions Show per-function line counts (stats command)
353
+ --include-decorated Include decorated/annotated symbols in deadcode
354
+ --framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
355
+ --exact Exact name match only (find)
356
+ --calls-only Only show call/test-case matches (tests)
357
+ --case-sensitive Case-sensitive text search (search)
358
+ --detailed List all symbols in toc (compact by default)
359
+ --top-level Show only top-level functions in toc
360
+ --max-lines=N Max source lines for class (large classes show summary)
361
+ --no-cache Disable caching
362
+ --clear-cache Clear cache before running
363
+ --base=<ref> Git ref for diff-impact (default: HEAD)
293
364
  ```
294
365
 
295
366
  ---
296
367
 
297
368
  ## Limitations
298
369
 
299
- UCN analyzes code structure statically - it doesn't run code.
370
+ - Single-project scope - follows imports within the project, not into `node_modules` or `site-packages`
371
+ - No runtime execution - static analysis only
372
+ - Dynamic dispatch and reflection are only partially visible or invisible
373
+ - JS, TS, and Python method calls can be uncertain when receiver type is unknown
374
+ - Large repos take a few seconds on the first query, then use cache
300
375
 
301
- - **5 languages + HTML** - JS/TS, Python, Go, Rust, Java. Falls back to text search for others.
302
- - **Static analysis only** - Can't follow `eval()`, `getattr()`, reflection, or other dynamic dispatch.
303
- - **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.
304
- - **Single project scope** - Follows imports within the project but not into `node_modules` or `site-packages`.
305
- - **First-query index time** - A few seconds on large projects. Cached incrementally after that.
376
+ If you need compiler diagnostics, taint analysis, or runtime semantics, those are different tools for different jobs. UCN trades that depth for speed, portability, and zero setup.
306
377
 
307
378
  ---
308
379
 
309
- ## License
310
-
311
380
  MIT
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'),
@@ -171,7 +171,7 @@ const VALUE_FLAGS = new Set([
171
171
  '--add-param', '--remove-param', '--rename-to', '--default',
172
172
  '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
173
173
  '--type', '--param', '--receiver', '--returns', '--decorator',
174
- '--limit', '--max-files', '--min-confidence', '--stack'
174
+ '--limit', '--max-files', '--min-confidence', '--stack', '--framework'
175
175
  ]);
176
176
 
177
177
  // Remove flags from args, then add args after -- (which are all positional)
@@ -1124,6 +1124,7 @@ Common Flags:
1124
1124
  --code-only Filter out comments and strings
1125
1125
  --with-types Include type definitions
1126
1126
  --include-tests Include test files
1127
+ --class-name=X Scope to specific class (e.g., --class-name=Repository)
1127
1128
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1128
1129
  --include-uncertain Include ambiguous/uncertain matches
1129
1130
  --show-confidence Show confidence scores per caller/callee edge
@@ -1248,7 +1249,7 @@ Flags can be added per-command: context myFunc --include-methods
1248
1249
  const tokens = input.split(/\s+/);
1249
1250
  const command = tokens[0];
1250
1251
  // Flags that take a space-separated value (--flag value)
1251
- const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name']);
1252
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--framework']);
1252
1253
  const flagTokens = [];
1253
1254
  const argTokens = [];
1254
1255
  const skipNext = new Set();
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
 
@@ -185,6 +189,7 @@ UNDERSTANDING CODE:
185
189
  - smart <name>: Get a function's source with all called functions expanded inline (not constants/variables). Use to understand or modify a function and its dependencies in one read.
186
190
  - trace <name>: Call tree from a function downward. Use to understand "what happens when X runs" — maps which modules a pipeline touches without reading files. Set depth (default: 3); setting depth expands all children.
187
191
  - example <name>: Best real-world usage example. Automatically scores call sites by quality and returns the top one with context. Use to understand expected calling patterns.
192
+ - reverse_trace <name>: Upward call chain to entry points — who calls this, who calls those callers, etc. Use to find all paths that lead to a function. Set depth (default: 5) to control how far up. Complement to trace (which goes downward).
188
193
  - related <name>: Sibling functions: same file, similar names, or shared callers/callees. Find companions to update together (e.g., serialize when you're changing deserialize). Name-based, not semantic.
189
194
 
190
195
  FINDING CODE:
@@ -195,6 +200,7 @@ FINDING CODE:
195
200
  - tests <name>: Find test files covering a function, test case names, and how it's called in tests. Use before modifying or to find test patterns to follow.
196
201
  - affected_tests <name>: Which tests to run after changing a function. Combines blast (transitive callers) with test detection. Shows test files, coverage %, and uncovered functions. Use depth= to control depth.
197
202
  - deadcode: Find dead code: functions/classes with zero callers. Use during cleanup to identify safely deletable code. Excludes exported, decorated, and test symbols by default — use include_exported/include_decorated/include_tests to expand.
203
+ - entrypoints: Detect framework entry points: routes, handlers, DI providers, tasks. Auto-detects Express, Flask, Spring, Gin, Actix, and more. Use framework= to filter by specific framework.
198
204
 
199
205
  EXTRACTING CODE (use instead of reading entire files):
200
206
  - fn <name>: Extract one or more functions. Comma-separated for bulk extraction (e.g. "parse,format,validate"). Use file to disambiguate.
@@ -207,6 +213,7 @@ FILE DEPENDENCIES (require file param):
207
213
  - exporters: Every file that imports/depends on this file — shows dependents rather than dependencies. Use before moving, renaming, or deleting.
208
214
  - file_exports: File's public API: all exported functions, classes, variables with signatures. Use to understand what a module offers before importing. Requires explicit export markers; use toc --detailed as fallback.
209
215
  - graph: File-level dependency tree. Use to understand module architecture — which files form a cluster, what the dependency chain looks like. Set direction ("imports"/"importers"/"both"). Can be noisy — use depth=1 for large codebases.
216
+ - circular_deps: Detect circular import chains. Shows cycle paths and involved files. Use file= to check a specific file, exclude= to ignore paths.
210
217
 
211
218
  REFACTORING:
212
219
  - verify <name>: Check all call sites match function signature (argument count). Run before adding/removing parameters to catch breakage early.
@@ -272,7 +279,8 @@ server.registerTool(
272
279
  returns: z.string().optional().describe('Filter by return type (structural search). E.g. "Promise", "error".'),
273
280
  decorator: z.string().optional().describe('Filter by decorator/annotation (structural search). E.g. "Route", "Test".'),
274
281
  exported: z.boolean().optional().describe('Only exported/public symbols (structural search).'),
275
- unused: z.boolean().optional().describe('Only symbols with zero callers (structural search).')
282
+ unused: z.boolean().optional().describe('Only symbols with zero callers (structural search).'),
283
+ framework: z.string().optional().describe('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.')
276
284
 
277
285
  })
278
286
  },
@@ -294,7 +302,7 @@ server.registerTool(
294
302
  // ── Commands using shared executor ─────────────────────────
295
303
 
296
304
  case 'about': {
297
- const index = getIndex(project_dir);
305
+ const index = getIndex(project_dir, ep);
298
306
  const { ok, result, error } = execute(index, 'about', ep);
299
307
  if (!ok) return toolResult(error); // soft error — won't kill sibling calls
300
308
  return toolResult(output.formatAbout(result, {
@@ -305,7 +313,7 @@ server.registerTool(
305
313
  }
306
314
 
307
315
  case 'context': {
308
- const index = getIndex(project_dir);
316
+ const index = getIndex(project_dir, ep);
309
317
  const { ok, result: ctx, error } = execute(index, 'context', ep);
310
318
  if (!ok) return toolResult(error); // context uses soft error (not toolError)
311
319
  const { text, expandable } = output.formatContext(ctx, {
@@ -317,14 +325,14 @@ server.registerTool(
317
325
  }
318
326
 
319
327
  case 'impact': {
320
- const index = getIndex(project_dir);
328
+ const index = getIndex(project_dir, ep);
321
329
  const { ok, result, error } = execute(index, 'impact', ep);
322
330
  if (!ok) return toolResult(error); // soft error
323
331
  return toolResult(output.formatImpact(result));
324
332
  }
325
333
 
326
334
  case 'blast': {
327
- const index = getIndex(project_dir);
335
+ const index = getIndex(project_dir, ep);
328
336
  const { ok, result, error } = execute(index, 'blast', ep);
329
337
  if (!ok) return toolResult(error); // soft error
330
338
  return toolResult(output.formatBlast(result, {
@@ -333,14 +341,14 @@ server.registerTool(
333
341
  }
334
342
 
335
343
  case 'smart': {
336
- const index = getIndex(project_dir);
344
+ const index = getIndex(project_dir, ep);
337
345
  const { ok, result, error } = execute(index, 'smart', ep);
338
346
  if (!ok) return toolResult(error); // soft error
339
347
  return toolResult(output.formatSmart(result));
340
348
  }
341
349
 
342
350
  case 'trace': {
343
- const index = getIndex(project_dir);
351
+ const index = getIndex(project_dir, ep);
344
352
  const { ok, result, error } = execute(index, 'trace', ep);
345
353
  if (!ok) return toolResult(error); // soft error
346
354
  return toolResult(output.formatTrace(result, {
@@ -350,7 +358,7 @@ server.registerTool(
350
358
  }
351
359
 
352
360
  case 'reverse_trace': {
353
- const index = getIndex(project_dir);
361
+ const index = getIndex(project_dir, ep);
354
362
  const { ok, result, error } = execute(index, 'reverseTrace', ep);
355
363
  if (!ok) return toolResult(error);
356
364
  return toolResult(output.formatReverseTrace(result, {
@@ -359,7 +367,7 @@ server.registerTool(
359
367
  }
360
368
 
361
369
  case 'example': {
362
- const index = getIndex(project_dir);
370
+ const index = getIndex(project_dir, ep);
363
371
  const { ok, result, error } = execute(index, 'example', ep);
364
372
  if (!ok) return toolResult(error);
365
373
  if (!result) return toolResult(`No usage examples found for "${ep.name}".`);
@@ -367,7 +375,7 @@ server.registerTool(
367
375
  }
368
376
 
369
377
  case 'related': {
370
- const index = getIndex(project_dir);
378
+ const index = getIndex(project_dir, ep);
371
379
  const { ok, result, error } = execute(index, 'related', ep);
372
380
  if (!ok) return toolResult(error);
373
381
  if (!result) return toolResult(`Symbol "${ep.name}" not found.`);
@@ -380,7 +388,7 @@ server.registerTool(
380
388
  // ── Finding Code ────────────────────────────────────────────
381
389
 
382
390
  case 'find': {
383
- const index = getIndex(project_dir);
391
+ const index = getIndex(project_dir, ep);
384
392
  const { ok, result, error, note } = execute(index, 'find', ep);
385
393
  if (!ok) return toolResult(error); // soft error
386
394
  let text = output.formatFind(result, ep.name, ep.top);
@@ -389,7 +397,7 @@ server.registerTool(
389
397
  }
390
398
 
391
399
  case 'usages': {
392
- const index = getIndex(project_dir);
400
+ const index = getIndex(project_dir, ep);
393
401
  const { ok, result, error, note } = execute(index, 'usages', ep);
394
402
  if (!ok) return toolResult(error); // soft error
395
403
  let text = output.formatUsages(result, ep.name);
@@ -398,7 +406,7 @@ server.registerTool(
398
406
  }
399
407
 
400
408
  case 'toc': {
401
- const index = getIndex(project_dir);
409
+ const index = getIndex(project_dir, ep);
402
410
  const { ok, result, error, note } = execute(index, 'toc', ep);
403
411
  if (!ok) return toolResult(error); // soft error
404
412
  let text = output.formatToc(result, {
@@ -409,7 +417,7 @@ server.registerTool(
409
417
  }
410
418
 
411
419
  case 'search': {
412
- const index = getIndex(project_dir);
420
+ const index = getIndex(project_dir, ep);
413
421
  const { ok, result, error, structural } = execute(index, 'search', ep);
414
422
  if (!ok) return toolResult(error); // soft error
415
423
  if (structural) {
@@ -419,21 +427,21 @@ server.registerTool(
419
427
  }
420
428
 
421
429
  case 'tests': {
422
- const index = getIndex(project_dir);
430
+ const index = getIndex(project_dir, ep);
423
431
  const { ok, result, error } = execute(index, 'tests', ep);
424
432
  if (!ok) return toolResult(error); // soft error
425
433
  return toolResult(output.formatTests(result, ep.name));
426
434
  }
427
435
 
428
436
  case 'affected_tests': {
429
- const index = getIndex(project_dir);
437
+ const index = getIndex(project_dir, ep);
430
438
  const { ok, result, error } = execute(index, 'affectedTests', ep);
431
439
  if (!ok) return toolResult(error);
432
440
  return toolResult(output.formatAffectedTests(result));
433
441
  }
434
442
 
435
443
  case 'deadcode': {
436
- const index = getIndex(project_dir);
444
+ const index = getIndex(project_dir, ep);
437
445
  const { ok, result, error, note } = execute(index, 'deadcode', ep);
438
446
  if (!ok) return toolResult(error); // soft error
439
447
  const dcNote = note;
@@ -447,7 +455,7 @@ server.registerTool(
447
455
  }
448
456
 
449
457
  case 'entrypoints': {
450
- const index = getIndex(project_dir);
458
+ const index = getIndex(project_dir, ep);
451
459
  const { ok, result, error } = execute(index, 'entrypoints', ep);
452
460
  if (!ok) return toolResult(error);
453
461
  return toolResult(output.formatEntrypoints(result));
@@ -456,28 +464,28 @@ server.registerTool(
456
464
  // ── File Dependencies ───────────────────────────────────────
457
465
 
458
466
  case 'imports': {
459
- const index = getIndex(project_dir);
467
+ const index = getIndex(project_dir, ep);
460
468
  const { ok, result, error } = execute(index, 'imports', ep);
461
469
  if (!ok) return toolResult(error); // soft error
462
470
  return toolResult(output.formatImports(result, ep.file));
463
471
  }
464
472
 
465
473
  case 'exporters': {
466
- const index = getIndex(project_dir);
474
+ const index = getIndex(project_dir, ep);
467
475
  const { ok, result, error } = execute(index, 'exporters', ep);
468
476
  if (!ok) return toolResult(error); // soft error
469
477
  return toolResult(output.formatExporters(result, ep.file));
470
478
  }
471
479
 
472
480
  case 'file_exports': {
473
- const index = getIndex(project_dir);
481
+ const index = getIndex(project_dir, ep);
474
482
  const { ok, result, error } = execute(index, 'fileExports', ep);
475
483
  if (!ok) return toolResult(error); // soft error
476
484
  return toolResult(output.formatFileExports(result, ep.file));
477
485
  }
478
486
 
479
487
  case 'graph': {
480
- const index = getIndex(project_dir);
488
+ const index = getIndex(project_dir, ep);
481
489
  const { ok, result, error } = execute(index, 'graph', ep);
482
490
  if (!ok) return toolResult(error); // soft error
483
491
  return toolResult(output.formatGraph(result, {
@@ -489,7 +497,7 @@ server.registerTool(
489
497
  }
490
498
 
491
499
  case 'circular_deps': {
492
- const index = getIndex(project_dir);
500
+ const index = getIndex(project_dir, ep);
493
501
  const { ok, result, error } = execute(index, 'circularDeps', ep);
494
502
  if (!ok) return toolResult(error);
495
503
  return toolResult(output.formatCircularDeps(result));
@@ -498,21 +506,21 @@ server.registerTool(
498
506
  // ── Refactoring ─────────────────────────────────────────────
499
507
 
500
508
  case 'verify': {
501
- const index = getIndex(project_dir);
509
+ const index = getIndex(project_dir, ep);
502
510
  const { ok, result, error } = execute(index, 'verify', ep);
503
511
  if (!ok) return toolResult(error); // soft error
504
512
  return toolResult(output.formatVerify(result));
505
513
  }
506
514
 
507
515
  case 'plan': {
508
- const index = getIndex(project_dir);
516
+ const index = getIndex(project_dir, ep);
509
517
  const { ok, result, error } = execute(index, 'plan', ep);
510
518
  if (!ok) return toolResult(error); // soft error
511
519
  return toolResult(output.formatPlan(result));
512
520
  }
513
521
 
514
522
  case 'diff_impact': {
515
- const index = getIndex(project_dir);
523
+ const index = getIndex(project_dir, ep);
516
524
  const { ok, result, error } = execute(index, 'diffImpact', ep);
517
525
  if (!ok) return toolResult(error); // soft error — e.g. "not a git repo"
518
526
  return toolResult(output.formatDiffImpact(result));
@@ -521,21 +529,21 @@ server.registerTool(
521
529
  // ── Other ───────────────────────────────────────────────────
522
530
 
523
531
  case 'typedef': {
524
- const index = getIndex(project_dir);
532
+ const index = getIndex(project_dir, ep);
525
533
  const { ok, result, error } = execute(index, 'typedef', ep);
526
534
  if (!ok) return toolResult(error); // soft error
527
535
  return toolResult(output.formatTypedef(result, ep.name));
528
536
  }
529
537
 
530
538
  case 'stacktrace': {
531
- const index = getIndex(project_dir);
539
+ const index = getIndex(project_dir, ep);
532
540
  const { ok, result, error } = execute(index, 'stacktrace', ep);
533
541
  if (!ok) return toolResult(error); // soft error
534
542
  return toolResult(output.formatStackTrace(result));
535
543
  }
536
544
 
537
545
  case 'api': {
538
- const index = getIndex(project_dir);
546
+ const index = getIndex(project_dir, ep);
539
547
  const { ok, result, error, note } = execute(index, 'api', ep);
540
548
  if (!ok) return toolResult(error); // soft error
541
549
  let apiText = output.formatApi(result, ep.file || '.');
@@ -544,7 +552,7 @@ server.registerTool(
544
552
  }
545
553
 
546
554
  case 'stats': {
547
- const index = getIndex(project_dir);
555
+ const index = getIndex(project_dir, ep);
548
556
  const { ok, result, error } = execute(index, 'stats', ep);
549
557
  if (!ok) return toolResult(error); // soft error
550
558
  return toolResult(output.formatStats(result, { top: ep.top || 0 }));
@@ -555,7 +563,7 @@ server.registerTool(
555
563
  case 'fn': {
556
564
  const err = requireName(ep.name);
557
565
  if (err) return err;
558
- const index = getIndex(project_dir);
566
+ const index = getIndex(project_dir, ep);
559
567
  const { ok, result, error } = execute(index, 'fn', ep);
560
568
  if (!ok) return toolResult(error); // soft error
561
569
  // MCP path security: validate all result files are within project root
@@ -573,7 +581,7 @@ server.registerTool(
573
581
  if (ep.maxLines !== undefined && (!Number.isInteger(ep.maxLines) || ep.maxLines < 1)) {
574
582
  return toolError(`Invalid max_lines: ${ep.maxLines}. Must be a positive integer.`);
575
583
  }
576
- const index = getIndex(project_dir);
584
+ const index = getIndex(project_dir, ep);
577
585
  const { ok, result, error } = execute(index, 'class', ep);
578
586
  if (!ok) return toolResult(error); // soft error (class not found)
579
587
  // MCP path security: validate all result files are within project root
@@ -586,7 +594,7 @@ server.registerTool(
586
594
  }
587
595
 
588
596
  case 'lines': {
589
- const index = getIndex(project_dir);
597
+ const index = getIndex(project_dir, ep);
590
598
  const { ok, result, error } = execute(index, 'lines', ep);
591
599
  if (!ok) return toolResult(error); // soft error
592
600
  // MCP path security: validate file is within project root
@@ -599,7 +607,7 @@ server.registerTool(
599
607
  if (ep.item === undefined || ep.item === null) {
600
608
  return toolError('Item number is required (e.g. item=1).');
601
609
  }
602
- const index = getIndex(project_dir);
610
+ const index = getIndex(project_dir, ep);
603
611
  const lookup = expandCacheInstance.lookup(index.root, ep.item);
604
612
  const { ok, result, error } = execute(index, 'expand', {
605
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.4",
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",