@askalf/dario 4.8.117 → 4.8.118

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
@@ -18,8 +18,6 @@
18
18
 
19
19
  > 🗓️ **2026-06-15 — Anthropic splits Claude billing.** Agent-SDK and `claude -p` (headless) traffic stops counting against your subscription pool and moves to a small separate monthly credit ($20 / $100 / $200 by plan), then metered per-token API rates. Most proxies forward your requests in exactly the shape that gets reclassified into that bucket. dario rewrites every request into interactive Claude Code wire-shape before it leaves your machine, so your traffic stays in the subscription pool you already pay for — same install, no config change for the cliff. **[What changes, and how to verify it on your own machine →](#the-deadline-2026-06-15)**
20
20
 
21
- > ⛔ **Claude Fable 5 / Mythos 5 — temporarily suspended for all Anthropic customers.** On 2026-06-12 a US-government legal directive disabled Fable 5 and Mythos 5 for **every** Anthropic plan and tier — `api.anthropic.com` now returns `not_found` for them, and no account or proxy can route around it ([details](https://www.anthropic.com/news/fable-mythos-access)). As of **v4.8.71**, dario filters both families out of `/v1/models` and rejects any spelling (`fable`, `fable1m`, `claude-fable-5`, `claude-fable-5[1m]`, `claude:fable`) up front with a clean `404` pointing at `claude-opus-4-8` / `claude-sonnet-5` — instead of forwarding into a confusing upstream error. Reversible without a code change once access is restored: set `DARIO_SUSPENDED_MODELS=` (empty) on the instance. `npm install -g @askalf/dario@latest`
22
- >
23
21
  > ⚠️ Still on a version **before 4.8.39**? Upgrade now — those could silently corrupt code/structured content routed through the proxy (the identifier scrub stripped tokens like the JS `continue` keyword). **[Details →](https://github.com/askalf/dario/issues/457)**
24
22
 
25
23
  ---
@@ -194,7 +192,7 @@ You point every tool at one URL. dario reads each request, decides which backend
194
192
 
195
193
  The tool doesn't know. The backend doesn't know. dario is the seam.
196
194
 
197
- **The full Claude lineup, autodetected.** Opus 4.8, Sonnet 5, and Haiku 4.5 — plus `[1m]` long-context variants, generated by one rule for every family — by full id (`claude-opus-4-8`) or shortcut (`opus` / `sonnet` / `haiku`, append `1m` for the long-context form). `GET /v1/models` asks Anthropic's live catalog (TTL-cached, baked fallback when offline), and the family shortcuts track it — a new model shows up and resolves the day it lands, no dario release needed; the model-specific wire shape (effort level, beta set, thinking config) is applied automatically. **Globally-suspended families are filtered out** of both the live catalog and the baked fallback, so `/v1/models` never advertises a model that 404s upstream and suspended ids are rejected locally with an actionable error currently Claude Fable 5 and Mythos 5 (US-government directive, 2026-06-12); governed by `DARIO_SUSPENDED_MODELS` (default `fable`), set it empty to re-enable when access is restored.
195
+ **The full Claude lineup, autodetected.** Opus 4.8, Sonnet 5, and Haiku 4.5 — plus `[1m]` long-context variants, generated by one rule for every family — by full id (`claude-opus-4-8`) or shortcut (`opus` / `sonnet` / `haiku`, append `1m` for the long-context form). `GET /v1/models` asks Anthropic's live catalog (TTL-cached, baked fallback when offline), and the family shortcuts track it — a new model shows up and resolves the day it lands, no dario release needed; the model-specific wire shape (effort level, beta set, thinking config) is applied automatically. **Suspended families are filtered out** of both the live catalog and the baked fallback, so `/v1/models` never advertises a model that 404s upstream and suspended ids are rejected locally with an actionable error. Nothing is suspended by default; the mechanism is retained via `DARIO_SUSPENDED_MODELS` (comma-separated families, every spelling caught) for if a family is ever pulled upstream again.
198
196
 
199
197
  ---
200
198
 
@@ -224,6 +222,7 @@ Tune via `~/.dario/config.json` → `overageGuard`, or CLI flags: `--overage-beh
224
222
  ## Capabilities
225
223
 
226
224
  - **Multi-account pool.** Drop Claude accounts in `~/.dario/accounts/` and pool mode auto-activates — one is enough to serve, so `dario accounts add` alone bootstraps a proxy with no `dario login` step. With more, every request routes to the account with the most headroom, multi-turn sessions pin to one account so the prompt cache survives, in-flight 429s fail over to a peer before your client sees an error. → [`docs/multi-account-pool.md`](./docs/multi-account-pool.md)
225
+ - **Headless admin API (`DARIO_ADMIN=1`).** Provision and manage pool accounts entirely over HTTP — start with zero accounts, `POST /admin/login/start`, paste the code back, and the account is routable the moment the `200` lands (live pool hot-reload, no restart). Token-gated even on loopback, audit-logged, rate-limited; `GET /admin/accounts` reports live per-account headroom. Built for Docker / k8s / Raspberry-Pi deployments where a console is the awkward part. → [`docs/admin-api.md`](./docs/admin-api.md)
227
226
  - **Behavioral stealth (`--stealth`).** Static wire fidelity covers *what* the request looks like; `--stealth` adds *when* it arrives — response-length-correlated think time and 1.2–4.2s session-start latency, the inter-arrival pattern real interactive sessions have and agent loops don't. → [`docs/wire-fidelity.md`](./docs/wire-fidelity.md)
228
227
  - **Runs any non-Claude-Code agent.** A 64-entry schema-verified `TOOL_MAP` pre-maps Cline, Roo, Kilo, Cursor, Windsurf, Continue, Copilot, OpenHands, OpenClaw, Hermes, [hands](https://github.com/askalf/hands) tool names to CC's native set. No flag, no validator errors. → [`docs/integrations/agent-compat.md`](./docs/integrations/agent-compat.md)
229
228
  - **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)
@@ -302,9 +301,6 @@ No. `five_hour` and `seven_day` are both subscription billing — different acco
302
301
  **Will the 2026-06-15 split break my setup? / What if Anthropic ships another silent change?**
303
302
  No, and it's caught automatically — see [The deadline](#the-deadline-2026-06-15) and [How it stays working](#how-it-works-and-how-it-stays-working). dario rewrites every request to interactive-CC shape before it reaches `api.anthropic.com`, and the three-class drift watcher picks up new changes (npm-release hourly, remote-config every 30 min, classifier-rule daily). v3.38.5 + v3.38.6 — 13 minutes apart, same day as v2.1.142's silent drops — are the prior art.
304
303
 
305
- **I'm getting `404 not_found` for `claude-fable-5` / `fable`. Did dario break?**
306
- No — Claude Fable 5 and Mythos 5 were disabled for **all** Anthropic customers by a US-government legal directive on 2026-06-12 (every plan and tier; [details](https://www.anthropic.com/news/fable-mythos-access)). `api.anthropic.com` returns `not_found` for them, so no subscription or proxy can route them. Since v4.8.71 dario drops both families from `/v1/models` and rejects them locally with a clear 404 pointing at `claude-opus-4-8` / `claude-sonnet-5`, instead of forwarding into a confusing upstream error. When access is restored, set `DARIO_SUSPENDED_MODELS=` (empty) on the instance to re-enable — no upgrade needed.
307
-
308
304
  Full FAQ: [`docs/faq.md`](./docs/faq.md)
309
305
 
310
306
  ---
@@ -20,6 +20,8 @@ export interface HealthStatusLike {
20
20
  expiresIn?: string;
21
21
  refreshFailures?: number;
22
22
  lastRefreshError?: string;
23
+ /** dario's package version, surfaced to internal callers on /health (#640). */
24
+ version?: string;
23
25
  }
24
26
  export interface HealthResponse {
25
27
  httpStatus: number;
@@ -69,6 +69,9 @@ export function buildHealthResponse(s, requestCount, viaPublicTunnel) {
69
69
  ? liveness
70
70
  : {
71
71
  ...liveness,
72
+ // Version for internal callers only — an update-check signal (#640),
73
+ // withheld from the public-tunnel view alongside the OAuth internals.
74
+ ...(s.version ? { version: s.version } : {}),
72
75
  oauth: s.status,
73
76
  expiresIn: s.expiresIn,
74
77
  requests: requestCount,
package/dist/proxy.js CHANGED
@@ -8,6 +8,7 @@ import { setDefaultResultOrder } from 'node:dns';
8
8
  import { arch, platform } from 'node:process';
9
9
  import { getAccessToken, getStatus } from './oauth.js';
10
10
  import { buildHealthResponse, derivePoolStatus } from './health-response.js';
11
+ import { darioVersion } from './version.js';
11
12
  import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, CC_TEMPLATE } from './cc-template.js';
12
13
  import { stampCch } from './cch.js';
13
14
  import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
@@ -1271,7 +1272,7 @@ export async function startProxy(opts = {}) {
1271
1272
  // Public requests arrive through the Cloudflare tunnel (the edge stamps
1272
1273
  // `cf-ray`); they get only the liveness verdict, never the OAuth internals.
1273
1274
  // See buildHealthResponse for the full rationale.
1274
- const { httpStatus, body } = buildHealthResponse(s, requestCount, req.headers['cf-ray'] !== undefined);
1275
+ const { httpStatus, body } = buildHealthResponse({ ...s, version: darioVersion() }, requestCount, req.headers['cf-ray'] !== undefined);
1275
1276
  res.writeHead(httpStatus, JSON_HEADERS);
1276
1277
  res.end(JSON.stringify(body));
1277
1278
  return;
@@ -1376,8 +1377,10 @@ export async function startProxy(opts = {}) {
1376
1377
  // Status endpoint
1377
1378
  if (urlPath === '/status') {
1378
1379
  const s = await currentStatus();
1380
+ // Version for auto-update checks (#640) — /status is key-gated (loopback
1381
+ // or DARIO_API_KEY), so no public-disclosure concern like /health has.
1379
1382
  res.writeHead(200, JSON_HEADERS);
1380
- res.end(JSON.stringify(s));
1383
+ res.end(JSON.stringify({ version: darioVersion(), ...s }));
1381
1384
  return;
1382
1385
  }
1383
1386
  // Pool status endpoint — shows loaded accounts, headroom, and the
@@ -1,8 +1,16 @@
1
1
  /**
2
2
  * Accounts tab — list of OAuth subscription accounts in the pool.
3
3
  *
4
- * Read-mostly. Mutations (add/remove) require the CLI the tab
5
- * shows the relevant command in the footer.
4
+ * Source of truth is the RUNNING PROXY's live pool (`GET /accounts`), not a
5
+ * local disk read: the TUI is its own process, and in a containerized / admin
6
+ * (#599) / login-less-pool (#630) deployment the accounts live in the proxy's
7
+ * volume, so reading `~/.dario/accounts/` in the TUI process comes up empty
8
+ * while the proxy serves several accounts fine (#641). We fall back to the disk
9
+ * read only when the proxy is unreachable, and flag it so the user knows the
10
+ * view may be stale.
11
+ *
12
+ * Read-mostly. Mutations (add/remove) require the CLI or the admin API — the
13
+ * tab shows the relevant command in the footer.
6
14
  *
7
15
  * Layout:
8
16
  *
@@ -16,17 +24,21 @@
16
24
  * To add: `dario accounts add <alias>`
17
25
  * To remove: `dario accounts remove <alias>`
18
26
  */
19
- import type { Tab } from '../tab.js';
27
+ import type { Tab, TabContext } from '../tab.js';
20
28
  export interface AccountsState {
21
29
  loading: boolean;
22
30
  accounts: Array<{
23
31
  alias: string;
24
32
  expiresAt: number;
25
- /** Optional rate-limit fields populated when /accounts endpoint exists. */
33
+ /** Live pool fields present when sourced from the proxy's `/accounts`. */
26
34
  util5h?: number;
27
35
  util7d?: number;
36
+ status?: string;
28
37
  }>;
29
38
  error: string | null;
39
+ /** Where the list came from: the running proxy's pool, the proxy's
40
+ * single-account mode, or a local disk fallback when the proxy is down. */
41
+ source?: 'pool' | 'single-account' | 'disk';
30
42
  }
31
43
  export declare const AccountsTab: Tab<AccountsState>;
32
- export declare function refreshAccounts(): Promise<AccountsState>;
44
+ export declare function refreshAccounts(ctx?: TabContext<AccountsState>): Promise<AccountsState>;
@@ -1,8 +1,16 @@
1
1
  /**
2
2
  * Accounts tab — list of OAuth subscription accounts in the pool.
3
3
  *
4
- * Read-mostly. Mutations (add/remove) require the CLI the tab
5
- * shows the relevant command in the footer.
4
+ * Source of truth is the RUNNING PROXY's live pool (`GET /accounts`), not a
5
+ * local disk read: the TUI is its own process, and in a containerized / admin
6
+ * (#599) / login-less-pool (#630) deployment the accounts live in the proxy's
7
+ * volume, so reading `~/.dario/accounts/` in the TUI process comes up empty
8
+ * while the proxy serves several accounts fine (#641). We fall back to the disk
9
+ * read only when the proxy is unreachable, and flag it so the user knows the
10
+ * view may be stale.
11
+ *
12
+ * Read-mostly. Mutations (add/remove) require the CLI or the admin API — the
13
+ * tab shows the relevant command in the footer.
6
14
  *
7
15
  * Layout:
8
16
  *
@@ -25,8 +33,8 @@ export const AccountsTab = {
25
33
  initialState() {
26
34
  return { loading: true, accounts: [], error: null };
27
35
  },
28
- async onMount(_state, _ctx) {
29
- return refreshAccounts();
36
+ async onMount(_state, ctx) {
37
+ return refreshAccounts(ctx);
30
38
  },
31
39
  onKey(state, key) {
32
40
  if (key.name === 'printable' && key.ch === 'r' && !key.ctrl) {
@@ -34,6 +42,18 @@ export const AccountsTab = {
34
42
  }
35
43
  return undefined;
36
44
  },
45
+ onTick(state, ctx) {
46
+ // onKey can only return new state, not run async work — so a manual
47
+ // refresh ('r') just sets loading:true and this tick drives the refetch.
48
+ // `refreshInFlight` guards against the 250ms tick stacking overlapping
49
+ // fetches while one is already running.
50
+ if (state.loading && !refreshInFlight) {
51
+ refreshInFlight = true;
52
+ void refreshAccounts(ctx)
53
+ .then((next) => ctx.setState(next))
54
+ .finally(() => { refreshInFlight = false; });
55
+ }
56
+ },
37
57
  render(state, dimv) {
38
58
  const lines = [];
39
59
  const w = dimv.cols;
@@ -45,18 +65,42 @@ export const AccountsTab = {
45
65
  }
46
66
  if (state.accounts.length === 0) {
47
67
  lines.push('');
48
- lines.push(' ' + dim('No accounts in the pool.'));
49
- lines.push(' ' + 'Add one: ' + fg('cyan', 'dario accounts add <alias>'));
68
+ if (state.source === 'single-account') {
69
+ lines.push(' ' + dim('Single-account mode (`dario login`) no pool.'));
70
+ lines.push(' ' + 'Start a pool: ' + fg('cyan', 'dario accounts add <alias>'));
71
+ }
72
+ else {
73
+ lines.push(' ' + dim('No accounts in the pool.'));
74
+ lines.push(' ' + 'Add one: ' + fg('cyan', 'dario accounts add <alias>'));
75
+ }
50
76
  return lines.join('\n');
51
77
  }
78
+ // Live pool data (from /accounts) carries utilization; the disk fallback
79
+ // doesn't, so show the util columns only when the pool populated them.
80
+ const hasUtil = state.accounts.some((a) => a.util5h !== undefined);
81
+ if (state.source === 'disk') {
82
+ lines.push(' ' + fg('yellow', 'proxy unreachable — showing on-disk accounts (may be stale)'));
83
+ }
52
84
  // Header row
53
- lines.push(' ' + dim(pad('alias', 20) + pad('expires', 16) + pad('source', 24)));
54
- lines.push(' ' + dim(''.repeat(Math.min(w - 4, 60))));
85
+ lines.push(' ' + dim(hasUtil
86
+ ? pad('alias', 20) + pad('expires', 14) + pad('util5h', 9) + pad('util7d', 9) + pad('status', 14)
87
+ : pad('alias', 20) + pad('expires', 16) + pad('source', 24)));
88
+ lines.push(' ' + dim('─'.repeat(Math.min(w - 4, 66))));
55
89
  for (const acc of state.accounts) {
56
90
  const aliasCol = pad(acc.alias, 20);
57
- const expiresCol = pad(formatExpiry(acc.expiresAt), 16);
58
- const sourceCol = '~/.dario/accounts/' + acc.alias + '.json';
59
- lines.push(' ' + aliasCol + expiresCol + dim(sourceCol));
91
+ if (hasUtil) {
92
+ const expiresCol = pad(formatExpiry(acc.expiresAt), 14);
93
+ const u5 = pad(acc.util5h !== undefined ? `${Math.round(acc.util5h * 100)}%` : '—', 9);
94
+ const u7 = pad(acc.util7d !== undefined ? `${Math.round(acc.util7d * 100)}%` : '—', 9);
95
+ const statusCol = acc.status ?? '—';
96
+ const statusFg = statusCol === 'auth-cooldown' ? fg('yellow', statusCol) : dim(statusCol);
97
+ lines.push(' ' + aliasCol + expiresCol + u5 + u7 + statusFg);
98
+ }
99
+ else {
100
+ const expiresCol = pad(formatExpiry(acc.expiresAt), 16);
101
+ const sourceCol = '~/.dario/accounts/' + acc.alias + '.json';
102
+ lines.push(' ' + aliasCol + expiresCol + dim(sourceCol));
103
+ }
60
104
  }
61
105
  lines.push('');
62
106
  lines.push(' ' + dim('Mutations via CLI:'));
@@ -69,27 +113,62 @@ export const AccountsTab = {
69
113
  return lines.join('\n');
70
114
  },
71
115
  };
72
- export async function refreshAccounts() {
116
+ /** Guards the onTick refetch against overlapping in-flight fetches. */
117
+ let refreshInFlight = false;
118
+ export async function refreshAccounts(ctx) {
119
+ // Preferred source: the running proxy's live pool. This is what actually
120
+ // serves traffic, and it works regardless of which process/host/volume the
121
+ // TUI itself runs on — the fix for #641, where a containerized proxy held
122
+ // the accounts and the TUI's local disk read came up empty.
123
+ if (ctx) {
124
+ try {
125
+ const r = await ctx.client.getJson('/accounts');
126
+ if (r.mode === 'single-account') {
127
+ return { loading: false, accounts: [], error: null, source: 'single-account' };
128
+ }
129
+ if (Array.isArray(r.accounts)) {
130
+ const now = Date.now();
131
+ return {
132
+ loading: false,
133
+ source: 'pool',
134
+ accounts: r.accounts.map((a) => ({
135
+ alias: a.alias,
136
+ expiresAt: now + (a.expiresInMs ?? 0),
137
+ util5h: a.util5h,
138
+ util7d: a.util7d,
139
+ status: a.status,
140
+ })),
141
+ error: null,
142
+ };
143
+ }
144
+ // Unknown shape — fall through to the disk read below.
145
+ }
146
+ catch {
147
+ // Proxy unreachable (not running, wrong port, missing key) — fall back
148
+ // to the on-disk view so a standalone TUI still shows something, flagged
149
+ // stale so the user knows it isn't the live pool.
150
+ return diskFallback();
151
+ }
152
+ }
153
+ return diskFallback();
154
+ }
155
+ async function diskFallback() {
73
156
  try {
74
157
  const { listAccountAliases, loadAllAccounts } = await import('../../accounts.js');
75
158
  const aliases = await listAccountAliases();
76
159
  if (aliases.length === 0) {
77
- // Single-account / login.json path show a synthetic "default"
78
- // entry sourced from ~/.dario/credentials.json or keychain.
79
- return { loading: false, accounts: [], error: null };
160
+ return { loading: false, accounts: [], error: null, source: 'disk' };
80
161
  }
81
162
  const all = await loadAllAccounts();
82
163
  return {
83
164
  loading: false,
84
- accounts: all.map(a => ({
85
- alias: a.alias,
86
- expiresAt: a.expiresAt,
87
- })),
165
+ source: 'disk',
166
+ accounts: all.map((a) => ({ alias: a.alias, expiresAt: a.expiresAt })),
88
167
  error: null,
89
168
  };
90
169
  }
91
170
  catch (e) {
92
- return { loading: false, accounts: [], error: e.message };
171
+ return { loading: false, accounts: [], error: e.message, source: 'disk' };
93
172
  }
94
173
  }
95
174
  function formatExpiry(expiresAt) {
@@ -0,0 +1,3 @@
1
+ export declare function darioVersion(): string;
2
+ /** Test-only: clear the memoized version. */
3
+ export declare function _resetVersionCacheForTest(): void;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dario's own package version, read once from the bundled package.json.
3
+ *
4
+ * Surfaced on `/status` and `/health` (#640) so a headless operator can confirm
5
+ * an auto-update actually rolled the running proxy — `curl /health | jq .version`
6
+ * beats exec-ing into the container to read package.json.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ let cached = null;
12
+ export function darioVersion() {
13
+ if (cached !== null)
14
+ return cached;
15
+ let v = 'unknown';
16
+ try {
17
+ // dist/version.js → ../package.json (same layout the MCP server + CLI use).
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8'));
20
+ if (typeof pkg.version === 'string')
21
+ v = pkg.version;
22
+ }
23
+ catch {
24
+ // package.json missing/malformed — keep 'unknown', never throw.
25
+ }
26
+ cached = v;
27
+ return v;
28
+ }
29
+ /** Test-only: clear the memoized version. */
30
+ export function _resetVersionCacheForTest() {
31
+ cached = null;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.117",
3
+ "version": "4.8.118",
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": {