@askalf/dario 3.31.12 → 3.31.14
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 +22 -4
- package/dist/accounts.d.ts +34 -0
- package/dist/accounts.js +68 -4
- package/dist/cc-authorize-probe.js +5 -1
- package/dist/cc-template-data.json +5 -5
- package/dist/cli.js +17 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oauth.js +6 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ OAuth-backed Claude Max, billed against your plan instead of the API. Activated
|
|
|
160
160
|
- **Drift detection** (v3.17). On startup dario probes the installed `claude` binary and compares against the captured template. Mismatch triggers a forced refresh and prints a one-line warning. Users never silently sit on a stale template again.
|
|
161
161
|
- **Compat matrix** (v3.17, bumped in v3.19.5). `SUPPORTED_CC_RANGE` is encoded in code; installed CC outside the band prints a warn (untested above) or fail (below min) — zero-dep dotted-numeric comparator, no `semver` import per the dep policy.
|
|
162
162
|
- **Billing tag** reconstructed using CC's own algorithm: `x-anthropic-billing-header: cc_version=<version>.<build_tag>; cc_entrypoint=cli; cch=<5-char-hex>;` where `build_tag = SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)`.
|
|
163
|
-
- **OAuth config auto-detection** from the installed CC binary. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release. Cache at `~/.dario/cc-oauth-cache-
|
|
163
|
+
- **OAuth config auto-detection** from the installed CC binary. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release. Cache at `~/.dario/cc-oauth-cache-v6.json`, keyed by the CC binary fingerprint. Cache path bumps each time the canonical OAuth config shape changes so stale caches regenerate automatically on upgrade.
|
|
164
164
|
- **Multi-account pool mode** — see [Multi-account pool mode](#multi-account-pool-mode). Automatic when 2+ accounts are configured.
|
|
165
165
|
- **Framework scrubbing** — known third-party identity markers (`OpenClaw`, `sessions_*` prefixes, orchestration tags) stripped from system prompt and message content before the request leaves your machine.
|
|
166
166
|
- **Atomic cache writes + cache corruption recovery** (v3.17). Template cache writes go through pid-qualified `.tmp` + `rename`, so an OS crash mid-write doesn't leave a half-written file. Unparseable cache files get quarantined to `cc-template.live.json.bad-<timestamp>` and dario self-heals on the next capture.
|
|
@@ -202,6 +202,8 @@ dario accounts list
|
|
|
202
202
|
dario proxy
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
If you already have a single-account `dario login` set up and run `dario accounts add <alias>` for the first time, dario **back-fills** your existing login credentials into the pool under the reserved alias `login` before running OAuth for the new alias. Net effect: your first `accounts add` gives you two pool accounts (login + new alias), pool mode activates immediately. Back-fill is one-shot, idempotent, and never touches your existing `credentials.json` — if you later `dario accounts remove` below the 2+ threshold, single-account mode reads it unchanged. Skipped if you explicitly pick `login` as the new alias — your intent wins.
|
|
206
|
+
|
|
205
207
|
Each request picks the account with the highest headroom:
|
|
206
208
|
|
|
207
209
|
```
|
|
@@ -334,11 +336,13 @@ A version marker (`<!-- dario-sub-agent-version: X -->`) embedded in the markdow
|
|
|
334
336
|
|---|---|
|
|
335
337
|
| `dario login [--manual]` | Log in to the Claude backend. Detects CC credentials or runs its own OAuth flow. `--manual` (v3.20) mirrors CC's code-paste flow for SSH / container setups without a browser. |
|
|
336
338
|
| `dario proxy` | Start the local API proxy on port 3456 |
|
|
337
|
-
| `dario doctor` | Aggregated health report — dario / Node / runtime-TLS / CC binary + compat / template + drift / OAuth / pool / backends / sub-agent |
|
|
339
|
+
| `dario doctor [--probe] [--auth-check] [--json]` | Aggregated health report — dario / Node / runtime-TLS / CC binary + compat / template + drift / OAuth / pool / backends / sub-agent. `--probe` (v3.31.7) hits the live `claude.ai/oauth/authorize` endpoint and surfaces the verdict, so scope-policy drift is catchable from a user's machine (not just CI). `--auth-check` (v3.31.9) opens a one-shot `x-api-key` listener and classifies whatever a client actually sends (match / mismatch / no-auth / timeout), with only redacted previews in output. `--json` (v3.31.8) emits structured output for claude-bridge's `/status`, deepdive's health probes, and CI scrapers. |
|
|
340
|
+
| `dario config [--json]` | Prints the effective dario configuration with credentials redacted. Complementary to `doctor` — doctor answers *is it working?*, config answers *what IS it?* (v3.31.10) |
|
|
341
|
+
| `dario upgrade` | Safe wrapper over `npm install -g @askalf/dario@latest` — probes npm for the `@latest` version first (3s timeout, 60s cache), refuses to run if already on latest, fails with a clear hint if npm is missing. (v3.31.10) |
|
|
338
342
|
| `dario status` | Show Claude backend OAuth token health and expiry |
|
|
339
343
|
| `dario refresh` | Force an immediate Claude token refresh |
|
|
340
344
|
| `dario logout` | Delete stored Claude credentials |
|
|
341
|
-
| `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management |
|
|
345
|
+
| `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management. `add <alias>` on a fresh pool auto back-fills your existing `dario login` credentials as `login`, so your first `add` trips the 2+ pool threshold on its own — see [Multi-account pool mode](#multi-account-pool-mode). |
|
|
342
346
|
| `dario backend list` / `add <name> --key=<key> [--base-url=<url>]` / `remove <name>` | OpenAI-compat backend management |
|
|
343
347
|
| `dario shim -- <cmd> [args...]` | Run a child process with the in-process fetch patch (see [Shim mode](#shim-mode)) |
|
|
344
348
|
| `dario subagent install` / `remove` / `status` | CC sub-agent lifecycle (v3.26 — see [sub-agent hook](#claude-code-sub-agent-hook-v326)) |
|
|
@@ -760,8 +764,19 @@ Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter,
|
|
|
760
764
|
**Something's wrong. Where do I start?**
|
|
761
765
|
`dario doctor`. One command, one aggregated report — dario version, Node, platform, runtime/TLS classification, CC binary compat, template source + age + drift, OAuth status, pool state, backends, sub-agent install state, home dir. Exit code 1 if any check fails. Paste the output when you file an issue. (If you're inside Claude Code, `dario subagent install` once and then ask CC to "use the dario sub-agent to run doctor" — same output, no context switch.)
|
|
762
766
|
|
|
767
|
+
**OpenClaw returns 401 after I set `DARIO_API_KEY` (or upgrade past v3.30.6).**
|
|
768
|
+
If you run `dario proxy --host=0.0.0.0` (non-loopback), dario requires `DARIO_API_KEY` to be set so it's not an open subscription relay. OpenClaw 2026.2.17+ prefers `~/.openclaw/agents/main/agent/auth-profiles.json` over `openclaw.json`'s `apiKey` field or the `ANTHROPIC_API_KEY` env var — so if you have a stale Anthropic token in `auth-profiles.json` from an earlier setup, OpenClaw sends *that* token instead of `dario`, and dario rejects the request with `Authorization present but value mismatch` (visible under `dario proxy -v`, added in v3.31.2).
|
|
769
|
+
|
|
770
|
+
Three fixes, in order of simplicity:
|
|
771
|
+
|
|
772
|
+
1. **Use loopback.** `dario proxy --host=127.0.0.1` — auth only enforced on non-loopback binds, no `DARIO_API_KEY` required, no OpenClaw changes. Best if you don't actually need LAN reach to dario.
|
|
773
|
+
2. **Delete the Anthropic auth profile.** Remove the `"anthropic:default"` entry from `~/.openclaw/agents/main/agent/auth-profiles.json`. OpenClaw then falls back through the config chain and picks up `ANTHROPIC_API_KEY=dario` from the env. Confirmed working by [@tetsuco in #97](https://github.com/askalf/dario/issues/97).
|
|
774
|
+
3. **Overwrite the auth profile.** `openclaw models auth paste-token --provider anthropic` and paste `dario`. Replaces whatever key was in there — keep a backup if you use it elsewhere.
|
|
775
|
+
|
|
776
|
+
Diagnose with `dario proxy -v` — the reject log (v3.31.2+) reports header-name only (never the value, since it may be a real credential you mistyped) and tells you which of the three configs is actually being hit.
|
|
777
|
+
|
|
763
778
|
**What happens when Anthropic rotates the OAuth config?**
|
|
764
|
-
Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-
|
|
779
|
+
Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v6.json`, keyed by the CC binary fingerprint. The cache path version bumps each time the canonical OAuth config shape changes so stale caches regenerate automatically on upgrade — v3 → v4 in v3.19.4 (scope-list flip CC v2.1.104 → v2.1.107), v4 → v5 in v3.31.3 (authorize URL `claude.com/cai/` → `claude.ai/` host normalization), v5 → v6 in v3.31.4 (6-scope restore after CC v2.1.116).
|
|
765
780
|
|
|
766
781
|
If Anthropic rotates the values before the detector is updated, you can temporarily override any field with env vars (`DARIO_OAUTH_CLIENT_ID`, `DARIO_OAUTH_AUTHORIZE_URL`, `DARIO_OAUTH_TOKEN_URL`, `DARIO_OAUTH_SCOPES`) or by writing `~/.dario/oauth-config.override.json`:
|
|
767
782
|
|
|
@@ -779,6 +794,9 @@ Env vars win over the file. Set `DARIO_OAUTH_DISABLE_OVERRIDE=1` to force pure a
|
|
|
779
794
|
**What happens when Anthropic changes the CC request template?**
|
|
780
795
|
Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
|
|
781
796
|
|
|
797
|
+
**Why does `dario accounts list` show an account called `login` I never added?**
|
|
798
|
+
That's your existing `dario login` credentials, back-filled into the pool automatically on your first `dario accounts add <alias>`. Pool mode activates at 2+ accounts in `~/.dario/accounts/`, and the single-account `credentials.json` store lives outside that directory — so without the back-fill, one `accounts add` would leave you at 1 pool entry and your login account orphaned. The `login` alias is reserved for this path. Safe to `dario accounts remove login` if you don't want it pooled; the original `credentials.json` is untouched by the back-fill, so single-account mode resumes reading it after removal drops you below the 2+ threshold. See [Multi-account pool mode](#multi-account-pool-mode) for the full picture.
|
|
799
|
+
|
|
782
800
|
**First time setup on a fresh Claude account.**
|
|
783
801
|
If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
|
|
784
802
|
```bash
|
package/dist/accounts.d.ts
CHANGED
|
@@ -23,3 +23,37 @@ export declare function _accountRefreshesInFlightSizeForTest(): number;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function addAccountViaOAuth(alias: string): Promise<AccountCredentials>;
|
|
25
25
|
export declare function getAccountsDir(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Alias reserved for credentials auto-migrated from the single-account
|
|
28
|
+
* `dario login` store. Named `login` so it's semantically obvious where
|
|
29
|
+
* the entry came from and unlikely to collide with user-chosen aliases
|
|
30
|
+
* like `work`, `personal`, etc. If a user specifically requests `login`
|
|
31
|
+
* as the alias for `dario accounts add`, the caller falls back to
|
|
32
|
+
* `default` so the migration doesn't step on the user's intent.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MIGRATED_LOGIN_ALIAS = "login";
|
|
35
|
+
/**
|
|
36
|
+
* Promote the user's existing single-account `dario login` credentials
|
|
37
|
+
* (`~/.dario/credentials.json`, `~/.claude/.credentials.json`, or OS
|
|
38
|
+
* keychain — whichever `loadCredentials` finds) into the pool under a
|
|
39
|
+
* reserved alias.
|
|
40
|
+
*
|
|
41
|
+
* Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
|
|
42
|
+
* A user with one `dario login` account + one `dario accounts add bar`
|
|
43
|
+
* ends up with only one account in `accounts/` (bar), pool mode never
|
|
44
|
+
* trips, and the login account is effectively orphaned while pool is off.
|
|
45
|
+
* Calling this on the first `dario accounts add` back-fills the login
|
|
46
|
+
* account into the pool so the second `add` crosses the threshold.
|
|
47
|
+
*
|
|
48
|
+
* Idempotent: no-op if `accounts/` already has any entry, no-op if no
|
|
49
|
+
* credentials are reachable anywhere. Returns the alias written to, or
|
|
50
|
+
* `null` when nothing happened.
|
|
51
|
+
*
|
|
52
|
+
* The source `credentials.json` (if present) is left untouched — single-
|
|
53
|
+
* account mode still reads it if the user later `accounts remove`s down
|
|
54
|
+
* below the pool threshold. Migration is copy-only, never destructive.
|
|
55
|
+
*
|
|
56
|
+
* @param preferredAlias caller may request a specific alias. If it's
|
|
57
|
+
* already the reserved `login` (or collides), falls back to `default`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ensureLoginCredentialsInPool(alias?: string): Promise<string | null>;
|
package/dist/accounts.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
* Multi-account credential storage.
|
|
3
3
|
*
|
|
4
4
|
* Accounts live at `~/.dario/accounts/<alias>.json`. Single-account dario
|
|
5
|
-
*
|
|
6
|
-
* When `~/.dario/accounts/` contains 2+ files the proxy
|
|
7
|
-
* (see pool.ts). Each account has its own independent
|
|
8
|
-
* can refresh without affecting the others.
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* `ensureLoginCredentialsInPool` (below) bridges the two stores on the
|
|
11
|
+
* 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.
|
|
9
14
|
*
|
|
10
15
|
* OAuth config (client_id, scopes, authorize URL, token URL) comes from
|
|
11
16
|
* dario's cc-oauth-detect scanner — the same source the single-account
|
|
@@ -17,6 +22,7 @@ import { homedir } from 'node:os';
|
|
|
17
22
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
18
23
|
import { createServer } from 'node:http';
|
|
19
24
|
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
25
|
+
import { loadCredentials } from './oauth.js';
|
|
20
26
|
const DARIO_DIR = join(homedir(), '.dario');
|
|
21
27
|
const ACCOUNTS_DIR = join(DARIO_DIR, 'accounts');
|
|
22
28
|
/**
|
|
@@ -308,3 +314,61 @@ export async function addAccountViaOAuth(alias) {
|
|
|
308
314
|
export function getAccountsDir() {
|
|
309
315
|
return ACCOUNTS_DIR;
|
|
310
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Alias reserved for credentials auto-migrated from the single-account
|
|
319
|
+
* `dario login` store. Named `login` so it's semantically obvious where
|
|
320
|
+
* the entry came from and unlikely to collide with user-chosen aliases
|
|
321
|
+
* like `work`, `personal`, etc. If a user specifically requests `login`
|
|
322
|
+
* as the alias for `dario accounts add`, the caller falls back to
|
|
323
|
+
* `default` so the migration doesn't step on the user's intent.
|
|
324
|
+
*/
|
|
325
|
+
export const MIGRATED_LOGIN_ALIAS = 'login';
|
|
326
|
+
/**
|
|
327
|
+
* Promote the user's existing single-account `dario login` credentials
|
|
328
|
+
* (`~/.dario/credentials.json`, `~/.claude/.credentials.json`, or OS
|
|
329
|
+
* keychain — whichever `loadCredentials` finds) into the pool under a
|
|
330
|
+
* reserved alias.
|
|
331
|
+
*
|
|
332
|
+
* Why: the pool activation threshold is 2+ accounts in `~/.dario/accounts/`.
|
|
333
|
+
* A user with one `dario login` account + one `dario accounts add bar`
|
|
334
|
+
* ends up with only one account in `accounts/` (bar), pool mode never
|
|
335
|
+
* trips, and the login account is effectively orphaned while pool is off.
|
|
336
|
+
* Calling this on the first `dario accounts add` back-fills the login
|
|
337
|
+
* account into the pool so the second `add` crosses the threshold.
|
|
338
|
+
*
|
|
339
|
+
* Idempotent: no-op if `accounts/` already has any entry, no-op if no
|
|
340
|
+
* credentials are reachable anywhere. Returns the alias written to, or
|
|
341
|
+
* `null` when nothing happened.
|
|
342
|
+
*
|
|
343
|
+
* The source `credentials.json` (if present) is left untouched — single-
|
|
344
|
+
* account mode still reads it if the user later `accounts remove`s down
|
|
345
|
+
* below the pool threshold. Migration is copy-only, never destructive.
|
|
346
|
+
*
|
|
347
|
+
* @param preferredAlias caller may request a specific alias. If it's
|
|
348
|
+
* already the reserved `login` (or collides), falls back to `default`.
|
|
349
|
+
*/
|
|
350
|
+
export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS) {
|
|
351
|
+
if (!safeAliasPath(alias))
|
|
352
|
+
return null;
|
|
353
|
+
const existing = await listAccountAliases();
|
|
354
|
+
if (existing.length > 0)
|
|
355
|
+
return null;
|
|
356
|
+
const creds = await loadCredentials();
|
|
357
|
+
const tok = creds?.claudeAiOauth;
|
|
358
|
+
if (!tok?.accessToken || !tok?.refreshToken)
|
|
359
|
+
return null;
|
|
360
|
+
const identity = (await detectClaudeIdentity()) ?? {
|
|
361
|
+
deviceId: randomUUID(),
|
|
362
|
+
accountUuid: randomUUID(),
|
|
363
|
+
};
|
|
364
|
+
await saveAccount({
|
|
365
|
+
alias,
|
|
366
|
+
accessToken: tok.accessToken,
|
|
367
|
+
refreshToken: tok.refreshToken,
|
|
368
|
+
expiresAt: tok.expiresAt,
|
|
369
|
+
scopes: tok.scopes ?? [],
|
|
370
|
+
deviceId: identity.deviceId,
|
|
371
|
+
accountUuid: identity.accountUuid,
|
|
372
|
+
});
|
|
373
|
+
return alias;
|
|
374
|
+
}
|
|
@@ -131,7 +131,11 @@ export function buildProbeAuthorizeUrl(cfg) {
|
|
|
131
131
|
scope: cfg.scopes,
|
|
132
132
|
code_challenge: pkceChallenge(),
|
|
133
133
|
code_challenge_method: 'S256',
|
|
134
|
-
|
|
134
|
+
// 32 bytes — match what CC v2.1.116+ actually sends. See dario#71.
|
|
135
|
+
// Shorter states produce "Invalid request format" from Anthropic's
|
|
136
|
+
// authorize endpoint, which the probe classifier would otherwise mis-
|
|
137
|
+
// attribute to drift when it's actually our own request shape.
|
|
138
|
+
state: base64url(randomBytes(32)),
|
|
135
139
|
});
|
|
136
140
|
return `${cfg.authorizeUrl}?${params.toString()}`;
|
|
137
141
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_version": "2.1.
|
|
3
|
-
"_captured": "2026-04-
|
|
2
|
+
"_version": "2.1.119",
|
|
3
|
+
"_captured": "2026-04-24T13:28:12.469Z",
|
|
4
4
|
"_source": "bundled",
|
|
5
5
|
"_schemaVersion": 3,
|
|
6
6
|
"agent_identity": "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
|
|
@@ -484,7 +484,7 @@
|
|
|
484
484
|
},
|
|
485
485
|
{
|
|
486
486
|
"name": "Monitor",
|
|
487
|
-
"description": "Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user, even if one lands while you're waiting for the user to answer a question.\n\
|
|
487
|
+
"description": "Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user, even if one lands while you're waiting for the user to answer a question.\n\nPick by how many notifications you need:\n- **One** (\"tell me when the server is ready / the build finishes\") → use **Bash with `run_in_background`** and a command that exits when the condition is true, e.g. `until grep -q \"Ready in\" dev.log; do sleep 0.5; done`. You get a single completion notification when it exits.\n- **One per occurrence, indefinitely** (\"tell me every time an ERROR line appears\") → Monitor with an unbounded command (`tail -f`, `inotifywait -m`, `while true`).\n- **One per occurrence, until a known end** (\"emit each CI step result, stop when the run completes\") → Monitor with a command that emits lines and then exits.\n\nYour script's stdout is the event stream. Each line becomes a notification. Exit ends the watch.\n\n # Each matching log line is an event\n tail -f /var/log/app.log | grep --line-buffered \"ERROR\"\n\n # Each file change is an event\n inotifywait -m --format '%e %f' /watched/dir\n\n # Poll GitHub for new PR comments and emit one line per new comment\n last=$(date -u +%Y-%m-%dT%H:%M:%SZ)\n while true; do\n now=$(date -u +%Y-%m-%dT%H:%M:%SZ)\n gh api \"repos/owner/repo/issues/123/comments?since=$last\" --jq '.[] | \"\\(.user.login): \\(.body)\"'\n last=$now; sleep 30\n done\n\n # Node script that emits events as they arrive (e.g. WebSocket listener)\n node watch-for-events.js\n\n # Per-occurrence with a natural end: emit each CI check as it lands, exit when the run completes\n prev=\"\"\n while true; do\n s=$(gh pr checks 123 --json name,bucket)\n cur=$(jq -r '.[] | select(.bucket!=\"pending\") | \"\\(.name): \\(.bucket)\"' <<<\"$s\" | sort)\n comm -13 <(echo \"$prev\") <(echo \"$cur\")\n prev=$cur\n jq -e 'all(.bucket!=\"pending\")' <<<\"$s\" >/dev/null && break\n sleep 30\n done\n\n**Don't use an unbounded command for a single notification.** `tail -f`, `inotifywait -m`, and `while true` never exit on their own, so the monitor stays armed until timeout even after the event has fired. For \"tell me when X is ready,\" use Bash `run_in_background` with an `until` loop instead (one notification, ends in seconds). Note that `tail -f log | grep -m 1 ...` does *not* fix this: if the log goes quiet after the match, `tail` never receives SIGPIPE and the pipeline hangs anyway.\n\n**Script quality:**\n- Always use `grep --line-buffered` in pipes — without it, pipe buffering delays events by minutes.\n- In poll loops, handle transient failures (`curl ... || true`) — one failed request shouldn't kill the monitor.\n- Poll intervals: 30s+ for remote APIs (rate limits), 0.5-1s for local checks.\n- Write a specific `description` — it appears in every notification (\"errors in deploy.log\" not \"watching logs\").\n- Only stdout is the event stream. Stderr goes to the output file (readable via Read) but does not trigger notifications — for a command you run directly (e.g. `python train.py 2>&1 | grep --line-buffered ...`), merge stderr with `2>&1` so its failures reach your filter. (No effect on `tail -f` of an existing log — that file only contains what its writer redirected.)\n\n**Coverage — silence is not success.** When watching a job or process for an outcome, your filter must match every terminal state, not just the happy path. A monitor that greps only for the success marker stays silent through a crashloop, a hung process, or an unexpected exit — and silence looks identical to \"still running.\" Before arming, ask: *if this process crashed right now, would my filter emit anything?* If not, widen it.\n\n # Wrong — silent on crash, hang, or any non-success exit\n tail -f run.log | grep --line-buffered \"elapsed_steps=\"\n\n # Right — one alternation covering progress + the failure signatures you'd act on\n tail -f run.log | grep -E --line-buffered \"elapsed_steps=|Traceback|Error|FAILED|assert|Killed|OOM\"\n\nFor poll loops checking job state, emit on every terminal status (`succeeded|failed|cancelled|timeout`), not just success. If you cannot confidently enumerate the failure signatures, broaden the grep alternation rather than narrow it — some extra noise is better than missing a crashloop.\n\n**Output volume**: Every stdout line is a conversation message, so the filter should be selective — but selective means \"the lines you'd act on,\" not \"only good news.\" Never pipe raw logs; use `grep --line-buffered`, `awk`, or a wrapper that emits exactly the success and failure signals you care about. Monitors that produce too many events are automatically stopped; restart with a tighter filter if this happens.\n\nStdout lines within 200ms are batched into a single notification, so multiline output from a single event groups naturally.\n\nThe script runs in the same shell environment as Bash. Exit ends the watch (exit code is reported). Timeout → killed. Set `persistent: true` for session-length watches (PR monitoring, log tails) — the monitor runs until you call TaskStop or the session ends. Use TaskStop to cancel early.",
|
|
488
488
|
"input_schema": {
|
|
489
489
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
490
490
|
"type": "object",
|
|
@@ -973,7 +973,7 @@
|
|
|
973
973
|
"anthropic_beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24,afk-mode-2026-01-31",
|
|
974
974
|
"header_values": {
|
|
975
975
|
"accept": "application/json",
|
|
976
|
-
"user-agent": "claude-cli/2.1.
|
|
976
|
+
"user-agent": "claude-cli/2.1.119 (external, sdk-cli)",
|
|
977
977
|
"x-stainless-arch": "x64",
|
|
978
978
|
"x-stainless-lang": "js",
|
|
979
979
|
"x-stainless-os": "Windows",
|
|
@@ -998,5 +998,5 @@
|
|
|
998
998
|
"output_config",
|
|
999
999
|
"stream"
|
|
1000
1000
|
],
|
|
1001
|
-
"_supportedMaxTested": "2.1.
|
|
1001
|
+
"_supportedMaxTested": "2.1.119"
|
|
1002
1002
|
}
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ import { homedir } from 'node:os';
|
|
|
38
38
|
import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
|
|
39
39
|
import { startProxy, sanitizeError } from './proxy.js';
|
|
40
40
|
import { VALID_EFFORT_VALUES } from './cc-template.js';
|
|
41
|
-
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount } from './accounts.js';
|
|
41
|
+
import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
|
|
42
42
|
import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
|
|
43
43
|
const args = process.argv.slice(2);
|
|
44
44
|
const command = args[0] ?? 'proxy';
|
|
@@ -463,6 +463,22 @@ async function accounts() {
|
|
|
463
463
|
console.error(`[dario] Account "${alias}" already exists. Remove it first with \`dario accounts remove ${alias}\`.`);
|
|
464
464
|
process.exit(1);
|
|
465
465
|
}
|
|
466
|
+
// If the user has `dario login` credentials on disk or in the keychain
|
|
467
|
+
// and the pool is empty, migrate those credentials into the pool first.
|
|
468
|
+
// Otherwise the new account lives alone in accounts/, pool mode never
|
|
469
|
+
// trips the 2+ threshold, and the login account is orphaned from the
|
|
470
|
+
// pool until the user figures out they have to re-`accounts add` it.
|
|
471
|
+
// Skip silently when the user explicitly picks the reserved alias —
|
|
472
|
+
// their intent wins, they can run `accounts add` again for the login
|
|
473
|
+
// migration under a different alias.
|
|
474
|
+
if (existing.length === 0 && alias !== MIGRATED_LOGIN_ALIAS) {
|
|
475
|
+
const migrated = await ensureLoginCredentialsInPool();
|
|
476
|
+
if (migrated) {
|
|
477
|
+
console.log('');
|
|
478
|
+
console.log(` Migrated your existing \`dario login\` account into the pool as "${migrated}".`);
|
|
479
|
+
console.log(` (Pool mode activates on 2+ accounts — this back-fill plus "${alias}" crosses that.)`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
466
482
|
console.log('');
|
|
467
483
|
console.log(` Adding account "${alias}" to the pool...`);
|
|
468
484
|
console.log('');
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { OAuthTokens, CredentialsFile } from './oauth.js';
|
|
|
9
9
|
export { startProxy, sanitizeError } from './proxy.js';
|
|
10
10
|
export { AccountPool, parseRateLimits } from './pool.js';
|
|
11
11
|
export type { PoolAccount, PoolStatus, RateLimitSnapshot, AccountIdentity } from './pool.js';
|
|
12
|
-
export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, getAccountsDir, } from './accounts.js';
|
|
12
|
+
export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS, getAccountsDir, } from './accounts.js';
|
|
13
13
|
export type { AccountCredentials } from './accounts.js';
|
|
14
14
|
export { Analytics } from './analytics.js';
|
|
15
15
|
export type { RequestRecord, AnalyticsSummary } from './analytics.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { startProxy, sanitizeError } from './proxy.js';
|
|
|
10
10
|
// contains 2+ accounts; see README for the progression from single-account
|
|
11
11
|
// mode to pool mode).
|
|
12
12
|
export { AccountPool, parseRateLimits } from './pool.js';
|
|
13
|
-
export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, getAccountsDir, } from './accounts.js';
|
|
13
|
+
export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS, getAccountsDir, } from './accounts.js';
|
|
14
14
|
export { Analytics } from './analytics.js';
|
|
15
15
|
// Multi-provider backends (v3.6.0+). Secondary OpenAI-compat providers
|
|
16
16
|
// (OpenAI, OpenRouter, Groq, local LiteLLM, etc.) configured via
|
package/dist/oauth.js
CHANGED
|
@@ -198,7 +198,10 @@ async function saveCredentials(creds) {
|
|
|
198
198
|
export async function startAutoOAuthFlow() {
|
|
199
199
|
const { createServer } = await import('node:http');
|
|
200
200
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
201
|
-
|
|
201
|
+
// 32 random bytes → 43-char base64url state. See dario#71 — Anthropic's
|
|
202
|
+
// authorize endpoint rejects shorter states with "Invalid request format";
|
|
203
|
+
// CC v2.1.116+ ships 32. Keep in lockstep with CC's entropy-per-state.
|
|
204
|
+
const state = base64url(randomBytes(32));
|
|
202
205
|
return new Promise((resolve, reject) => {
|
|
203
206
|
const server = createServer((req, res) => {
|
|
204
207
|
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
@@ -387,7 +390,8 @@ export function detectHeadlessEnvironment() {
|
|
|
387
390
|
*/
|
|
388
391
|
export async function startManualOAuthFlow() {
|
|
389
392
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
390
|
-
|
|
393
|
+
// 32 bytes — same reason as startAutoOAuthFlow. See dario#71.
|
|
394
|
+
const state = base64url(randomBytes(32));
|
|
391
395
|
const cfg = await getOAuthConfig();
|
|
392
396
|
const authUrl = buildManualAuthorizeUrl(cfg, codeChallenge, state);
|
|
393
397
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.31.
|
|
3
|
+
"version": "3.31.14",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"node": ">=18.0.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@types/node": "^
|
|
72
|
+
"@types/node": "^25.6.0",
|
|
73
73
|
"tsx": "^4.19.0",
|
|
74
74
|
"typescript": "^5.7.0"
|
|
75
75
|
}
|