@unerr-ai/unerr 0.2.10 → 0.2.11

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.
Files changed (3) hide show
  1. package/README.md +102 -116
  2. package/dist/cli.js +116 -18
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -3,16 +3,17 @@
3
3
  </h1>
4
4
 
5
5
  <p align="center">
6
- <strong>Your AI agent has read your codebase. It still can't safely change it.</strong>
6
+ <strong>To make a coding agent work well on real code, you end up bolting a bunch of separate things onto it —<br/>
7
+ one to find the right code, one to stop it forgetting, one to trim the clutter, your rules, a few checks to catch<br/>
8
+ mistakes. You set them all up, you keep them running, and it still ignores half of them — because each one is<br/>
9
+ only advice it can skip, and they all pull at its attention at once.</strong>
7
10
  </p>
8
11
 
9
12
  <p align="center">
10
- Every tool built to help hands your agent <em>advice it can ignore</em> a memory it has to remember to check,<br/>
11
- a graph it has to choose to query, a reviewer that only speaks up after the break is already written.<br/>
12
- <strong>unerr is the guardrail it can't skip.</strong> The moment your agent edits a function, unerr puts the live call graph<br/>
13
- and the rule you pinned to that exact function <em>into the edit itself</em> automatically, not on request and re-anchors<br/>
14
- that rule when the code moves, so it never goes quietly stale. The 24 callers and the standard it's about to break are<br/>
15
- on screen <em>before</em> the function changes. Every time. Whether or not the agent thought to ask.
13
+ unerr puts all of that into one piece, built into the way the agent already worksso it's not one more thing the<br/>
14
+ agent can choose to ignore. As the agent goes, it finds the right code, keeps your rules in front of it, trims the<br/>
15
+ clutter, and catches a break before it lands all together, with nothing for you to set up. The agent wastes less<br/>
16
+ time, money, and attention redoing work, and you waste less of yours setting tools up and cleaning up after it.
16
17
  </p>
17
18
 
18
19
  <p align="center">
@@ -25,13 +26,18 @@
25
26
  <img src="https://img.shields.io/badge/runtime-Node.js_≥20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" />
26
27
  <img src="https://img.shields.io/badge/protocol-MCP-7C3AED?style=flat-square" alt="MCP" />
27
28
  <img src="https://img.shields.io/badge/local--first-no_cloud-22D3EE?style=flat-square" alt="Local-first" />
28
- <img src="https://img.shields.io/badge/license-ELv2-A1A1AA?style=flat-square" alt="License" />
29
+ <img src="https://img.shields.io/badge/license-Apache--2.0-A1A1AA?style=flat-square" alt="License" />
29
30
  </p>
30
31
 
31
32
  <p align="center">
32
33
  <code>npm install -g @unerr-ai/unerr</code>
33
34
  <br /><br />
34
- <sub>Zero configuration. Install, restart your IDE, and the next prompt already knows your repo.</sub>
35
+ <sub>Install, restart your IDE, and the next prompt already knows your repo. No config, no account, nothing leaves your machine.</sub>
36
+ </p>
37
+
38
+ <p align="center">
39
+ <a href="https://youtu.be/pL1izMwYZpI"><img src="https://unerr.dev/open-cli/video/unerr-cascade.gif" alt="unerr firing inside a live Claude Code session — 12 dependent call sites surfaced before a signature edit" width="760" /></a>
40
+ <br/><sub><strong>Live, inside the agent</strong> · the agent tries to change <code>extractFilePath</code>; before the edit lands, unerr surfaces the <strong>12 places that depend on it across 4 files</strong> — so it fixes every one in the same turn instead of breaking them silently. ▶ <a href="https://youtu.be/pL1izMwYZpI">Watch the full demo</a>.</sub>
35
41
  </p>
36
42
 
37
43
  ---
@@ -39,103 +45,86 @@
39
45
  <details>
40
46
  <summary><strong>Contents</strong></summary>
41
47
 
42
- - [The gap nobody else closes](#the-gap-nobody-else-closes)
43
- - [The pains this fixes](#the-pains-this-fixes)
44
- - [What changes when you install it](#what-changes-when-you-install-it)
48
+ - [Why I built this](#why-i-built-this)
49
+ - [What's actually going wrong](#whats-actually-going-wrong)
50
+ - [What changes when you use it](#what-changes-when-you-use-it)
45
51
  - [See it in action](#see-it-in-action)
46
52
  - [Quick Start](#quick-start)
47
53
  - [Who it's for](#who-its-for)
48
- - [Why a guardrail has to be one runtime, not five tools](#why-a-guardrail-has-to-be-one-runtime-not-five-tools)
49
- - [How the runtime works](#how-the-runtime-works)
50
- - [Fewer tokens, as a side effect](#fewer-tokens-as-a-side-effect)
54
+ - [Why it's one thing and not five plugins](#why-its-one-thing-and-not-five-plugins)
55
+ - [What it does under the hood](#what-it-does-under-the-hood)
56
+ - [About the fewer tokens](#about-the-fewer-tokens)
51
57
  - [License](#license)
52
58
 
53
59
  </details>
54
60
 
55
61
  ---
56
62
 
57
- ## The gap nobody else closes
63
+ ## Why I built this
58
64
 
59
- On a small or greenfield project the agent holds the whole repo in its head and reading the live code is enough — you don't need us. The wall is the *large, existing, multi-contributor* codebase, and it's the same wall every time: the agent can't fit the whole thing in context, so it acts on the slice it can see and never reads the rest. It changes a signature and breaks 7 of 24 callers it never read. It writes a fourth copy of a pattern your team standardized months ago — even with the rule spelled out in `.cursorrules`. Neither shows up as an error. They show up as a senior engineer's afternoon.
65
+ I built unerr because I got tired of cleaning up after my own coding agent.
60
66
 
61
- The knowledge that would have stopped itwho calls this function, which pattern is load-bearing already exists. The whole market is built on getting that knowledge to the agent. And it falls into two shapes, both of which leak:
67
+ I was running coding agents on real work not toy projects and to stop them from messing things up I kept bolting on extra stuff. A memory file here, some rules there, something to keep the agent from forgetting what it was doing, a few guardrails. And two things drove me crazy.
62
68
 
63
- | What it does | The shape | Why it leaks |
64
- |---|---|---|
65
- | **Tells the agent things.** Memory stores, code-graph servers, context packers, rule files. | A tool the agent calls *when it remembers to.* | Optional context is optional. Agents skip the retrieval tool **~58% of the time even when explicitly told to use it** ([CodeCompass, 2026](https://arxiv.org/abs/2602.20048)). Advice it can ignore, it ignores. |
66
- | **Checks the agent afterward.** Reviewers, linters, CI gates. | A pass over the diff *after the code is written.* | The break already happened. Now it's a comment on a pull request and a second round of work — not a change that never broke anything. |
69
+ One: setting all that up is its own job, and every one of those things is really just a *suggestion* to the agent. A rule it can acknowledge and then ignore once it gets busy. A memory it has to remember to check. A reviewer that only speaks up after the break is already written. They don't work together — they compete for the agent's attention, and half of them get dropped exactly when you need them.
67
70
 
68
- There's a third shape, and almost no one ships it: **guidance wired into the moment of the edit, that the agent can't route around, and that re-anchors itself when the code moves so it never goes quietly stale.** Not a tool it chooses to consult. Not a review after the fact. A guardrail that fires *as it edits* — and stays true to the code because it's recomputed from the code, not from a doc that rots.
71
+ Two: while all that's going on, the agent is burning time and money redoing work and breaking things it shouldn't have touched and I'm sitting there babysitting it, because the one time I look away is the time it quietly breaks something that matters.
69
72
 
70
- That's unerr. The agent doesn't have to ask. Before the edit lands, it already sees the callers it would break and the standard it's about to violate.
73
+ unerr is the thing I wanted to exist: one piece that does all of that itself, right while the agent is working — so you don't have to assemble a toolchain, or write a flawless prompt every time, or sit through the back-and-forth just to trust what it ships. The agent wastes less of its own time and a lot less of your money, and you spend far less effort watching over it.
71
74
 
72
- | The old way | With unerr |
73
- |---|---|
74
- | The agent changes a function without reading its 24 callers — 7 sites break silently. | **Cascade guard** puts the call graph in front of the edit *before it runs* — every caller on screen, no asking required. |
75
- | You wrote the rule in `.cursorrules`. The agent acknowledged it, then ignored it once context filled up. | **Anchored rules** surface the standard the instant the agent touches that scope — and re-anchor when the code moves instead of going stale. |
76
- | A rule or spec stays confident long after the code moved out from under it. Nothing recomputes it. | Every fact is pinned to a live entity in the graph. When the code moves, the fact **fails loud** instead of staying silently wrong. |
75
+ It's free, open source, and runs entirely on your machine.
77
76
 
78
77
  ---
79
78
 
80
- ## The pains this fixes
79
+ ## What's actually going wrong
80
+
81
+ On any codebase big enough to matter, the agent can't hold the whole thing in its head. So it works from the slice it can see and never looks at the rest. It changes a function and breaks the other places that call it — places it never read. It writes a fourth copy of a pattern your team already settled on, even with the rule sitting right there in your `.cursorrules`. Neither of those shows up as an error. They show up later, as your afternoon.
81
82
 
82
- You know this feeling, and it gets *worse* as the repo grows, not better:
83
+ The usual fixes both leak:
83
84
 
84
- - **You're babysitting it.** You can't fire-and-forget, because the one time you look away is the time it quietly breaks something load-bearing. You've become its scheduler and its safety net at once.
85
- - **You don't trust it to touch anything important.** It treats your codebase as a flat wall of text locally correct, globally wrong so the load-bearing changes still land on you.
86
- - **The rule you wrote gets acknowledged, then dropped.** A few turns later the context fills up and your `.cursorrules` line may as well not exist.
87
- - **Approval fatigue.** You approve so many reasonable edits that the dangerous one slides through — the hundredth confirmation looks exactly like the first.
85
+ - **Things that *tell* the agent stuff** — memory stores, rule files, context tools only help when the agent remembers to use them. Optional advice is optional, and a busy agent skips it.
86
+ - **Things that *check* the agent afterward** reviewers, linters, CI only speak up after the code is already written. By then it's a pull-request comment and a second round of work, not a break that never happened.
88
87
 
89
- These aren't four problems. They're one: **the agent acts on a codebase it can't hold in its head, and nothing it can't bypass is watching the change.** You babysit because there's no guardrail it can't skip. unerr is that guardrail so you can look away.
88
+ And every one of these is a separate thing you have to install, configure, and keep current. The more you add, the more they pull against each other for the agent's limited attention, and the more of your time goes into maintaining the setup instead of shipping.
89
+
90
+ unerr closes that gap by doing the work at the moment it matters — when the agent reads and when it edits — instead of waiting to be asked or waiting to complain after the fact. The agent doesn't have to remember anything. The thing that would have stopped the break is already in front of it, before the change lands.
90
91
 
91
92
  ---
92
93
 
93
- ## What changes when you install it
94
+ ## What changes when you use it
94
95
 
95
- | You feel | What unerr does |
96
+ | What you feel | What's happening |
96
97
  |---|---|
97
- | **You stop babysitting.** The agent runs for an hour and you're not bracing for a silent break. | Every edit is preceded — automatically — by a graph lookup. All 24 callers are visible *before* it touches the function. The guardrail fires whether or not the agent thought to ask. |
98
- | **Your rules finally get honored.** The standard you set is applied at the edit, not acknowledged and forgotten. | unerr pins each rule and decision to the file or entity it governs and surfaces it the instant the agent touches that scopethen re-anchors it when the code moves. Keep your `.cursorrules` and specs; unerr makes sure they're actually applied. |
99
- | **It stops thrashing.** No more watching it retry the same broken fix three times. | A **loop breaker** watches the timeline and stops the agent re-trying a change that already failed twice before it burns your turn and your patience. |
100
- | **The agent stays sharp at turn 50.** | `file_read({entity})` returns 200 lines instead of 3,000; shell output is trimmed automatically. The window stays uncluttered, so the model isn't fighting "lost in the middle." |
98
+ | **You stop babysitting.** The agent runs for an hour and you're not bracing for a silent break. | Before it changes a function, unerr shows it every other place that depends on that function on its own, without the agent asking. |
99
+ | **Your rules finally stick.** The standard you set gets applied at the edit, not acknowledged and forgotten three turns later. | unerr ties each rule to the part of the code it's about and brings it up the moment the agent touches that partand keeps it pinned there even after the code moves. |
100
+ | **It stops going in circles.** No more watching it try the same broken fix three times. | unerr notices when the agent is re-trying something that already failed and stops it before it burns another turn. |
101
+ | **It stays sharp deep into a long session.** | unerr hands the agent the small, relevant slice of a file or a command's output instead of dumping thousands of lines into the window, so the model isn't drowning in noise by turn 50. |
101
102
 
102
- **What it looks like in your chat** before the Edit tool runs, unerr injects this into the agent's context, on its own:
103
+ Here's what it actually looks like in your chat. Before the edit runs, unerr drops a line like this into the agent's context, on its own:
103
104
 
104
- > ⚡ unerr · cascade guard: editing `src/payments/gateway.ts` changes a signature with callers that must be updated in the same change — `processPayment`: **24 callers at risk across 6 files** (19 source, 5 test). Call `get_references({key:'processPayment', direction:'callers'})` and update every caller before finishing.
105
+ > ⚡ unerr · editing `src/payments/gateway.ts` changes a function that **24 other places depend on, across 6 files**. Update every one of them in this same change before finishing.
105
106
 
106
- The outcome: **agents that behave like senior engineers** checking dependencies before editing, honoring the standard, and refusing to thrash on a function they've already failed on three times.
107
+ The result is an agent that behaves a lot more like a careful senior engineer: it checks what a change affects before making it, honors the standards you set, and doesn't keep retrying something that already failed.
107
108
 
108
109
  ---
109
110
 
110
111
  ## See it in action
111
112
 
112
- **Watch it run** a real Claude Code session in this repo. The agent attempts a signature change to `extractFilePath`; *before* the edit lands, unerr surfaces **12 callers at risk across 4 files**, so the agent updates every one of them in the same turn instead of breaking them silently.
113
+ The demo above is one moment, caught live. Day to day, there are two places you watch it working in the chat, and in a browser.
113
114
 
114
- <p align="center">
115
- <a href="https://youtu.be/pL1izMwYZpI"><img src="https://unerr.dev/open-cli/video/unerr-cascade.gif" alt="unerr cascade guard firing inside a live Claude Code session — 12 callers surfaced before a signature edit" width="760" /></a>
116
- <br/><sub><strong>Cascade guard, live</strong> · unerr catches the 12 callers of <code>extractFilePath</code> before the edit ripples. ▶ <a href="https://youtu.be/pL1izMwYZpI">Watch the full demo on YouTube</a>.</sub>
117
- </p>
118
-
119
- Two places unerr shows up so you know it's working — inside the chat, and in a browser.
115
+ **In the chat.** Every coding turn opens with one line naming what unerr brought in ("brought in a convention you wrote yesterday for `src/payments/gateway.ts`…") and closes with one line totalling what it caught and saved you. The catches are named, countable events — not a vague percentage.
120
116
 
121
- **Inside the chat.** Every coding turn opens with one line naming what unerr loaded ("loaded a convention you wrote yesterday for `src/payments/gateway.ts`…") and closes with one line totalling what it caught and saved ("this turn: 2 catches · ≈ 4.2k tokens saved · +5 turns of headroom this session"). Catches are *named, countable events*, not a ratio.
122
-
123
- **In a browser.** A live dashboard at `http://localhost:9847` reads from the same store the agent reads from over MCP — the graph it navigates, the facts it remembers, the breaks it caught, and the score showing which of those facts actually shaped the next answer.
117
+ **In a browser.** A live dashboard at `http://localhost:9847` reads from the same place the agent reads from what it remembers, what it caught, and which of those things actually shaped the next answer.
124
118
 
125
119
  <p align="center">
126
- <img src="https://unerr.dev/open-cli/screenshots/end-of-turn-receipt.png" alt="unerr end-of-turn receipt — tokens saved and headroom kept open this turn" width="380" />
127
- <img src="https://unerr.dev/open-cli/screenshots/end-of-turn-receipt-2.png" alt="unerr end-of-turn receipt — named, countable catches totalled at the close of a turn" width="380" />
128
- <br/><sub><strong>End-of-turn receipt</strong> · every coding turn closes with one line totalling what unerr caught and saved you — named, countable catches, not a ratio.</sub>
120
+ <img src="https://unerr.dev/open-cli/screenshots/end-of-turn-receipt.png" alt="unerr end-of-turn receipt — what it caught and saved this turn" width="380" />
121
+ <img src="https://unerr.dev/open-cli/screenshots/end-of-turn-receipt-2.png" alt="unerr end-of-turn receipt — named, countable catches at the close of a turn" width="380" />
122
+ <br/><sub><strong>End-of-turn receipt</strong> · every turn closes with one line totalling what unerr caught and saved you — named, countable, not a ratio.</sub>
129
123
  </p>
130
124
 
131
125
  <p align="center">
132
126
  <img src="https://unerr.dev/open-cli/screenshots/dashboard.png" alt="unerr dashboard — live overview" width="300" />
133
- <br/><sub><strong>Dashboard</strong> · live overview — active sessions, recent tool calls, breaks caught, tokens the agent skipped this turn.</sub>
134
- </p>
135
-
136
- <p align="center">
137
- <img src="https://unerr.dev/open-cli/screenshots/token-trace-main.png" alt="unerr token trace" width="300" />
138
- <br/><sub><strong>Token Trace</strong> · context kept out of the window, broken down by mechanism — graph hits, skipped re-reads, compressed shell output, deduped fetches.</sub>
127
+ <br/><sub><strong>Dashboard</strong> · live overview — active sessions, recent activity, breaks caught.</sub>
139
128
  </p>
140
129
 
141
130
  <p align="center"><sub>More views in the <a href="https://www.unerr.dev/">full dashboard tour</a>.</sub></p>
@@ -152,16 +141,16 @@ Three steps. Step 1 is once per machine; steps 2–3 are per repo.
152
141
  npm install -g @unerr-ai/unerr
153
142
  ```
154
143
 
155
- Puts the `unerr` binary on your PATH. If your shell can't find it (common with nvm, fnm, volta, pnpm), run `unerr doctor` once — it patches your shell config and won't need to run again.
144
+ Puts the `unerr` binary on your PATH. If your shell can't find it afterward (this happens with nvm, fnm, volta, and pnpm), run `unerr doctor` once — it patches your shell config and won't need to run again.
156
145
 
157
- ### 2. Install for your agent (per repo)
146
+ ### 2. Set it up for your agent (per repo)
158
147
 
159
148
  ```bash
160
149
  cd ~/your-project
161
150
  unerr install cursor
162
151
  ```
163
152
 
164
- Writes the MCP config, skills, hooks, and instructions for that agent in the current repo. Swap `cursor` for any of the supported agents:
153
+ That writes the MCP config, skills, hooks, and instructions for that agent in the current repo. Swap `cursor` for any of the supported agents:
165
154
 
166
155
  ```bash
167
156
  unerr install claude-code
@@ -172,73 +161,60 @@ unerr install gemini-cli
172
161
  unerr install github-copilot-cli
173
162
  ```
174
163
 
175
- Install multiple agents in the same repo — each writes its own config. Idempotent: re-running updates if content changed, skips if identical. Remove with `unerr uninstall`.
164
+ You can install more than one agent in the same repo — each writes its own config. Re-running updates the setup if anything changed and skips it if nothing did. Remove it with `unerr uninstall`.
176
165
 
177
166
  ### 3. Restart your IDE
178
167
 
179
- Close and reopen your IDE (or start a new chat session). Your agent picks up unerr through MCP graph-backed tools, persistent memory, the edit-time guardrail all available immediately.
168
+ Close and reopen your IDE, or start a new chat session. Your agent picks up unerr through MCP and everything is available from the next prompt.
180
169
 
181
- > **Dashboard:** <http://localhost:9847> — open any time to watch unerr at work in real time.
170
+ > **Dashboard:** <http://localhost:9847> — open it any time to watch unerr work.
182
171
 
183
- > Need manual setup or any other MCP client? `unerr install --show-instructions <agent>` prints copy-pasteable steps.
172
+ > Using a different MCP client, or setting it up by hand? `unerr install --show-instructions <agent>` prints copy-pasteable steps.
184
173
 
185
174
  ---
186
175
 
187
176
  ## Who it's for
188
177
 
189
- - **Engineers on large, existing codebases.** The dependency graph, the load-bearing patterns, and the prior incidents a senior engineer carries in their head — handed to the agent before every edit, so it stops breaking callers it never read.
190
- - **Teams with conventions worth enforcing.** The standard you agreed on once, applied every time the agent touches that scope — no `.cursorrules` file to hand-maintain, re-paste, or merge-conflict over, and no hoping the agent remembers to look.
191
- - **Solo builders shipping into a codebase that's already grown.** The continuous thread across tools — switch from Claude Code in the terminal to Cursor in the IDE and the graph, rules, and history come with you, instead of relearning the repo every session.
178
+ - **Engineers working in large, existing codebases.** The things a senior engineer keeps in their head what depends on what, which patterns are load-bearing, what broke here before — handed to the agent before every edit, so it stops breaking code it never read.
179
+ - **Teams with conventions worth keeping.** The standard you agreed on once, applied every time the agent touches that part of the code — no rules file to hand-maintain, re-paste, or fight merge conflicts over, and no hoping the agent remembers to look.
180
+ - **Solo builders and vibe coders shipping into a codebase that's already grown.** One continuous thread across your tools — move from Claude Code in the terminal to Cursor in the IDE and what unerr knows about your repo comes with you, instead of relearning it every session.
192
181
 
193
182
  ---
194
183
 
195
- ## Why a guardrail has to be one runtime, not five tools
196
-
197
- A guardrail the agent *can't skip* can't be a tool the agent chooses to call. That's the whole reason unerr is one local runtime sitting *behind* the MCP every agent already speaks — not a fifth server in the agent's tool list.
184
+ ## Why it's one thing and not five plugins
198
185
 
199
- Every coding agent on your machine Claude Code, Cursor, Windsurf, Antigravity — speaks MCP. MCP carries tool calls; it does not carry context, and it does not fire anything on its own. So a memory server, a graph server, and a compressor sit there *waiting to be invoked* — and an agent under context pressure skips them. unerr instead intercepts at the moment that matters — the read, the edit — and injects the one scoped thing that's relevant, automatically. The agent can't forget to call something that isn't waiting to be called.
186
+ This is the part that took me a while to get right, so it's worth saying plainly.
200
187
 
201
- That only works if the pieces live in **one** process. The guardrails worth having each fire on a *join* no single tool can make:
188
+ Every coding agent on your machine speaks the same protocol, MCP. MCP carries requests the agent *chooses* to make — it doesn't hand the agent context on its own, and it doesn't fire anything by itself. So a memory plugin, a code-search plugin, and a context trimmer all just sit there waiting to be called. And an agent that's busy or low on room skips the thing it has to remember to call. That's the whole leak.
202
189
 
203
- - **Cascade guard** needs the call graph *and* the edit-intent ledger on the same process, at the same instant.
204
- - **Drift** needs memory that's anchored to a live graph — so the fact knows the moment its code moved.
205
- - **Convention drift** needs the auto-detected pattern store *and* the new-code stream in the same memory space.
206
- - **Loop breaker** needs the full timeline of what the agent already tried.
190
+ unerr doesn't sit and wait. It steps in at the moments that matter — when the agent reads a file, when it's about to make a change — and puts the one relevant thing in front of it automatically. You can't forget to call something that isn't waiting to be called.
207
191
 
208
- These aren't features you can buy individually and bolt together. They're emergent properties of one runtime and they're exactly what turns "context the agent might read" into "a guardrail it can't skip."
192
+ The catch is that this only works if the pieces live together, because the useful ones each need information no single plugin has on its own:
209
193
 
210
- ---
211
-
212
- ## How the runtime works
194
+ - Catching a breaking change needs to know both *what the agent is about to edit* and *what depends on it* — at the same instant.
195
+ - Knowing a saved rule has gone stale needs that rule tied to the actual code, so it notices the moment the code moves.
196
+ - Spotting a convention slipping needs both the patterns your codebase already uses and the new code being written, side by side.
197
+ - Stopping a retry-loop needs the full history of what the agent already tried this session.
213
198
 
214
- One local process per repo. Four mechanisms, joined deterministically the **mechanisms** are how; the **guardrail** is what you get.
215
-
216
- | Mechanism (the how) | What's inside | What it powers (the what) |
217
- |---|---|---|
218
- | **Live code graph** | CozoDB · tree-sitter ASTs · SCIP-verified call graphs · 18+ languages · <5ms queries | The agent opens 50 targeted lines and a caller list — not 3,000 lines and a guess. Read *before every file read*, so cascade guard knows what an edit breaks. |
219
- | **Anchored memory** | Typed facts · conventions auto-detected at ≥70% adherence · decay-adjusted confidence | Every fact is pinned to a file or entity in the graph. When the code moves, the fact gets a **drift signal** — never silent staleness. |
220
- | **Context delivery** | Shell output compression (645+ command classifiers) · web fetches (5–10× via Defuddle + BM25) · entity-targeted file reads | The relevant slice arrives automatically at the read — the agent never has to remember which tool to invoke for which content. |
221
- | **Behaviour modules** | cascade guard · convention drift · loop breaker · session continuity · auto-doc · change narrative · architecture guard | Each guardrail fires on a join of the three above, *at the moment of the edit* — not as a tool the agent chose, not as a review after the fact. |
199
+ You can't buy those as five separate tools and bolt them together — they only exist when everything lives in one place. That's why unerr is one local thing, not a fifth plugin in your agent's list. And one thing instead of five means the agent isn't spending its attention deciding which plugin to call — a real cost once that list gets long. Researchers have measured a routine set of these add-ons eating [more than 20% of an agent's context window before it does any actual work](https://eclipsesource.com/blogs/2026/01/22/mcp-context-overload/).
222
200
 
223
- **The unifying point.** Drift detection requires memory anchored to a live graph. Cascade guard requires the graph and the edit-intent ledger on one process. Convention drift requires the pattern store and the new-code stream in the same memory space. Spread these across five disconnected MCP servers and none of them can fire — they can only sit and wait to be called, which is the failure mode this whole thing exists to fix. That's the difference between a stack of tools and a guardrail.
201
+ (This isn't an MCP gateway that bundles your existing servers behind one address those still hand the agent every tool up front. unerr replaces what those add-ons *do*, so there's nothing left to bundle.)
224
202
 
225
203
  ---
226
204
 
227
- ## Fewer tokens, as a side effect
205
+ ## What it does under the hood
228
206
 
229
- unerr was built to stop bad changes, not to save tokens. But a guardrail that only ever hands over *the one scoped fact that matters* the rule for the entity in front of the agent, 50 lines instead of 3,000 — spends far fewer tokens almost by accident. So you get this for free:
207
+ One local process per repo. You don't have to think about any of this to use itbut if you want to know what's actually running, here it is.
230
208
 
231
- - **86–90%** of an agent's code-navigation tokens removed in head-to-head benchmarks vs grep+read real tokenizer, fidelity-gated (any "saving" that lost the answer is discarded), reproducible on any repo. [See the benchmarks →](./benchmarks/README.md)
232
- - **~84%** of an AI coding agent's tokens are tool output, mostly file reads ([JetBrains, NeurIPS 2025](https://blog.jetbrains.com/research/2025/12/efficient-context-management/)) — unerr intercepts at the read layer, so the window isn't diluted.
233
- - **Tool-selection accuracy collapses 58% 26% as MCP tools go from 9 to 51** ([LangChain ReAct benchmark](https://blog.langchain.com/react-agent-benchmarking/)) unerr is one runtime instead of five servers, so it doesn't eat the agent's tool-selection budget. Anthropic itself acknowledged this in Jan 2026 by shipping [MCP Tool Search](https://www.anthropic.com/engineering/code-execution-with-mcp).
234
- - **0** LLM calls per query in the core facts, conventions, drift signals, and graph lookups are all algorithmic. No API keys, no per-turn inference cost, no telemetry.
235
-
236
- The point was never the token number. It's that the agent lands on the right code, sees the right guardrail, and you stop paying in tokens *and* in afternoonsfor the changes it would otherwise have to undo.
237
-
238
- ---
209
+ | The piece | What's in it | What it gives the agent |
210
+ |---|---|---|
211
+ | **A live map of your code** | CozoDB · tree-sitter · SCIP-verified call data · 18+ languages · sub-5ms lookups | Before any file read, the agent gets the 50 lines that matter and the list of what depends on them not 3,000 lines and a guess. |
212
+ | **Memory tied to the code** | typed facts · conventions auto-detected once a pattern holds ≥70% of the time · confidence that decays over time | Every saved fact is pinned to a real file or function. When that code moves, the fact flags itself instead of quietly going wrong. |
213
+ | **The right slice, delivered automatically** | shell-output trimming (645+ command types) · web pages fetched at 5–10× less bulk · function-targeted file reads | The relevant piece shows up at the moment the agent reads — it never has to remember which tool to reach for. |
214
+ | **The behaviors that catch problems** | breaking-change guard · convention-slip guard · retry-loop breaker · session continuity · auto-doc · change narrative · architecture guard | Each one fires on a combination of the three above, *at the moment of the edit* not as a tool the agent picked, not as a review after the fact. |
239
215
 
240
216
  <details>
241
- <summary><strong>Under the hood — architecture, CLI commands, MCP tools, dev setup</strong></summary>
217
+ <summary><strong>Architecture, CLI commands, MCP tools, manual config, dev setup</strong></summary>
242
218
 
243
219
  ### Architecture
244
220
 
@@ -265,19 +241,17 @@ AI Agent (Claude Code / Cursor / Windsurf / any MCP client)
265
241
 
266
242
  One local DB per repo. Zero network calls. No API keys. No cloud. Your code never leaves the machine.
267
243
 
268
- Full module map and source-tree breakdown: **[ARCHITECTURE.md](./docs/ARCHITECTURE.md)**.
244
+ **Design principles** zero network calls; stdout is sacred (MCP JSON-RPC only, everything else to stderr); sub-5ms query responses; first useful output in under 5s (shallow index first, deep enrichment in the background); graceful degradation (the agent still works if unerr is down — you just lose the extra layer).
269
245
 
270
- **Design principles** — zero network calls; stdout is sacred (MCP JSON-RPC only, everything else to stderr); <5 ms query responses; first useful output <5 s (shallow index first, deep enrichment in background); graceful degradation (the agent still works if unerr is down, you just lose the guardrail layer).
271
-
272
- **Tech stack** TypeScript (ESM) · CozoDB (Rust/NAPI) · web-tree-sitter (WASM) · MCP SDK · Ink (React CLI) · React + Vite (dashboard) · tsup · Vitest
246
+ **Tech stack** — TypeScript (ESM) · CozoDB (Rust/NAPI) · web-tree-sitter (WASM) · MCP SDK · Ink (React CLI) · React + Vite (dashboard) · tsup · Vitest
273
247
 
274
248
  ### CLI commands
275
249
 
276
250
  ```bash
277
251
  unerr install <agent> # MCP config + skills + hooks + instructions for one agent
278
- unerr uninstall # Remove unerr integration from this repo
252
+ unerr uninstall # Remove unerr from this repo
279
253
  unerr doctor # Check PATH + environment, auto-fix if unerr isn't on all shells
280
- unerr status # Proxy health, entity count, graph age
254
+ unerr status # Process health, entity count, graph age
281
255
  unerr stats # Session statistics (tokens, tool calls, compression)
282
256
  unerr --mcp # Stdio bridge — what your IDE invokes via .mcp.json
283
257
 
@@ -286,7 +260,7 @@ unerr pm logs # Tail ~/.unerr/logs/unerrd.log
286
260
  unerr pm dashboard # Open http://localhost:9847
287
261
  ```
288
262
 
289
- `unerrd` is a lightweight Node process that supervises every registered repo. Your IDE invocation auto-spawns it; it exits cleanly after 30 minutes of no MCP activity. `unerr pm --help` lists the rest.
263
+ `unerrd` is a lightweight Node process that supervises every registered repo. Your IDE invocation auto-spawns it; it exits cleanly after 30 minutes of no activity. `unerr pm --help` lists the rest.
290
264
 
291
265
  ### MCP tools (22)
292
266
 
@@ -300,7 +274,7 @@ Grouped by what the agent gets, not by file:
300
274
  - **Web fetch (1)** — `fetch_url` (DOM-extracted markdown, BM25 re-ranking, content-hash cache). Replaces built-in WebFetch.
301
275
  - **Code review (1)** — `review_changes` (graph-evidenced review of a diff — flags breaking callers, contract drift, duplicate logic).
302
276
 
303
- Every response carries inline `ur|<tag>` signals for high-priority guidance — drift, blast-radius warnings, circuit-breaker halts — so the agent acts on what it just learned without burning a turn.
277
+ Every response carries inline `ur|<tag>` signals for high-priority guidance — drift, breaking-change warnings, loop-breaker halts — so the agent acts on what it just learned without burning a turn.
304
278
 
305
279
  ### Manual MCP config (any MCP-compatible client)
306
280
 
@@ -321,12 +295,24 @@ unerr removes **86–90% of the tokens** an agent would otherwise spend navigati
321
295
 
322
296
  ### Contributing
323
297
 
324
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, day-to-day commands, code conventions, and pre-PR checklist.
298
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, day-to-day commands, code conventions, and the pre-PR checklist.
325
299
 
326
300
  </details>
327
301
 
328
302
  ---
329
303
 
304
+ ## About the fewer tokens
305
+
306
+ I didn't build unerr to save tokens — I built it to stop bad changes. But a tool that only ever hands the agent the one relevant thing — the rule for the function in front of it, 50 lines instead of 3,000 — ends up spending far fewer tokens almost by accident. So you get that too:
307
+
308
+ - **86–90%** of an agent's code-navigation tokens removed in head-to-head benchmarks against grep-and-read — real tokenizer, fidelity-gated, reproducible on any repo. [See the benchmarks →](./benchmarks/README.md)
309
+ - Roughly **84%** of an agent's tokens are tool output, mostly file reads ([JetBrains, NeurIPS 2025](https://blog.jetbrains.com/research/2025/12/efficient-context-management/)). unerr steps in at the read, so the window doesn't fill up with noise.
310
+ - **0** AI calls per query in the core — the lookups, facts, and warnings are all computed directly. No API keys, no per-turn inference cost, no telemetry.
311
+
312
+ But the token number was never the point. The point is that the agent lands on the right code, sees the thing that would have stopped the break, and you stop paying — in money *and* in afternoons — for work it would otherwise have had to undo.
313
+
314
+ ---
315
+
330
316
  ## License
331
317
 
332
318
  [Apache License 2.0](./LICENSE) — free to use, modify, and distribute, including commercially. Includes an explicit patent grant.
package/dist/cli.js CHANGED
@@ -9331,8 +9331,21 @@ __export(metrics_store_exports, {
9331
9331
  openMetricsStore: () => openMetricsStore
9332
9332
  });
9333
9333
  import { mkdirSync as mkdirSync11 } from "fs";
9334
+ import { createRequire as createRequire2 } from "module";
9334
9335
  import { join as join21 } from "path";
9335
- import Database from "better-sqlite3";
9336
+ function loadDatabaseCtor() {
9337
+ if (cachedDriver !== void 0) return cachedDriver;
9338
+ try {
9339
+ cachedDriver = requireFromHere("better-sqlite3");
9340
+ } catch (err2) {
9341
+ cachedDriver = null;
9342
+ process.stderr.write(
9343
+ `[unerr] WARN: better-sqlite3 native driver unavailable (${err2 instanceof Error ? err2.message : String(err2)}); metrics/telemetry disabled (dashboard counters stay empty). Install the prebuilt binary or a build toolchain to enable them \u2014 core graph tools are unaffected.
9344
+ `
9345
+ );
9346
+ }
9347
+ return cachedDriver;
9348
+ }
9336
9349
  function reconcileAdditiveColumns(db) {
9337
9350
  for (const { table: table2, column, decl } of ADDITIVE_COLUMNS) {
9338
9351
  const cols = db.prepare(`PRAGMA table_info(${table2})`).all();
@@ -9366,10 +9379,11 @@ function closeAllMetricsStores() {
9366
9379
  instances.delete(dir);
9367
9380
  }
9368
9381
  }
9369
- var SCHEMA, SCHEMA_VERSION, ADDITIVE_COLUMNS, POST_RECONCILE_INDEXES, MetricsStore, instances;
9382
+ var requireFromHere, cachedDriver, SCHEMA, SCHEMA_VERSION, ADDITIVE_COLUMNS, POST_RECONCILE_INDEXES, MetricsStore, instances;
9370
9383
  var init_metrics_store = __esm({
9371
9384
  "src/tracking/metrics-store.ts"() {
9372
9385
  "use strict";
9386
+ requireFromHere = createRequire2(import.meta.url);
9373
9387
  SCHEMA = `
9374
9388
  CREATE TABLE IF NOT EXISTS compression_events (
9375
9389
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -9551,9 +9565,17 @@ CREATE INDEX IF NOT EXISTS idx_token_flow_agent ON token_flow_events(agent);
9551
9565
  CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9552
9566
  `;
9553
9567
  MetricsStore = class {
9568
+ // Null when the better-sqlite3 native driver is unavailable — the store then
9569
+ // degrades to a no-op (writes drop, reads return empty). See loadDatabaseCtor.
9554
9570
  db;
9555
9571
  stmt;
9556
9572
  constructor(dbPath) {
9573
+ const Database = loadDatabaseCtor();
9574
+ if (!Database) {
9575
+ this.db = null;
9576
+ this.stmt = null;
9577
+ return;
9578
+ }
9557
9579
  this.db = new Database(dbPath);
9558
9580
  this.db.pragma("journal_mode = WAL");
9559
9581
  this.db.pragma("synchronous = NORMAL");
@@ -9732,89 +9754,111 @@ CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9732
9754
  };
9733
9755
  }
9734
9756
  upsertFetchCacheRow(row) {
9757
+ if (!this.stmt) return;
9735
9758
  this.stmt.upsertFetchCache.run(row);
9736
9759
  }
9737
9760
  getFetchCacheRow(url) {
9761
+ if (!this.stmt) return null;
9738
9762
  return this.stmt.getFetchCache.get({ url }) ?? null;
9739
9763
  }
9740
9764
  bumpFetchCacheHitFor(url) {
9765
+ if (!this.stmt) return;
9741
9766
  this.stmt.bumpFetchCacheHit.run({ url });
9742
9767
  }
9743
9768
  // ── Agent Transcripts ───────────────────────────────────────────────
9744
9769
  upsertAgentTranscript(row) {
9770
+ if (!this.stmt) return;
9745
9771
  this.stmt.upsertAgentTranscript.run(row);
9746
9772
  }
9747
9773
  getAgentTranscriptsForSession(session_id) {
9774
+ if (!this.stmt) return [];
9748
9775
  return this.stmt.agentTranscriptsBySession.all({ session_id });
9749
9776
  }
9750
9777
  getAgentTranscriptsForTurn(session_id, turn) {
9778
+ if (!this.stmt) return [];
9751
9779
  return this.stmt.agentTranscriptsBySessionTurn.all({
9752
9780
  session_id,
9753
9781
  turn
9754
9782
  });
9755
9783
  }
9756
9784
  hasAgentTranscripts(session_id) {
9785
+ if (!this.stmt) return false;
9757
9786
  return !!this.stmt.agentTranscriptSessionExists.get({ session_id });
9758
9787
  }
9759
9788
  // ── Writes ──────────────────────────────────────────────────────────
9760
9789
  insertCompression(row) {
9790
+ if (!this.stmt) return 0;
9761
9791
  return Number(this.stmt.insertCompression.run(row).lastInsertRowid);
9762
9792
  }
9763
9793
  insertFileRead(row) {
9794
+ if (!this.stmt) return 0;
9764
9795
  return Number(this.stmt.insertFileRead.run(row).lastInsertRowid);
9765
9796
  }
9766
9797
  insertTokenFlow(row) {
9798
+ if (!this.stmt) return 0;
9767
9799
  const withAgent = { ...row, agent: row.agent ?? "unknown" };
9768
9800
  return Number(this.stmt.insertTokenFlow.run(withAgent).lastInsertRowid);
9769
9801
  }
9770
9802
  insertBehaviorEvent(row) {
9803
+ if (!this.stmt) return 0;
9771
9804
  const withAgent = { ...row, agent: row.agent ?? "unknown" };
9772
9805
  return Number(this.stmt.insertBehaviorEvent.run(withAgent).lastInsertRowid);
9773
9806
  }
9774
9807
  upsertSessionHistory(row) {
9808
+ if (!this.stmt) return;
9775
9809
  this.stmt.upsertSessionHistory.run(row);
9776
9810
  }
9777
9811
  upsertSessionSummary(row) {
9812
+ if (!this.stmt) return;
9778
9813
  this.stmt.upsertSessionSummary.run(row);
9779
9814
  }
9780
9815
  // ── Reads ───────────────────────────────────────────────────────────
9781
9816
  recentCompression(limit) {
9817
+ if (!this.stmt) return [];
9782
9818
  return this.stmt.recentCompression.all({ limit });
9783
9819
  }
9784
9820
  recentFileReads(limit) {
9821
+ if (!this.stmt) return [];
9785
9822
  return this.stmt.recentFileReads.all({ limit });
9786
9823
  }
9787
9824
  /** Poll API used by the log-tailer. */
9788
9825
  compressionSince(lastId, limit = 500) {
9826
+ if (!this.stmt) return [];
9789
9827
  return this.stmt.compressionSince.all({
9790
9828
  lastId,
9791
9829
  limit
9792
9830
  });
9793
9831
  }
9794
9832
  fileReadsSince(lastId, limit = 500) {
9833
+ if (!this.stmt) return [];
9795
9834
  return this.stmt.fileReadsSince.all({
9796
9835
  lastId,
9797
9836
  limit
9798
9837
  });
9799
9838
  }
9800
9839
  tokenFlowSince(lastId, limit = 500) {
9840
+ if (!this.stmt) return [];
9801
9841
  return this.stmt.tokenFlowSince.all({
9802
9842
  lastId,
9803
9843
  limit
9804
9844
  });
9805
9845
  }
9806
9846
  allTokenFlow() {
9847
+ if (!this.stmt) return [];
9807
9848
  return this.stmt.tokenFlowAll.all({});
9808
9849
  }
9809
9850
  tokenFlowBySession(sessionId) {
9851
+ if (!this.stmt) return [];
9810
9852
  return this.stmt.tokenFlowBySession.all({
9811
9853
  sessionId
9812
9854
  });
9813
9855
  }
9814
9856
  allBehaviorEvents() {
9857
+ if (!this.stmt) return [];
9815
9858
  return this.stmt.behaviorEventsAll.all({});
9816
9859
  }
9817
9860
  behaviorEventsBySession(sessionId) {
9861
+ if (!this.stmt) return [];
9818
9862
  return this.stmt.behaviorEventsBySession.all({
9819
9863
  sessionId
9820
9864
  });
@@ -9838,6 +9882,7 @@ CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9838
9882
  * `hash` is `null` for legacy rows written before the digest field existed.
9839
9883
  */
9840
9884
  latestUserPromptBoundary(sessionId) {
9885
+ if (!this.db) return null;
9841
9886
  const row = this.db.prepare(
9842
9887
  "SELECT ts, detail FROM behavior_events WHERE session_id = ? AND type = 'user_prompt_received' ORDER BY ts DESC LIMIT 1"
9843
9888
  ).get(sessionId);
@@ -9851,18 +9896,22 @@ CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9851
9896
  return { ts: row.ts, hash: hash2 };
9852
9897
  }
9853
9898
  behaviorEventsSince(lastId, limit = 500) {
9899
+ if (!this.stmt) return [];
9854
9900
  return this.stmt.behaviorEventsSince.all({
9855
9901
  lastId,
9856
9902
  limit
9857
9903
  });
9858
9904
  }
9859
9905
  allSessionHistory() {
9906
+ if (!this.stmt) return [];
9860
9907
  return this.stmt.allSessionHistory.all({});
9861
9908
  }
9862
9909
  sessionSummary(sessionId) {
9910
+ if (!this.stmt) return null;
9863
9911
  return this.stmt.sessionSummaryById.get({ sessionId }) ?? null;
9864
9912
  }
9865
9913
  allSessionSummaries() {
9914
+ if (!this.stmt) return [];
9866
9915
  return this.stmt.allSessionSummaries.all({});
9867
9916
  }
9868
9917
  /**
@@ -9871,6 +9920,8 @@ CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9871
9920
  * `COALESCE(MAX(id), 0)` keeps the call O(1) on an indexed PK.
9872
9921
  */
9873
9922
  lastIds() {
9923
+ if (!this.db)
9924
+ return { compression: 0, fileRead: 0, tokenFlow: 0, behaviorEvent: 0 };
9874
9925
  const c = this.db.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM compression_events").get();
9875
9926
  const f = this.db.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM file_read_events").get();
9876
9927
  const t = this.db.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM token_flow_events").get();
@@ -9884,10 +9935,12 @@ CREATE INDEX IF NOT EXISTS idx_behavior_events_agent ON behavior_events(agent);
9884
9935
  }
9885
9936
  // ── Lifecycle ───────────────────────────────────────────────────────
9886
9937
  close() {
9938
+ if (!this.db) return;
9887
9939
  this.db.close();
9888
9940
  }
9889
9941
  /** Test-only — wipe every metric table. */
9890
9942
  reset() {
9943
+ if (!this.db) return;
9891
9944
  this.db.exec(`
9892
9945
  DELETE FROM compression_events;
9893
9946
  DELETE FROM file_read_events;
@@ -11050,8 +11103,8 @@ async function openReadOnly(path7) {
11050
11103
  if (!existsSync27(path7)) return null;
11051
11104
  try {
11052
11105
  const mod = await import("better-sqlite3");
11053
- const Database2 = mod.default ?? mod;
11054
- const db = new Database2(path7, { readonly: true, fileMustExist: true });
11106
+ const Database = mod.default ?? mod;
11107
+ const db = new Database(path7, { readonly: true, fileMustExist: true });
11055
11108
  try {
11056
11109
  db.pragma("busy_timeout = 250");
11057
11110
  } catch {
@@ -12472,8 +12525,8 @@ async function createSqliteDb(dbPath) {
12472
12525
  }
12473
12526
  async function enableWalMode(dbPath) {
12474
12527
  try {
12475
- const { default: Database2 } = await import("better-sqlite3");
12476
- const sqlite = new Database2(dbPath);
12528
+ const { default: Database } = await import("better-sqlite3");
12529
+ const sqlite = new Database(dbPath);
12477
12530
  try {
12478
12531
  sqlite.pragma("journal_mode = WAL");
12479
12532
  } finally {
@@ -12488,8 +12541,8 @@ async function enableWalMode(dbPath) {
12488
12541
  }
12489
12542
  async function checkpointWal(dbPath) {
12490
12543
  try {
12491
- const { default: Database2 } = await import("better-sqlite3");
12492
- const sqlite = new Database2(dbPath);
12544
+ const { default: Database } = await import("better-sqlite3");
12545
+ const sqlite = new Database(dbPath);
12493
12546
  try {
12494
12547
  sqlite.pragma("busy_timeout = 2000");
12495
12548
  const MAX_ATTEMPTS = 6;
@@ -56490,10 +56543,10 @@ ${signalFooter.trimEnd()}` : "";
56490
56543
  startup.addStep(
56491
56544
  "MCP ready",
56492
56545
  "done",
56493
- `PARSE mode (${parseStats?.entityCount ?? 0} entities)`
56546
+ `PARSE mode \u2014 graph engine unavailable (${parseStats?.entityCount ?? 0} entities)`
56494
56547
  );
56495
56548
  log21.info(
56496
- `MCP server running on stdio \u2014 PARSE mode (${parseStats?.entityCount ?? 0} entities from ${parseStats?.fileCount ?? 0} files)`
56549
+ `MCP server running on stdio \u2014 PARSE mode (reduced accuracy): ${proxyModeReason} Serving ${parseStats?.entityCount ?? 0} entities from ${parseStats?.fileCount ?? 0} files via regex extraction (no call graph, drift, or rules). Run \`unerr doctor\` for how to restore the full graph engine.`
56497
56550
  );
56498
56551
  } else {
56499
56552
  const localToolCount = 14;
@@ -59650,31 +59703,73 @@ Otherwise unerrd scans the next ~100 ports for a free one at startup.`
59650
59703
  });
59651
59704
  });
59652
59705
  }
59706
+ function isModuleMissing(err2) {
59707
+ const code = err2?.code;
59708
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND")
59709
+ return true;
59710
+ const msg = err2 instanceof Error ? err2.message : String(err2);
59711
+ return /cannot find (module|package)|module not found/i.test(msg);
59712
+ }
59713
+ var NATIVE_FIX_HINT = "If the prebuilt binary download was blocked, set a proxy and reinstall:\n npm config set proxy http://<host>:<port> ; npm config set https-proxy http://<host>:<port>\n npm i -g @unerr-ai/unerr\nOtherwise install a build toolchain so the source fallback can compile:\n Windows: `npm i -g windows-build-tools` or Visual Studio Build Tools + Python 3 (https://github.com/nodejs/node-gyp#on-windows)";
59653
59714
  async function checkNativeModule() {
59654
59715
  try {
59655
59716
  const cozo = await import("cozo-node");
59656
59717
  if (cozo?.CozoDb) {
59657
59718
  return {
59658
- name: "Native module",
59719
+ name: "Graph engine (cozo-node)",
59659
59720
  status: "ok",
59660
59721
  message: "cozo-node loaded"
59661
59722
  };
59662
59723
  }
59663
59724
  return {
59664
- name: "Native module",
59725
+ name: "Graph engine (cozo-node)",
59665
59726
  status: "warn",
59666
- message: "cozo-node loaded but CozoDb export missing"
59727
+ message: "cozo-node loaded but CozoDb export missing \u2014 unerr will run in PARSE mode (regex graph, reduced accuracy)"
59667
59728
  };
59668
59729
  } catch (err2) {
59669
59730
  const msg = err2 instanceof Error ? err2.message : String(err2);
59731
+ if (isModuleMissing(err2)) {
59732
+ return {
59733
+ name: "Graph engine (cozo-node)",
59734
+ status: "warn",
59735
+ message: "cozo-node not installed \u2014 unerr runs in PARSE mode (regex graph, reduced accuracy)",
59736
+ detail: "cozo-node is a required native module, but its prebuilt binary could not be downloaded or built at install time.\n" + NATIVE_FIX_HINT
59737
+ };
59738
+ }
59670
59739
  return {
59671
- name: "Native module",
59672
- status: "fail",
59673
- message: "cozo-node failed to load",
59740
+ name: "Graph engine (cozo-node)",
59741
+ status: "warn",
59742
+ message: "cozo-node failed to load \u2014 unerr runs in PARSE mode (regex graph, reduced accuracy)",
59674
59743
  detail: `${msg}
59675
59744
  The native binary is likely built for a different Node ABI or platform.
59676
- Reinstall under the node you intend to use: \`npm i -g @unerr-ai/unerr\`.`,
59677
- blocking: true
59745
+ Reinstall under the node you intend to use: \`npm i -g @unerr-ai/unerr\`.`
59746
+ };
59747
+ }
59748
+ }
59749
+ async function checkMetricsDriver() {
59750
+ try {
59751
+ const mod = await import("better-sqlite3");
59752
+ if (mod?.default) {
59753
+ return {
59754
+ name: "Telemetry driver (better-sqlite3)",
59755
+ status: "ok",
59756
+ message: "better-sqlite3 loaded"
59757
+ };
59758
+ }
59759
+ return {
59760
+ name: "Telemetry driver (better-sqlite3)",
59761
+ status: "warn",
59762
+ message: "better-sqlite3 loaded but Database export missing \u2014 dashboard metrics disabled"
59763
+ };
59764
+ } catch (err2) {
59765
+ const missing = isModuleMissing(err2);
59766
+ const msg = err2 instanceof Error ? err2.message : String(err2);
59767
+ return {
59768
+ name: "Telemetry driver (better-sqlite3)",
59769
+ status: "warn",
59770
+ message: missing ? "better-sqlite3 not installed \u2014 dashboard metrics disabled (core graph tools unaffected)" : "better-sqlite3 failed to load \u2014 dashboard metrics disabled (core graph tools unaffected)",
59771
+ detail: (missing ? "" : `${msg}
59772
+ `) + NATIVE_FIX_HINT
59678
59773
  };
59679
59774
  }
59680
59775
  }
@@ -59702,6 +59797,9 @@ async function runEnvironmentChecks(opts) {
59702
59797
  const native = await checkNativeModule();
59703
59798
  printCheckResult(native);
59704
59799
  results.push(native);
59800
+ const metricsDriver = await checkMetricsDriver();
59801
+ printCheckResult(metricsDriver);
59802
+ results.push(metricsDriver);
59705
59803
  const blocking = results.some(
59706
59804
  (r) => r.status === "fail" && r.blocking === true
59707
59805
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unerr-ai/unerr",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "mcpName": "io.github.unerr-ai/unerr",
5
5
  "description": "Your AI agent has read your codebase but still can't safely change it. unerr is a local guardrail that hands the agent the call graph and your rules at the moment it edits.",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "pnpm run build:ui && pnpm run build:cli",
19
- "build:cli": "tsup src/entrypoints/cli.ts --format esm --target node20 --dts --no-splitting",
19
+ "build:cli": "tsup src/entrypoints/cli.ts --format esm --target node20 --dts --no-splitting --external cozo-node --external better-sqlite3",
20
20
  "dev": "tsx watch src/entrypoints/cli.ts",
21
21
  "test": "vitest",
22
22
  "test:run": "vitest run < /dev/null",