@askalf/dario 6.20.3 → 6.22.0

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/dist/admin-api.js CHANGED
@@ -49,6 +49,104 @@ const pendingLogins = new Map();
49
49
  // The ChatGPT seats' pending logins live apart: an alias may name a Claude
50
50
  // seat and a ChatGPT seat at once (the stores are separate directories).
51
51
  const pendingCodexLogins = new Map();
52
+ const PENDING_FULL = 'too many pending logins; complete or wait for one to expire';
53
+ const NO_PENDING = 'no pending login for that alias (unknown or expired): start a new login';
54
+ const NO_PENDING_CODEX = 'no pending codex login for that alias (unknown or expired): start a new login';
55
+ const DEFAULT_ALIAS_TRIES = 8;
56
+ /**
57
+ * Publish a new pending login on the shared service when one is configured.
58
+ * 'failed' (the service did not answer or does not know the endpoint) leaves
59
+ * the login in this process only, which is what a single instance has always
60
+ * done. The service holds the MAX_PENDING cap for the whole fleet; 'full'
61
+ * means it is reached. `create` (a generated alias) refuses to replace a
62
+ * live login another instance published under it: 'taken'.
63
+ */
64
+ async function publishPending(engine, alias, login, deps, create) {
65
+ if (!deps.sharedLogins)
66
+ return 'local';
67
+ return deps.sharedLogins.put(engine, alias, { codeVerifier: login.codeVerifier, state: login.state, expiresAt: login.expiresAt }, PENDING_TTL_MS, create);
68
+ }
69
+ /** Aliases with a login pending on any instance, so a generated default does not take another instance's. */
70
+ async function sharedAliases(engine, deps) {
71
+ if (!deps.sharedLogins)
72
+ return [];
73
+ const out = await deps.sharedLogins.aliases(engine);
74
+ return out === 'failed' ? [] : out;
75
+ }
76
+ /**
77
+ * The pending login for `alias`. With the shared service on, the service is
78
+ * the source of truth when it answers: a login another instance started is
79
+ * found there, and one another instance already completed is gone from it
80
+ * even though this process still holds a copy, which is dropped. This
81
+ * process's own copy answers when the service does not, and when the login
82
+ * was never published.
83
+ */
84
+ async function findPending(engine, map, alias, deps) {
85
+ const own = map.get(alias);
86
+ if (!deps.sharedLogins)
87
+ return { p: own, viaService: false };
88
+ const got = await deps.sharedLogins.get(engine, alias);
89
+ if (got === 'failed')
90
+ return { p: own, viaService: false };
91
+ if (got)
92
+ return { p: got, viaService: true };
93
+ if (own?.shared) {
94
+ if (map.get(alias) === own)
95
+ map.delete(alias);
96
+ return { p: undefined, viaService: false };
97
+ }
98
+ return { p: own, viaService: false };
99
+ }
100
+ /**
101
+ * Take the login found for `alias`: single-use, so false means a concurrent
102
+ * completion or a restart got there first and the caller must not exchange.
103
+ * The lookup awaits, so taking this process's copy is a synchronous check
104
+ * that the map still holds the very login the lookup returned (deleting by
105
+ * alias alone would delete a login started since). A published login is then
106
+ * deleted on the service by its state, which answers true to one of two
107
+ * instances. A definite false from the service is always a lost claim. When
108
+ * the service does not answer, the claim stands only for the caller that took
109
+ * an unconsumed copy held here; a login found on the service alone cannot be
110
+ * claimed during an outage.
111
+ */
112
+ async function claimPending(engine, map, alias, found, deps) {
113
+ const p = found.p;
114
+ const own = map.get(alias);
115
+ // This process still holds this very login; taking it here is synchronous, so only one caller gets it.
116
+ const mine = found.viaService ? own?.state === p.state : own === p;
117
+ if (!found.viaService && !mine)
118
+ return false;
119
+ if (mine)
120
+ map.delete(alias);
121
+ if (!deps.sharedLogins || !(found.viaService || own?.shared))
122
+ return true;
123
+ const removed = await deps.sharedLogins.remove(engine, alias, p.state);
124
+ if (removed === 'failed')
125
+ return mine;
126
+ return removed;
127
+ }
128
+ /** Room for `alias` in the map. A shared copy is only this instance's fallback, since the service holds the login and its cap, so the oldest one gives way; logins kept here alone do not. */
129
+ function makeRoom(map, alias) {
130
+ if (map.has(alias) || map.size < MAX_PENDING)
131
+ return true;
132
+ for (const [k, v] of map) {
133
+ if (v.shared) {
134
+ map.delete(k);
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
140
+ /** Forget a login that was found but lapsed, here and on the service, by identity. */
141
+ async function dropPending(engine, map, alias, found, deps) {
142
+ const p = found.p;
143
+ if (!p)
144
+ return;
145
+ if (map.get(alias) === p || (found.viaService && map.get(alias)?.state === p.state))
146
+ map.delete(alias);
147
+ if (deps.sharedLogins && (found.viaService || p.shared))
148
+ await deps.sharedLogins.remove(engine, alias, p.state);
149
+ }
52
150
  function prunePending(now) {
53
151
  for (const map of [pendingLogins, pendingCodexLogins]) {
54
152
  for (const [id, p] of map) {
@@ -65,6 +163,25 @@ function nextDefaultAlias(taken, prefix = 'account') {
65
163
  return candidate; // taken is finite → always terminates
66
164
  }
67
165
  }
166
+ function takenResult(alias) {
167
+ return { ok: false, status: 409, error: `alias "${alias}" has a login pending on another instance; try again or pass an "alias"`, taken: true };
168
+ }
169
+ /**
170
+ * Start a login under a generated alias. The shared service creates it only
171
+ * if no live login holds it, so two instances picking the same default at
172
+ * once cannot replace each other's; the loser takes the next candidate.
173
+ */
174
+ async function startDefaultLogin(start, taken, prefix) {
175
+ let last = takenResult(nextDefaultAlias(taken, prefix));
176
+ for (let i = 0; i < DEFAULT_ALIAS_TRIES; i++) {
177
+ const alias = nextDefaultAlias(taken, prefix);
178
+ last = await start(alias);
179
+ if (last.ok || !last.taken)
180
+ return last;
181
+ taken.add(alias);
182
+ }
183
+ return last;
184
+ }
68
185
  /**
69
186
  * Core of `/admin/login/start`, factored out so `/admin/login/start-needed`
70
187
  * (#913) can drive the same PKCE-start + pending-login-registration + audit
@@ -73,15 +190,19 @@ function nextDefaultAlias(taken, prefix = 'account') {
73
190
  * caller looping over many aliases can skip one bad alias without aborting
74
191
  * the rest.
75
192
  */
76
- async function doStartLogin(alias, now, deps, remote) {
193
+ async function doStartLogin(alias, now, deps, remote, create = false) {
77
194
  // Only a brand-new alias grows the map; a repeat start replaces in place.
78
- if (!pendingLogins.has(alias) && pendingLogins.size >= MAX_PENDING) {
79
- return { ok: false, status: 429, error: 'too many pending logins; complete or wait for one to expire' };
80
- }
195
+ if (!makeRoom(pendingLogins, alias))
196
+ return { ok: false, status: 429, error: PENDING_FULL };
81
197
  try {
82
198
  const { authorizeUrl, codeVerifier, state } = await startAddAccount(alias);
83
199
  const expiresAt = now + PENDING_TTL_MS;
84
- pendingLogins.set(alias, { codeVerifier, state, expiresAt });
200
+ const published = await publishPending('claude', alias, { codeVerifier, state, expiresAt }, deps, create);
201
+ if (published === 'full')
202
+ return { ok: false, status: 429, error: PENDING_FULL };
203
+ if (published === 'taken')
204
+ return takenResult(alias);
205
+ pendingLogins.set(alias, { codeVerifier, state, expiresAt, shared: published === 'stored' });
85
206
  deps.audit?.({ action: 'login_start', ok: true, status: 200, alias, remote });
86
207
  return { ok: true, alias, authorizeUrl, expiresAt };
87
208
  }
@@ -100,10 +221,11 @@ async function doStartLogin(alias, now, deps, remote) {
100
221
  async function doCompleteLogin(alias, rawCode, now, deps, remote) {
101
222
  if (!alias || !rawCode)
102
223
  return { ok: false, status: 400, error: 'missing "alias" or "code"' };
103
- const p = pendingLogins.get(alias);
224
+ const found = await findPending('claude', pendingLogins, alias, deps);
225
+ const p = found.p;
104
226
  if (!p || p.expiresAt <= now) {
105
- pendingLogins.delete(alias);
106
- return { ok: false, status: 410, error: 'no pending login for that alias (unknown or expired) — start a new login' };
227
+ await dropPending('claude', pendingLogins, alias, found, deps);
228
+ return { ok: false, status: 410, error: NO_PENDING };
107
229
  }
108
230
  // Accept "code#state" or a bare code; verify the embedded state if present.
109
231
  const { code, state: pastedState } = parseManualPaste(rawCode);
@@ -112,7 +234,11 @@ async function doCompleteLogin(alias, rawCode, now, deps, remote) {
112
234
  if (pastedState && pastedState !== p.state) {
113
235
  return { ok: false, status: 400, error: 'state mismatch — code is from a different login attempt' };
114
236
  }
115
- pendingLogins.delete(alias); // single-use, regardless of exchange outcome
237
+ // Single-use, regardless of exchange outcome.
238
+ if (!(await claimPending('claude', pendingLogins, alias, found, deps))) {
239
+ // Completed or replaced since the lookup. A replacement is left for its own paste.
240
+ return { ok: false, status: 410, error: NO_PENDING };
241
+ }
116
242
  try {
117
243
  const creds = await completeAddAccount(alias, code, p.codeVerifier, p.state);
118
244
  await deps.onAccountsChanged?.();
@@ -125,17 +251,21 @@ async function doCompleteLogin(alias, rawCode, now, deps, remote) {
125
251
  return { ok: false, status: 400, error: message };
126
252
  }
127
253
  }
128
- async function doStartCodexLogin(alias, now, deps, remote) {
129
- if (!pendingCodexLogins.has(alias) && pendingCodexLogins.size >= MAX_PENDING) {
130
- return { ok: false, status: 429, error: 'too many pending logins; complete or wait for one to expire' };
131
- }
254
+ async function doStartCodexLogin(alias, now, deps, remote, create = false) {
255
+ if (!makeRoom(pendingCodexLogins, alias))
256
+ return { ok: false, status: 429, error: PENDING_FULL };
132
257
  if ((await listCodexAccountAliases()).includes(alias)) {
133
258
  return { ok: false, status: 409, error: `codex account "${alias}" already exists — DELETE /admin/codex/accounts/${alias} first` };
134
259
  }
135
260
  try {
136
261
  const { authorizeUrl, codeVerifier, state } = await startAddCodexAccount(alias);
137
262
  const expiresAt = now + PENDING_TTL_MS;
138
- pendingCodexLogins.set(alias, { codeVerifier, state, expiresAt });
263
+ const published = await publishPending('codex', alias, { codeVerifier, state, expiresAt }, deps, create);
264
+ if (published === 'full')
265
+ return { ok: false, status: 429, error: PENDING_FULL };
266
+ if (published === 'taken')
267
+ return takenResult(alias);
268
+ pendingCodexLogins.set(alias, { codeVerifier, state, expiresAt, shared: published === 'stored' });
139
269
  deps.audit?.({ action: 'login_start', ok: true, status: 200, alias, remote, engine: 'codex' });
140
270
  return { ok: true, alias, authorizeUrl, expiresAt };
141
271
  }
@@ -146,10 +276,11 @@ async function doStartCodexLogin(alias, now, deps, remote) {
146
276
  async function doCompleteCodexLogin(alias, rawCode, now, deps, remote) {
147
277
  if (!alias || !rawCode)
148
278
  return { ok: false, status: 400, error: 'missing "alias" or "code"' };
149
- const p = pendingCodexLogins.get(alias);
279
+ const found = await findPending('codex', pendingCodexLogins, alias, deps);
280
+ const p = found.p;
150
281
  if (!p || p.expiresAt <= now) {
151
- pendingCodexLogins.delete(alias);
152
- return { ok: false, status: 410, error: 'no pending codex login for that alias (unknown or expired) — start a new login' };
282
+ await dropPending('codex', pendingCodexLogins, alias, found, deps);
283
+ return { ok: false, status: 410, error: NO_PENDING_CODEX };
153
284
  }
154
285
  // The whole redirect URL (what the CLI asks the user to paste) or a bare code; the
155
286
  // state in a URL is checked against the login it was printed for, as the CLI does.
@@ -159,7 +290,17 @@ async function doCompleteCodexLogin(alias, rawCode, now, deps, remote) {
159
290
  if (pastedState !== null && pastedState !== p.state) {
160
291
  return { ok: false, status: 400, error: 'state mismatch — the redirect is from a different login attempt' };
161
292
  }
162
- pendingCodexLogins.delete(alias); // single-use, regardless of exchange outcome
293
+ // The login may have been started on an instance that lacks this alias, and without the shared
294
+ // registry a seat is on one instance only. Checked here, before the claim, so nothing is consumed
295
+ // and the login still completes on an instance without the seat.
296
+ if ((await listCodexAccountAliases()).includes(alias)) {
297
+ return { ok: false, status: 409, error: `codex account "${alias}" already exists on this instance: DELETE /admin/codex/accounts/${alias} first` };
298
+ }
299
+ // Single-use, regardless of exchange outcome.
300
+ if (!(await claimPending('codex', pendingCodexLogins, alias, found, deps))) {
301
+ // Completed or replaced since the lookup. A replacement is left for its own paste.
302
+ return { ok: false, status: 410, error: NO_PENDING_CODEX };
303
+ }
163
304
  try {
164
305
  const creds = await completeAddCodexAccount(alias, code, p.codeVerifier);
165
306
  await deps.onCodexAccountsChanged?.();
@@ -335,15 +476,22 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
335
476
  }
336
477
  const body = await readJsonBody(req);
337
478
  let alias = typeof body.alias === 'string' ? body.alias.trim() : '';
479
+ let result;
338
480
  // Alias is optional: a headless caller can omit it and get a generated,
339
481
  // non-colliding default (account-1, account-2, …) back in the response to
340
482
  // thread into /complete. Explicit aliases still win for named setups.
341
483
  if (!alias) {
342
484
  const records = await (deps.listAccounts ?? defaultListAccounts)();
343
- const taken = new Set([...records.map((r) => r.alias), ...pendingLogins.keys()]);
344
- alias = nextDefaultAlias(taken);
485
+ const taken = new Set([...records.map((r) => r.alias), ...pendingLogins.keys(), ...(await sharedAliases('claude', deps))]);
486
+ const made = await startDefaultLogin((a) => doStartLogin(a, now, deps, remote, true), taken);
487
+ if (!made.ok) {
488
+ send(res, made.status, { error: made.error });
489
+ return true;
490
+ }
491
+ alias = made.alias;
492
+ result = made;
345
493
  }
346
- const result = await doStartLogin(alias, now, deps, remote);
494
+ result ??= await doStartLogin(alias, now, deps, remote);
347
495
  if (!result.ok) {
348
496
  send(res, result.status, { error: result.error });
349
497
  return true;
@@ -364,11 +512,18 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
364
512
  }
365
513
  const body = await readJsonBody(req);
366
514
  let alias = typeof body.alias === 'string' ? body.alias.trim() : '';
515
+ let result;
367
516
  if (!alias) {
368
- const taken = new Set([...(await listCodexAccountAliases()), ...pendingCodexLogins.keys()]);
369
- alias = nextDefaultAlias(taken, 'altman');
517
+ const taken = new Set([...(await listCodexAccountAliases()), ...pendingCodexLogins.keys(), ...(await sharedAliases('codex', deps))]);
518
+ const made = await startDefaultLogin((a) => doStartCodexLogin(a, now, deps, remote, true), taken, 'altman');
519
+ if (!made.ok) {
520
+ send(res, made.status, { error: made.error });
521
+ return true;
522
+ }
523
+ alias = made.alias;
524
+ result = made;
370
525
  }
371
- const result = await doStartCodexLogin(alias, now, deps, remote);
526
+ result ??= await doStartCodexLogin(alias, now, deps, remote);
372
527
  if (!result.ok) {
373
528
  send(res, result.status, { error: result.error });
374
529
  return true;
package/dist/cli.d.ts CHANGED
@@ -192,7 +192,7 @@ export declare const SHORT_HELP = "\n dario: use your Claude or ChatGPT subscri
192
192
  /** The help `argv` asks for: the full reference with `--all`, else the short one. Pure, exported for tests. */
193
193
  export declare function helpText(argv: readonly string[]): string;
194
194
  /** Every command and flag, `dario --help --all`. */
195
- export declare const FULL_HELP = "\n dario: use your Claude or ChatGPT subscription as an API.\n\n Usage:\n dario login [--manual] [--no-proxy] [--force-reauth]\n Detect credentials + start proxy (or run OAuth).\n --manual (alias: --headless) for container / SSH\n setups \u2014 prints an authorize URL and reads the\n code you paste back instead of a local redirect.\n --no-proxy stops after auth \u2014 do not start the\n proxy (implied by --manual). Use this when the\n proxy is already running in a separate process /\n container so login doesn't collide on the port.\n --force-reauth (alias: --force) ignores any\n existing credentials and runs a fresh OAuth\n flow \u2014 for when the refresh token is dead and\n /health still reports access-token countdown.\n dario proxy [options] Start the API proxy server. Flags only: a bare\n word after \"proxy\" is an error, and \"dario proxy\n status\" prints the report instead of starting.\n dario status Check authentication status\n dario refresh Force token refresh\n dario resume Clear the overage-guard halt on a running proxy.\n v4.1.0+. Idempotent: returns \"no-op\" if the\n proxy isn't halted. Errors with a friendly hint\n if no proxy is running on localhost:3456.\n POSTs /admin/resume on the local proxy. (dario#288)\n dario logout Remove saved credentials\n dario accounts list List accounts in the multi-account pool\n (--live: the running proxy's view \u2014 status,\n window, 429s, organization, shared windows)\n dario accounts add NAME [--manual] [--from-keychain[=<target>]]\n Add a new account to the pool (runs OAuth flow).\n --manual (alias: --headless) prints an authorize\n URL and reads the code you paste back \u2014 for\n container / SSH / no-browser-on-this-machine\n setups, or as the on-Windows escape hatch when\n the URL dispatch chain truncates query params.\n --from-keychain skips OAuth and imports an\n existing Claude Code keychain entry on this\n host. With no value: uses the only/first match\n (errors with the candidate list if multiple\n entries exist). With =<target>: picks a specific\n entry by its platform identifier (Linux account\n attribute, Windows TargetName).\n dario accounts remove N Remove an account from the pool\n dario keys create NAME [--seat=ALIAS] [--models=a,b,prefix*] [--expires=30d]\n Mint a named key for one developer on a shared\n dario (v6.8). Prints the secret once; stores a\n hash in ~/.dario/keys.json. Requests it\n authenticates are attributed to NAME in\n /analytics, the ledger and the log, next to\n the root DARIO_API_KEY which keeps working.\n --seat prefers a pool seat when it has\n headroom (normal routing otherwise); --models\n refuses any other model with 403 before\n anything goes upstream; --expires refuses the\n key after 30d / 12h / 2w / an ISO date. The\n running proxy sees it on the next request.\n dario keys list Named keys: status, seat, last used, expiry,\n models. --json for the raw list.\n dario keys revoke NAME Refuse a key from now on (kept in the list).\n dario keys rotate NAME New secret, same name and settings; the old\n secret stops at once.\n dario keys remove NAME Forget a key entirely.\n dario codex list List ChatGPT-subscription accounts, served on\n /v1/chat/completions.\n dario codex add NAME Add a ChatGPT-subscription account (prints an\n authorize URL; paste the redirect URL back).\n dario codex remove NAME Remove a ChatGPT-subscription account.\n dario add altman [NAME] Attach a ChatGPT subscription \u2014 the same thing\n as 'dario codex add', named by whose plan it is.\n 'dario add amodei' attaches a Claude account.\n dario backend list List configured OpenAI-compat backends\n dario backend add NAME --key=sk-... [--base-url=...]\n Add an OpenAI-compat backend (OpenAI, OpenRouter, Groq, etc.)\n dario backend remove N Remove an OpenAI-compat backend\n dario subagent install Register ~/.claude/agents/dario.md so Claude Code\n can delegate dario diagnostics / template-refresh\n operations to a named sub-agent (v3.26)\n dario subagent remove Remove the registered sub-agent file\n dario subagent status Show whether the sub-agent is installed\n dario mcp Run dario as an MCP (Model Context Protocol)\n server on stdio. Exposes read-only tools\n (doctor, status, accounts_list, backends_list,\n subagent_status, fingerprint_info) so MCP\n clients (Claude Desktop, IDEs, etc.) can\n inspect dario's state. No destructive ops\n are exposed \u2014 mutations still require the\n CLI. (v3.27)\n dario doctor Print a health report: dario / Node / CC /\n template / drift / OAuth / pool / backends\n dario doctor --probe Also hit Anthropic's authorize endpoint with\n dario's effective OAuth config and surface\n the server's verdict \u2014 the single reliable\n signal for scope-policy drift (dario#42/#71\n class). One GET to claude.ai; no PII.\n dario doctor --usage Fire one minimal Haiku request through your\n OAuth and surface the rate-limit snapshot:\n All-models 5h/7d, per-model 7d buckets\n (Sonnet only, Opus only when Anthropic ships\n them), overage. Mirrors the user-dashboard\n usage page. Costs ~1 subscription request.\n dario doctor --obedience\n Probe each model family THROUGH the running\n proxy with a client system prompt (\"reply\n with ONLY the word PONG\") and assert the\n reply obeys. Catches upstream behavioral\n drift in client-system steering \u2014 the\n 2026-06-12 sonnet class (dario#509) that\n 200s, billing, and template checks all\n miss. Needs `dario proxy` running; costs\n at most a few tiny subscription requests.\n dario doctor --json Emit the check report as structured JSON\n for machine consumption (status bots,\n CI scripts, etc.) instead of the\n human-readable table.\n dario doctor --auth-check\n One-shot listener on an ephemeral loopback\n port. Send ONE request from your client\n (OpenClaw, Hermes, curl, etc.) and dario\n classifies the auth headers it sees against\n DARIO_API_KEY \u2014 with redacted previews and\n a targeted diagnosis (dario#97 class). Use\n --timeout-ms=N to adjust the 30s default.\n dario doctor --bun-bootstrap\n One-shot Bun installer. Closes the gap\n between \"doctor warned about Node-only TLS\n fingerprint\" and \"Bun on PATH\" without\n copy-pasting a curl-to-shell line. Skips\n when Bun is already installed. Pure\n delegation to the official installer at\n bun.com \u2014 dario does not vendor or pin a\n Bun version.\n dario config Print the effective configuration (port,\n host, DARIO_API_KEY state, OAuth status,\n pool, backends, paths) with credentials\n redacted. Safe to paste into bug reports.\n --json for structured output.\n dario usage Burn-rate summary of the running proxy's\n traffic (last 60 min): requests, token\n totals, subscription % vs. extra-usage,\n per-account rotation if pool mode is on.\n Hits /analytics on the local proxy. Works\n only when proxy is running; for a one-off\n rate-limit snapshot from Anthropic, see\n `dario doctor --usage`. --port=N to target\n a non-default port; --json for the raw\n /analytics payload. Above the window: the\n lifetime API-equivalent spend from the\n ledger (read from disk when the proxy is\n down). --card[=file.svg] writes a share\n card of that number (default\n dario-api-equivalent.svg). (v6.6)\n --donut[=file.svg] writes it as three\n rings: by model, by key, subscription\n vs metered (default dario-spend-donuts.svg).\n --by-key splits the lifetime number per\n consumer: named key, x-dario-consumer\n header, or hashed user id. (v6.8)\n dario compare Read the shadow-compare log written by\n requests carrying `x-dario-compare`:\n per-model calls, success rate, median\n latency, how often the answer parsed as\n JSON, average length \u2014 plus why any\n comparison was skipped. --dir=PATH to read\n elsewhere, --json for the summary. (v6.7)\n dario upgrade npm install -g @askalf/dario@latest with a\n pre-flight current-vs-latest check.\n\n Proxy options:\n --model=MODEL Force a model for all requests\n Shortcuts: fable, fable1m, opus, sonnet, haiku\n Full IDs: claude-fable-5, claude-opus-5-5,\n claude-sonnet-5 (append [1m] for 1M context)\n Provider prefix: openai:gpt-4o, groq:llama-3.3-70b,\n claude:opus, local:qwen-coder (forces backend)\n Default: passthrough (client decides)\n --fast-model=MODEL Route Haiku-tier sub-agent requests to MODEL\n instead of --model, so Claude Code's cheap\n sub-agents aren't upgraded to the forced model.\n Same MODEL forms as --model. No effect unless set.\n --analytics-token=TOKEN Read-only credential for /analytics*, /metrics and\n the /analytics/ui page (env DARIO_ANALYTICS_TOKEN).\n Refused on /v1/*, /accounts, /status, /admin/*:\n a scraper or a browser gets the numbers, never\n a request slot. Gates nothing without DARIO_API_KEY.\n --no-claude-auth Don't load or refresh the Claude OAuth token \u2014\n for OpenAI-only proxies (e.g. --model=openai:...).\n Prevents dario rotating a shared refresh token out\n from under an interactive Claude Code on the same\n machine. Claude-bound requests then return a clean\n unauthenticated error.\n --passthrough, --thin Thin proxy \u2014 OAuth swap only, no injection\n --preserve-tools Forward client tool schemas unchanged\n Loses subscription routing; use for custom agents\n --hybrid-tools Remap to CC tools, inject sessionId/requestId/etc.\n Keeps subscription routing for custom agents\n --merge-tools Send CC's canonical tools first, append the\n client's custom tools after (deduped by name).\n Model can call either; tool calls flow back\n unchanged. EXPERIMENTAL \u2014 Anthropic's billing\n classifier may flip routing on the appended\n tail. Validate with --verbose on the first\n 1-2 requests. Mutually exclusive with\n --preserve-tools and --hybrid-tools.\n --no-auto-detect Disable Cline/Kilo/Roo auto-preserve-tools\n (v3.19.3 behavior). Keeps CC fingerprint\n intact even when a text-tool client is\n detected; use --preserve-tools per session\n when edits are needed. (dario#40)\n --strict-tls Refuse to start proxy mode unless this process\n runs under Bun at a version whose JA3 is verified\n to match Claude Code (\u2265 v1.3.14). Bun is what\n Claude Code uses; matching its TLS stack keeps the\n proxy's JA3/JA4 ClientHello indistinguishable\n from a stock CC request \u2014 but an older Bun ships an\n older BoringSSL whose ClientHello diverges (#813).\n Install/upgrade Bun (https://bun.sh) so dario\n auto-relaunches under it. (v3.23)\n --stealth Single-flag behavioral-stealth preset.\n Flips pace-jitter, think-time, and\n session-start defaults from 0 to non-zero\n values sized for real-CC inter-arrival\n statistics (pace-jitter=300, think\n base/perToken/jitter=800/4/1500 capped at\n 25s, session-start min/jitter=1200/3000).\n Per-knob --pace-jitter / --think-time-* /\n --session-start-* flags and env vars\n still win \u2014 flip stealth on, tune any\n axis afterwards. Env: DARIO_STEALTH.\n --pace-min=MS Minimum ms between upstream requests\n (default: 500). Prevents request floods\n that are distinguishable from human-paced\n CC traffic.\n --pace-jitter=MS Max additional uniform-random jitter (ms)\n added on top of --pace-min per request.\n Default: 0 (off). Set to e.g. 300 to hide\n the floor from long-run inter-arrival\n statistics. (v3.24)\n --think-time-base=MS Post-response \"think time\" base \u2014 constant\n ms added before the next request fires.\n Models the wall-clock pause between an\n interactive CC user reading a response and\n typing the next message. Default: 0 (off).\n Env: DARIO_THINK_TIME_BASE_MS.\n --think-time-per-token=MS\n Additional ms per output token of the\n previous response (linear). e.g. 5 \u2192 a\n 1000-token response adds 5s of read time\n before the next request. Default: 0.\n Env: DARIO_THINK_TIME_PER_TOKEN_MS.\n --think-time-jitter=MS Max uniform-random jitter on top of\n base+perToken*tokens. Hides the formula\n from long-run inter-arrival statistics.\n Default: 0.\n Env: DARIO_THINK_TIME_JITTER_MS.\n --think-time-max=MS Upper bound on think time so a 50k-token\n response doesn't pause for minutes.\n Default: 30000 (30s).\n Env: DARIO_THINK_TIME_MAX_MS.\n --session-start-min=MS Floor on session-start delay \u2014 applied to\n the first request only (lastResponseTime\n === 0). Real CC sessions open with seconds\n of startup latency, not microseconds.\n Default: 0 (off).\n Env: DARIO_SESSION_START_MIN_MS.\n --session-start-jitter=MS\n Max uniform-random jitter on session-start\n delay. Default: 0.\n Env: DARIO_SESSION_START_JITTER_MS.\n --drain-on-close When the client disconnects mid-stream,\n keep consuming the upstream SSE to EOF\n so Anthropic sees the same read-to-\n completion pattern native Claude Code\n produces. Trades tokens (the response\n is fully generated even if nobody reads\n it) for fingerprint fidelity. Bounded by\n the 5-minute upstream timeout. (v3.25)\n --no-midstream-continue Do not finish a streamed answer that dies\n mid-way from the other subscription. By\n default a stream that breaks with content\n already on the wire (socket reset,\n overloaded_error, codex response.failed)\n is resumed through dario's own front door\n at the other provider's entry in\n --pool-fallback, spliced onto the same\n client stream, and closed cleanly; the\n client sees one message. Off, or with no\n fallback entry for the other provider,\n the stream ends truncated as before.\n Env: DARIO_MIDSTREAM_CONTINUE=0. (v6.1)\n --no-ledger Do not keep the lifetime ledger\n (~/.dario/ledger.json): per-day, per-model\n token totals that let /analytics and\n `dario usage` say what the traffic would\n have cost on the metered API since the\n first request, across restarts. Env:\n DARIO_LEDGER=0; DARIO_LEDGER_PATH=<file>\n moves it. (v6.6)\n --no-keys Ignore named keys (~/.dario/keys.json): only\n DARIO_API_KEY authenticates. Env: DARIO_KEYS=0;\n --keys-path=<file> / DARIO_KEYS_PATH moves the\n file. See `dario keys`. (v6.8)\n --key-socket=<path>=<key>\n Also listen on a unix socket bound to one\n named key: every request on it is that\n key's, with no secret sent or stored by the\n caller. Who may connect is the socket's\n directory and group (it is created 0660).\n Repeatable. Env: DARIO_KEY_SOCKETS=<spec>,..\n --session-idle-rotate=MS Idle ms before an account's session id\n rotates (default: 900000 = 15 min).\n Real CC rotates once per conversation, not\n per call; the default matches its observed\n cadence. Applies per account \u2014 a pool of one\n rotates as the pre-v5 single account did. (v3.28)\n --session-rotate-jitter=MS\n Max additional uniform-random jitter (ms)\n added to the idle threshold, sampled once\n per session at creation. Default: 0 (off).\n Hides the exact threshold from long-run\n rotation statistics. (v3.28)\n --session-max-age=MS Hard cap on a session id's lifetime\n regardless of activity. Default: off. Set\n for always-on pipelines where an idle\n window would never trigger. (v3.28)\n --session-per-client Give each upstream client (keyed by\n x-session-id / x-client-session-id\n header) its own rotated session id.\n Default: off (single session across all\n clients, v3.27 behaviour). (v3.28)\n --preserve-orchestration-tags[=TAG,TAG]\n Opt specific orchestration wrapper tags\n (<system-reminder>, <env>, <thinking>,\n etc.) out of the scrub. Bare flag =\n preserve all. Value form = preserve only\n those listed; everything else is still\n stripped. Default: strip every tag in\n ORCHESTRATION_TAG_NAMES. Env mirror:\n DARIO_PRESERVE_ORCHESTRATION_TAGS=*\n or =tag1,tag2. (v3.30.7, dario#78)\n --no-live-capture Skip the background live-fingerprint\n refresh entirely. dario uses the bundled\n snapshot and will NOT spawn the installed\n Claude Code binary. For air-gapped /\n reproducible-build / CI-harness runs.\n Env: DARIO_NO_LIVE_CAPTURE=1.\n (v3.30.8, dario#77)\n --strict-template Refuse to start if the loaded template\n is the bundled snapshot (no live capture\n ever succeeded) or drifts from the\n installed CC version. Same philosophy\n as --strict-tls: make the unsafe state\n require intent. Env: DARIO_STRICT_TEMPLATE=1.\n (v3.30.8, dario#77)\n --shutdown-grace=MS How long SIGTERM waits for in-flight requests\n to finish before exiting (default: 90000).\n The listener closes at once. Set the\n container's stop grace above it, or Docker\n SIGKILLs the drain. Env:\n DARIO_SHUTDOWN_GRACE_MS. (dario#1370)\n --upstream-timeout=MS How long dario waits on upstream (default:\n 300000). Until the response streams it caps\n the whole wait; while a 2xx streams it is\n the longest gap between two chunks, so a\n live stream is never cut. 0 keeps the\n default. Env: DARIO_UPSTREAM_TIMEOUT_MS.\n --max-concurrent=N Max in-flight requests across the WHOLE proxy,\n not per seat (default: 10; a pool defaults to\n 10 per seat). Past it, requests wait in dario.\n Env: DARIO_MAX_CONCURRENT. (dario#80, #1244)\n --max-queued=N Max requests buffered waiting for a\n concurrency slot before dario returns\n 429 \"queue-full\" (default: 128).\n Env: DARIO_MAX_QUEUED. (dario#80)\n --max-concurrent-per-consumer=N\n Max in-flight requests per consumer, keyed\n by the x-dario-consumer request header; a\n consumer at the cap waits while others keep\n flowing (default: 0 = off).\n Env: DARIO_MAX_CONCURRENT_PER_CONSUMER.\n --pool-shared-state Share rate-limit readings and sticky bindings\n with the other dario instances through the\n refresh-lock service (needs\n DARIO_REFRESH_LOCK_URL / _TOKEN). Fails open.\n Env: DARIO_POOL_SHARED_STATE=1.\n --pool-shared-state-interval=MS\n How often to pull peers' readings\n (default: 2000). Env:\n DARIO_POOL_SHARED_STATE_INTERVAL_MS.\n --pool-shared-accounts Keep the Claude accounts in a registry on the\n refresh-lock service, merged at startup and\n every 15s, so a new replica starts with its\n peers' accounts and adds and removals reach\n every instance. The registry holds every\n seat's credentials. Fails open.\n Env: DARIO_POOL_SHARED_ACCOUNTS=1.\n --queue-timeout=MS Max ms a queued request waits before\n dario returns 504 \"queue-timeout\"\n (default: 60000).\n Env: DARIO_QUEUE_TIMEOUT_MS. (dario#80)\n --pool-strategy=<headroom|headroom-by-plan|fill-first|expiring-first>\n Where new conversations land in a multi-\n account pool. headroom (default) spreads\n them to the seat with the most slack;\n headroom-by-plan (opt-in) counts each\n seat's slack by plan size (Max Nx = N,\n Pro = 1); fill-first concentrates them on the\n alphabetically-first eligible seat until\n it drains to the floor, then spills;\n expiring-first does the same in order of\n each seat's 7-day reset, soonest first.\n Sticky bindings are unaffected.\n Env: DARIO_POOL_STRATEGY.\n --pool-headroom-floor=<ratio|percent>\n Headroom at or below which a seat counts as\n drained: a sticky session rebinds off it and\n new conversations skip it. Setting this makes\n the floor a hard stop. 0.05 or 5%; implicit\n default 2% is a preference, max 50%. Use it when your seats\n answer with API errors in the last percent\n of a window (dario#1333).\n Env: DARIO_POOL_HEADROOM_FLOOR;\n config: pool.headroomFloor.\n --pool-fallback=<model> When every pool seat is drained or cooling,\n serve the request as <model> from whichever\n provider can, instead of surfacing the\n 429/503: a Codex/ChatGPT subscription that\n lists <model> (both wire shapes), else the\n openai-compat backend (OpenAI shape only).\n Response carries x-dario-pool-fallback.\n Needs one of those configured. Empty value\n disables. Env: DARIO_POOL_FALLBACK. Config:\n poolFallback.model.\n An entry written credits:<model> is a rung on\n the API credits a Max plan includes: with\n DARIO_CREDITS_API_KEY set (env only, never a\n flag) the request goes to the Claude API on\n that key as <model>, both wire shapes, after\n every seat and subscription rung; /analytics\n and the dashboard file it under credits, the\n ledger counts it as metered. Without the key\n the entry is inert and dario doctor says so.\n --family-fallback=<family=model,...>\n When every pool seat is parked for a model\n family, serve its requests as <model> on the\n Claude pool instead, while some seat can\n serve that model's family. For Fable, which\n runs out of included credit while Opus still\n has room: fable=claude-opus-5-5. Response\n carries x-dario-family-fallback. Empty value\n disables. Env: DARIO_FAMILY_FALLBACK.\n Config: familyFallback.\n --model-alias=<name=target>\n User-defined model alias, repeatable.\n Applied to the client's model name before\n provider-prefix parsing, so the target may\n carry a prefix to retarget the backend\n (--model-alias=my-fast=openai:gpt-4o-mini).\n Advertised on /v1/models. Names match\n case-insensitively; one step, never\n recursive. Env: DARIO_MODEL_ALIASES=\n name=target,name2=target2. Config:\n modelAliases.\n --effort=<low|medium|high|xhigh|ultracode|max|client>\n Pin the outbound output_config.effort on\n non-haiku requests, overriding the\n client's choice. Default (unset) forwards\n the client's own effort \u2014 it's a user\n knob; real CC wires whatever the user\n tuned \u2014 falling back to 'high' when the\n client sent none. 'client' is a compat\n alias for the default. 'ultracode' rides\n the wire as 'xhigh'.\n WARNING: pinned values override every\n client and may cause Anthropic's\n classifier to flip requests to 'overage'\n billing; watch -v logs for\n representative-claim changes.\n Env: DARIO_EFFORT. (dario#87)\n --max-tokens=<N|client> Override outbound max_tokens. Default\n (unset) pins 32000 (CC 2.1.116 wire default).\n Set a number to pin that value; set 'client'\n to pass through the client's requested\n max_tokens (Hermes requests 64k\u2013128k; the\n default pin silently truncates its output\n capacity). Anthropic enforces the per-model\n ceiling server-side, so too-high values\n return a clean 400.\n Env: DARIO_MAX_TOKENS. (dario#88)\n --no-overage-guard Disable the overage-guard (v4.1.0+). Default\n behavior halts the proxy on any response with\n representative-claim=overage and returns 503\n with an Anthropic-shaped error body until\n cooldown expires or `dario resume` clears\n the state. Subscribers should never see an\n overage hit during normal operation; one\n means something is wrong (wire drift,\n classifier change, account misconfig).\n Env: DARIO_OVERAGE_GUARD=off. (dario#288)\n --overage-behavior=<halt|warn>\n Behavior when overage is detected (v4.1.0+):\n halt \u2014 return 503 to new /v1/messages\n requests until resume / cooldown.\n Default. Strongest protection.\n warn \u2014 emit events + OS notification only,\n keep forwarding. Visibility mode for\n operators who want the signal without\n cutting off traffic.\n Env: DARIO_OVERAGE_BEHAVIOR.\n --overage-cooldown=MS Ms to wait before auto-clearing the halt\n state (v4.1.0+). Default: 1800000 (30 min).\n Manual `dario resume` clears immediately\n regardless of this value.\n Env: DARIO_OVERAGE_COOLDOWN.\n --no-overage-notify Suppress the native desktop notification on\n halt (v4.1.0+). Terminal BEL is the\n unconditional floor; TUI banner + SSE event\n still fire regardless. Use in headless / CI\n contexts where toast popups don't make sense.\n Env: DARIO_OVERAGE_NOTIFY=off.\n --port=PORT Port to listen on (default: 3456)\n --host=ADDRESS Address to bind to (default: 127.0.0.1)\n Use 0.0.0.0 for LAN; see README for DARIO_API_KEY\n --verbose, -v Log all requests\n --verbose=2, -vv Also dump redacted request bodies\n (env: DARIO_LOG_BODIES=1)\n --cache-trace One line per ChatGPT (codex) request on what it\n shared with the previous request on its cache\n key: the gap, whether the input extends it, a\n seat or prompt change (env: DARIO_CACHE_TRACE=1)\n --log-file=PATH Append one JSON-ND record per completed\n request to PATH. Useful for backgrounded\n proxies where stdout is unobserved (where\n --verbose can't help). Secrets scrubbed,\n no request bodies. Env: DARIO_LOG_FILE.\n --passthrough-betas=CSV Beta flags to ALWAYS forward upstream\n regardless of CC's captured set or the\n client's anthropic-beta header. Bypasses\n the billable-beta filter. Per-account\n rejection cache still applies (so a flag\n upstream 400's gets dropped, not retried\n forever). Use when you know a beta works\n on your account but isn't in the captured\n template. Env: DARIO_PASSTHROUGH_BETAS.\n\n --skip-fields=CSV CC body fields to NOT inject into outbound\n requests. Allowed: thinking,\n context_management, output_config. Headers\n and metadata stay intact (Max billing\n unchanged). Use when an upstream model 400s\n \"Extra inputs are not permitted\" on one of\n these fields \u2014 typically a non-CC SDK\n client routed through dario to a model\n that rejects the field despite the beta\n header. Env: DARIO_SKIP_FIELDS.\n\n --honor-client-thinking Pass the client body's `thinking` field\n through to upstream instead of dario's\n default `{type:\"adaptive\"}`. Lets SDK\n clients explicitly enable extended\n thinking with their own budget (e.g.\n `{type:\"enabled\", budget_tokens:8000}`).\n When honored, the paired\n `context_management.clear_thinking_*`\n edit is suppressed (shape-specific).\n No effect on Haiku or when client omits\n `thinking`. Env:\n DARIO_HONOR_CLIENT_THINKING.\n\n --preserve-output-format\n Pass the client body's\n `output_config.format` (structured-output\n JSON schema) through to upstream instead of\n dropping it during the CC rebuild. Lets SDK\n clients (e.g. the Vercel AI SDK's\n `generateObject`) get schema-constrained\n output through dario. No effect when the\n client omits `output_config.format`. Env:\n DARIO_PRESERVE_OUTPUT_FORMAT.\n\n --upstream-proxy=URL / --via=URL\n Route all of dario's outbound fetch\n calls (api.anthropic.com, OpenAI-compat\n backends, OAuth) through an HTTP/HTTPS\n proxy. Localhost calls bypass. Useful\n with a VPN provider's HTTP proxy mode\n (Mullvad, AirVPN, corporate proxy,\n privoxy/Tor) when you don't want to put\n the whole system on a system VPN.\n Requires Bun runtime. SOCKS5 not\n supported (Bun fetch limitation). See\n docs/vpn-routing.md. Env: DARIO_UPSTREAM_PROXY.\n (v3.35.0)\n\n --system-prompt=<MODE> System-prompt mode for outbound CC-shaped\n requests (v3.34.0). One of:\n verbatim \u2014 CC unchanged (default)\n partial \u2014 strip behavioral constraints\n (Tone-and-style, Text-output,\n verbosity / comment / scope\n bullets in Doing-tasks).\n Recovers ~1.2-2.8x output on\n open-ended work.\n aggressive \u2014 partial + remove prompt-level\n RLHF restatements + Executing-\n actions-with-care section.\n Adds <3% over partial; RLHF\n refusals on harmful content\n unaffected (alignment is in\n the weights, not the prompt).\n <filepath> \u2014 replace the slot entirely\n with the file's contents.\n Empirically validated as unfingerprinted by\n the billing classifier \u2014 see docs/research/\n system-prompt.md. Env: DARIO_SYSTEM_PROMPT.\n\n Quick start:\n dario login # auto-detects Claude Code credentials\n dario proxy --model=opus # or: dario proxy --passthrough\n\n Then point any Anthropic SDK at http://localhost:3456:\n export ANTHROPIC_BASE_URL=http://localhost:3456\n export ANTHROPIC_API_KEY=dario\n\n Examples:\n curl http://localhost:3456/v1/messages \\\n -H \"Content-Type: application/json\" \\\n -H \"anthropic-version: 2023-06-01\" \\\n -d '{\"model\":\"claude-fable-5\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'\n\n Your subscription handles the billing. No API key needed.\n Tokens auto-refresh in the background \u2014 set it and forget it.\n";
195
+ export declare const FULL_HELP = "\n dario: use your Claude or ChatGPT subscription as an API.\n\n Usage:\n dario login [--manual] [--no-proxy] [--force-reauth]\n Detect credentials + start proxy (or run OAuth).\n --manual (alias: --headless) for container / SSH\n setups \u2014 prints an authorize URL and reads the\n code you paste back instead of a local redirect.\n --no-proxy stops after auth \u2014 do not start the\n proxy (implied by --manual). Use this when the\n proxy is already running in a separate process /\n container so login doesn't collide on the port.\n --force-reauth (alias: --force) ignores any\n existing credentials and runs a fresh OAuth\n flow \u2014 for when the refresh token is dead and\n /health still reports access-token countdown.\n dario proxy [options] Start the API proxy server. Flags only: a bare\n word after \"proxy\" is an error, and \"dario proxy\n status\" prints the report instead of starting.\n dario status Check authentication status\n dario refresh Force token refresh\n dario resume Clear the overage-guard halt on a running proxy.\n v4.1.0+. Idempotent: returns \"no-op\" if the\n proxy isn't halted. Errors with a friendly hint\n if no proxy is running on localhost:3456.\n POSTs /admin/resume on the local proxy. (dario#288)\n dario logout Remove saved credentials\n dario accounts list List accounts in the multi-account pool\n (--live: the running proxy's view \u2014 status,\n window, 429s, organization, shared windows)\n dario accounts add NAME [--manual] [--from-keychain[=<target>]]\n Add a new account to the pool (runs OAuth flow).\n --manual (alias: --headless) prints an authorize\n URL and reads the code you paste back \u2014 for\n container / SSH / no-browser-on-this-machine\n setups, or as the on-Windows escape hatch when\n the URL dispatch chain truncates query params.\n --from-keychain skips OAuth and imports an\n existing Claude Code keychain entry on this\n host. With no value: uses the only/first match\n (errors with the candidate list if multiple\n entries exist). With =<target>: picks a specific\n entry by its platform identifier (Linux account\n attribute, Windows TargetName).\n dario accounts remove N Remove an account from the pool\n dario keys create NAME [--seat=ALIAS] [--models=a,b,prefix*] [--expires=30d]\n Mint a named key for one developer on a shared\n dario (v6.8). Prints the secret once; stores a\n hash in ~/.dario/keys.json. Requests it\n authenticates are attributed to NAME in\n /analytics, the ledger and the log, next to\n the root DARIO_API_KEY which keeps working.\n --seat prefers a pool seat when it has\n headroom (normal routing otherwise); --models\n refuses any other model with 403 before\n anything goes upstream; --expires refuses the\n key after 30d / 12h / 2w / an ISO date. The\n running proxy sees it on the next request.\n dario keys list Named keys: status, seat, last used, expiry,\n models. --json for the raw list.\n dario keys revoke NAME Refuse a key from now on (kept in the list).\n dario keys rotate NAME New secret, same name and settings; the old\n secret stops at once.\n dario keys remove NAME Forget a key entirely.\n dario codex list List ChatGPT-subscription accounts, served on\n /v1/chat/completions.\n dario codex add NAME Add a ChatGPT-subscription account (prints an\n authorize URL; paste the redirect URL back).\n dario codex remove NAME Remove a ChatGPT-subscription account.\n dario add altman [NAME] Attach a ChatGPT subscription \u2014 the same thing\n as 'dario codex add', named by whose plan it is.\n 'dario add amodei' attaches a Claude account.\n dario backend list List configured OpenAI-compat backends\n dario backend add NAME --key=sk-... [--base-url=...]\n Add an OpenAI-compat backend (OpenAI, OpenRouter, Groq, etc.)\n dario backend remove N Remove an OpenAI-compat backend\n dario subagent install Register ~/.claude/agents/dario.md so Claude Code\n can delegate dario diagnostics / template-refresh\n operations to a named sub-agent (v3.26)\n dario subagent remove Remove the registered sub-agent file\n dario subagent status Show whether the sub-agent is installed\n dario mcp Run dario as an MCP (Model Context Protocol)\n server on stdio. Exposes read-only tools\n (doctor, status, accounts_list, backends_list,\n subagent_status, fingerprint_info) so MCP\n clients (Claude Desktop, IDEs, etc.) can\n inspect dario's state. No destructive ops\n are exposed \u2014 mutations still require the\n CLI. (v3.27)\n dario doctor Print a health report: dario / Node / CC /\n template / drift / OAuth / pool / backends\n dario doctor --probe Also hit Anthropic's authorize endpoint with\n dario's effective OAuth config and surface\n the server's verdict \u2014 the single reliable\n signal for scope-policy drift (dario#42/#71\n class). One GET to claude.ai; no PII.\n dario doctor --usage Fire one minimal Haiku request through your\n OAuth and surface the rate-limit snapshot:\n All-models 5h/7d, per-model 7d buckets\n (Sonnet only, Opus only when Anthropic ships\n them), overage. Mirrors the user-dashboard\n usage page. Costs ~1 subscription request.\n dario doctor --obedience\n Probe each model family THROUGH the running\n proxy with a client system prompt (\"reply\n with ONLY the word PONG\") and assert the\n reply obeys. Catches upstream behavioral\n drift in client-system steering \u2014 the\n 2026-06-12 sonnet class (dario#509) that\n 200s, billing, and template checks all\n miss. Needs `dario proxy` running; costs\n at most a few tiny subscription requests.\n dario doctor --json Emit the check report as structured JSON\n for machine consumption (status bots,\n CI scripts, etc.) instead of the\n human-readable table.\n dario doctor --auth-check\n One-shot listener on an ephemeral loopback\n port. Send ONE request from your client\n (OpenClaw, Hermes, curl, etc.) and dario\n classifies the auth headers it sees against\n DARIO_API_KEY \u2014 with redacted previews and\n a targeted diagnosis (dario#97 class). Use\n --timeout-ms=N to adjust the 30s default.\n dario doctor --bun-bootstrap\n One-shot Bun installer. Closes the gap\n between \"doctor warned about Node-only TLS\n fingerprint\" and \"Bun on PATH\" without\n copy-pasting a curl-to-shell line. Skips\n when Bun is already installed. Pure\n delegation to the official installer at\n bun.com \u2014 dario does not vendor or pin a\n Bun version.\n dario config Print the effective configuration (port,\n host, DARIO_API_KEY state, OAuth status,\n pool, backends, paths) with credentials\n redacted. Safe to paste into bug reports.\n --json for structured output.\n dario usage Burn-rate summary of the running proxy's\n traffic (last 60 min): requests, token\n totals, subscription % vs. extra-usage,\n per-account rotation if pool mode is on.\n Hits /analytics on the local proxy. Works\n only when proxy is running; for a one-off\n rate-limit snapshot from Anthropic, see\n `dario doctor --usage`. --port=N to target\n a non-default port; --json for the raw\n /analytics payload. Above the window: the\n lifetime API-equivalent spend from the\n ledger (read from disk when the proxy is\n down). --card[=file.svg] writes a share\n card of that number (default\n dario-api-equivalent.svg). (v6.6)\n --donut[=file.svg] writes it as three\n rings: by model, by key, subscription\n vs metered (default dario-spend-donuts.svg).\n --by-key splits the lifetime number per\n consumer: named key, x-dario-consumer\n header, or hashed user id. (v6.8)\n dario compare Read the shadow-compare log written by\n requests carrying `x-dario-compare`:\n per-model calls, success rate, median\n latency, how often the answer parsed as\n JSON, average length \u2014 plus why any\n comparison was skipped. --dir=PATH to read\n elsewhere, --json for the summary. (v6.7)\n dario upgrade npm install -g @askalf/dario@latest with a\n pre-flight current-vs-latest check.\n\n Proxy options:\n --model=MODEL Force a model for all requests\n Shortcuts: fable, fable1m, opus, sonnet, haiku\n Full IDs: claude-fable-5, claude-opus-5-5,\n claude-sonnet-5 (append [1m] for 1M context)\n Provider prefix: openai:gpt-4o, groq:llama-3.3-70b,\n claude:opus, local:qwen-coder (forces backend)\n Default: passthrough (client decides)\n --fast-model=MODEL Route Haiku-tier sub-agent requests to MODEL\n instead of --model, so Claude Code's cheap\n sub-agents aren't upgraded to the forced model.\n Same MODEL forms as --model. No effect unless set.\n --analytics-token=TOKEN Read-only credential for /analytics*, /metrics and\n the /analytics/ui page (env DARIO_ANALYTICS_TOKEN).\n Refused on /v1/*, /accounts, /status, /admin/*:\n a scraper or a browser gets the numbers, never\n a request slot. Gates nothing without DARIO_API_KEY.\n --no-claude-auth Don't load or refresh the Claude OAuth token \u2014\n for OpenAI-only proxies (e.g. --model=openai:...).\n Prevents dario rotating a shared refresh token out\n from under an interactive Claude Code on the same\n machine. Claude-bound requests then return a clean\n unauthenticated error.\n --passthrough, --thin Thin proxy \u2014 OAuth swap only, no injection\n --preserve-tools Forward client tool schemas unchanged\n Loses subscription routing; use for custom agents\n --hybrid-tools Remap to CC tools, inject sessionId/requestId/etc.\n Keeps subscription routing for custom agents\n --merge-tools Send CC's canonical tools first, append the\n client's custom tools after (deduped by name).\n Model can call either; tool calls flow back\n unchanged. EXPERIMENTAL \u2014 Anthropic's billing\n classifier may flip routing on the appended\n tail. Validate with --verbose on the first\n 1-2 requests. Mutually exclusive with\n --preserve-tools and --hybrid-tools.\n --no-auto-detect Disable Cline/Kilo/Roo auto-preserve-tools\n (v3.19.3 behavior). Keeps CC fingerprint\n intact even when a text-tool client is\n detected; use --preserve-tools per session\n when edits are needed. (dario#40)\n --strict-tls Refuse to start proxy mode unless this process\n runs under Bun at a version whose JA3 is verified\n to match Claude Code (\u2265 v1.3.14). Bun is what\n Claude Code uses; matching its TLS stack keeps the\n proxy's JA3/JA4 ClientHello indistinguishable\n from a stock CC request \u2014 but an older Bun ships an\n older BoringSSL whose ClientHello diverges (#813).\n Install/upgrade Bun (https://bun.sh) so dario\n auto-relaunches under it. (v3.23)\n --stealth Single-flag behavioral-stealth preset.\n Flips pace-jitter, think-time, and\n session-start defaults from 0 to non-zero\n values sized for real-CC inter-arrival\n statistics (pace-jitter=300, think\n base/perToken/jitter=800/4/1500 capped at\n 25s, session-start min/jitter=1200/3000).\n Per-knob --pace-jitter / --think-time-* /\n --session-start-* flags and env vars\n still win \u2014 flip stealth on, tune any\n axis afterwards. Env: DARIO_STEALTH.\n --pace-min=MS Minimum ms between upstream requests\n (default: 500). Prevents request floods\n that are distinguishable from human-paced\n CC traffic.\n --pace-jitter=MS Max additional uniform-random jitter (ms)\n added on top of --pace-min per request.\n Default: 0 (off). Set to e.g. 300 to hide\n the floor from long-run inter-arrival\n statistics. (v3.24)\n --think-time-base=MS Post-response \"think time\" base \u2014 constant\n ms added before the next request fires.\n Models the wall-clock pause between an\n interactive CC user reading a response and\n typing the next message. Default: 0 (off).\n Env: DARIO_THINK_TIME_BASE_MS.\n --think-time-per-token=MS\n Additional ms per output token of the\n previous response (linear). e.g. 5 \u2192 a\n 1000-token response adds 5s of read time\n before the next request. Default: 0.\n Env: DARIO_THINK_TIME_PER_TOKEN_MS.\n --think-time-jitter=MS Max uniform-random jitter on top of\n base+perToken*tokens. Hides the formula\n from long-run inter-arrival statistics.\n Default: 0.\n Env: DARIO_THINK_TIME_JITTER_MS.\n --think-time-max=MS Upper bound on think time so a 50k-token\n response doesn't pause for minutes.\n Default: 30000 (30s).\n Env: DARIO_THINK_TIME_MAX_MS.\n --session-start-min=MS Floor on session-start delay \u2014 applied to\n the first request only (lastResponseTime\n === 0). Real CC sessions open with seconds\n of startup latency, not microseconds.\n Default: 0 (off).\n Env: DARIO_SESSION_START_MIN_MS.\n --session-start-jitter=MS\n Max uniform-random jitter on session-start\n delay. Default: 0.\n Env: DARIO_SESSION_START_JITTER_MS.\n --drain-on-close When the client disconnects mid-stream,\n keep consuming the upstream SSE to EOF\n so Anthropic sees the same read-to-\n completion pattern native Claude Code\n produces. Trades tokens (the response\n is fully generated even if nobody reads\n it) for fingerprint fidelity. Bounded by\n the 5-minute upstream timeout. (v3.25)\n --no-midstream-continue Do not finish a streamed answer that dies\n mid-way from the other subscription. By\n default a stream that breaks with content\n already on the wire (socket reset,\n overloaded_error, codex response.failed)\n is resumed through dario's own front door\n at the other provider's entry in\n --pool-fallback, spliced onto the same\n client stream, and closed cleanly; the\n client sees one message. Off, or with no\n fallback entry for the other provider,\n the stream ends truncated as before.\n Env: DARIO_MIDSTREAM_CONTINUE=0. (v6.1)\n --no-ledger Do not keep the lifetime ledger\n (~/.dario/ledger.json): per-day, per-model\n token totals that let /analytics and\n `dario usage` say what the traffic would\n have cost on the metered API since the\n first request, across restarts. Env:\n DARIO_LEDGER=0; DARIO_LEDGER_PATH=<file>\n moves it. (v6.6)\n --no-keys Ignore named keys (~/.dario/keys.json): only\n DARIO_API_KEY authenticates. Env: DARIO_KEYS=0;\n --keys-path=<file> / DARIO_KEYS_PATH moves the\n file. See `dario keys`. (v6.8)\n --key-socket=<path>=<key>\n Also listen on a unix socket bound to one\n named key: every request on it is that\n key's, with no secret sent or stored by the\n caller. Who may connect is the socket's\n directory and group (it is created 0660).\n Repeatable. Env: DARIO_KEY_SOCKETS=<spec>,..\n --session-idle-rotate=MS Idle ms before an account's session id\n rotates (default: 900000 = 15 min).\n Real CC rotates once per conversation, not\n per call; the default matches its observed\n cadence. Applies per account \u2014 a pool of one\n rotates as the pre-v5 single account did. (v3.28)\n --session-rotate-jitter=MS\n Max additional uniform-random jitter (ms)\n added to the idle threshold, sampled once\n per session at creation. Default: 0 (off).\n Hides the exact threshold from long-run\n rotation statistics. (v3.28)\n --session-max-age=MS Hard cap on a session id's lifetime\n regardless of activity. Default: off. Set\n for always-on pipelines where an idle\n window would never trigger. (v3.28)\n --session-per-client Give each upstream client (keyed by\n x-session-id / x-client-session-id\n header) its own rotated session id.\n Default: off (single session across all\n clients, v3.27 behaviour). (v3.28)\n --preserve-orchestration-tags[=TAG,TAG]\n Opt specific orchestration wrapper tags\n (<system-reminder>, <env>, <thinking>,\n etc.) out of the scrub. Bare flag =\n preserve all. Value form = preserve only\n those listed; everything else is still\n stripped. Default: strip every tag in\n ORCHESTRATION_TAG_NAMES. Env mirror:\n DARIO_PRESERVE_ORCHESTRATION_TAGS=*\n or =tag1,tag2. (v3.30.7, dario#78)\n --no-live-capture Skip the background live-fingerprint\n refresh entirely. dario uses the bundled\n snapshot and will NOT spawn the installed\n Claude Code binary. For air-gapped /\n reproducible-build / CI-harness runs.\n Env: DARIO_NO_LIVE_CAPTURE=1.\n (v3.30.8, dario#77)\n --strict-template Refuse to start if the loaded template\n is the bundled snapshot (no live capture\n ever succeeded) or drifts from the\n installed CC version. Same philosophy\n as --strict-tls: make the unsafe state\n require intent. Env: DARIO_STRICT_TEMPLATE=1.\n (v3.30.8, dario#77)\n --shutdown-grace=MS How long SIGTERM waits for in-flight requests\n to finish before exiting (default: 90000).\n The listener closes at once. Set the\n container's stop grace above it, or Docker\n SIGKILLs the drain. Env:\n DARIO_SHUTDOWN_GRACE_MS. (dario#1370)\n --upstream-timeout=MS How long dario waits on upstream (default:\n 300000). Until the response streams it caps\n the whole wait; while a 2xx streams it is\n the longest gap between two chunks, so a\n live stream is never cut. 0 keeps the\n default. Env: DARIO_UPSTREAM_TIMEOUT_MS.\n --max-concurrent=N Max in-flight requests across the WHOLE proxy,\n not per seat (default: 10; a pool defaults to\n 10 per seat). Past it, requests wait in dario.\n Env: DARIO_MAX_CONCURRENT. (dario#80, #1244)\n --max-queued=N Max requests buffered waiting for a\n concurrency slot before dario returns\n 429 \"queue-full\" (default: 128).\n Env: DARIO_MAX_QUEUED. (dario#80)\n --max-concurrent-per-consumer=N\n Max in-flight requests per consumer, keyed\n by the x-dario-consumer request header; a\n consumer at the cap waits while others keep\n flowing (default: 0 = off).\n Env: DARIO_MAX_CONCURRENT_PER_CONSUMER.\n --pool-shared-state Share rate-limit readings and sticky bindings\n with the other dario instances through the\n refresh-lock service (needs\n DARIO_REFRESH_LOCK_URL / _TOKEN). Fails open.\n Env: DARIO_POOL_SHARED_STATE=1.\n --pool-shared-state-interval=MS\n How often to pull peers' readings\n (default: 2000). Env:\n DARIO_POOL_SHARED_STATE_INTERVAL_MS.\n --pool-shared-accounts Keep the Claude accounts and ChatGPT seats in\n registries on the refresh-lock service, merged\n at startup and\n every 15s, so a new replica starts with its\n peers' accounts and adds and removals reach\n every instance. The registry holds every\n seat's credentials. Fails open.\n Env: DARIO_POOL_SHARED_ACCOUNTS=1.\n --queue-timeout=MS Max ms a queued request waits before\n dario returns 504 \"queue-timeout\"\n (default: 60000).\n Env: DARIO_QUEUE_TIMEOUT_MS. (dario#80)\n --pool-strategy=<headroom|headroom-by-plan|fill-first|expiring-first>\n Where new conversations land in a multi-\n account pool. headroom (default) spreads\n them to the seat with the most slack;\n headroom-by-plan (opt-in) counts each\n seat's slack by plan size (Max Nx = N,\n Pro = 1); fill-first concentrates them on the\n alphabetically-first eligible seat until\n it drains to the floor, then spills;\n expiring-first does the same in order of\n each seat's 7-day reset, soonest first.\n Sticky bindings are unaffected.\n Env: DARIO_POOL_STRATEGY.\n --pool-headroom-floor=<ratio|percent>\n Headroom at or below which a seat counts as\n drained: a sticky session rebinds off it and\n new conversations skip it. Setting this makes\n the floor a hard stop. 0.05 or 5%; implicit\n default 2% is a preference, max 50%. Use it when your seats\n answer with API errors in the last percent\n of a window (dario#1333).\n Env: DARIO_POOL_HEADROOM_FLOOR;\n config: pool.headroomFloor.\n --pool-fallback=<model> When every pool seat is drained or cooling,\n serve the request as <model> from whichever\n provider can, instead of surfacing the\n 429/503: a Codex/ChatGPT subscription that\n lists <model> (both wire shapes), else the\n openai-compat backend (OpenAI shape only).\n Response carries x-dario-pool-fallback.\n Needs one of those configured. Empty value\n disables. Env: DARIO_POOL_FALLBACK. Config:\n poolFallback.model.\n An entry written credits:<model> is a rung on\n the API credits a Max plan includes: with\n DARIO_CREDITS_API_KEY set (env only, never a\n flag) the request goes to the Claude API on\n that key as <model>, both wire shapes, after\n every seat and subscription rung; /analytics\n and the dashboard file it under credits, the\n ledger counts it as metered. Without the key\n the entry is inert and dario doctor says so.\n --family-fallback=<family=model,...>\n When every pool seat is parked for a model\n family, serve its requests as <model> on the\n Claude pool instead, while some seat can\n serve that model's family. For Fable, which\n runs out of included credit while Opus still\n has room: fable=claude-opus-5-5. Response\n carries x-dario-family-fallback. Empty value\n disables. Env: DARIO_FAMILY_FALLBACK.\n Config: familyFallback.\n --model-alias=<name=target>\n User-defined model alias, repeatable.\n Applied to the client's model name before\n provider-prefix parsing, so the target may\n carry a prefix to retarget the backend\n (--model-alias=my-fast=openai:gpt-4o-mini).\n Advertised on /v1/models. Names match\n case-insensitively; one step, never\n recursive. Env: DARIO_MODEL_ALIASES=\n name=target,name2=target2. Config:\n modelAliases.\n --effort=<low|medium|high|xhigh|ultracode|max|client>\n Pin the outbound output_config.effort on\n non-haiku requests, overriding the\n client's choice. Default (unset) forwards\n the client's own effort \u2014 it's a user\n knob; real CC wires whatever the user\n tuned \u2014 falling back to 'high' when the\n client sent none. 'client' is a compat\n alias for the default. 'ultracode' rides\n the wire as 'xhigh'.\n WARNING: pinned values override every\n client and may cause Anthropic's\n classifier to flip requests to 'overage'\n billing; watch -v logs for\n representative-claim changes.\n Env: DARIO_EFFORT. (dario#87)\n --max-tokens=<N|client> Override outbound max_tokens. Default\n (unset) pins 32000 (CC 2.1.116 wire default).\n Set a number to pin that value; set 'client'\n to pass through the client's requested\n max_tokens (Hermes requests 64k\u2013128k; the\n default pin silently truncates its output\n capacity). Anthropic enforces the per-model\n ceiling server-side, so too-high values\n return a clean 400.\n Env: DARIO_MAX_TOKENS. (dario#88)\n --no-overage-guard Disable the overage-guard (v4.1.0+). Default\n behavior halts the proxy on any response with\n representative-claim=overage and returns 503\n with an Anthropic-shaped error body until\n cooldown expires or `dario resume` clears\n the state. Subscribers should never see an\n overage hit during normal operation; one\n means something is wrong (wire drift,\n classifier change, account misconfig).\n Env: DARIO_OVERAGE_GUARD=off. (dario#288)\n --overage-behavior=<halt|warn>\n Behavior when overage is detected (v4.1.0+):\n halt \u2014 return 503 to new /v1/messages\n requests until resume / cooldown.\n Default. Strongest protection.\n warn \u2014 emit events + OS notification only,\n keep forwarding. Visibility mode for\n operators who want the signal without\n cutting off traffic.\n Env: DARIO_OVERAGE_BEHAVIOR.\n --overage-cooldown=MS Ms to wait before auto-clearing the halt\n state (v4.1.0+). Default: 1800000 (30 min).\n Manual `dario resume` clears immediately\n regardless of this value.\n Env: DARIO_OVERAGE_COOLDOWN.\n --no-overage-notify Suppress the native desktop notification on\n halt (v4.1.0+). Terminal BEL is the\n unconditional floor; TUI banner + SSE event\n still fire regardless. Use in headless / CI\n contexts where toast popups don't make sense.\n Env: DARIO_OVERAGE_NOTIFY=off.\n --port=PORT Port to listen on (default: 3456)\n --host=ADDRESS Address to bind to (default: 127.0.0.1)\n Use 0.0.0.0 for LAN; see README for DARIO_API_KEY\n --verbose, -v Log all requests\n --verbose=2, -vv Also dump redacted request bodies\n (env: DARIO_LOG_BODIES=1)\n --cache-trace One line per ChatGPT (codex) request on what it\n shared with the previous request on its cache\n key: the gap, whether the input extends it, a\n seat or prompt change (env: DARIO_CACHE_TRACE=1)\n --log-file=PATH Append one JSON-ND record per completed\n request to PATH. Useful for backgrounded\n proxies where stdout is unobserved (where\n --verbose can't help). Secrets scrubbed,\n no request bodies. Env: DARIO_LOG_FILE.\n --passthrough-betas=CSV Beta flags to ALWAYS forward upstream\n regardless of CC's captured set or the\n client's anthropic-beta header. Bypasses\n the billable-beta filter. Per-account\n rejection cache still applies (so a flag\n upstream 400's gets dropped, not retried\n forever). Use when you know a beta works\n on your account but isn't in the captured\n template. Env: DARIO_PASSTHROUGH_BETAS.\n\n --skip-fields=CSV CC body fields to NOT inject into outbound\n requests. Allowed: thinking,\n context_management, output_config. Headers\n and metadata stay intact (Max billing\n unchanged). Use when an upstream model 400s\n \"Extra inputs are not permitted\" on one of\n these fields \u2014 typically a non-CC SDK\n client routed through dario to a model\n that rejects the field despite the beta\n header. Env: DARIO_SKIP_FIELDS.\n\n --honor-client-thinking Pass the client body's `thinking` field\n through to upstream instead of dario's\n default `{type:\"adaptive\"}`. Lets SDK\n clients explicitly enable extended\n thinking with their own budget (e.g.\n `{type:\"enabled\", budget_tokens:8000}`).\n When honored, the paired\n `context_management.clear_thinking_*`\n edit is suppressed (shape-specific).\n No effect on Haiku or when client omits\n `thinking`. Env:\n DARIO_HONOR_CLIENT_THINKING.\n\n --preserve-output-format\n Pass the client body's\n `output_config.format` (structured-output\n JSON schema) through to upstream instead of\n dropping it during the CC rebuild. Lets SDK\n clients (e.g. the Vercel AI SDK's\n `generateObject`) get schema-constrained\n output through dario. No effect when the\n client omits `output_config.format`. Env:\n DARIO_PRESERVE_OUTPUT_FORMAT.\n\n --upstream-proxy=URL / --via=URL\n Route all of dario's outbound fetch\n calls (api.anthropic.com, OpenAI-compat\n backends, OAuth) through an HTTP/HTTPS\n proxy. Localhost calls bypass. Useful\n with a VPN provider's HTTP proxy mode\n (Mullvad, AirVPN, corporate proxy,\n privoxy/Tor) when you don't want to put\n the whole system on a system VPN.\n Requires Bun runtime. SOCKS5 not\n supported (Bun fetch limitation). See\n docs/vpn-routing.md. Env: DARIO_UPSTREAM_PROXY.\n (v3.35.0)\n\n --system-prompt=<MODE> System-prompt mode for outbound CC-shaped\n requests (v3.34.0). One of:\n verbatim \u2014 CC unchanged (default)\n partial \u2014 strip behavioral constraints\n (Tone-and-style, Text-output,\n verbosity / comment / scope\n bullets in Doing-tasks).\n Recovers ~1.2-2.8x output on\n open-ended work.\n aggressive \u2014 partial + remove prompt-level\n RLHF restatements + Executing-\n actions-with-care section.\n Adds <3% over partial; RLHF\n refusals on harmful content\n unaffected (alignment is in\n the weights, not the prompt).\n <filepath> \u2014 replace the slot entirely\n with the file's contents.\n Empirically validated as unfingerprinted by\n the billing classifier \u2014 see docs/research/\n system-prompt.md. Env: DARIO_SYSTEM_PROMPT.\n\n Quick start:\n dario login # auto-detects Claude Code credentials\n dario proxy --model=opus # or: dario proxy --passthrough\n\n Then point any Anthropic SDK at http://localhost:3456:\n export ANTHROPIC_BASE_URL=http://localhost:3456\n export ANTHROPIC_API_KEY=dario\n\n Examples:\n curl http://localhost:3456/v1/messages \\\n -H \"Content-Type: application/json\" \\\n -H \"anthropic-version: 2023-06-01\" \\\n -d '{\"model\":\"claude-fable-5\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'\n\n Your subscription handles the billing. No API key needed.\n Tokens auto-refresh in the background \u2014 set it and forget it.\n";
196
196
  /**
197
197
  * Decide whether this module is being invoked as the CLI entry point or
198
198
  * imported as a library. Pure, exported for tests; the file-bottom uses
package/dist/cli.js CHANGED
@@ -63,7 +63,8 @@ import { VALID_EFFORT_VALUES } from './cc-template.js';
63
63
  import { grantAge, describeGrantAge } from './refresh-grant.js';
64
64
  import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, addAccountViaManualOAuth, addAccountFromKeychain, KeychainImportError, removeAccount, loadAccount, ensureLoginCredentialsInPool, resyncLoginFromCredentialsIfStale, MIGRATED_LOGIN_ALIAS } from './accounts.js';
65
65
  import { removeAccountEverywhere } from './account-sync.js';
66
- import { listCodexAccountAliases, loadAllCodexAccounts, startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, parseCodexManualPaste } from './codex-accounts.js';
66
+ import { removeCodexAccountEverywhere } from './codex-account-sync.js';
67
+ import { listCodexAccountAliases, loadAllCodexAccounts, loadCodexAccount, startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, parseCodexManualPaste } from './codex-accounts.js';
67
68
  import { describeCodexWindow } from './codex-usage.js';
68
69
  import { listBackends, saveBackend, removeBackend, defaultBackendBaseUrl } from './openai-backend.js';
69
70
  import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-proxy.js';
@@ -572,8 +573,8 @@ async function proxy() {
572
573
  || process.env['DARIO_POOL_SHARED_STATE'] === 'true';
573
574
  const poolSharedStateIntervalMs = parsePositiveIntFlag('--pool-shared-state-interval=')
574
575
  ?? parsePositiveIntEnv(process.env['DARIO_POOL_SHARED_STATE_INTERVAL_MS']);
575
- // --pool-shared-accounts: keep the Claude accounts in a registry on the
576
- // same service, merged at startup and every 15s (docs/multi-instance.md).
576
+ // --pool-shared-accounts: keep the Claude accounts and ChatGPT seats in
577
+ // registries on the same service, merged at startup and every 15s (docs/multi-instance.md).
577
578
  const poolSharedAccounts = poolSharedAccountsEnabled();
578
579
  // --effort=low|medium|high|xhigh|ultracode|max|client — pin the outbound
579
580
  // output_config.effort (dario#87). Default (unset) forwards the client's
@@ -1265,7 +1266,34 @@ export function resolveUpstreamTimeoutMs(flag, env) {
1265
1266
  const ms = flag ?? parsePositiveIntEnv(env);
1266
1267
  return ms === 0 ? undefined : ms;
1267
1268
  }
1268
- /** --pool-shared-accounts or DARIO_POOL_SHARED_ACCOUNTS: read by the proxy and by `dario accounts remove`. */
1269
+ /**
1270
+ * Remove an account with the shared registry on, and say how it went: the removal goes to the
1271
+ * registry first, and anything but a clean removal exits non-zero.
1272
+ */
1273
+ async function removeEverywhereOrExit(label, lockUrl, alias, remove) {
1274
+ let result;
1275
+ try {
1276
+ result = await remove(lockUrl, process.env['DARIO_REFRESH_LOCK_TOKEN'] ?? '', alias);
1277
+ }
1278
+ catch (err) {
1279
+ console.error(`[dario] ${label} "${alias}" NOT removed: the shared account registry at ${lockUrl} did not take the removal (${err instanceof Error ? err.message : err}), and the other instances would put the account back. Try again once it answers, or unset DARIO_POOL_SHARED_ACCOUNTS to remove this instance's file only.`);
1280
+ process.exit(1);
1281
+ }
1282
+ if (result === 'registry-newer') {
1283
+ console.error(`[dario] ${label} "${alias}" NOT removed: the shared account registry holds a later login for it, from another instance, so nothing was changed. Run the command again to remove that login too.`);
1284
+ process.exit(1);
1285
+ }
1286
+ if (result === 'local-newer') {
1287
+ console.error(`[dario] ${label} "${alias}" removed from the shared account registry, but the file here holds a later login, which stays and goes back to the registry on the next sync. Run the command again to remove that login too.`);
1288
+ process.exit(1);
1289
+ }
1290
+ if (result === 'local-failed') {
1291
+ console.error(`[dario] ${label} "${alias}" removed from the shared account registry, but its file here could not be deleted. Check that the accounts directory is writable and run the command again; until the file goes, this instance keeps serving the account.`);
1292
+ process.exit(1);
1293
+ }
1294
+ console.log(`[dario] ${label} "${alias}" removed, here and from the shared account registry; the other instances drop it on their next sync.`);
1295
+ }
1296
+ /** --pool-shared-accounts or DARIO_POOL_SHARED_ACCOUNTS: read by the proxy, `dario accounts remove` and `dario codex remove`. */
1269
1297
  function poolSharedAccountsEnabled() {
1270
1298
  return args.includes('--pool-shared-accounts')
1271
1299
  || process.env['DARIO_POOL_SHARED_ACCOUNTS'] === '1'
@@ -1692,27 +1720,7 @@ async function accounts() {
1692
1720
  console.error(`[dario] No account "${alias}" found.`);
1693
1721
  process.exit(1);
1694
1722
  }
1695
- let result;
1696
- try {
1697
- result = await removeAccountEverywhere(lockUrl, process.env['DARIO_REFRESH_LOCK_TOKEN'] ?? '', alias);
1698
- }
1699
- catch (err) {
1700
- console.error(`[dario] Account "${alias}" NOT removed: the shared account registry at ${lockUrl} did not take the removal (${err instanceof Error ? err.message : err}), and the other instances would put the account back. Try again once it answers, or unset DARIO_POOL_SHARED_ACCOUNTS to remove this instance's file only.`);
1701
- process.exit(1);
1702
- }
1703
- if (result === 'registry-newer') {
1704
- console.error(`[dario] Account "${alias}" NOT removed: the shared account registry holds a later login for it, from another instance, so nothing was changed. Run the command again to remove that login too.`);
1705
- process.exit(1);
1706
- }
1707
- if (result === 'local-newer') {
1708
- console.error(`[dario] Account "${alias}" removed from the shared account registry, but the file here holds a later login, which stays and goes back to the registry on the next sync. Run the command again to remove that login too.`);
1709
- process.exit(1);
1710
- }
1711
- if (result === 'local-failed') {
1712
- console.error(`[dario] Account "${alias}" removed from the shared account registry, but its file here could not be deleted. Check that the accounts directory is writable and run the command again; until the file goes, this instance keeps serving the account.`);
1713
- process.exit(1);
1714
- }
1715
- console.log(`[dario] Account "${alias}" removed, here and from the shared account registry; the other instances drop it on their next sync.`);
1723
+ await removeEverywhereOrExit('Account', lockUrl, alias, removeAccountEverywhere);
1716
1724
  return;
1717
1725
  }
1718
1726
  const ok = await removeAccount(alias);
@@ -1837,6 +1845,17 @@ async function codex() {
1837
1845
  console.error('');
1838
1846
  process.exit(1);
1839
1847
  }
1848
+ // As in `dario accounts remove`: the removal is published before the file
1849
+ // goes, or a replica's next sync pushes the seat back from its own copy.
1850
+ const lockUrl = process.env['DARIO_REFRESH_LOCK_URL'];
1851
+ if (poolSharedAccountsEnabled() && lockUrl) {
1852
+ if (!(await loadCodexAccount(alias))) {
1853
+ console.error(`[dario] No Codex account "${alias}" found.`);
1854
+ process.exit(1);
1855
+ }
1856
+ await removeEverywhereOrExit('Codex account', lockUrl, alias, removeCodexAccountEverywhere);
1857
+ return;
1858
+ }
1840
1859
  const ok = await removeCodexAccount(alias);
1841
1860
  if (ok) {
1842
1861
  console.log(`[dario] Codex account "${alias}" removed.`);
@@ -2370,8 +2389,9 @@ export const FULL_HELP = `
2370
2389
  How often to pull peers' readings
2371
2390
  (default: 2000). Env:
2372
2391
  DARIO_POOL_SHARED_STATE_INTERVAL_MS.
2373
- --pool-shared-accounts Keep the Claude accounts in a registry on the
2374
- refresh-lock service, merged at startup and
2392
+ --pool-shared-accounts Keep the Claude accounts and ChatGPT seats in
2393
+ registries on the refresh-lock service, merged
2394
+ at startup and
2375
2395
  every 15s, so a new replica starts with its
2376
2396
  peers' accounts and adds and removals reach
2377
2397
  every instance. The registry holds every