@askalf/dario 4.8.114 → 4.8.115

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
@@ -223,7 +223,7 @@ Tune via `~/.dario/config.json` → `overageGuard`, or CLI flags: `--overage-beh
223
223
 
224
224
  ## Capabilities
225
225
 
226
- - **Multi-account pool.** Drop 2+ Claude accounts in `~/.dario/accounts/` and pool mode auto-activates: 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)
226
+ - **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)
227
227
  - **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
228
  - **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
229
  - **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)
@@ -103,20 +103,20 @@ export declare const MIGRATED_LOGIN_ALIAS = "login";
103
103
  * keychain — whichever `loadCredentials` finds) into the pool under a
104
104
  * reserved alias.
105
105
  *
106
- * Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
107
- * A user with one `dario login` account + one `dario accounts add bar`
108
- * ends up with only one account in `accounts/` (bar), pool mode never
109
- * trips, and the login account is effectively orphaned while pool is off.
110
- * Calling this on the first `dario accounts add` back-fills the login
111
- * account into the pool so the second `add` crosses the threshold.
106
+ * Why: any entry in `~/.dario/accounts/` activates pool mode (dario#618),
107
+ * and the pool routes only across `accounts/` entries. A user with one
108
+ * `dario login` account who runs `dario accounts add bar` would otherwise
109
+ * end up with a pool that serves only `bar` the login account silently
110
+ * dropped from routing. Calling this on the first `dario accounts add`
111
+ * back-fills the login account so both keep serving.
112
112
  *
113
113
  * Idempotent: no-op if `accounts/` already has any entry, no-op if no
114
114
  * credentials are reachable anywhere. Returns the alias written to, or
115
115
  * `null` when nothing happened.
116
116
  *
117
117
  * The source `credentials.json` (if present) is left untouched — single-
118
- * account mode still reads it if the user later `accounts remove`s down
119
- * below the pool threshold. Migration is copy-only, never destructive.
118
+ * account mode still reads it if the user later `accounts remove`s the
119
+ * pool empty. Migration is copy-only, never destructive.
120
120
  *
121
121
  * @param preferredAlias caller may request a specific alias. If it's
122
122
  * already the reserved `login` (or collides), falls back to `default`.
@@ -126,7 +126,7 @@ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<st
126
126
  * Detect divergence between `accounts/login.json` and the current
127
127
  * `credentials.json` (or whichever store loadCredentials finds), and
128
128
  * re-sync if they differ. Returns one of:
129
- * - 'no-pool' : pool is single-account, nothing to do
129
+ * - 'no-pool' : accounts/ is empty (pool inactive), nothing to do
130
130
  * - 'no-login' : pool active but no `login` alias — back-fill
131
131
  * was never run, nothing to do
132
132
  * - 'no-creds' : login.json exists but no current credentials
@@ -142,5 +142,10 @@ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<st
142
142
  * time — is now wrong on both fields, but its `expiresAt` metadata still
143
143
  * says "healthy" so the selector keeps picking it. Detect this at startup
144
144
  * and overwrite with the current canonical content. dario#235.
145
+ *
146
+ * Runs at any pool size ≥ 1: a lone `login` entry is a live pool member
147
+ * since pool-at-one (dario#618) and can go stale exactly the same way —
148
+ * e.g. after `accounts remove` shrinks a migrated pool back to just the
149
+ * back-filled snapshot.
145
150
  */
146
151
  export declare function resyncLoginFromCredentialsIfStale(): Promise<'no-pool' | 'no-login' | 'no-creds' | 'in-sync' | 'resynced'>;
package/dist/accounts.js CHANGED
@@ -3,14 +3,15 @@
3
3
  *
4
4
  * Accounts live at `~/.dario/accounts/<alias>.json`. Single-account dario
5
5
  * uses `~/.dario/credentials.json` (plus the CC file + OS keychain fallback
6
- * paths in oauth.ts). When `~/.dario/accounts/` contains 2+ files the proxy
7
- * activates pool mode (see pool.ts). Each account has its own independent
8
- * OAuth lifecycle and can refresh without affecting the others.
6
+ * paths in oauth.ts). Any file in `~/.dario/accounts/` activates the proxy's
7
+ * pool mode (one account is enough — dario#618; see pool.ts). Each account
8
+ * has its own independent OAuth lifecycle and can refresh without affecting
9
+ * the others.
9
10
  *
10
11
  * `ensureLoginCredentialsInPool` (below) bridges the two stores on the
11
12
  * first `dario accounts add` — it promotes the user's existing login
12
- * credentials into the pool under a reserved alias so that adding a
13
- * second account actually trips the 2+ threshold and activates pooling.
13
+ * credentials into the pool under a reserved alias so the login account
14
+ * keeps serving once pool routing takes over.
14
15
  *
15
16
  * OAuth config (client_id, scopes, authorize URL, token URL) comes from
16
17
  * dario's cc-oauth-detect scanner — the same source the single-account
@@ -503,20 +504,20 @@ export const MIGRATED_LOGIN_ALIAS = 'login';
503
504
  * keychain — whichever `loadCredentials` finds) into the pool under a
504
505
  * reserved alias.
505
506
  *
506
- * Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
507
- * A user with one `dario login` account + one `dario accounts add bar`
508
- * ends up with only one account in `accounts/` (bar), pool mode never
509
- * trips, and the login account is effectively orphaned while pool is off.
510
- * Calling this on the first `dario accounts add` back-fills the login
511
- * account into the pool so the second `add` crosses the threshold.
507
+ * Why: any entry in `~/.dario/accounts/` activates pool mode (dario#618),
508
+ * and the pool routes only across `accounts/` entries. A user with one
509
+ * `dario login` account who runs `dario accounts add bar` would otherwise
510
+ * end up with a pool that serves only `bar` the login account silently
511
+ * dropped from routing. Calling this on the first `dario accounts add`
512
+ * back-fills the login account so both keep serving.
512
513
  *
513
514
  * Idempotent: no-op if `accounts/` already has any entry, no-op if no
514
515
  * credentials are reachable anywhere. Returns the alias written to, or
515
516
  * `null` when nothing happened.
516
517
  *
517
518
  * The source `credentials.json` (if present) is left untouched — single-
518
- * account mode still reads it if the user later `accounts remove`s down
519
- * below the pool threshold. Migration is copy-only, never destructive.
519
+ * account mode still reads it if the user later `accounts remove`s the
520
+ * pool empty. Migration is copy-only, never destructive.
520
521
  *
521
522
  * @param preferredAlias caller may request a specific alias. If it's
522
523
  * already the reserved `login` (or collides), falls back to `default`.
@@ -550,7 +551,7 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
550
551
  * Detect divergence between `accounts/login.json` and the current
551
552
  * `credentials.json` (or whichever store loadCredentials finds), and
552
553
  * re-sync if they differ. Returns one of:
553
- * - 'no-pool' : pool is single-account, nothing to do
554
+ * - 'no-pool' : accounts/ is empty (pool inactive), nothing to do
554
555
  * - 'no-login' : pool active but no `login` alias — back-fill
555
556
  * was never run, nothing to do
556
557
  * - 'no-creds' : login.json exists but no current credentials
@@ -566,10 +567,15 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
566
567
  * time — is now wrong on both fields, but its `expiresAt` metadata still
567
568
  * says "healthy" so the selector keeps picking it. Detect this at startup
568
569
  * and overwrite with the current canonical content. dario#235.
570
+ *
571
+ * Runs at any pool size ≥ 1: a lone `login` entry is a live pool member
572
+ * since pool-at-one (dario#618) and can go stale exactly the same way —
573
+ * e.g. after `accounts remove` shrinks a migrated pool back to just the
574
+ * back-filled snapshot.
569
575
  */
570
576
  export async function resyncLoginFromCredentialsIfStale() {
571
577
  const aliases = await listAccountAliases();
572
- if (aliases.length < 2)
578
+ if (aliases.length === 0)
573
579
  return 'no-pool';
574
580
  if (!aliases.includes(MIGRATED_LOGIN_ALIAS))
575
581
  return 'no-login';
package/dist/cli.js CHANGED
@@ -778,7 +778,7 @@ async function accounts() {
778
778
  console.log(' No multi-account pool configured.');
779
779
  console.log('');
780
780
  console.log(' Pool mode activates automatically when ~/.dario/accounts/');
781
- console.log(' has 2+ entries. Add the first with:');
781
+ console.log(' has any entry. Add the first with:');
782
782
  console.log(' dario accounts add <alias>');
783
783
  console.log('');
784
784
  console.log(' Single-account dario (the default) keeps working as-is');
@@ -791,7 +791,7 @@ async function accounts() {
791
791
  const now = Date.now();
792
792
  console.log(` ${aliases.length} account${aliases.length === 1 ? '' : 's'} configured`);
793
793
  if (aliases.length === 1) {
794
- console.log(' (Pool mode needs 2+ accountssingle-account mode until another is added.)');
794
+ console.log(' (Pool routing serves this accountadd another to load-balance across subscriptions.)');
795
795
  }
796
796
  console.log('');
797
797
  for (const a of loaded) {
@@ -825,9 +825,9 @@ async function accounts() {
825
825
  }
826
826
  // If the user has `dario login` credentials on disk or in the keychain
827
827
  // and the pool is empty, migrate those credentials into the pool first.
828
- // Otherwise the new account lives alone in accounts/, pool mode never
829
- // trips the 2+ threshold, and the login account is orphaned from the
830
- // pool until the user figures out they have to re-`accounts add` it.
828
+ // Otherwise the new account alone activates pool routing (#618) and the
829
+ // login account is orphaned from it until the user figures out they
830
+ // have to re-`accounts add` it.
831
831
  // Skip silently when the user explicitly picks the reserved alias —
832
832
  // their intent wins, they can run `accounts add` again for the login
833
833
  // migration under a different alias.
@@ -836,7 +836,7 @@ async function accounts() {
836
836
  if (migrated) {
837
837
  console.log('');
838
838
  console.log(` Migrated your existing \`dario login\` account into the pool as "${migrated}".`);
839
- console.log(` (Pool mode activates on 2+ accounts — this back-fill plus "${alias}" crosses that.)`);
839
+ console.log(` (It keeps serving alongside "${alias}" once pool routing takes over.)`);
840
840
  }
841
841
  }
842
842
  const manualAccountFlag = args.includes('--manual') || args.includes('--headless');
@@ -882,13 +882,15 @@ async function accounts() {
882
882
  console.log(` Account "${alias}" added.`);
883
883
  console.log(` Token expires in ${minutes} minutes (auto-refreshes in the background).`);
884
884
  const total = (await listAccountAliases()).length;
885
+ console.log('');
885
886
  if (total >= 2) {
886
- console.log('');
887
887
  console.log(' Pool mode is now active. Restart `dario proxy` to pick up the new account.');
888
888
  }
889
889
  else {
890
+ console.log(' `dario proxy` serves this account directly — no `dario login` needed.');
891
+ console.log(' (Restart the proxy if it is already running.)');
890
892
  console.log('');
891
- console.log(' Add at least one more account to activate pool routing:');
893
+ console.log(' Add more accounts to load-balance across subscriptions:');
892
894
  console.log(' dario accounts add <another-alias>');
893
895
  }
894
896
  console.log('');
package/dist/doctor.js CHANGED
@@ -747,11 +747,11 @@ export async function runChecks(opts = {}) {
747
747
  const now = Date.now();
748
748
  const expired = loaded.filter((a) => a.expiresAt <= now).length;
749
749
  checks.push({
750
- status: expired > 0 ? 'warn' : aliases.length >= 2 ? 'ok' : 'info',
750
+ status: expired > 0 ? 'warn' : 'ok',
751
751
  label: 'Pool',
752
752
  detail: `${aliases.length} account${aliases.length === 1 ? '' : 's'}` +
753
753
  (expired > 0 ? `, ${expired} expired` : '') +
754
- (aliases.length < 2 ? ' (pool activates at 2+)' : ''),
754
+ (aliases.length === 1 ? ' (pool active add more to load-balance)' : ''),
755
755
  });
756
756
  // Next-account-in-rotation surfacing. The proxy's per-request
757
757
  // selector picks by max headroom (with 7d_<family> per-model
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
8
8
  export { startProxy, sanitizeError } from './proxy.js';
9
9
  // Multi-account pool API (pool activates automatically when ~/.dario/accounts/
10
- // contains 2+ accounts; see README for the progression from single-account
10
+ // contains any account; see README for the progression from single-account
11
11
  // mode to pool mode).
12
12
  export { AccountPool, parseRateLimits } from './pool.js';
13
13
  export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS, getAccountsDir, } from './accounts.js';
package/dist/mcp/tools.js CHANGED
@@ -77,8 +77,8 @@ export function buildToolRegistry(data) {
77
77
  const expiry = msLeft > 0 ? `${hours}h ${mins}m` : 'expired';
78
78
  return ` ${a.alias.padEnd(20)} token expires in ${expiry}`;
79
79
  });
80
- const note = accounts.length < 2
81
- ? '\n\nPool mode activates at 2+ accounts currently single-account.'
80
+ const note = accounts.length === 1
81
+ ? '\n\nPool routing serves this accountadd another to load-balance across subscriptions.'
82
82
  : '';
83
83
  return textResult(`${accounts.length} account${accounts.length === 1 ? '' : 's'}:\n${lines.join('\n')}${note}`);
84
84
  },
@@ -151,7 +151,7 @@ export function buildToolRegistry(data) {
151
151
  return textResult([
152
152
  'Mode: single-account',
153
153
  '',
154
- 'Analytics history is collected only in pool mode (2+ accounts in ~/.dario/accounts/).',
154
+ 'Analytics history is collected only in pool mode (any account in ~/.dario/accounts/).',
155
155
  'For a one-off rate-limit snapshot from Anthropic, run `dario doctor --usage`.',
156
156
  ].join('\n'));
157
157
  }
package/dist/pool.js CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Account pool — rate limit tracking, headroom routing, failover.
3
3
  *
4
- * Activated automatically when `~/.dario/accounts/` contains 2+ accounts.
5
- * Single-account dario (`~/.dario/credentials.json`) keeps the same code
6
- * path it has always had; the pool only runs when there are multiple
7
- * accounts to distribute against.
4
+ * Activated automatically when `~/.dario/accounts/` contains any account
5
+ * (one is enough — dario#618). Login-only dario (`~/.dario/credentials.json`,
6
+ * no accounts/ entries) keeps the same code path it has always had.
8
7
  */
9
8
  import { createHash, randomUUID } from 'node:crypto';
10
9
  /**
package/dist/proxy.d.ts CHANGED
@@ -436,5 +436,15 @@ export declare function describeAuthReject(headers: IncomingMessage['headers']):
436
436
  * Pro/Max OAuth bearer. Pure + exported for unit testing.
437
437
  */
438
438
  export declare function upstreamAuthHeaders(upstreamApiKey: string, accessToken: string): Record<string, string>;
439
+ /**
440
+ * Whether the proxy routes through the account pool. Any entry in
441
+ * `~/.dario/accounts/` activates the pool (#618) — so a cold
442
+ * `dario accounts add` bootstraps a servable proxy with no `dario login`
443
+ * step — and admin mode always does (#599), where the pool may start empty
444
+ * and be populated over HTTP. Login-only setups (no accounts/ entries) keep
445
+ * the single-account credentials.json path. Pure + exported for unit
446
+ * testing.
447
+ */
448
+ export declare function shouldUsePool(accountCount: number, adminEnabled: boolean): boolean;
439
449
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
440
450
  export {};
package/dist/proxy.js CHANGED
@@ -673,6 +673,18 @@ export function upstreamAuthHeaders(upstreamApiKey, accessToken) {
673
673
  ? { 'x-api-key': upstreamApiKey }
674
674
  : { 'Authorization': `Bearer ${accessToken}` };
675
675
  }
676
+ /**
677
+ * Whether the proxy routes through the account pool. Any entry in
678
+ * `~/.dario/accounts/` activates the pool (#618) — so a cold
679
+ * `dario accounts add` bootstraps a servable proxy with no `dario login`
680
+ * step — and admin mode always does (#599), where the pool may start empty
681
+ * and be populated over HTTP. Login-only setups (no accounts/ entries) keep
682
+ * the single-account credentials.json path. Pure + exported for unit
683
+ * testing.
684
+ */
685
+ export function shouldUsePool(accountCount, adminEnabled) {
686
+ return accountCount >= 1 || adminEnabled;
687
+ }
676
688
  export async function startProxy(opts = {}) {
677
689
  const port = opts.port ?? DEFAULT_PORT;
678
690
  const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
@@ -828,9 +840,9 @@ export async function startProxy(opts = {}) {
828
840
  if (openaiBackend) {
829
841
  console.log(` OpenAI-compat backend: ${openaiBackend.name} → ${openaiBackend.baseUrl}`);
830
842
  }
831
- // Multi-account pool — activated when ~/.dario/accounts/ has 2+ entries (or
832
- // whenever admin mode is on; see the #599 block below). Single-account dario
833
- // keeps its existing code path unchanged.
843
+ // Multi-account pool — activated whenever ~/.dario/accounts/ has any entry
844
+ // (#618; or admin mode is on, see the #599 block below). Login-only dario
845
+ // (no accounts/ entries) keeps its existing credentials.json code path.
834
846
  //
835
847
  // Before loading the pool, check whether the back-filled `login` snapshot
836
848
  // has gone stale relative to credentials.json (dario#235). The single-
@@ -858,7 +870,7 @@ export async function startProxy(opts = {}) {
858
870
  catch { /* non-fatal — pool just starts empty */ }
859
871
  }
860
872
  const accountsList = await loadAllAccounts();
861
- const pool = (accountsList.length >= 2 || adminEnabled) ? new AccountPool() : null;
873
+ const pool = shouldUsePool(accountsList.length, adminEnabled) ? new AccountPool() : null;
862
874
  // Per-model rate-limit bucket families seen during this proxy run. First-
863
875
  // sight is logged once when verbose so a new Anthropic bucket (e.g. an
864
876
  // eventual `7d_opus`) doesn't slip past unnoticed. Pure observability —
@@ -951,9 +963,10 @@ export async function startProxy(opts = {}) {
951
963
  }
952
964
  }
953
965
  else {
954
- // Single-account mode — the classic `dario login` path. Admin mode never
955
- // lands here: it always routes through the pool above (#599), which starts
956
- // even with zero accounts and returns a clean 503 until one is added.
966
+ // Single-account mode — the classic `dario login` path, reached only when
967
+ // ~/.dario/accounts/ is empty. Any accounts/ entry routes through the pool
968
+ // above (#618), and admin mode always does (#599) it starts even with
969
+ // zero accounts and returns a clean 503 until one is added.
957
970
  status = await getStatus();
958
971
  if (!status.authenticated) {
959
972
  console.error('[dario] Not authenticated. Run `dario login` first.');
@@ -3073,7 +3086,9 @@ export async function startProxy(opts = {}) {
3073
3086
  // feature is visible to single-account users (was previously only
3074
3087
  // logged when pool mode was active).
3075
3088
  const poolLine = pool
3076
- ? `Pool: ${accountsList.length} accounts loaded — headroom-routed, sticky for multi-turn`
3089
+ ? accountsList.length === 1
3090
+ ? 'Pool: 1 account loaded — add more with `dario accounts add <alias>` to load-balance'
3091
+ : `Pool: ${accountsList.length} accounts loaded — headroom-routed, sticky for multi-turn`
3077
3092
  : 'Pool: single-account (run `dario accounts add <alias>` to pool multiple subscriptions)';
3078
3093
  // Display URL uses `localhost` for loopback binds and the literal host
3079
3094
  // for exposed binds, so the printed URL is the one a client would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.114",
3
+ "version": "4.8.115",
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": {