@askalf/dario 3.4.6 → 3.5.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/README.md +68 -16
- package/dist/accounts.d.ts +23 -0
- package/dist/accounts.js +253 -0
- package/dist/analytics.d.ts +99 -0
- package/dist/analytics.js +198 -0
- package/dist/cli.js +113 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/pool.d.ts +68 -0
- package/dist/pool.js +212 -0
- package/dist/proxy.js +142 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,9 +29,11 @@ dario is a local process that turns your Claude Max or Pro subscription into an
|
|
|
29
29
|
|
|
30
30
|
Install it once, log in once (using your existing Claude Code credentials if you have them), and from that point on every tool that speaks the Anthropic API or the OpenAI chat completions API — Cursor, Continue, Aider, LiteLLM, your own scripts, whatever — can reach Claude through `http://localhost:3456`.
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
**Single-account mode is the default.** You don't need an account anywhere, you don't need to wait for anything, and nothing phones home. Install and run.
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**Pool mode** (new in v3.5.0) lifts multi-account routing into dario itself. Add two or more Claude subscriptions with `dario accounts add`, and dario starts selecting per request by the account with the most headroom, marking exhausted accounts rejected until they reset. No hosted platform required — you run the pool on your machine, against your own subscriptions. See [Multi-Account Pool Mode](#multi-account-pool-mode) below for the details.
|
|
35
|
+
|
|
36
|
+
Separately, [askalf](https://askalf.org) is the hosted platform that does the things a local proxy on your machine can't — browser and desktop control, scheduling, persistent memory, 24/7 hosted fleets. Different problem, different tool. Dario does not depend on askalf, and askalf is not required to use any dario feature.
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
|
@@ -75,9 +77,9 @@ No separate API key. No Extra Usage charges. No rebuilding your workflow around
|
|
|
75
77
|
|
|
76
78
|
**Use dario if** you already pay for Claude Max or Pro and you want Claude inside the tools you already use, without paying API rates for every request or routing your work through a second hosted stack.
|
|
77
79
|
|
|
78
|
-
**Use
|
|
80
|
+
**Use dario pool mode if** you're running multi-agent workloads and hitting per-subscription rate limits — add 2–N accounts with `dario accounts add` and dario handles headroom-aware routing across them, all on your machine, against your own subscriptions. No hosted stack to sign up for. See [Multi-Account Pool Mode](#multi-account-pool-mode).
|
|
79
81
|
|
|
80
|
-
**Use
|
|
82
|
+
**Use the Anthropic API directly if** you need platform-native primitives, vendor-managed production usage, high-scale control, or SLAs your subscription tier doesn't cover. Dario isn't trying to replace the API — it's trying to unlock the subscription you already bought.
|
|
81
83
|
|
|
82
84
|
**Don't use dario if** you want a subprocess bridge that shells out to `claude --print` under the hood. Those tools (openclaw-claude-bridge and similar) work well for single-team single-machine workloads that can accept a one-subscription rate ceiling and a one-machine deployment. Dario is the API-path alternative, which trades that simplicity for pooling-friendly behavior on the wire. Different tradeoffs, different tool.
|
|
83
85
|
|
|
@@ -148,33 +150,80 @@ Use it when the upstream tool already builds a Claude-Code-shaped request on its
|
|
|
148
150
|
|
|
149
151
|
### Detection scope
|
|
150
152
|
|
|
151
|
-
Dario is a **per-request layer**. Every request it sends upstream is designed to be indistinguishable from a Claude Code request, and the per-request scrubbing hardened in v3.4.5 makes that meaningfully harder to fingerprint than it was when v3.0 first shipped. What dario cannot do at the per-request level is defend against Anthropic's session-level
|
|
153
|
+
Dario is a **per-request layer**. Every request it sends upstream is designed to be indistinguishable from a Claude Code request, and the per-request scrubbing hardened in v3.4.5 makes that meaningfully harder to fingerprint than it was when v3.0 first shipped. What dario cannot do at the per-request level is defend against Anthropic's session-level behavioral classifiers — those operate on cumulative per-OAuth aggregates (token throughput, conversation depth, streaming duration, inter-arrival timing) and no amount of per-request hardening reaches them. The practical answer to that problem is *distributing* load across multiple subscriptions so no single account accumulates enough signal to trip the classifier — which is what pool mode (below) does.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Multi-Account Pool Mode
|
|
158
|
+
|
|
159
|
+
*New in v3.5.0.* Dario can manage multiple Claude subscriptions and route each request to the account with the most headroom. Single-account dario is unchanged and remains the default — pool mode activates **only** when `~/.dario/accounts/` contains 2+ accounts.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Add accounts to the pool. Each runs its own OAuth flow.
|
|
163
|
+
dario accounts add work
|
|
164
|
+
dario accounts add personal
|
|
165
|
+
dario accounts add side-project
|
|
166
|
+
|
|
167
|
+
# List them
|
|
168
|
+
dario accounts list
|
|
169
|
+
|
|
170
|
+
# Start the proxy — pool mode activates automatically
|
|
171
|
+
dario proxy
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### How it routes
|
|
175
|
+
|
|
176
|
+
Each incoming request picks the account with the highest **headroom**:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
headroom = 1 - max(util_5h, util_7d)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The response's `anthropic-ratelimit-unified-*` headers are parsed back into the pool so the next request sees fresh utilization. An account that returns a 429 is marked `rejected` and routed around until its window resets. When every account is exhausted, incoming requests queue for up to 60 seconds waiting for headroom to reappear, with backoff-aware draining.
|
|
183
|
+
|
|
184
|
+
Accounts can use different plans — mix Max and Pro accounts freely. The pool doesn't care about tier, only headroom.
|
|
185
|
+
|
|
186
|
+
### Why pool over per-request tricks alone
|
|
187
|
+
|
|
188
|
+
Per-request template replay is necessary but not sufficient for multi-agent workloads. Anthropic's classifier operates on cumulative per-OAuth-session aggregates (see the [FAQ entry](#faq) on multi-agent reclassification), and no amount of per-request hardening reaches that layer. The practical answer is *distribution* — spread load so no single account accumulates enough signal to trip anything. Pool mode is the piece that does that, and the headroom-aware selection means you don't have to think about which account is which; dario picks.
|
|
189
|
+
|
|
190
|
+
### Inspection endpoints
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Live pool snapshot — per-account utilization, claim, status
|
|
194
|
+
curl http://localhost:3456/accounts
|
|
195
|
+
|
|
196
|
+
# Pool analytics — per-account / per-model stats, burn-rate, exhaustion predictions
|
|
197
|
+
curl http://localhost:3456/analytics
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Known scope for v3.5.0
|
|
201
|
+
|
|
202
|
+
Pool mode v3.5.0 ships **headroom-aware selection across requests**. It does not yet retry a single in-flight request against a different account when that request 429s — that ships in v3.5.1 along with analytics recording wiring. Across-request routing is already effective: a 429 on one request immediately marks that account rejected, and the next request goes somewhere else.
|
|
152
203
|
|
|
153
204
|
---
|
|
154
205
|
|
|
155
|
-
##
|
|
206
|
+
## Dario and askalf
|
|
156
207
|
|
|
157
|
-
Dario is fully useful on its own
|
|
208
|
+
Dario is fully useful on its own — single-account mode is the default, pool mode (above) scales to as many Claude subscriptions as you want to add, and neither mode requires an account anywhere. Everything dario does is open-source and self-hosted.
|
|
158
209
|
|
|
159
|
-
|
|
210
|
+
[askalf](https://askalf.org) is the hosted platform built on top of the same OAuth and billing infrastructure, targeting the things a local proxy can't deliver by design:
|
|
160
211
|
|
|
161
|
-
| | dario
|
|
212
|
+
| | dario | askalf |
|
|
162
213
|
|---|---|---|
|
|
163
|
-
| **Accounts** | 1 (
|
|
164
|
-
| **Rate limits** |
|
|
165
|
-
| **Session shaping** | Per-request scrub | Per-session cumulative tracking, rotation, classifier avoidance |
|
|
214
|
+
| **Accounts** | 1 (single) or N (pool mode) | Managed pool, no setup |
|
|
215
|
+
| **Rate limits** | Distributed across your own pool | Distributed across the hosted fleet |
|
|
166
216
|
| **Browser / desktop control** | No | Yes — full computer use |
|
|
167
217
|
| **Scheduling** | No | Cron, webhooks, triggers |
|
|
168
218
|
| **Persistent memory** | No | Per-agent context and state |
|
|
169
219
|
| **Hosted dashboard** | No | Yes |
|
|
170
|
-
| **
|
|
220
|
+
| **Runs where** | Your machine | Hosted |
|
|
221
|
+
| **Price** | Free | Paid |
|
|
171
222
|
|
|
172
|
-
|
|
223
|
+
Pool mode in dario covers the "I want multi-account routing on my own machine with my own subscriptions" case. askalf covers the "I want someone else to run this, with a dashboard, and 24/7 fleet capabilities my own machine can't give me" case. Dario is and will remain open-source and free.
|
|
173
224
|
|
|
174
225
|
**[Join the askalf waitlist →](https://askalf.org)**
|
|
175
226
|
|
|
176
|
-
Dario will always be open-source and free. askalf is the hosted tier for teams who need the layer above.
|
|
177
|
-
|
|
178
227
|
---
|
|
179
228
|
|
|
180
229
|
## Commands
|
|
@@ -186,6 +235,9 @@ Dario will always be open-source and free. askalf is the hosted tier for teams w
|
|
|
186
235
|
| `dario status` | Show OAuth token health and expiry |
|
|
187
236
|
| `dario refresh` | Force an immediate token refresh |
|
|
188
237
|
| `dario logout` | Delete stored credentials |
|
|
238
|
+
| `dario accounts list` | List accounts in the multi-account pool |
|
|
239
|
+
| `dario accounts add <alias>` | Add a new account to the pool (runs OAuth flow) |
|
|
240
|
+
| `dario accounts remove <alias>` | Remove an account from the pool |
|
|
189
241
|
| `dario help` | Full command reference |
|
|
190
242
|
|
|
191
243
|
### Proxy options
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AccountCredentials {
|
|
2
|
+
alias: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
scopes: string[];
|
|
7
|
+
deviceId: string;
|
|
8
|
+
accountUuid: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function listAccountAliases(): Promise<string[]>;
|
|
11
|
+
export declare function loadAccount(alias: string): Promise<AccountCredentials | null>;
|
|
12
|
+
export declare function loadAllAccounts(): Promise<AccountCredentials[]>;
|
|
13
|
+
export declare function saveAccount(creds: AccountCredentials): Promise<void>;
|
|
14
|
+
export declare function removeAccount(alias: string): Promise<boolean>;
|
|
15
|
+
/** Refresh an account's OAuth token using dario's auto-detected CC OAuth config. */
|
|
16
|
+
export declare function refreshAccountToken(creds: AccountCredentials): Promise<AccountCredentials>;
|
|
17
|
+
/**
|
|
18
|
+
* Interactive OAuth flow that adds a new account to the pool. Uses dario's
|
|
19
|
+
* auto-detected CC OAuth config (same scanner the single-account path uses).
|
|
20
|
+
* Saves to `~/.dario/accounts/<alias>.json` on success.
|
|
21
|
+
*/
|
|
22
|
+
export declare function addAccountViaOAuth(alias: string): Promise<AccountCredentials>;
|
|
23
|
+
export declare function getAccountsDir(): string;
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account credential storage.
|
|
3
|
+
*
|
|
4
|
+
* Accounts live at `~/.dario/accounts/<alias>.json`. Single-account dario
|
|
5
|
+
* still uses `~/.dario/credentials.json` and does not touch this module.
|
|
6
|
+
* When `~/.dario/accounts/` contains 2+ files the proxy activates pool mode
|
|
7
|
+
* (see pool.ts). Each account has its own independent OAuth lifecycle and
|
|
8
|
+
* can refresh without affecting the others.
|
|
9
|
+
*
|
|
10
|
+
* OAuth config (client_id, scopes, authorize URL, token URL) comes from
|
|
11
|
+
* dario's cc-oauth-detect scanner — the same source the single-account
|
|
12
|
+
* path already uses. No hardcoded client IDs here.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, writeFile, mkdir, readdir, unlink, rename } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
18
|
+
import { createServer } from 'node:http';
|
|
19
|
+
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
20
|
+
const DARIO_DIR = join(homedir(), '.dario');
|
|
21
|
+
const ACCOUNTS_DIR = join(DARIO_DIR, 'accounts');
|
|
22
|
+
async function ensureDir() {
|
|
23
|
+
await mkdir(ACCOUNTS_DIR, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
export async function listAccountAliases() {
|
|
26
|
+
try {
|
|
27
|
+
await ensureDir();
|
|
28
|
+
const entries = await readdir(ACCOUNTS_DIR);
|
|
29
|
+
return entries.filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function loadAccount(alias) {
|
|
36
|
+
const path = join(ACCOUNTS_DIR, `${alias}.json`);
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(path, 'utf-8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function loadAllAccounts() {
|
|
46
|
+
const aliases = await listAccountAliases();
|
|
47
|
+
const loaded = await Promise.all(aliases.map(a => loadAccount(a)));
|
|
48
|
+
return loaded.filter((a) => a !== null);
|
|
49
|
+
}
|
|
50
|
+
export async function saveAccount(creds) {
|
|
51
|
+
await ensureDir();
|
|
52
|
+
const path = join(ACCOUNTS_DIR, `${creds.alias}.json`);
|
|
53
|
+
const tmp = `${path}.tmp.${randomBytes(4).toString('hex')}`;
|
|
54
|
+
await writeFile(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
55
|
+
try {
|
|
56
|
+
await rename(tmp, path);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Windows can fail renames on busy files — fall back to direct write
|
|
60
|
+
await writeFile(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
61
|
+
try {
|
|
62
|
+
await unlink(tmp);
|
|
63
|
+
}
|
|
64
|
+
catch { /* ignore */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function removeAccount(alias) {
|
|
68
|
+
const path = join(ACCOUNTS_DIR, `${alias}.json`);
|
|
69
|
+
try {
|
|
70
|
+
await unlink(path);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Detect deviceId + accountUuid from an installed Claude Code. */
|
|
78
|
+
async function detectClaudeIdentity() {
|
|
79
|
+
const paths = [
|
|
80
|
+
join(homedir(), '.claude', '.claude.json'),
|
|
81
|
+
join(homedir(), '.claude.json'),
|
|
82
|
+
];
|
|
83
|
+
for (const p of paths) {
|
|
84
|
+
try {
|
|
85
|
+
const raw = await readFile(p, 'utf-8');
|
|
86
|
+
const data = JSON.parse(raw);
|
|
87
|
+
const deviceId = data.userID || data.installId || data.deviceId || '';
|
|
88
|
+
const accountUuid = data.oauthAccount?.accountUuid || data.accountUuid || '';
|
|
89
|
+
if (deviceId || accountUuid) {
|
|
90
|
+
return { deviceId, accountUuid };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { /* try next */ }
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/** Refresh an account's OAuth token using dario's auto-detected CC OAuth config. */
|
|
98
|
+
export async function refreshAccountToken(creds) {
|
|
99
|
+
const cfg = await detectCCOAuthConfig();
|
|
100
|
+
const res = await fetch(cfg.tokenUrl, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
103
|
+
body: new URLSearchParams({
|
|
104
|
+
grant_type: 'refresh_token',
|
|
105
|
+
refresh_token: creds.refreshToken,
|
|
106
|
+
client_id: cfg.clientId,
|
|
107
|
+
}).toString(),
|
|
108
|
+
signal: AbortSignal.timeout(15_000),
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const errBody = await res.text().catch(() => '');
|
|
112
|
+
throw new Error(`Refresh failed for ${creds.alias} (${res.status}): ${errBody.slice(0, 200)}`);
|
|
113
|
+
}
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
const updated = {
|
|
116
|
+
...creds,
|
|
117
|
+
accessToken: data.access_token,
|
|
118
|
+
refreshToken: data.refresh_token,
|
|
119
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
120
|
+
};
|
|
121
|
+
await saveAccount(updated);
|
|
122
|
+
return updated;
|
|
123
|
+
}
|
|
124
|
+
// ── PKCE OAuth flow for adding a new account ────────────────────────────
|
|
125
|
+
function base64url(buf) {
|
|
126
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
127
|
+
}
|
|
128
|
+
function generatePKCE() {
|
|
129
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
130
|
+
const codeChallenge = base64url(createHash('sha256').update(codeVerifier).digest());
|
|
131
|
+
return { codeVerifier, codeChallenge };
|
|
132
|
+
}
|
|
133
|
+
function openBrowser(url) {
|
|
134
|
+
const { exec } = require('node:child_process');
|
|
135
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
136
|
+
: process.platform === 'darwin' ? `open "${url}"`
|
|
137
|
+
: `xdg-open "${url}"`;
|
|
138
|
+
exec(cmd, () => { });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Interactive OAuth flow that adds a new account to the pool. Uses dario's
|
|
142
|
+
* auto-detected CC OAuth config (same scanner the single-account path uses).
|
|
143
|
+
* Saves to `~/.dario/accounts/<alias>.json` on success.
|
|
144
|
+
*/
|
|
145
|
+
export async function addAccountViaOAuth(alias) {
|
|
146
|
+
const cfg = await detectCCOAuthConfig();
|
|
147
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
148
|
+
const state = base64url(randomBytes(16));
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
let port = 0;
|
|
151
|
+
const server = createServer(async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
154
|
+
if (url.pathname !== '/callback') {
|
|
155
|
+
res.writeHead(404);
|
|
156
|
+
res.end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const code = url.searchParams.get('code');
|
|
160
|
+
const returnedState = url.searchParams.get('state');
|
|
161
|
+
if (!code) {
|
|
162
|
+
res.writeHead(400);
|
|
163
|
+
res.end('No authorization code received');
|
|
164
|
+
server.close();
|
|
165
|
+
reject(new Error('No authorization code received'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (returnedState !== state) {
|
|
169
|
+
res.writeHead(400);
|
|
170
|
+
res.end('Invalid state parameter');
|
|
171
|
+
server.close();
|
|
172
|
+
reject(new Error('OAuth state mismatch — possible CSRF'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
res.writeHead(302, {
|
|
176
|
+
Location: 'https://platform.claude.com/oauth/code/success?app=claude-code',
|
|
177
|
+
});
|
|
178
|
+
res.end();
|
|
179
|
+
server.close();
|
|
180
|
+
// Exchange code for tokens
|
|
181
|
+
const tokenRes = await fetch(cfg.tokenUrl, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
grant_type: 'authorization_code',
|
|
186
|
+
client_id: cfg.clientId,
|
|
187
|
+
code,
|
|
188
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
189
|
+
code_verifier: codeVerifier,
|
|
190
|
+
state,
|
|
191
|
+
}),
|
|
192
|
+
signal: AbortSignal.timeout(30_000),
|
|
193
|
+
});
|
|
194
|
+
if (!tokenRes.ok) {
|
|
195
|
+
const body = await tokenRes.text().catch(() => '');
|
|
196
|
+
throw new Error(`Token exchange failed (${tokenRes.status}): ${body.slice(0, 200)}`);
|
|
197
|
+
}
|
|
198
|
+
const tokens = await tokenRes.json();
|
|
199
|
+
// Prefer CC identity if installed; otherwise generate fresh IDs.
|
|
200
|
+
const identity = (await detectClaudeIdentity()) ?? {
|
|
201
|
+
deviceId: randomUUID(),
|
|
202
|
+
accountUuid: randomUUID(),
|
|
203
|
+
};
|
|
204
|
+
const creds = {
|
|
205
|
+
alias,
|
|
206
|
+
accessToken: tokens.access_token,
|
|
207
|
+
refreshToken: tokens.refresh_token,
|
|
208
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
209
|
+
scopes: tokens.scope?.split(' ') ?? cfg.scopes.split(' '),
|
|
210
|
+
deviceId: identity.deviceId,
|
|
211
|
+
accountUuid: identity.accountUuid,
|
|
212
|
+
};
|
|
213
|
+
await saveAccount(creds);
|
|
214
|
+
resolve(creds);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
server.close();
|
|
218
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
server.listen(0, 'localhost', () => {
|
|
222
|
+
const addr = server.address();
|
|
223
|
+
port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
224
|
+
const params = new URLSearchParams({
|
|
225
|
+
code: 'true',
|
|
226
|
+
client_id: cfg.clientId,
|
|
227
|
+
response_type: 'code',
|
|
228
|
+
redirect_uri: `http://localhost:${port}/callback`,
|
|
229
|
+
scope: cfg.scopes,
|
|
230
|
+
code_challenge: codeChallenge,
|
|
231
|
+
code_challenge_method: 'S256',
|
|
232
|
+
state,
|
|
233
|
+
});
|
|
234
|
+
const authUrl = `${cfg.authorizeUrl}?${params.toString()}`;
|
|
235
|
+
console.log(` Opening browser to add account "${alias}"...`);
|
|
236
|
+
console.log(` If the browser didn't open, visit:`);
|
|
237
|
+
console.log(` ${authUrl}`);
|
|
238
|
+
console.log();
|
|
239
|
+
openBrowser(authUrl);
|
|
240
|
+
});
|
|
241
|
+
server.on('error', (err) => {
|
|
242
|
+
reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
|
|
243
|
+
});
|
|
244
|
+
const timeout = setTimeout(() => {
|
|
245
|
+
server.close();
|
|
246
|
+
reject(new Error('OAuth flow timed out after 5 minutes. Try `dario accounts add` again.'));
|
|
247
|
+
}, 300_000);
|
|
248
|
+
timeout.unref();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
export function getAccountsDir() {
|
|
252
|
+
return ACCOUNTS_DIR;
|
|
253
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token analytics — per-request billing tracking, utilization trends,
|
|
3
|
+
* window exhaustion predictions, cost estimation.
|
|
4
|
+
*
|
|
5
|
+
* In-memory rolling window; exposed via the /analytics endpoint when
|
|
6
|
+
* pool mode is active.
|
|
7
|
+
*/
|
|
8
|
+
export interface RequestRecord {
|
|
9
|
+
timestamp: number;
|
|
10
|
+
account: string;
|
|
11
|
+
model: string;
|
|
12
|
+
inputTokens: number;
|
|
13
|
+
outputTokens: number;
|
|
14
|
+
cacheReadTokens: number;
|
|
15
|
+
cacheCreateTokens: number;
|
|
16
|
+
thinkingTokens: number;
|
|
17
|
+
claim: string;
|
|
18
|
+
util5h: number;
|
|
19
|
+
util7d: number;
|
|
20
|
+
overageUtil: number;
|
|
21
|
+
latencyMs: number;
|
|
22
|
+
status: number;
|
|
23
|
+
isStream: boolean;
|
|
24
|
+
isOpenAI: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class Analytics {
|
|
27
|
+
private records;
|
|
28
|
+
private maxRecords;
|
|
29
|
+
constructor(maxRecords?: number);
|
|
30
|
+
record(r: RequestRecord): void;
|
|
31
|
+
/** Parse usage from a non-streaming Anthropic response body. */
|
|
32
|
+
static parseUsage(body: Record<string, unknown>): {
|
|
33
|
+
inputTokens: number;
|
|
34
|
+
outputTokens: number;
|
|
35
|
+
cacheReadTokens: number;
|
|
36
|
+
cacheCreateTokens: number;
|
|
37
|
+
thinkingTokens: number;
|
|
38
|
+
model: string;
|
|
39
|
+
};
|
|
40
|
+
summary(windowMinutes?: number): AnalyticsSummary;
|
|
41
|
+
private computeStats;
|
|
42
|
+
private perAccountStats;
|
|
43
|
+
private perModelStats;
|
|
44
|
+
private utilizationTrend;
|
|
45
|
+
private predict;
|
|
46
|
+
}
|
|
47
|
+
interface PerAccountStat {
|
|
48
|
+
requests: number;
|
|
49
|
+
inputTokens: number;
|
|
50
|
+
outputTokens: number;
|
|
51
|
+
estimatedCost: number;
|
|
52
|
+
currentUtil5h: number;
|
|
53
|
+
currentUtil7d: number;
|
|
54
|
+
lastClaim: string;
|
|
55
|
+
}
|
|
56
|
+
interface PerModelStat {
|
|
57
|
+
requests: number;
|
|
58
|
+
avgInputTokens: number;
|
|
59
|
+
avgOutputTokens: number;
|
|
60
|
+
avgThinkingTokens: number;
|
|
61
|
+
estimatedCost: number;
|
|
62
|
+
}
|
|
63
|
+
export interface AnalyticsSummary {
|
|
64
|
+
window: {
|
|
65
|
+
minutes: number;
|
|
66
|
+
requests: number;
|
|
67
|
+
totalInputTokens: number;
|
|
68
|
+
totalOutputTokens: number;
|
|
69
|
+
totalThinkingTokens: number;
|
|
70
|
+
estimatedCost: number;
|
|
71
|
+
avgLatencyMs: number;
|
|
72
|
+
errorRate: number;
|
|
73
|
+
claimBreakdown: Record<string, number>;
|
|
74
|
+
};
|
|
75
|
+
allTime: {
|
|
76
|
+
requests: number;
|
|
77
|
+
totalInputTokens: number;
|
|
78
|
+
totalOutputTokens: number;
|
|
79
|
+
totalThinkingTokens: number;
|
|
80
|
+
estimatedCost: number;
|
|
81
|
+
avgLatencyMs: number;
|
|
82
|
+
errorRate: number;
|
|
83
|
+
claimBreakdown: Record<string, number>;
|
|
84
|
+
};
|
|
85
|
+
perAccount: Record<string, PerAccountStat>;
|
|
86
|
+
perModel: Record<string, PerModelStat>;
|
|
87
|
+
utilization: Array<{
|
|
88
|
+
timestamp: number;
|
|
89
|
+
avgUtil5h: number;
|
|
90
|
+
avgUtil7d: number;
|
|
91
|
+
requests: number;
|
|
92
|
+
}>;
|
|
93
|
+
predictions: {
|
|
94
|
+
estimatedExhaustionMinutes: number | null;
|
|
95
|
+
tokenBurnRate: number;
|
|
96
|
+
costBurnRate: number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export {};
|