@askalf/dario 5.2.9 → 5.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -134,6 +134,9 @@ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<st
134
134
  * - 'in-sync' : tokens match; no action
135
135
  * - 'resynced' : login.json was stale; overwrote with current
136
136
  * credentials. Caller should reload pool state
137
+ * - 'creds-stale' : login.json is STRICTLY NEWER than credentials.json
138
+ * (the pool refreshed it; the legacy file is stale) —
139
+ * left login.json untouched. dario#805.
137
140
  *
138
141
  * Why: the single-account path keeps refreshing `credentials.json` in
139
142
  * the background (proxy startup auth check, periodic refresh in oauth.ts).
@@ -143,9 +146,50 @@ export declare function ensureLoginCredentialsInPool(alias?: string): Promise<st
143
146
  * says "healthy" so the selector keeps picking it. Detect this at startup
144
147
  * and overwrite with the current canonical content. dario#235.
145
148
  *
149
+ * BUT the divergence runs BOTH ways. In pool mode the pool's own refresh
150
+ * loop advances `login.json` while `credentials.json` stays frozen (the
151
+ * pool never writes it). Blindly overwriting login.json from credentials.json
152
+ * would then clobber a live token with the stale legacy one whose refresh
153
+ * Anthropic already rotated → invalid_grant on every startup → fleet-wide
154
+ * auth outage. So we reconcile by FRESHNESS, not by assuming credentials.json
155
+ * wins. dario#805 (the second outage of this class within 12h).
156
+ *
146
157
  * Runs at any pool size ≥ 1: a lone `login` entry is a live pool member
147
158
  * since pool-at-one (dario#618) and can go stale exactly the same way —
148
159
  * e.g. after `accounts remove` shrinks a migrated pool back to just the
149
160
  * back-filled snapshot.
150
161
  */
151
- export declare function resyncLoginFromCredentialsIfStale(): Promise<'no-pool' | 'no-login' | 'no-creds' | 'in-sync' | 'resynced'>;
162
+ export declare function resyncLoginFromCredentialsIfStale(): Promise<'no-pool' | 'no-login' | 'no-creds' | 'in-sync' | 'resynced' | 'creds-stale'>;
163
+ /**
164
+ * Mirror the pool's freshly-refreshed `login` account token back into the
165
+ * legacy `~/.dario/credentials.json` store. dario#808.
166
+ *
167
+ * The divergence #805 fixed runs one way (credentials.json stale, login.json
168
+ * fresh) but left the two files diverged: the pool's refresh loop advances
169
+ * accounts/login.json while nothing ever writes credentials.json, so the
170
+ * legacy file stays frozen at the last `dario login`. Consequences:
171
+ * - `dario doctor` reads credentials.json for its OAuth row and prints
172
+ * 'expired'/'expiring' in pool-of-1 mode even when the live pool token is
173
+ * fresh and the fleet is 200-healthy — an alarm-inducing false positive.
174
+ * - any other reader of credentials.json (a co-resident Claude Code, an ops
175
+ * script) sees a stale token indefinitely.
176
+ *
177
+ * Fix: whenever the pool refreshes the `login` account, mirror the new token
178
+ * into credentials.json so the legacy file tracks the pool store. Only the
179
+ * `login` alias is mirrored — it's the one back-filled FROM credentials.json
180
+ * (ensureLoginCredentialsInPool), so it's the only account whose canonical
181
+ * home is that file; `work`/`personal` accounts have no legacy counterpart.
182
+ *
183
+ * Freshness-guarded, symmetric with #805: mirror only when the refreshed login
184
+ * token is strictly NEWER than the current credentials.json (by `expiresAt`).
185
+ * A newer-or-equal credentials.json means another process (e.g. a concurrent
186
+ * `dario login --force-reauth`) just wrote a fresher family — never clobber it;
187
+ * resyncLoginFromCredentialsIfStale pulls that direction on next startup.
188
+ *
189
+ * Best-effort: a mirror failure must never fail the refresh that produced the
190
+ * live token. Returns one of:
191
+ * - 'skip-not-login' : alias isn't the reserved `login` — nothing to mirror
192
+ * - 'mirrored' : credentials.json updated to the pool token
193
+ * - 'creds-newer' : credentials.json is newer-or-equal — left untouched
194
+ */
195
+ export declare function mirrorLoginToCredentials(refreshed: AccountCredentials): Promise<'skip-not-login' | 'mirrored' | 'creds-newer'>;
package/dist/accounts.js CHANGED
@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
23
23
  import { randomUUID, randomBytes, createHash } from 'node:crypto';
24
24
  import { createServer } from 'node:http';
25
25
  import { detectCCOAuthConfig } from './cc-oauth-detect.js';
26
- import { loadCredentials, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
26
+ import { loadCredentials, saveCredentialsTokens, buildManualAuthorizeUrl, parseManualPaste, readLineFromStdin, enumerateKeychainCredentials } from './oauth.js';
27
27
  import { openBrowser } from './open-browser.js';
28
28
  import { redactSecrets } from './redact.js';
29
29
  import { durableWriteFile } from './durable-write.js';
@@ -553,6 +553,9 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
553
553
  * - 'in-sync' : tokens match; no action
554
554
  * - 'resynced' : login.json was stale; overwrote with current
555
555
  * credentials. Caller should reload pool state
556
+ * - 'creds-stale' : login.json is STRICTLY NEWER than credentials.json
557
+ * (the pool refreshed it; the legacy file is stale) —
558
+ * left login.json untouched. dario#805.
556
559
  *
557
560
  * Why: the single-account path keeps refreshing `credentials.json` in
558
561
  * the background (proxy startup auth check, periodic refresh in oauth.ts).
@@ -562,6 +565,14 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
562
565
  * says "healthy" so the selector keeps picking it. Detect this at startup
563
566
  * and overwrite with the current canonical content. dario#235.
564
567
  *
568
+ * BUT the divergence runs BOTH ways. In pool mode the pool's own refresh
569
+ * loop advances `login.json` while `credentials.json` stays frozen (the
570
+ * pool never writes it). Blindly overwriting login.json from credentials.json
571
+ * would then clobber a live token with the stale legacy one whose refresh
572
+ * Anthropic already rotated → invalid_grant on every startup → fleet-wide
573
+ * auth outage. So we reconcile by FRESHNESS, not by assuming credentials.json
574
+ * wins. dario#805 (the second outage of this class within 12h).
575
+ *
565
576
  * Runs at any pool size ≥ 1: a lone `login` entry is a live pool member
566
577
  * since pool-at-one (dario#618) and can go stale exactly the same way —
567
578
  * e.g. after `accounts remove` shrinks a migrated pool back to just the
@@ -584,9 +595,19 @@ export async function resyncLoginFromCredentialsIfStale() {
584
595
  loginAcc.refreshToken === tok.refreshToken) {
585
596
  return 'in-sync';
586
597
  }
587
- // Tokens diverged credentials.json has refreshed since last back-fill.
588
- // Overwrite the snapshot, preserving deviceId/accountUuid (they don't
589
- // rotate with token refresh; they're pool-internal identity).
598
+ // Tokens diverged. Reconcile by FRESHNESS a strictly-newer login.json is
599
+ // the pool having refreshed it (credentials.json is the stale legacy copy),
600
+ // and overwriting it from credentials.json would swap a live token for one
601
+ // whose refresh Anthropic already rotated → invalid_grant → outage (#805).
602
+ // Only the strict case is skipped; equal expiresAt keeps the #235 behaviour
603
+ // (overwrite), since a real refresh always advances expiresAt.
604
+ if (loginAcc.expiresAt > tok.expiresAt) {
605
+ return 'creds-stale';
606
+ }
607
+ // credentials.json is the newer (or equal-age) token — the #235 case: the
608
+ // single-account path refreshed it and the pool snapshot is stale. Overwrite,
609
+ // preserving deviceId/accountUuid (they don't rotate with token refresh;
610
+ // they're pool-internal identity).
590
611
  await saveAccount({
591
612
  alias: MIGRATED_LOGIN_ALIAS,
592
613
  accessToken: tok.accessToken,
@@ -598,3 +619,53 @@ export async function resyncLoginFromCredentialsIfStale() {
598
619
  });
599
620
  return 'resynced';
600
621
  }
622
+ /**
623
+ * Mirror the pool's freshly-refreshed `login` account token back into the
624
+ * legacy `~/.dario/credentials.json` store. dario#808.
625
+ *
626
+ * The divergence #805 fixed runs one way (credentials.json stale, login.json
627
+ * fresh) but left the two files diverged: the pool's refresh loop advances
628
+ * accounts/login.json while nothing ever writes credentials.json, so the
629
+ * legacy file stays frozen at the last `dario login`. Consequences:
630
+ * - `dario doctor` reads credentials.json for its OAuth row and prints
631
+ * 'expired'/'expiring' in pool-of-1 mode even when the live pool token is
632
+ * fresh and the fleet is 200-healthy — an alarm-inducing false positive.
633
+ * - any other reader of credentials.json (a co-resident Claude Code, an ops
634
+ * script) sees a stale token indefinitely.
635
+ *
636
+ * Fix: whenever the pool refreshes the `login` account, mirror the new token
637
+ * into credentials.json so the legacy file tracks the pool store. Only the
638
+ * `login` alias is mirrored — it's the one back-filled FROM credentials.json
639
+ * (ensureLoginCredentialsInPool), so it's the only account whose canonical
640
+ * home is that file; `work`/`personal` accounts have no legacy counterpart.
641
+ *
642
+ * Freshness-guarded, symmetric with #805: mirror only when the refreshed login
643
+ * token is strictly NEWER than the current credentials.json (by `expiresAt`).
644
+ * A newer-or-equal credentials.json means another process (e.g. a concurrent
645
+ * `dario login --force-reauth`) just wrote a fresher family — never clobber it;
646
+ * resyncLoginFromCredentialsIfStale pulls that direction on next startup.
647
+ *
648
+ * Best-effort: a mirror failure must never fail the refresh that produced the
649
+ * live token. Returns one of:
650
+ * - 'skip-not-login' : alias isn't the reserved `login` — nothing to mirror
651
+ * - 'mirrored' : credentials.json updated to the pool token
652
+ * - 'creds-newer' : credentials.json is newer-or-equal — left untouched
653
+ */
654
+ export async function mirrorLoginToCredentials(refreshed) {
655
+ if (refreshed.alias !== MIGRATED_LOGIN_ALIAS)
656
+ return 'skip-not-login';
657
+ const creds = await loadCredentials();
658
+ const currentExpiry = creds?.claudeAiOauth?.expiresAt ?? 0;
659
+ // Only mirror a strictly-newer pool token. Equal expiry means the files
660
+ // already agree (or a same-second write elsewhere); newer credentials.json
661
+ // is another process's fresher family we must not overwrite (#805 direction).
662
+ if (currentExpiry >= refreshed.expiresAt)
663
+ return 'creds-newer';
664
+ await saveCredentialsTokens({
665
+ accessToken: refreshed.accessToken,
666
+ refreshToken: refreshed.refreshToken,
667
+ expiresAt: refreshed.expiresAt,
668
+ scopes: refreshed.scopes ?? creds?.claudeAiOauth?.scopes ?? [],
669
+ });
670
+ return 'mirrored';
671
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_version": "2.1.214",
3
- "_captured": "2026-07-14T06:32:01.705Z",
3
+ "_captured": "2026-07-18T14:14:16.825Z",
4
4
  "_source": "bundled",
5
5
  "_schemaVersion": 3,
6
6
  "agent_identity": "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
@@ -8,7 +8,7 @@
8
8
  "tools": [
9
9
  {
10
10
  "name": "Agent",
11
- "description": "Launch a new agent to handle complex, multi-step tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types are listed in <system-reminder> messages in the conversation.\n\nWhen using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.\n\n## When to use\n\nReach for this when the task matches an available agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate it and you keep the conclusion, not the file dumps. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you've delegated a search, don't also run it yourself — wait for the result.\n\n- The agent's final message is returned to you as the tool result; it is not shown to the user — relay what matters.\n- Use SendMessage with the agent's ID or name to continue a previously spawned agent with its context intact; a new Agent call starts fresh.\n- Each agent type's model, reasoning effort, and tools come from its definition (`.claude/agents/*.md` frontmatter or SDK `agents`).\n- `isolation: \"worktree\"` gives the agent its own git worktree (auto-cleaned if unchanged).\n- Subagents run in the background by default; you'll be notified when one completes. Pass `run_in_background: false` for a synchronous run when you need the result before continuing.",
11
+ "description": "Launch a new agent to handle complex, multi-step tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types are listed in <system-reminder> messages in the conversation.\n\nWhen using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.\n\n## When to use\n\nReach for this when the task matches an available agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate it and you keep the conclusion, not the file dumps. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you've delegated a search, don't also run it yourself — wait for the result.\n\n- The agent's final report is not shown to the user — relay what matters.\n- Use SendMessage with the agent's ID or name to continue a previously spawned agent with its context intact; a new Agent call starts fresh.\n- Each agent type's model, reasoning effort, and tools come from its definition (`.claude/agents/*.md` frontmatter or SDK `agents`).\n- `isolation: \"worktree\"` gives the agent its own git worktree (auto-cleaned if unchanged).\n- Subagents run in the background by default; you'll be notified when one completes. Pass `run_in_background: false` for a synchronous run when you need the result before continuing. Never fabricate or predict a pending agent's results — the notification is never something you write yourself; if the user asks before it arrives, say it's still running.",
12
12
  "input_schema": {
13
13
  "$schema": "https://json-schema.org/draft/2020-12/schema",
14
14
  "type": "object",
@@ -939,6 +939,11 @@
939
939
  "description": "One-sentence statement of the defect",
940
940
  "type": "string"
941
941
  },
942
+ "short_summary": {
943
+ "description": "Compressed label for compact UI (≤60 chars): the claim alone, no rationale or consequence clause",
944
+ "type": "string",
945
+ "maxLength": 60
946
+ },
942
947
  "failure_scenario": {
943
948
  "description": "Concrete inputs/state → wrong output/crash",
944
949
  "type": "string"
@@ -1038,7 +1043,7 @@
1038
1043
  },
1039
1044
  {
1040
1045
  "name": "Skill",
1041
- "description": "Execute a skill within the main conversation\n\nWhen users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.\n\nWhen users reference a \"slash command\" or \"/<something>\", they are referring to a skill. Use this tool to invoke it.\n\nHow to invoke:\n- Set `skill` to the exact name of an available skill (no leading slash). For plugin-namespaced skills use the fully qualified `plugin:skill` form.\n- Set `args` to pass optional arguments.\n- Some skills are scoped to a directory: their name is prefixed with the directory (e.g. `apps/web:deploy`) and their description says which directory they apply to. When a skill name has both a scoped and an unscoped variant, pick by the files you are working on: if the files are under a variant's directory, invoke that variant (most specific directory wins); otherwise invoke the unscoped one.\n\nImportant:\n- Available skills are listed in system-reminder messages in the conversation\n- Only invoke a skill that appears in that list, or one the user explicitly typed as `/<name>` in their message. Never guess or invent a skill name from training data; otherwise do not call this tool\n- When a skill matches the user's request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task\n- NEVER mention a skill without actually calling this tool\n- Do not invoke a skill that is already running\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\n- If you see a <command-name> tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling this tool again\n",
1046
+ "description": "Invoke a skill.\n\nA skill is a packaged set of instructions the user or project has set up for a particular kind of task (deploy steps, a review checklist, a repo-specific workflow). Available skills appear in a system-reminder listing with one-line descriptions. When the task at hand is one a listed skill covers, call this tool first the skill's instructions load into the turn for you to follow in place of your default approach; some skills instead run in a subagent and return the finished result. Users may also ask for one by name (`/<name>`, or \"slash command\"); that's a request to invoke it.\n\n- `skill`: exact name from the listing, no leading slash. Plugin skills use `plugin:skill`. Directory-scoped skills are listed with a path prefix (`apps/web:deploy`); when both scoped and unscoped variants of a name exist, pick the one whose directory contains the files you're working on (most specific wins; unscoped otherwise).\n- `args`: optional arguments to pass through.\n\nOnly names from the listing (or that the user typed explicitly) are valid. Built-in CLI commands (`/help`, `/clear`, ) aren't skills. If a `<command-name>` block is already present this turn, the skill is loaded follow it directly rather than calling again.\n",
1042
1047
  "input_schema": {
1043
1048
  "$schema": "https://json-schema.org/draft/2020-12/schema",
1044
1049
  "type": "object",
package/dist/oauth.d.ts CHANGED
@@ -73,6 +73,15 @@ export declare function loadCredentials(): Promise<CredentialsFile | null>;
73
73
  * tiebreaker preference. Exported for direct testing.
74
74
  */
75
75
  export declare function pickFreshestCredentials(candidates: CredentialsFile[]): CredentialsFile | null;
76
+ /**
77
+ * Mirror a set of OAuth tokens into the legacy `~/.dario/credentials.json`
78
+ * store. Thin durable-write wrapper over `saveCredentials` for callers outside
79
+ * this module — specifically the pool's login-account mirror (dario#808), which
80
+ * keeps credentials.json tracking the pool store after a background/startup
81
+ * refresh advanced `accounts/login.json`. Shares saveCredentials' durability
82
+ * (fsync temp + dir, #790), cache invalidation, and refresh-dead-flag clear.
83
+ */
84
+ export declare function saveCredentialsTokens(tokens: OAuthTokens): Promise<void>;
76
85
  /**
77
86
  * Automatic OAuth flow using a local callback server (same as Claude Code).
78
87
  * Opens browser, captures the authorization code automatically.
package/dist/oauth.js CHANGED
@@ -424,6 +424,17 @@ async function saveCredentials(creds) {
424
424
  credentialsCache = creds;
425
425
  credentialsCacheTime = Date.now();
426
426
  }
427
+ /**
428
+ * Mirror a set of OAuth tokens into the legacy `~/.dario/credentials.json`
429
+ * store. Thin durable-write wrapper over `saveCredentials` for callers outside
430
+ * this module — specifically the pool's login-account mirror (dario#808), which
431
+ * keeps credentials.json tracking the pool store after a background/startup
432
+ * refresh advanced `accounts/login.json`. Shares saveCredentials' durability
433
+ * (fsync temp + dir, #790), cache invalidation, and refresh-dead-flag clear.
434
+ */
435
+ export async function saveCredentialsTokens(tokens) {
436
+ await saveCredentials({ claudeAiOauth: tokens });
437
+ }
427
438
  /**
428
439
  * Automatic OAuth flow using a local callback server (same as Claude Code).
429
440
  * Opens browser, captures the authorization code automatically.
package/dist/proxy.js CHANGED
@@ -16,7 +16,7 @@ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCo
16
16
  import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS } from './analytics.js';
17
17
  import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
18
18
  import { notify as osNotify } from './notify.js';
19
- import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool } from './accounts.js';
19
+ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials } from './accounts.js';
20
20
  import { handleAdminRequest } from './admin-api.js';
21
21
  import { createTokenBucket } from './rate-limit.js';
22
22
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
@@ -1139,6 +1139,9 @@ export async function startProxy(opts = {}) {
1139
1139
  if (resyncResult === 'resynced') {
1140
1140
  console.log('[dario] re-synced pool `login` account from current credentials.json (was stale; dario#235)');
1141
1141
  }
1142
+ else if (resyncResult === 'creds-stale') {
1143
+ console.log('[dario] kept the newer pool `login` token; credentials.json is stale — NOT overwriting (dario#805)');
1144
+ }
1142
1145
  // Admin mode (#599) manages the pool over HTTP: it may legitimately start
1143
1146
  // with zero accounts and returns a clean 503 until one is added via
1144
1147
  // POST /admin/login/*, taking effect with no restart (see onAccountsChanged).
@@ -1230,6 +1233,11 @@ export async function startProxy(opts = {}) {
1230
1233
  return;
1231
1234
  const refreshed = await refreshAccountToken(saved);
1232
1235
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1236
+ // Mirror a refreshed `login` token back to credentials.json so the
1237
+ // legacy file (and `dario doctor`) tracks the pool store (#808).
1238
+ await mirrorLoginToCredentials(refreshed).catch((err) => {
1239
+ console.error(`[dario] login→credentials.json mirror failed (startup) for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
1240
+ });
1233
1241
  console.error(`[dario] Startup refresh recovered account ${acc.alias} (was expired/expiring).`);
1234
1242
  }
1235
1243
  catch (err) {
@@ -1250,6 +1258,11 @@ export async function startProxy(opts = {}) {
1250
1258
  continue;
1251
1259
  const refreshed = await refreshAccountToken(saved);
1252
1260
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1261
+ // Mirror a refreshed `login` token back to credentials.json so the
1262
+ // legacy file (and `dario doctor`) tracks the pool store (#808).
1263
+ await mirrorLoginToCredentials(refreshed).catch((err) => {
1264
+ console.error(`[dario] login→credentials.json mirror failed (background) for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
1265
+ });
1253
1266
  }
1254
1267
  catch (err) {
1255
1268
  console.error(`[dario] Background refresh failed for ${acc.alias}: ${err instanceof Error ? err.message : err}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.2.9",
3
+ "version": "5.2.11",
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": {