@askalf/dario 1.1.3 → 1.2.1

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
@@ -2,10 +2,18 @@
2
2
  <h1 align="center">dario</h1>
3
3
  <p align="center"><strong>Use your Claude subscription as an API.</strong></p>
4
4
  <p align="center">
5
- Two commands. No API key. Your Claude Max/Pro subscription becomes a local API endpoint<br/>that any tool, SDK, or framework can use.
5
+ No API key needed. Your Claude Max/Pro subscription becomes a local API endpoint<br/>that any tool, SDK, or framework can use.
6
6
  </p>
7
7
  </p>
8
8
 
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/v/@askalf/dario?color=blue" alt="npm version"></a>
11
+ <a href="https://github.com/askalf/dario/actions/workflows/ci.yml"><img src="https://github.com/askalf/dario/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
12
+ <a href="https://github.com/askalf/dario/actions/workflows/codeql.yml"><img src="https://github.com/askalf/dario/actions/workflows/codeql.yml/badge.svg" alt="CodeQL"></a>
13
+ <a href="https://github.com/askalf/dario/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@askalf/dario" alt="License"></a>
14
+ <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario" alt="Downloads"></a>
15
+ </p>
16
+
9
17
  <p align="center">
10
18
  <a href="#quick-start">Quick Start</a> &bull;
11
19
  <a href="#how-it-works">How It Works</a> &bull;
@@ -17,8 +25,7 @@
17
25
  ---
18
26
 
19
27
  ```bash
20
- npx @askalf/dario login # authenticate with Claude
21
- npx @askalf/dario proxy # start local API on :3456
28
+ npx @askalf/dario login # detects Claude Code credentials, starts proxy
22
29
 
23
30
  # now use it from anywhere
24
31
  export ANTHROPIC_BASE_URL=http://localhost:3456
@@ -33,12 +40,18 @@ That's it. Any tool that speaks the Anthropic API now uses your subscription.
33
40
 
34
41
  You pay $100-200/mo for Claude Max or Pro. But that subscription only works on claude.ai and Claude Code. If you want to use Claude with **any other tool** — OpenClaw, Cursor, Continue, Aider, your own scripts — you need a separate API key with separate billing.
35
42
 
36
- **Note:** Claude subscriptions have [usage limits](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) that reset on rolling 5-hour and 7-day windows. When exceeded, Opus and Sonnet may return 429 errors while Haiku continues working. Use `--cli` mode to route through Claude Code's binary, which is not affected by these limits.
43
+ **Note:** Claude subscriptions have [usage limits](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) that reset on rolling 5-hour and 7-day windows. When exceeded, Opus and Sonnet may return 429 errors while Haiku continues working. You can check your utilization via Claude Code's `/usage` command or [statusline](https://code.claude.com/docs/en/statusline). Use `--cli` mode to route through Claude Code's binary, which is not affected by these limits.
37
44
 
38
45
  **dario fixes this.** It creates a local proxy that translates API key auth into your subscription's OAuth tokens — and with `--cli` mode, routes through the Claude Code binary for uninterrupted access.
39
46
 
40
47
  ## Quick Start
41
48
 
49
+ ### Prerequisites
50
+
51
+ [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed and logged in (recommended). Dario detects your existing Claude Code credentials automatically.
52
+
53
+ If Claude Code isn't installed, dario runs its own OAuth flow — opens your browser, you authorize, done.
54
+
42
55
  ### Install
43
56
 
44
57
  ```bash
@@ -49,7 +62,6 @@ Or use npx (no install needed):
49
62
 
50
63
  ```bash
51
64
  npx @askalf/dario login
52
- npx @askalf/dario proxy
53
65
  ```
54
66
 
55
67
  ### Login
@@ -58,7 +70,8 @@ npx @askalf/dario proxy
58
70
  dario login
59
71
  ```
60
72
 
61
- Opens your browser to Claude's OAuth page. Log in, authorize, paste the redirect URL back. Takes 10 seconds.
73
+ - **With Claude Code installed:** Detects your credentials automatically and starts the proxy. No browser needed.
74
+ - **Without Claude Code:** Opens your browser to Claude's OAuth page. Authorize, and dario captures the token automatically via a local callback server. Then run `dario proxy` to start the server.
62
75
 
63
76
  ### Start the proxy
64
77
 
@@ -115,8 +128,6 @@ Backend: Claude CLI (bypasses rate limits)
115
128
  Model: claude-opus-4-6 (all requests)
116
129
  ```
117
130
 
118
- **Requirements:** Claude Code must be installed (`npm install -g @anthropic-ai/claude-code` or already installed via the desktop app).
119
-
120
131
  **Trade-offs vs direct API mode:**
121
132
 
122
133
  | | Direct API (default) | CLI Backend (`--cli`) |
@@ -249,7 +260,7 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
249
260
  └──────────┘ └─────────────────┘ └──────────────────┘
250
261
  ```
251
262
 
252
- 1. **`dario login`** — Standard PKCE OAuth flow. Opens Claude's auth page in your browser. You authorize, dario stores the tokens locally in `~/.dario/credentials.json`. No server involved, no secrets leave your machine.
263
+ 1. **`dario login`** — Detects your existing Claude Code credentials (`~/.claude/.credentials.json`) and starts the proxy automatically. If Claude Code isn't installed, runs a PKCE OAuth flow with a local callback server to capture the token automatically.
253
264
 
254
265
  2. **`dario proxy`** — Starts an HTTP server on localhost that implements the Anthropic Messages API. In direct mode, it swaps your API key for an OAuth bearer token. In CLI mode, it routes through the Claude Code binary.
255
266
 
@@ -259,7 +270,7 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
259
270
 
260
271
  | Command | Description |
261
272
  |---------|-------------|
262
- | `dario login` | Authenticate with your Claude account |
273
+ | `dario login` | Detect credentials and start proxy |
263
274
  | `dario proxy` | Start the local API proxy |
264
275
  | `dario status` | Check if your token is healthy |
265
276
  | `dario refresh` | Force an immediate token refresh |
@@ -319,11 +330,11 @@ curl http://localhost:3456/health
319
330
 
320
331
  | Concern | How dario handles it |
321
332
  |---------|---------------------|
322
- | Credential storage | `~/.dario/credentials.json` with `0600` permissions (owner-only) |
333
+ | Credential storage | Reads from Claude Code (`~/.claude/.credentials.json`) or its own store (`~/.dario/credentials.json`) with `0600` permissions |
323
334
  | OAuth flow | PKCE (Proof Key for Code Exchange) — no client secret needed |
324
335
  | Token transmission | OAuth tokens never leave localhost. Only forwarded to `api.anthropic.com` over HTTPS |
325
336
  | Network exposure | Proxy binds to `127.0.0.1` only — not accessible from other machines |
326
- | SSRF protection | Allowlisted API paths only. Internal networks and cloud metadata blocked |
337
+ | SSRF protection | Hardcoded allowlist of API paths only `/v1/messages`, `/v1/models`, `/v1/complete` are proxied |
327
338
  | Token rotation | Refresh tokens rotate on every use (single-use) |
328
339
  | Error sanitization | Token patterns redacted from all error messages |
329
340
  | Data collection | Zero. No telemetry, no analytics, no phoning home |
@@ -331,7 +342,7 @@ curl http://localhost:3456/health
331
342
  ## FAQ
332
343
 
333
344
  **Does this violate Anthropic's terms of service?**
334
- Dario uses the same public OAuth client ID and PKCE flow that Claude Code uses. It authenticates you as you, with your subscription, through Anthropic's official OAuth endpoints. The `--cli` mode literally uses Claude Code itself as the backend.
345
+ Dario uses your existing Claude Code credentials with the same OAuth tokens. It authenticates you as you, with your subscription, through Anthropic's official API. The `--cli` mode literally uses Claude Code itself as the backend.
335
346
 
336
347
  **What subscription plans work?**
337
348
  Claude Max and Claude Pro. Any plan that lets you use Claude Code.
@@ -339,17 +350,20 @@ Claude Max and Claude Pro. Any plan that lets you use Claude Code.
339
350
  **Does it work with Claude Team / Enterprise?**
340
351
  Should work if your plan includes Claude Code access. Not tested yet — please open an issue with results.
341
352
 
353
+ **Do I need Claude Code installed?**
354
+ Recommended but not required. If Claude Code is installed and logged in, `dario login` picks up your credentials automatically. Without Claude Code, dario runs its own OAuth flow to authenticate directly. Note: `--cli` mode requires Claude Code (`npm install -g @anthropic-ai/claude-code`).
355
+
342
356
  **What happens when my token expires?**
343
- Dario auto-refreshes tokens 30 minutes before expiry. You should never see an auth error in normal use. If something goes wrong, `dario refresh` forces an immediate refresh, or `dario login` to re-authenticate.
357
+ Dario auto-refreshes tokens 30 minutes before expiry. You should never see an auth error in normal use. If something goes wrong, `dario refresh` forces an immediate refresh.
344
358
 
345
359
  **I'm getting rate limited on Opus. What do I do?**
346
360
  Use `--cli` mode: `dario proxy --cli`. This routes through the Claude Code binary, which continues working when direct API calls are rate limited. You can also enable [extra usage](https://support.claude.com/en/articles/12429409-manage-extra-usage-for-paid-claude-plans) in your Anthropic account settings to extend your limits at API rates.
347
361
 
348
362
  **What are the usage limits?**
349
- Claude subscriptions have rolling 5-hour and 7-day usage windows shared across claude.ai and Claude Code. See [Anthropic's docs](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) for details. Check your current status with `echo "hi" | ANTHROPIC_LOG=debug claude --print --model claude-haiku-4-5 2>&1 | grep ratelimit`.
363
+ Claude subscriptions have rolling 5-hour and 7-day usage windows shared across claude.ai and Claude Code. See [Anthropic's docs](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) for details. In Claude Code, use `/usage` to check your current limits, or configure the [statusline](https://code.claude.com/docs/en/statusline) to show real-time 5h and 7d utilization percentages.
350
364
 
351
365
  **Can I run this on a server?**
352
- Dario binds to localhost by default. For server use, you'd need to handle the initial browser-based login on a machine with a browser, then copy `~/.dario/credentials.json` to your server. Auto-refresh will keep it alive from there.
366
+ Dario binds to localhost by default. For server use, you'd need to handle the initial login on a machine with a browser, then copy `~/.claude/.credentials.json` (or `~/.dario/credentials.json`) to your server. Auto-refresh will keep it alive from there.
353
367
 
354
368
  **Why "dario"?**
355
369
  Named after [Dario Amodei](https://en.wikipedia.org/wiki/Dario_Amodei), CEO of Anthropic.
@@ -375,13 +389,39 @@ const status = await getStatus();
375
389
  console.log(status.expiresIn); // "11h 42m"
376
390
  ```
377
391
 
392
+ ## Trust & Transparency
393
+
394
+ Dario handles your OAuth tokens. Here's why you can trust it:
395
+
396
+ | Signal | Status |
397
+ |--------|--------|
398
+ | **Source code** | ~1000 lines of TypeScript — small enough to read in one sitting |
399
+ | **Dependencies** | 1 production dep (`@anthropic-ai/sdk`). Verify: `npm ls --production` |
400
+ | **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
401
+ | **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
402
+ | **Credential handling** | Tokens never logged, redacted from errors, stored with 0600 permissions |
403
+ | **Network scope** | Binds to 127.0.0.1 only. Upstream traffic goes exclusively to `api.anthropic.com` over HTTPS |
404
+ | **No telemetry** | Zero analytics, tracking, or data collection of any kind |
405
+ | **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
406
+ | **Branch protection** | CI must pass before merge. CODEOWNERS enforces review |
407
+
408
+ Verify the npm package matches this repo:
409
+
410
+ ```bash
411
+ # Check provenance attestation
412
+ npm audit signatures 2>/dev/null; npm view @askalf/dario dist.integrity
413
+
414
+ # Check dependency tree (should be minimal)
415
+ cd $(npm root -g)/@askalf/dario && npm ls --production
416
+ ```
417
+
378
418
  ## Contributing
379
419
 
380
- PRs welcome. The codebase is ~700 lines of TypeScript across 4 files:
420
+ PRs welcome. The codebase is ~1000 lines of TypeScript across 4 files:
381
421
 
382
422
  | File | Purpose |
383
423
  |------|---------|
384
- | `src/oauth.ts` | PKCE flow, token storage, refresh logic |
424
+ | `src/oauth.ts` | Token storage, refresh logic, Claude Code credential detection, auto OAuth flow |
385
425
  | `src/proxy.ts` | HTTP proxy server + CLI backend |
386
426
  | `src/cli.ts` | CLI entry point |
387
427
  | `src/index.ts` | Library exports |
package/dist/cli.js CHANGED
@@ -9,60 +9,39 @@
9
9
  * dario refresh — Force token refresh
10
10
  * dario logout — Remove saved credentials
11
11
  */
12
- import { createInterface } from 'node:readline';
13
- import { unlink } from 'node:fs/promises';
12
+ import { readFile, unlink } from 'node:fs/promises';
14
13
  import { join } from 'node:path';
15
14
  import { homedir } from 'node:os';
16
- import { startOAuthFlow, exchangeCode, getStatus, refreshTokens } from './oauth.js';
15
+ import { startAutoOAuthFlow, getStatus, refreshTokens } from './oauth.js';
17
16
  import { startProxy } from './proxy.js';
18
17
  const args = process.argv.slice(2);
19
18
  const command = args[0] ?? (process.stdin.isTTY ? 'proxy' : 'proxy');
20
- function ask(question) {
21
- const rl = createInterface({ input: process.stdin, output: process.stdout });
22
- return new Promise(resolve => {
23
- rl.question(question, answer => {
24
- rl.close();
25
- resolve(answer.trim());
26
- });
27
- });
28
- }
29
19
  async function login() {
30
20
  console.log('');
31
- console.log(' dario — Claude OAuth Login');
32
- console.log(' ─────────────────────────');
33
- console.log('');
34
- const { authUrl, codeVerifier } = startOAuthFlow();
35
- console.log(' Step 1: Open this URL in your browser:');
36
- console.log('');
37
- console.log(` ${authUrl}`);
21
+ console.log(' dario — Claude Login');
22
+ console.log(' ───────────────────');
38
23
  console.log('');
39
- console.log(' Step 2: Log in to your Claude account and authorize.');
40
- console.log('');
41
- console.log(' Step 3: After authorization, you\'ll be redirected to a page');
42
- console.log(' that shows a code. Copy the FULL URL from your browser\'s');
43
- console.log(' address bar (it contains the authorization code).');
44
- console.log('');
45
- const input = await ask(' Paste the redirect URL or authorization code: ');
46
- // Extract code from URL or use raw input
47
- let code = input;
24
+ // Check if Claude Code credentials exist
25
+ const ccPath = join(homedir(), '.claude', '.credentials.json');
48
26
  try {
49
- const url = new URL(input);
50
- // Only extract from trusted Anthropic redirect URLs
51
- if (url.hostname === 'platform.claude.com' || url.hostname === 'claude.ai') {
52
- code = url.searchParams.get('code') ?? input;
27
+ const raw = await readFile(ccPath, 'utf-8');
28
+ const parsed = JSON.parse(raw);
29
+ if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
30
+ const expiresAt = parsed.claudeAiOauth.expiresAt;
31
+ if (expiresAt > Date.now()) {
32
+ console.log(' Found Claude Code credentials. Starting proxy...');
33
+ console.log('');
34
+ await proxy();
35
+ return;
36
+ }
53
37
  }
54
38
  }
55
- catch {
56
- // Not a URL, use as-is (raw code)
57
- }
58
- if (!code || code.length < 10 || code.length > 2048) {
59
- console.error(' Invalid authorization code.');
60
- process.exit(1);
61
- }
39
+ catch { /* no Claude Code credentials, fall through to OAuth */ }
40
+ console.log(' No Claude Code credentials found. Starting OAuth flow...');
41
+ console.log('');
62
42
  try {
63
- const tokens = await exchangeCode(code, codeVerifier);
43
+ const tokens = await startAutoOAuthFlow();
64
44
  const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 60000);
65
- console.log('');
66
45
  console.log(' Login successful!');
67
46
  console.log(` Token expires in ${expiresIn} minutes (auto-refreshes).`);
68
47
  console.log('');
@@ -142,7 +121,7 @@ async function help() {
142
121
  dario — Use your Claude subscription as an API.
143
122
 
144
123
  Usage:
145
- dario login Authenticate with your Claude account
124
+ dario login Detect credentials + start proxy (or run OAuth)
146
125
  dario proxy [options] Start the API proxy server
147
126
  dario status Check authentication status
148
127
  dario refresh Force token refresh
@@ -151,27 +130,25 @@ async function help() {
151
130
  Proxy options:
152
131
  --model=MODEL Force a model for all requests
153
132
  Shortcuts: opus, sonnet, haiku
133
+ Full IDs: claude-opus-4-6, claude-sonnet-4-6
154
134
  Default: passthrough (client decides)
155
135
  --cli Use Claude CLI as backend (bypasses rate limits)
156
136
  --port=PORT Port to listen on (default: 3456)
157
137
  --verbose, -v Log all requests
158
138
 
159
- How it works:
160
- 1. Run \`dario login\` to authenticate with your Claude Max/Pro subscription
161
- 2. Run \`dario proxy\` to start a local API server
162
- 3. Point any Anthropic SDK at http://localhost:3456
163
-
164
- Example with OpenClaw, or any tool that uses the Anthropic API:
165
- ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario openclaw start
139
+ Quick start:
140
+ dario login # auto-detects Claude Code credentials
141
+ dario proxy # or: dario proxy --cli --model=opus
166
142
 
167
- Example with the Anthropic Python SDK:
168
- import anthropic
169
- client = anthropic.Anthropic(base_url="http://localhost:3456", api_key="dario")
143
+ Then point any Anthropic SDK at http://localhost:3456:
144
+ export ANTHROPIC_BASE_URL=http://localhost:3456
145
+ export ANTHROPIC_API_KEY=dario
170
146
 
171
- Example with curl:
172
- curl http://localhost:3456/v1/messages \\
173
- -H "Content-Type: application/json" \\
174
- -d '{"model":"claude-opus-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
147
+ Examples:
148
+ curl http://localhost:3456/v1/messages \\
149
+ -H "Content-Type: application/json" \\
150
+ -H "anthropic-version: 2023-06-01" \\
151
+ -d '{"model":"claude-opus-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
175
152
 
176
153
  Your subscription handles the billing. No API key needed.
177
154
  Tokens auto-refresh in the background — set it and forget it.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@
4
4
  * Use this if you want to embed dario in your own app
5
5
  * instead of running the CLI.
6
6
  */
7
- export { startOAuthFlow, exchangeCode, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
7
+ export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
8
8
  export type { OAuthTokens, CredentialsFile } from './oauth.js';
9
9
  export { startProxy } from './proxy.js';
package/dist/index.js CHANGED
@@ -4,5 +4,5 @@
4
4
  * Use this if you want to embed dario in your own app
5
5
  * instead of running the CLI.
6
6
  */
7
- export { startOAuthFlow, exchangeCode, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
7
+ export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
8
8
  export { startProxy } from './proxy.js';
package/dist/oauth.d.ts CHANGED
@@ -15,18 +15,10 @@ export interface CredentialsFile {
15
15
  }
16
16
  export declare function loadCredentials(): Promise<CredentialsFile | null>;
17
17
  /**
18
- * Start the OAuth flow. Returns the authorization URL and PKCE state
19
- * needed for the exchange step.
18
+ * Automatic OAuth flow using a local callback server (same as Claude Code).
19
+ * Opens browser, captures the authorization code automatically.
20
20
  */
21
- export declare function startOAuthFlow(): {
22
- authUrl: string;
23
- state: string;
24
- codeVerifier: string;
25
- };
26
- /**
27
- * Exchange authorization code for tokens and save them.
28
- */
29
- export declare function exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokens>;
21
+ export declare function startAutoOAuthFlow(): Promise<OAuthTokens>;
30
22
  /**
31
23
  * Refresh the access token using the refresh token.
32
24
  * Retries with exponential backoff on transient failures.
package/dist/oauth.js CHANGED
@@ -8,11 +8,11 @@ import { randomBytes, createHash } from 'node:crypto';
8
8
  import { readFile, writeFile, mkdir, chmod, rename } from 'node:fs/promises';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
- // Claude CLI's public OAuth client (PKCE, no secret needed)
11
+ // Claude Code's public OAuth client (PKCE, no secret needed)
12
12
  const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
13
- const OAUTH_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
13
+ const OAUTH_AUTHORIZE_URL = 'https://platform.claude.com/oauth/authorize';
14
14
  const OAUTH_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
15
- const OAUTH_REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
15
+ const OAUTH_SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
16
16
  // Refresh 30 min before expiry
17
17
  const REFRESH_BUFFER_MS = 30 * 60 * 1000;
18
18
  // In-memory credential cache — avoids disk reads on every request
@@ -29,31 +29,34 @@ function generatePKCE() {
29
29
  const codeChallenge = base64url(createHash('sha256').update(codeVerifier).digest());
30
30
  return { codeVerifier, codeChallenge };
31
31
  }
32
- function getCredentialsPath() {
32
+ function getDarioCredentialsPath() {
33
33
  return join(homedir(), '.dario', 'credentials.json');
34
34
  }
35
+ function getClaudeCodeCredentialsPath() {
36
+ return join(homedir(), '.claude', '.credentials.json');
37
+ }
35
38
  export async function loadCredentials() {
36
39
  // Return cached if fresh
37
40
  if (credentialsCache && Date.now() - credentialsCacheTime < CACHE_TTL_MS) {
38
41
  return credentialsCache;
39
42
  }
40
- try {
41
- const raw = await readFile(getCredentialsPath(), 'utf-8');
42
- const parsed = JSON.parse(raw);
43
- // Validate structure
44
- if (!parsed?.claudeAiOauth?.accessToken || !parsed?.claudeAiOauth?.refreshToken) {
45
- return null;
43
+ // Try dario's own credentials first, then fall back to Claude Code's
44
+ for (const path of [getDarioCredentialsPath(), getClaudeCodeCredentialsPath()]) {
45
+ try {
46
+ const raw = await readFile(path, 'utf-8');
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed?.claudeAiOauth?.accessToken && parsed?.claudeAiOauth?.refreshToken) {
49
+ credentialsCache = parsed;
50
+ credentialsCacheTime = Date.now();
51
+ return credentialsCache;
52
+ }
46
53
  }
47
- credentialsCache = parsed;
48
- credentialsCacheTime = Date.now();
49
- return credentialsCache;
50
- }
51
- catch {
52
- return null;
54
+ catch { /* try next */ }
53
55
  }
56
+ return null;
54
57
  }
55
58
  async function saveCredentials(creds) {
56
- const path = getCredentialsPath();
59
+ const path = getDarioCredentialsPath();
57
60
  await mkdir(dirname(path), { recursive: true });
58
61
  // Write atomically: write to temp file, then rename
59
62
  const tmpPath = `${path}.tmp.${Date.now()}`;
@@ -69,45 +72,101 @@ async function saveCredentials(creds) {
69
72
  credentialsCacheTime = Date.now();
70
73
  }
71
74
  /**
72
- * Start the OAuth flow. Returns the authorization URL and PKCE state
73
- * needed for the exchange step.
75
+ * Automatic OAuth flow using a local callback server (same as Claude Code).
76
+ * Opens browser, captures the authorization code automatically.
74
77
  */
75
- export function startOAuthFlow() {
78
+ export async function startAutoOAuthFlow() {
79
+ const { createServer } = await import('node:http');
76
80
  const { codeVerifier, codeChallenge } = generatePKCE();
77
81
  const state = base64url(randomBytes(16));
78
- const params = new URLSearchParams({
79
- response_type: 'code',
80
- client_id: OAUTH_CLIENT_ID,
81
- redirect_uri: OAUTH_REDIRECT_URI,
82
- scope: 'user:inference user:profile',
83
- state,
84
- code_challenge: codeChallenge,
85
- code_challenge_method: 'S256',
82
+ return new Promise((resolve, reject) => {
83
+ const server = createServer((req, res) => {
84
+ const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
85
+ if (url.pathname !== '/callback') {
86
+ res.writeHead(404);
87
+ res.end();
88
+ return;
89
+ }
90
+ const code = url.searchParams.get('code');
91
+ const returnedState = url.searchParams.get('state');
92
+ if (!code) {
93
+ res.writeHead(400);
94
+ res.end('No authorization code received');
95
+ server.close();
96
+ reject(new Error('No authorization code received'));
97
+ return;
98
+ }
99
+ if (returnedState !== state) {
100
+ res.writeHead(400);
101
+ res.end('Invalid state parameter');
102
+ server.close();
103
+ reject(new Error('Invalid state parameter'));
104
+ return;
105
+ }
106
+ // Redirect browser to success page
107
+ res.writeHead(302, { Location: 'https://platform.claude.com/oauth/code/success?app=claude-code' });
108
+ res.end();
109
+ // Exchange the code for tokens
110
+ server.close();
111
+ exchangeCodeWithRedirect(code, codeVerifier, state, port)
112
+ .then(resolve)
113
+ .catch(reject);
114
+ });
115
+ let port = 0;
116
+ server.listen(0, 'localhost', () => {
117
+ const addr = server.address();
118
+ port = typeof addr === 'object' && addr ? addr.port : 0;
119
+ const params = new URLSearchParams({
120
+ code: 'true',
121
+ client_id: OAUTH_CLIENT_ID,
122
+ response_type: 'code',
123
+ redirect_uri: `http://localhost:${port}/callback`,
124
+ scope: OAUTH_SCOPES,
125
+ code_challenge: codeChallenge,
126
+ code_challenge_method: 'S256',
127
+ state,
128
+ });
129
+ const authUrl = `${OAUTH_AUTHORIZE_URL}?${params.toString()}`;
130
+ // Open browser
131
+ console.log(' Opening browser to sign in...');
132
+ console.log(` If the browser didn't open, visit: ${authUrl}`);
133
+ console.log('');
134
+ // Open browser using platform-specific commands (no external deps)
135
+ const { exec } = require('node:child_process');
136
+ const cmd = process.platform === 'win32' ? `start "" "${authUrl}"`
137
+ : process.platform === 'darwin' ? `open "${authUrl}"`
138
+ : `xdg-open "${authUrl}"`;
139
+ exec(cmd, () => { });
140
+ });
141
+ server.on('error', (err) => {
142
+ reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
143
+ });
144
+ // Timeout after 5 minutes
145
+ setTimeout(() => {
146
+ server.close();
147
+ reject(new Error('OAuth flow timed out. Try again with `dario login`.'));
148
+ }, 300_000);
86
149
  });
87
- return {
88
- authUrl: `${OAUTH_AUTHORIZE_URL}?${params.toString()}`,
89
- state,
90
- codeVerifier,
91
- };
92
150
  }
93
151
  /**
94
- * Exchange authorization code for tokens and save them.
152
+ * Exchange code using the localhost redirect URI.
95
153
  */
96
- export async function exchangeCode(code, codeVerifier) {
154
+ async function exchangeCodeWithRedirect(code, codeVerifier, state, port) {
97
155
  const res = await fetch(OAUTH_TOKEN_URL, {
98
156
  method: 'POST',
99
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
100
- body: new URLSearchParams({
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({
101
159
  grant_type: 'authorization_code',
102
160
  client_id: OAUTH_CLIENT_ID,
103
161
  code,
104
- redirect_uri: OAUTH_REDIRECT_URI,
162
+ redirect_uri: `http://localhost:${port}/callback`,
105
163
  code_verifier: codeVerifier,
164
+ state,
106
165
  }),
107
166
  signal: AbortSignal.timeout(30000),
108
167
  });
109
168
  if (!res.ok) {
110
- throw new Error(`Token exchange failed (${res.status}). Check your authorization code and try again.`);
169
+ throw new Error(`Token exchange failed (${res.status}). Try again with \`dario login\`.`);
111
170
  }
112
171
  const data = await res.json();
113
172
  const tokens = {
package/dist/proxy.js CHANGED
@@ -176,8 +176,10 @@ export async function startProxy(opts = {}) {
176
176
  res.end();
177
177
  return;
178
178
  }
179
+ // Strip query parameters for endpoint matching
180
+ const urlPath = req.url?.split('?')[0] ?? '';
179
181
  // Health check
180
- if (req.url === '/health' || req.url === '/') {
182
+ if (urlPath === '/health' || urlPath === '/') {
181
183
  const s = await getStatus();
182
184
  res.writeHead(200, { 'Content-Type': 'application/json' });
183
185
  res.end(JSON.stringify({
@@ -189,20 +191,19 @@ export async function startProxy(opts = {}) {
189
191
  return;
190
192
  }
191
193
  // Status endpoint
192
- if (req.url === '/status') {
194
+ if (urlPath === '/status') {
193
195
  const s = await getStatus();
194
196
  res.writeHead(200, { 'Content-Type': 'application/json' });
195
197
  res.end(JSON.stringify(s));
196
198
  return;
197
199
  }
198
200
  // Allowlisted API paths — only these are proxied (prevents SSRF)
199
- const rawPath = req.url?.split('?')[0] ?? '';
200
201
  const allowedPaths = {
201
202
  '/v1/messages': `${ANTHROPIC_API}/v1/messages`,
202
203
  '/v1/models': `${ANTHROPIC_API}/v1/models`,
203
204
  '/v1/complete': `${ANTHROPIC_API}/v1/complete`,
204
205
  };
205
- const targetBase = allowedPaths[rawPath];
206
+ const targetBase = allowedPaths[urlPath];
206
207
  if (!targetBase) {
207
208
  res.writeHead(403, { 'Content-Type': 'application/json' });
208
209
  res.end(JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' }));
@@ -232,7 +233,7 @@ export async function startProxy(opts = {}) {
232
233
  }
233
234
  const body = Buffer.concat(chunks);
234
235
  // CLI backend mode: route through claude --print
235
- if (useCli && rawPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
236
+ if (useCli && urlPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
236
237
  const cliResult = await handleViaCli(body, modelOverride, verbose);
237
238
  requestCount++;
238
239
  res.writeHead(cliResult.status, {
@@ -265,6 +266,7 @@ export async function startProxy(opts = {}) {
265
266
  'interleaved-thinking-2025-05-14',
266
267
  'prompt-caching-scope-2026-01-05',
267
268
  'claude-code-20250219',
269
+ 'context-management-2025-06-27',
268
270
  ]);
269
271
  if (clientBeta) {
270
272
  for (const flag of clientBeta.split(',')) {
@@ -280,6 +282,7 @@ export async function startProxy(opts = {}) {
280
282
  'anthropic-version': req.headers['anthropic-version'] || '2023-06-01',
281
283
  'anthropic-beta': [...betaFlags].join(','),
282
284
  'anthropic-dangerous-direct-browser-access': 'true',
285
+ 'anthropic-client-platform': 'cli',
283
286
  'user-agent': `claude-cli/${cliVersion} (external, cli)`,
284
287
  'x-app': 'cli',
285
288
  'x-claude-code-session-id': SESSION_ID,
@@ -307,11 +310,11 @@ export async function startProxy(opts = {}) {
307
310
  'Content-Type': contentType || 'application/json',
308
311
  'Access-Control-Allow-Origin': CORS_ORIGIN,
309
312
  };
310
- // Forward rate limit headers
311
- for (const h of ['x-ratelimit-limit-requests', 'x-ratelimit-limit-tokens', 'x-ratelimit-remaining-requests', 'x-ratelimit-remaining-tokens', 'request-id']) {
312
- const v = upstream.headers.get(h);
313
- if (v)
314
- responseHeaders[h] = v;
313
+ // Forward rate limit headers (including unified subscription headers)
314
+ for (const [key, value] of upstream.headers.entries()) {
315
+ if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
316
+ responseHeaders[key] = value;
317
+ }
315
318
  }
316
319
  requestCount++;
317
320
  res.writeHead(upstream.status, responseHeaders);
@@ -382,6 +385,33 @@ export async function startProxy(opts = {}) {
382
385
  console.log(` ${modelLine}`);
383
386
  console.log('');
384
387
  });
388
+ // Session presence heartbeat — registers this proxy as an active Claude Code session
389
+ // Claude Code sends this every 5 seconds; the server uses it for priority routing
390
+ const clientId = randomUUID();
391
+ const connectedAt = new Date().toISOString();
392
+ let lastPresencePulse = 0;
393
+ const presenceInterval = setInterval(async () => {
394
+ const now = Date.now();
395
+ if (now - lastPresencePulse < 5000)
396
+ return;
397
+ lastPresencePulse = now;
398
+ try {
399
+ const token = await getAccessToken();
400
+ const presenceUrl = `${ANTHROPIC_API}/v1/code/sessions/${SESSION_ID}/client/presence`;
401
+ await fetch(presenceUrl, {
402
+ method: 'POST',
403
+ headers: {
404
+ 'Authorization': `Bearer ${token}`,
405
+ 'Content-Type': 'application/json',
406
+ 'anthropic-version': '2023-06-01',
407
+ 'anthropic-client-platform': 'cli',
408
+ },
409
+ body: JSON.stringify({ client_id: clientId, connected_at: connectedAt }),
410
+ signal: AbortSignal.timeout(5000),
411
+ }).catch(() => { });
412
+ }
413
+ catch { /* presence is best-effort */ }
414
+ }, 5000);
385
415
  // Periodic token refresh (every 15 minutes)
386
416
  const refreshInterval = setInterval(async () => {
387
417
  try {
@@ -398,6 +428,7 @@ export async function startProxy(opts = {}) {
398
428
  // Graceful shutdown
399
429
  const shutdown = () => {
400
430
  console.log('\n[dario] Shutting down...');
431
+ clearInterval(presenceInterval);
401
432
  clearInterval(refreshInterval);
402
433
  server.close(() => process.exit(0));
403
434
  // Force exit after 5s if connections don't close
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "1.1.3",
4
- "description": "Use your Claude subscription as an API. Two commands, no API key. OAuth bridge for Claude Max/Pro.",
3
+ "version": "1.2.1",
4
+ "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dario": "./dist/cli.js"
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc",
24
+ "audit": "npm audit --production --audit-level=high",
24
25
  "prepublishOnly": "npm run build",
25
26
  "start": "node dist/cli.js",
26
27
  "dev": "tsx src/cli.ts"
@@ -40,7 +41,7 @@
40
41
  "cli",
41
42
  "developer-tools"
42
43
  ],
43
- "author": "askalf",
44
+ "author": "askalf (https://github.com/askalf)",
44
45
  "license": "MIT",
45
46
  "repository": {
46
47
  "type": "git",