@askalf/dario 1.2.0 → 2.0.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 CHANGED
@@ -2,15 +2,23 @@
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
- One command. 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
- <a href="#how-it-works">How It Works</a> &bull;
12
- <a href="#usage-examples">Examples</a> &bull;
19
+ <a href="#openai-compatibility">OpenAI Compat</a> &bull;
13
20
  <a href="#cli-backend">CLI Backend</a> &bull;
21
+ <a href="#usage-examples">Examples</a> &bull;
14
22
  <a href="#faq">FAQ</a>
15
23
  </p>
16
24
 
@@ -40,7 +48,9 @@ You pay $100-200/mo for Claude Max or Pro. But that subscription only works on c
40
48
 
41
49
  ### Prerequisites
42
50
 
43
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) must be installed and logged in. Dario uses your existing Claude Code credentials — no separate authentication needed.
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.
44
54
 
45
55
  ### Install
46
56
 
@@ -60,9 +70,8 @@ npx @askalf/dario login
60
70
  dario login
61
71
  ```
62
72
 
63
- If Claude Code is installed and authenticated, dario detects your credentials automatically and starts the proxy. No browser, no OAuth flow, no pasting URLs.
64
-
65
- If Claude Code credentials aren't found, dario falls back to a manual OAuth flow.
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.
66
75
 
67
76
  ### Start the proxy
68
77
 
@@ -148,6 +157,37 @@ Combine with `--cli` for rate-limit-proof Opus:
148
157
  dario proxy --cli --model=opus
149
158
  ```
150
159
 
160
+ ## OpenAI Compatibility
161
+
162
+ Dario speaks both Anthropic and OpenAI API formats. Any tool built for OpenAI works with your Claude subscription — Cursor, Continue, LiteLLM, anything.
163
+
164
+ ```bash
165
+ # Use with any OpenAI SDK or tool
166
+ export OPENAI_BASE_URL=http://localhost:3456/v1
167
+ export OPENAI_API_KEY=dario
168
+ ```
169
+
170
+ ```python
171
+ from openai import OpenAI
172
+
173
+ client = OpenAI(base_url="http://localhost:3456/v1", api_key="dario")
174
+ response = client.chat.completions.create(
175
+ model="claude-opus-4-6", # or use "gpt-4" — auto-maps to Opus
176
+ messages=[{"role": "user", "content": "Hello!"}]
177
+ )
178
+ ```
179
+
180
+ Model mapping (automatic):
181
+
182
+ | OpenAI model | Maps to |
183
+ |---|---|
184
+ | `gpt-4`, `gpt-4o`, `o1`, `o3` | `claude-opus-4-6` |
185
+ | `o1-mini`, `o3-mini` | `claude-sonnet-4-6` |
186
+ | `gpt-3.5-turbo`, `gpt-4o-mini` | `claude-haiku-4-5` |
187
+ | Any `claude-*` model | Passed through directly |
188
+
189
+ Streaming, system prompts, temperature, and stop sequences all translate automatically.
190
+
151
191
  ## Usage Examples
152
192
 
153
193
  ### curl
@@ -251,7 +291,7 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
251
291
  └──────────┘ └─────────────────┘ └──────────────────┘
252
292
  ```
253
293
 
254
- 1. **`dario login`** — Detects your existing Claude Code credentials (`~/.claude/.credentials.json`) and starts the proxy automatically. If Claude Code isn't installed, falls back to a manual PKCE OAuth flow.
294
+ 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.
255
295
 
256
296
  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.
257
297
 
@@ -281,7 +321,8 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
281
321
 
282
322
  ### Direct API Mode
283
323
  - All Claude models (Opus 4.6, Sonnet 4.6, Haiku 4.5)
284
- - Streaming and non-streaming
324
+ - **OpenAI-compatible** (`/v1/chat/completions`) — works with any OpenAI SDK or tool
325
+ - Streaming and non-streaming (both Anthropic and OpenAI SSE formats)
285
326
  - Tool use / function calling
286
327
  - System prompts and multi-turn conversations
287
328
  - Prompt caching and extended thinking
@@ -298,7 +339,9 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
298
339
 
299
340
  | Path | Description |
300
341
  |------|-------------|
301
- | `POST /v1/messages` | Anthropic Messages API (main endpoint) |
342
+ | `POST /v1/messages` | Anthropic Messages API |
343
+ | `POST /v1/chat/completions` | OpenAI-compatible Chat API |
344
+ | `GET /v1/models` | Model list (works with both SDKs) |
302
345
  | `GET /health` | Proxy health + OAuth status + request count |
303
346
  | `GET /status` | Detailed OAuth token status |
304
347
 
@@ -321,11 +364,11 @@ curl http://localhost:3456/health
321
364
 
322
365
  | Concern | How dario handles it |
323
366
  |---------|---------------------|
324
- | Credential storage | Uses Claude Code's credentials or `~/.dario/credentials.json` with `0600` permissions |
367
+ | Credential storage | Reads from Claude Code (`~/.claude/.credentials.json`) or its own store (`~/.dario/credentials.json`) with `0600` permissions |
325
368
  | OAuth flow | PKCE (Proof Key for Code Exchange) — no client secret needed |
326
369
  | Token transmission | OAuth tokens never leave localhost. Only forwarded to `api.anthropic.com` over HTTPS |
327
370
  | Network exposure | Proxy binds to `127.0.0.1` only — not accessible from other machines |
328
- | SSRF protection | Allowlisted API paths only. Internal networks and cloud metadata blocked |
371
+ | SSRF protection | Hardcoded allowlist of API paths only `/v1/messages`, `/v1/models`, `/v1/complete` are proxied |
329
372
  | Token rotation | Refresh tokens rotate on every use (single-use) |
330
373
  | Error sanitization | Token patterns redacted from all error messages |
331
374
  | Data collection | Zero. No telemetry, no analytics, no phoning home |
@@ -342,7 +385,7 @@ Claude Max and Claude Pro. Any plan that lets you use Claude Code.
342
385
  Should work if your plan includes Claude Code access. Not tested yet — please open an issue with results.
343
386
 
344
387
  **Do I need Claude Code installed?**
345
- Yes. Dario reads your Claude Code credentials for authentication. Run `claude` to install and log in, then `dario login` picks up your credentials automatically.
388
+ 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`).
346
389
 
347
390
  **What happens when my token expires?**
348
391
  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.
@@ -354,7 +397,7 @@ Use `--cli` mode: `dario proxy --cli`. This routes through the Claude Code binar
354
397
  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.
355
398
 
356
399
  **Can I run this on a server?**
357
- Dario binds to localhost by default. For server use, you'd need to handle the initial Claude Code login on a machine with a browser, then copy `~/.claude/.credentials.json` to your server. Auto-refresh will keep it alive from there.
400
+ 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.
358
401
 
359
402
  **Why "dario"?**
360
403
  Named after [Dario Amodei](https://en.wikipedia.org/wiki/Dario_Amodei), CEO of Anthropic.
@@ -380,13 +423,39 @@ const status = await getStatus();
380
423
  console.log(status.expiresIn); // "11h 42m"
381
424
  ```
382
425
 
426
+ ## Trust & Transparency
427
+
428
+ Dario handles your OAuth tokens. Here's why you can trust it:
429
+
430
+ | Signal | Status |
431
+ |--------|--------|
432
+ | **Source code** | ~1000 lines of TypeScript — small enough to read in one sitting |
433
+ | **Dependencies** | 1 production dep (`@anthropic-ai/sdk`). Verify: `npm ls --production` |
434
+ | **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
435
+ | **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
436
+ | **Credential handling** | Tokens never logged, redacted from errors, stored with 0600 permissions |
437
+ | **Network scope** | Binds to 127.0.0.1 only. Upstream traffic goes exclusively to `api.anthropic.com` over HTTPS |
438
+ | **No telemetry** | Zero analytics, tracking, or data collection of any kind |
439
+ | **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
440
+ | **Branch protection** | CI must pass before merge. CODEOWNERS enforces review |
441
+
442
+ Verify the npm package matches this repo:
443
+
444
+ ```bash
445
+ # Check provenance attestation
446
+ npm audit signatures 2>/dev/null; npm view @askalf/dario dist.integrity
447
+
448
+ # Check dependency tree (should be minimal)
449
+ cd $(npm root -g)/@askalf/dario && npm ls --production
450
+ ```
451
+
383
452
  ## Contributing
384
453
 
385
- PRs welcome. The codebase is ~700 lines of TypeScript across 4 files:
454
+ PRs welcome. The codebase is ~1000 lines of TypeScript across 4 files:
386
455
 
387
456
  | File | Purpose |
388
457
  |------|---------|
389
- | `src/oauth.ts` | Token storage, refresh logic, Claude Code credential detection |
458
+ | `src/oauth.ts` | Token storage, refresh logic, Claude Code credential detection, auto OAuth flow |
390
459
  | `src/proxy.ts` | HTTP proxy server + CLI backend |
391
460
  | `src/cli.ts` | CLI entry point |
392
461
  | `src/index.ts` | Library exports |
package/dist/cli.js CHANGED
@@ -9,23 +9,13 @@
9
9
  * dario refresh — Force token refresh
10
10
  * dario logout — Remove saved credentials
11
11
  */
12
- import { createInterface } from 'node:readline';
13
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
21
  console.log(' dario — Claude Login');
@@ -49,38 +39,9 @@ async function login() {
49
39
  catch { /* no Claude Code credentials, fall through to OAuth */ }
50
40
  console.log(' No Claude Code credentials found. Starting OAuth flow...');
51
41
  console.log('');
52
- const { authUrl, codeVerifier } = startOAuthFlow();
53
- console.log(' Step 1: Open this URL in your browser:');
54
- console.log('');
55
- console.log(` ${authUrl}`);
56
- console.log('');
57
- console.log(' Step 2: Log in to your Claude account and authorize.');
58
- console.log('');
59
- console.log(' Step 3: After authorization, you\'ll be redirected to a page');
60
- console.log(' that shows a code. Copy the FULL URL from your browser\'s');
61
- console.log(' address bar (it contains the authorization code).');
62
- console.log('');
63
- const input = await ask(' Paste the redirect URL or authorization code: ');
64
- // Extract code from URL or use raw input
65
- let code = input;
66
- try {
67
- const url = new URL(input);
68
- // Only extract from trusted Anthropic redirect URLs
69
- if (url.hostname === 'platform.claude.com' || url.hostname === 'claude.ai') {
70
- code = url.searchParams.get('code') ?? input;
71
- }
72
- }
73
- catch {
74
- // Not a URL, use as-is (raw code)
75
- }
76
- if (!code || code.length < 10 || code.length > 2048) {
77
- console.error(' Invalid authorization code.');
78
- process.exit(1);
79
- }
80
42
  try {
81
- const tokens = await exchangeCode(code, codeVerifier);
43
+ const tokens = await startAutoOAuthFlow();
82
44
  const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 60000);
83
- console.log('');
84
45
  console.log(' Login successful!');
85
46
  console.log(` Token expires in ${expiresIn} minutes (auto-refreshes).`);
86
47
  console.log('');
@@ -160,7 +121,7 @@ async function help() {
160
121
  dario — Use your Claude subscription as an API.
161
122
 
162
123
  Usage:
163
- dario login Authenticate with your Claude account
124
+ dario login Detect credentials + start proxy (or run OAuth)
164
125
  dario proxy [options] Start the API proxy server
165
126
  dario status Check authentication status
166
127
  dario refresh Force token refresh
@@ -169,27 +130,25 @@ async function help() {
169
130
  Proxy options:
170
131
  --model=MODEL Force a model for all requests
171
132
  Shortcuts: opus, sonnet, haiku
133
+ Full IDs: claude-opus-4-6, claude-sonnet-4-6
172
134
  Default: passthrough (client decides)
173
135
  --cli Use Claude CLI as backend (bypasses rate limits)
174
136
  --port=PORT Port to listen on (default: 3456)
175
137
  --verbose, -v Log all requests
176
138
 
177
- How it works:
178
- 1. Run \`dario login\` to authenticate with your Claude Max/Pro subscription
179
- 2. Run \`dario proxy\` to start a local API server
180
- 3. Point any Anthropic SDK at http://localhost:3456
181
-
182
- Example with OpenClaw, or any tool that uses the Anthropic API:
183
- 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
184
142
 
185
- Example with the Anthropic Python SDK:
186
- import anthropic
187
- 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
188
146
 
189
- Example with curl:
190
- curl http://localhost:3456/v1/messages \\
191
- -H "Content-Type: application/json" \\
192
- -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"}]}'
193
152
 
194
153
  Your subscription handles the billing. No API key needed.
195
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
@@ -12,7 +12,7 @@ import { homedir } from 'node:os';
12
12
  const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
13
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
@@ -72,46 +72,101 @@ async function saveCredentials(creds) {
72
72
  credentialsCacheTime = Date.now();
73
73
  }
74
74
  /**
75
- * Start the OAuth flow. Returns the authorization URL and PKCE state
76
- * 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.
77
77
  */
78
- export function startOAuthFlow() {
78
+ export async function startAutoOAuthFlow() {
79
+ const { createServer } = await import('node:http');
79
80
  const { codeVerifier, codeChallenge } = generatePKCE();
80
81
  const state = base64url(randomBytes(16));
81
- const params = new URLSearchParams({
82
- code: 'true',
83
- client_id: OAUTH_CLIENT_ID,
84
- response_type: 'code',
85
- redirect_uri: OAUTH_REDIRECT_URI,
86
- scope: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
87
- code_challenge: codeChallenge,
88
- code_challenge_method: 'S256',
89
- state,
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);
90
149
  });
91
- return {
92
- authUrl: `${OAUTH_AUTHORIZE_URL}?${params.toString()}`,
93
- state,
94
- codeVerifier,
95
- };
96
150
  }
97
151
  /**
98
- * Exchange authorization code for tokens and save them.
152
+ * Exchange code using the localhost redirect URI.
99
153
  */
100
- export async function exchangeCode(code, codeVerifier) {
154
+ async function exchangeCodeWithRedirect(code, codeVerifier, state, port) {
101
155
  const res = await fetch(OAUTH_TOKEN_URL, {
102
156
  method: 'POST',
103
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
104
- body: new URLSearchParams({
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({
105
159
  grant_type: 'authorization_code',
106
160
  client_id: OAUTH_CLIENT_ID,
107
161
  code,
108
- redirect_uri: OAUTH_REDIRECT_URI,
162
+ redirect_uri: `http://localhost:${port}/callback`,
109
163
  code_verifier: codeVerifier,
164
+ state,
110
165
  }),
111
166
  signal: AbortSignal.timeout(30000),
112
167
  });
113
168
  if (!res.ok) {
114
- 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\`.`);
115
170
  }
116
171
  const data = await res.json();
117
172
  const tokens = {
package/dist/proxy.js CHANGED
@@ -55,6 +55,128 @@ const MODEL_ALIASES = {
55
55
  'sonnet': 'claude-sonnet-4-6',
56
56
  'haiku': 'claude-haiku-4-5',
57
57
  };
58
+ // OpenAI model name → Anthropic model name
59
+ const OPENAI_MODEL_MAP = {
60
+ 'gpt-4': 'claude-opus-4-6',
61
+ 'gpt-4o': 'claude-opus-4-6',
62
+ 'gpt-4-turbo': 'claude-opus-4-6',
63
+ 'gpt-4o-mini': 'claude-haiku-4-5',
64
+ 'gpt-3.5-turbo': 'claude-haiku-4-5',
65
+ 'o1': 'claude-opus-4-6',
66
+ 'o1-mini': 'claude-sonnet-4-6',
67
+ 'o1-preview': 'claude-opus-4-6',
68
+ 'o3': 'claude-opus-4-6',
69
+ 'o3-mini': 'claude-sonnet-4-6',
70
+ };
71
+ /**
72
+ * Translate OpenAI chat completion request → Anthropic Messages request.
73
+ */
74
+ function openaiToAnthropic(body, modelOverride) {
75
+ const messages = body.messages;
76
+ if (!messages)
77
+ return body;
78
+ // Extract system messages
79
+ const systemMessages = messages.filter(m => m.role === 'system');
80
+ const nonSystemMessages = messages.filter(m => m.role !== 'system');
81
+ // Map model name
82
+ const requestModel = String(body.model || '');
83
+ const model = modelOverride || OPENAI_MODEL_MAP[requestModel] || requestModel;
84
+ const result = {
85
+ model,
86
+ messages: nonSystemMessages.map(m => ({
87
+ role: m.role === 'assistant' ? 'assistant' : 'user',
88
+ content: m.content,
89
+ })),
90
+ max_tokens: body.max_tokens ?? body.max_completion_tokens ?? 8192,
91
+ };
92
+ if (systemMessages.length > 0) {
93
+ result.system = systemMessages.map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content)).join('\n');
94
+ }
95
+ if (body.stream)
96
+ result.stream = true;
97
+ if (body.temperature != null)
98
+ result.temperature = body.temperature;
99
+ if (body.top_p != null)
100
+ result.top_p = body.top_p;
101
+ if (body.stop)
102
+ result.stop_sequences = Array.isArray(body.stop) ? body.stop : [body.stop];
103
+ return result;
104
+ }
105
+ /**
106
+ * Translate Anthropic Messages response → OpenAI chat completion response.
107
+ */
108
+ function anthropicToOpenai(body) {
109
+ const content = body.content;
110
+ const text = content?.find(c => c.type === 'text')?.text ?? '';
111
+ const usage = body.usage;
112
+ return {
113
+ id: `chatcmpl-${(body.id || '').replace('msg_', '')}`,
114
+ object: 'chat.completion',
115
+ created: Math.floor(Date.now() / 1000),
116
+ model: body.model,
117
+ choices: [{
118
+ index: 0,
119
+ message: { role: 'assistant', content: text },
120
+ finish_reason: body.stop_reason === 'end_turn' ? 'stop' : body.stop_reason === 'max_tokens' ? 'length' : 'stop',
121
+ }],
122
+ usage: {
123
+ prompt_tokens: usage?.input_tokens ?? 0,
124
+ completion_tokens: usage?.output_tokens ?? 0,
125
+ total_tokens: (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0),
126
+ },
127
+ };
128
+ }
129
+ /**
130
+ * Translate Anthropic SSE stream → OpenAI SSE stream.
131
+ */
132
+ function translateStreamChunk(line) {
133
+ if (!line.startsWith('data: '))
134
+ return null;
135
+ const json = line.slice(6).trim();
136
+ if (json === '[DONE]')
137
+ return 'data: [DONE]\n\n';
138
+ try {
139
+ const event = JSON.parse(json);
140
+ if (event.type === 'content_block_delta') {
141
+ const delta = event.delta;
142
+ if (delta?.type === 'text_delta' && delta.text) {
143
+ return `data: ${JSON.stringify({
144
+ id: 'chatcmpl-dario',
145
+ object: 'chat.completion.chunk',
146
+ created: Math.floor(Date.now() / 1000),
147
+ model: 'claude',
148
+ choices: [{ index: 0, delta: { content: delta.text }, finish_reason: null }],
149
+ })}\n\n`;
150
+ }
151
+ }
152
+ if (event.type === 'message_stop') {
153
+ return `data: ${JSON.stringify({
154
+ id: 'chatcmpl-dario',
155
+ object: 'chat.completion.chunk',
156
+ created: Math.floor(Date.now() / 1000),
157
+ model: 'claude',
158
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
159
+ })}\n\ndata: [DONE]\n\n`;
160
+ }
161
+ }
162
+ catch { /* skip unparseable */ }
163
+ return null;
164
+ }
165
+ /**
166
+ * OpenAI-compatible models list.
167
+ */
168
+ function openaiModelsList() {
169
+ const models = ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
170
+ return {
171
+ object: 'list',
172
+ data: models.map(id => ({
173
+ id,
174
+ object: 'model',
175
+ created: 1700000000,
176
+ owned_by: 'anthropic',
177
+ })),
178
+ };
179
+ }
58
180
  function sanitizeError(err) {
59
181
  const msg = err instanceof Error ? err.message : String(err);
60
182
  // Never leak tokens in error messages
@@ -176,8 +298,10 @@ export async function startProxy(opts = {}) {
176
298
  res.end();
177
299
  return;
178
300
  }
301
+ // Strip query parameters for endpoint matching
302
+ const urlPath = req.url?.split('?')[0] ?? '';
179
303
  // Health check
180
- if (req.url === '/health' || req.url === '/') {
304
+ if (urlPath === '/health' || urlPath === '/') {
181
305
  const s = await getStatus();
182
306
  res.writeHead(200, { 'Content-Type': 'application/json' });
183
307
  res.end(JSON.stringify({
@@ -189,27 +313,34 @@ export async function startProxy(opts = {}) {
189
313
  return;
190
314
  }
191
315
  // Status endpoint
192
- if (req.url === '/status') {
316
+ if (urlPath === '/status') {
193
317
  const s = await getStatus();
194
318
  res.writeHead(200, { 'Content-Type': 'application/json' });
195
319
  res.end(JSON.stringify(s));
196
320
  return;
197
321
  }
322
+ // OpenAI-compatible models list
323
+ if (urlPath === '/v1/models' && req.method === 'GET') {
324
+ requestCount++;
325
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': CORS_ORIGIN });
326
+ res.end(JSON.stringify(openaiModelsList()));
327
+ return;
328
+ }
329
+ // Detect OpenAI-format requests
330
+ const isOpenAI = urlPath === '/v1/chat/completions';
198
331
  // Allowlisted API paths — only these are proxied (prevents SSRF)
199
- const rawPath = req.url?.split('?')[0] ?? '';
200
332
  const allowedPaths = {
201
333
  '/v1/messages': `${ANTHROPIC_API}/v1/messages`,
202
- '/v1/models': `${ANTHROPIC_API}/v1/models`,
203
334
  '/v1/complete': `${ANTHROPIC_API}/v1/complete`,
204
335
  };
205
- const targetBase = allowedPaths[rawPath];
336
+ const targetBase = isOpenAI ? `${ANTHROPIC_API}/v1/messages` : allowedPaths[urlPath];
206
337
  if (!targetBase) {
207
338
  res.writeHead(403, { 'Content-Type': 'application/json' });
208
339
  res.end(JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' }));
209
340
  return;
210
341
  }
211
- // Only allow POST (Messages API) and GET (models)
212
- if (req.method !== 'POST' && req.method !== 'GET') {
342
+ // Only allow POST (Messages/Chat API) and GET (models)
343
+ if (req.method !== 'POST') {
213
344
  res.writeHead(405, { 'Content-Type': 'application/json' });
214
345
  res.end(JSON.stringify({ error: 'Method not allowed' }));
215
346
  return;
@@ -232,7 +363,7 @@ export async function startProxy(opts = {}) {
232
363
  }
233
364
  const body = Buffer.concat(chunks);
234
365
  // CLI backend mode: route through claude --print
235
- if (useCli && rawPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
366
+ if (useCli && urlPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
236
367
  const cliResult = await handleViaCli(body, modelOverride, verbose);
237
368
  requestCount++;
238
369
  res.writeHead(cliResult.status, {
@@ -242,9 +373,18 @@ export async function startProxy(opts = {}) {
242
373
  res.end(cliResult.body);
243
374
  return;
244
375
  }
245
- // Override model in request body if --model flag was set
376
+ // Translate OpenAI Anthropic format if needed
246
377
  let finalBody = body.length > 0 ? body : undefined;
247
- if (modelOverride && body.length > 0) {
378
+ if (isOpenAI && body.length > 0) {
379
+ try {
380
+ const parsed = JSON.parse(body.toString());
381
+ const translated = openaiToAnthropic(parsed, modelOverride);
382
+ finalBody = Buffer.from(JSON.stringify(translated));
383
+ }
384
+ catch { /* not JSON, send as-is */ }
385
+ }
386
+ else if (modelOverride && body.length > 0) {
387
+ // Override model in request body if --model flag was set
248
388
  try {
249
389
  const parsed = JSON.parse(body.toString());
250
390
  parsed.model = modelOverride;
@@ -320,12 +460,33 @@ export async function startProxy(opts = {}) {
320
460
  if (isStream && upstream.body) {
321
461
  // Stream SSE chunks through
322
462
  const reader = upstream.body.getReader();
463
+ const decoder = new TextDecoder();
323
464
  try {
465
+ let buffer = '';
324
466
  while (true) {
325
467
  const { done, value } = await reader.read();
326
468
  if (done)
327
469
  break;
328
- res.write(value);
470
+ if (isOpenAI) {
471
+ // Translate Anthropic SSE → OpenAI SSE
472
+ buffer += decoder.decode(value, { stream: true });
473
+ const lines = buffer.split('\n');
474
+ buffer = lines.pop() ?? '';
475
+ for (const line of lines) {
476
+ const translated = translateStreamChunk(line);
477
+ if (translated)
478
+ res.write(translated);
479
+ }
480
+ }
481
+ else {
482
+ res.write(value);
483
+ }
484
+ }
485
+ // Flush remaining buffer
486
+ if (isOpenAI && buffer.trim()) {
487
+ const translated = translateStreamChunk(buffer);
488
+ if (translated)
489
+ res.write(translated);
329
490
  }
330
491
  }
331
492
  catch (err) {
@@ -337,7 +498,19 @@ export async function startProxy(opts = {}) {
337
498
  else {
338
499
  // Buffer and forward
339
500
  const responseBody = await upstream.text();
340
- res.end(responseBody);
501
+ if (isOpenAI && upstream.status >= 200 && upstream.status < 300) {
502
+ // Translate Anthropic response → OpenAI format
503
+ try {
504
+ const parsed = JSON.parse(responseBody);
505
+ res.end(JSON.stringify(anthropicToOpenai(parsed)));
506
+ }
507
+ catch {
508
+ res.end(responseBody);
509
+ }
510
+ }
511
+ else {
512
+ res.end(responseBody);
513
+ }
341
514
  // Quick token estimate for logging
342
515
  if (verbose && responseBody) {
343
516
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "1.2.0",
4
- "description": "Use your Claude subscription as an API. Two commands, no API key. OAuth bridge for Claude Max/Pro.",
3
+ "version": "2.0.0",
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",