@askalf/dario 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,8 @@ That's the whole setup. Every tool that honors those env vars now runs on your s
34
34
 
35
35
  **New in v4:** type `dario` (no args) in another terminal to open the interactive TUI — live request stream, per-model burn-rate, rate-limit utilization, and a config editor that writes to `~/.dario/config.json`. Migrating from v3? See [MIGRATION.md](MIGRATION.md).
36
36
 
37
+ **New in v4.1 — overage-guard:** dario halts itself the moment a single response carries `representative-claim: overage` and returns 503 with a clean error body until you run `dario resume` or the cooldown clears. Subscribers should never see an overage hit during normal operation; one means something is wrong, and continuing to forward requests bleeds against per-token billing. Active protection by default; flip to warn-only with `--overage-behavior=warn` or off entirely with `--no-overage-guard`.
38
+
37
39
  ```
38
40
  ┌─ dario v4 ──────────────────────────[ q quit · Tab next · ? help ]──┐
39
41
  │ Status Config ▎Analytics▎ Hits Accounts Backends │
@@ -154,6 +156,45 @@ Reclassification flips the request from `five_hour` (your subscription) to `over
154
156
 
155
157
  ---
156
158
 
159
+ ## What dario does when overage lands (v4.1)
160
+
161
+ v4 made the billing bucket visible per-request in the TUI's Hits tab. v4.1 turns that visibility into active protection.
162
+
163
+ The moment any upstream response carries `representative-claim: overage`, dario **halts the proxy**. Every subsequent `/v1/messages`, `/v1/complete`, `/v1/chat/completions` request returns `503` with an Anthropic-shaped error body the client surfaces verbatim:
164
+
165
+ ```json
166
+ {
167
+ "type": "error",
168
+ "error": {
169
+ "type": "dario_overage_guard",
170
+ "message": "dario halted to prevent API-rate bleed. A request was classified as 'overage' (per-token billing) instead of your subscription pool. To resume: run `dario resume` in another terminal, or wait until <ISO ts> for the cooldown to auto-clear. Details: github.com/askalf/dario/issues/288"
171
+ }
172
+ }
173
+ ```
174
+
175
+ The TUI's Status tab pins the loud version:
176
+
177
+ ```
178
+ ┌─ dario v4 ──────────────────────────[ q quit · Tab next · ? help ]──┐
179
+ │ ▎Status▎ Config Analytics Hits Accounts Backends │
180
+ ├─────────────────────────────────────────────────────────────────────┤
181
+ │ Overage-guard │
182
+ │ ⚠ HALTED overage detected 12s ago │
183
+ │ Request: claude-opus-4-7 account=default │
184
+ │ Cause: representative-claim = overage │
185
+ │ Auto-resume in 29m 48s │
186
+ │ Manual resume press R here, or `dario resume` from any shell │
187
+ │ │
188
+ │ Last refresh: just now. r refresh · R resume. │
189
+ └─────────────────────────────────────────────────────────────────────┘
190
+ ```
191
+
192
+ Why "halt at hit #1" is the right default: subscribers should never see a single overage response during normal operation. One means something is wrong — wire-shape drift, classifier change, account misconfig — and continuing to forward requests in the same shape bleeds real money for accounts with extra-usage enabled, or returns wall-of-rejections for accounts without it. The first hit is the signal; the second through hundredth are damage.
193
+
194
+ **Resume paths** — `dario resume` from any shell, `R` on the TUI Status tab, or the cooldown timer (default 30 min). **Configuration** — `~/.dario/config.json` → `overageGuard`, or CLI flags (`--overage-behavior=warn` for visibility-only, `--no-overage-guard` to disable, `--overage-cooldown=<ms>` to tune). **OS notification** — best-effort native toast (osascript / notify-send / BurntToast) plus terminal BEL as the unconditional floor. See [#288](https://github.com/askalf/dario/issues/288).
195
+
196
+ ---
197
+
157
198
  ## Does it actually work?
158
199
 
159
200
  Four LLMs reviewed the codebase cold, same prompt ([`reviews/PROMPT.md`](./reviews/PROMPT.md)), each signed a verdict:
@@ -238,6 +279,7 @@ The tool doesn't know. The backend doesn't know. Dario is the seam.
238
279
  - **Shim mode.** Take the proxy off the wire entirely — `dario shim -- claude --print "hi"` patches `globalThis.fetch` in-process. No HTTP hop, no port, no `BASE_URL`. → [`docs/shim.md`](./docs/shim.md)
239
280
  - **Recover output capability.** `dario proxy --system-prompt=partial` strips CC's tone/verbosity/no-comments constraints for 1.2–2.8× more output on open-ended work — empirically without flipping billing (the classifier doesn't read that slot). [Discussion #183](https://github.com/askalf/dario/discussions/183) has the per-prompt receipts. → [`docs/system-prompt.md`](./docs/system-prompt.md)
240
281
  - **Reachable from inside CC / any MCP client.** `dario subagent install` registers a CC sub-agent for in-session diagnostics; `dario mcp` exposes dario as a read-only MCP server. → [`docs/sub-agent.md`](./docs/sub-agent.md) · [`docs/mcp-server.md`](./docs/mcp-server.md)
282
+ - **Active overage protection (v4.1).** Halts the proxy on any `representative-claim: overage` response and returns 503 to subsequent requests until you run `dario resume` or the cooldown clears. Visibility-only mode (`--overage-behavior=warn`) for operators who want the signal without disrupting traffic. Halt state visible in TUI Status/Hits/Analytics tabs, surfaced as named SSE events, and as a best-effort native desktop notification. [#288](https://github.com/askalf/dario/issues/288).
241
283
 
242
284
  ---
243
285
 
@@ -245,11 +287,11 @@ The tool doesn't know. The backend doesn't know. Dario is the seam.
245
287
 
246
288
  | Signal | Status |
247
289
  |---|---|
248
- | Source | **17,507** lines of TypeScript across **42** files — auditable in a weekend |
290
+ | Source | **~18.5k** lines of TypeScript across **44** files — auditable in a weekend |
249
291
  | Dependencies | **0 runtime.** Verify: `npm ls --production` |
250
- | Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore (v4.0.0 published 2026-05-16T11:46:01Z; Rekor logIndex `1553210791`) |
292
+ | Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore. v4.1.0 published 2026-05-16T15:13:24Z |
251
293
  | Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push and weekly |
252
- | Tests | **2,080** assertions across 77 test files (71 in default `npm test` suite) — green on every release |
294
+ | Tests | **80 test files**, **74 in default `npm test` suite** — green on every release |
253
295
  | Drift response | [`cc-drift-watch.yml`](./.github/workflows/cc-drift-watch.yml) hourly cron, [`cc-drift-auto-release.yml`](./.github/workflows/cc-drift-auto-release.yml) auto-publish on merge — median CC-release → dario-release under one hour |
254
296
  | Credentials | Never logged, redacted from errors, `0600` on disk in `0700` dirs; MCP server redacts at the tool boundary |
255
297
  | Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allowlist |
@@ -273,7 +315,7 @@ cd $(npm root -g)/@askalf/dario && npm ls --production
273
315
 
274
316
  ## Commands
275
317
 
276
- `dario` (TUI) · `login` · `proxy` · `doctor` · `accounts {list,add,remove}` · `backend {list,add,remove}` · `shim` · `mcp` · `subagent {install,status,remove}` · `usage` · `config` · `upgrade` · `status` · `refresh` · `logout` · `help`
318
+ `dario` (TUI) · `login` · `proxy` · `doctor` · `accounts {list,add,remove}` · `backend {list,add,remove}` · `shim` · `mcp` · `subagent {install,status,remove}` · `usage` · `config` · `upgrade` · `status` · `refresh` · `resume` · `logout` · `help`
277
319
 
278
320
  Full flag/env reference: [`docs/commands.md`](./docs/commands.md) · SDK examples + per-tool setup: [`docs/usage.md`](./docs/usage.md)
279
321
 
@@ -285,7 +327,10 @@ Full flag/env reference: [`docs/commands.md`](./docs/commands.md) · SDK example
285
327
  Mechanically, dario uses your existing Claude Code OAuth tokens — it authenticates you as you, with your subscription, through Anthropic's official endpoints. Whether any particular use complies with current terms is between you and Anthropic; consult their terms and your agreement. Independent, unofficial, third-party — see [DISCLAIMER.md](DISCLAIMER.md).
286
328
 
287
329
  **What does the v4 TUI actually do?**
288
- Open `dario` with no args. Six tabs: **Status** shows proxy health + OAuth expiry + config source; **Config** edits `~/.dario/config.json` in place (bool toggles inline, numbers/strings open a prompt, `s` saves); **Analytics** polls `/analytics` every 2s and renders per-model bars + rate-limit utilization + billing buckets; **Hits** subscribes to `/analytics/stream` SSE for the live request feed with per-record detail drilldown; **Accounts** lists the pool; **Backends** lists OpenAI-compat backends. Pure ANSI, zero new runtime deps. Migration from v3: [MIGRATION.md](MIGRATION.md).
330
+ Open `dario` with no args. Six tabs: **Status** shows proxy health + OAuth expiry + config source + overage-guard state (v4.1: halt banner with countdown + `R` to resume); **Config** edits `~/.dario/config.json` in place (bool toggles inline, numbers/strings open a prompt, `s` saves); **Analytics** polls `/analytics` every 2s and renders per-model bars + rate-limit utilization + an Overage bar that's red the moment count is non-zero (v4.1); **Hits** subscribes to `/analytics/stream` SSE for the live request feed with per-record detail drilldown and a pinned halt banner when overage is detected (v4.1); **Accounts** lists the pool; **Backends** lists OpenAI-compat backends. Pure ANSI, zero new runtime deps. Migration from v3: [MIGRATION.md](MIGRATION.md).
331
+
332
+ **What if a request lands in `overage` despite the wire-shape replay?**
333
+ v4.1+ halts the proxy on the first overage response and returns 503 to subsequent requests until you investigate. See [What dario does when overage lands](#what-dario-does-when-overage-lands-v41). The TUI Status tab shows the triggering request + countdown to auto-resume; `dario resume` from any shell clears the halt immediately; `--overage-behavior=warn` switches to visibility-only mode if you'd rather see the signal than block traffic.
289
334
 
290
335
  **Do I need Claude Code installed?**
291
336
  Recommended, not required. With CC, `dario login` picks up credentials automatically and the live template extractor reads your binary on every startup. Without it, dario runs its own OAuth flow and falls back to the bundled (scrubbed) template snapshot.
package/dist/cli.js CHANGED
@@ -216,8 +216,11 @@ async function resume() {
216
216
  catch (err) {
217
217
  const msg = err.message;
218
218
  // The proxy-not-running case is the common failure path; surface a
219
- // friendly hint instead of a raw Node fetch error.
220
- if (/ECONNREFUSED|fetch failed/i.test(msg)) {
219
+ // friendly hint instead of a raw fetch error. Match across runtimes:
220
+ // - Node: 'ECONNREFUSED', 'fetch failed', 'ENOTFOUND', 'ETIMEDOUT'
221
+ // - Bun: 'Unable to connect' (different fetch error wording)
222
+ // - Both: 'connect EHOSTUNREACH', 'getaddrinfo' (DNS path)
223
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EHOSTUNREACH|fetch failed|unable to connect|getaddrinfo/i.test(msg)) {
221
224
  console.error(`[dario] No proxy running on localhost:${port}. Start one with \`dario proxy\` (overage-guard state is per-process; there's nothing to resume on a stopped proxy).`);
222
225
  process.exit(1);
223
226
  }
@@ -1024,6 +1027,11 @@ async function help() {
1024
1027
  dario proxy [options] Start the API proxy server
1025
1028
  dario status Check authentication status
1026
1029
  dario refresh Force token refresh
1030
+ dario resume Clear the overage-guard halt on a running proxy.
1031
+ v4.1.0+. Idempotent: returns "no-op" if the
1032
+ proxy isn't halted. Errors with a friendly hint
1033
+ if no proxy is running on localhost:3456.
1034
+ POSTs /admin/resume on the local proxy. (dario#288)
1027
1035
  dario logout Remove saved credentials
1028
1036
  dario accounts list List accounts in the multi-account pool
1029
1037
  dario accounts add NAME [--manual] [--from-keychain[=<target>]]
@@ -1281,6 +1289,37 @@ async function help() {
1281
1289
  ceiling server-side, so too-high values
1282
1290
  return a clean 400.
1283
1291
  Env: DARIO_MAX_TOKENS. (dario#88)
1292
+ --no-overage-guard Disable the overage-guard (v4.1.0+). Default
1293
+ behavior halts the proxy on any response with
1294
+ representative-claim=overage and returns 503
1295
+ with an Anthropic-shaped error body until
1296
+ cooldown expires or \`dario resume\` clears
1297
+ the state. Subscribers should never see an
1298
+ overage hit during normal operation; one
1299
+ means something is wrong (wire drift,
1300
+ classifier change, account misconfig).
1301
+ Env: DARIO_OVERAGE_GUARD=off. (dario#288)
1302
+ --overage-behavior=<halt|warn>
1303
+ Behavior when overage is detected (v4.1.0+):
1304
+ halt — return 503 to new /v1/messages
1305
+ requests until resume / cooldown.
1306
+ Default. Strongest protection.
1307
+ warn — emit events + OS notification only,
1308
+ keep forwarding. Visibility mode for
1309
+ operators who want the signal without
1310
+ cutting off traffic.
1311
+ Env: DARIO_OVERAGE_BEHAVIOR.
1312
+ --overage-cooldown=MS Ms to wait before auto-clearing the halt
1313
+ state (v4.1.0+). Default: 1800000 (30 min).
1314
+ Manual \`dario resume\` clears immediately
1315
+ regardless of this value.
1316
+ Env: DARIO_OVERAGE_COOLDOWN.
1317
+ --no-overage-notify Suppress the native desktop notification on
1318
+ halt (v4.1.0+). Terminal BEL is the
1319
+ unconditional floor; TUI banner + SSE event
1320
+ still fire regardless. Use in headless / CI
1321
+ contexts where toast popups don't make sense.
1322
+ Env: DARIO_OVERAGE_NOTIFY=off.
1284
1323
  --port=PORT Port to listen on (default: 3456)
1285
1324
  --host=ADDRESS Address to bind to (default: 127.0.0.1)
1286
1325
  Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
@@ -217,15 +217,45 @@ function commitEdit(state) {
217
217
  if (!Number.isFinite(n)) {
218
218
  return { ...state, editBuffer: null, statusMessage: `Not a number: "${state.editBuffer}"`, statusKind: 'error' };
219
219
  }
220
+ // Path-specific guards. cooldownMs must be non-negative — silently
221
+ // dropping a bad value on next config-file load is correct but lets
222
+ // the user save an invalid file. Surface immediately. (v4.1.1)
223
+ if (f.path === 'overageGuard.cooldownMs' && n < 0) {
224
+ return { ...state, editBuffer: null, statusMessage: `overageGuard.cooldownMs must be >= 0 (got ${n})`, statusKind: 'error' };
225
+ }
220
226
  parsed = n;
221
227
  }
222
228
  }
229
+ else if (f.type === 'string') {
230
+ // String enums: validate so we reject bad input at commit time rather
231
+ // than let the proxy's sanitize() silently drop it on next load. v4.1.1
232
+ // adds the overageGuard.behavior enum; future enums register here.
233
+ const enumValues = STRING_ENUMS[f.path];
234
+ if (enumValues && !enumValues.includes(state.editBuffer)) {
235
+ return {
236
+ ...state,
237
+ editBuffer: null,
238
+ statusMessage: `${f.label} must be one of: ${enumValues.join(', ')} (got "${state.editBuffer}")`,
239
+ statusKind: 'error',
240
+ };
241
+ }
242
+ parsed = state.editBuffer;
243
+ }
223
244
  else {
224
245
  parsed = state.editBuffer;
225
246
  }
226
247
  const next = setByPath(state.config, f.path, parsed);
227
248
  return { ...state, config: next, editBuffer: null, statusMessage: `Updated ${f.label}.`, statusKind: 'success' };
228
249
  }
250
+ /**
251
+ * Allowed values for string-enum fields. Keyed by FIELDS path. Anything
252
+ * absent here is treated as free-text (no enum validation). v4.1.1+ —
253
+ * additive: registering a new entry forces enum validation on the next
254
+ * commit without touching the rest of the editor.
255
+ */
256
+ const STRING_ENUMS = {
257
+ 'overageGuard.behavior': ['halt', 'warn'],
258
+ };
229
259
  function doSave(state) {
230
260
  try {
231
261
  saveConfig(undefined, { ...state.config, version: CONFIG_SCHEMA_VERSION });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {