@askalf/dario 1.0.7 → 1.0.9

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/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { homedir } from 'node:os';
16
16
  import { startOAuthFlow, exchangeCode, getStatus, refreshTokens } from './oauth.js';
17
17
  import { startProxy } from './proxy.js';
18
18
  const args = process.argv.slice(2);
19
- const command = args[0] ?? 'proxy';
19
+ const command = args[0] ?? (process.stdin.isTTY ? 'proxy' : 'proxy');
20
20
  function ask(question) {
21
21
  const rl = createInterface({ input: process.stdin, output: process.stdout });
22
22
  return new Promise(resolve => {
@@ -83,10 +83,18 @@ async function status() {
83
83
  console.log(' ─────────────');
84
84
  console.log('');
85
85
  if (!s.authenticated) {
86
- console.log(` Status: ${s.status === 'expired' ? 'Expired (will auto-refresh)' : 'Not authenticated'}`);
87
- if (s.status === 'none') {
86
+ if (s.status === 'expired' && s.canRefresh) {
87
+ console.log(' Status: Expired (will auto-refresh when proxy starts)');
88
+ console.log(' Run `dario refresh` to refresh now, or `dario proxy` to start.');
89
+ }
90
+ else if (s.status === 'none') {
91
+ console.log(' Status: Not authenticated');
88
92
  console.log(' Run `dario login` to authenticate.');
89
93
  }
94
+ else {
95
+ console.log(` Status: ${s.status}`);
96
+ console.log(' Run `dario login` to re-authenticate.');
97
+ }
90
98
  }
91
99
  else {
92
100
  console.log(` Status: ${s.status}`);
@@ -124,7 +132,9 @@ async function proxy() {
124
132
  process.exit(1);
125
133
  }
126
134
  const verbose = args.includes('--verbose') || args.includes('-v');
127
- await startProxy({ port, verbose });
135
+ const modelArg = args.find(a => a.startsWith('--model='));
136
+ const model = modelArg ? modelArg.split('=')[1] : undefined;
137
+ await startProxy({ port, verbose, model });
128
138
  }
129
139
  async function help() {
130
140
  console.log(`
@@ -138,6 +148,9 @@ async function help() {
138
148
  dario logout Remove saved credentials
139
149
 
140
150
  Proxy options:
151
+ --model=MODEL Force a model for all requests
152
+ Shortcuts: opus, sonnet, haiku
153
+ Default: passthrough (client decides)
141
154
  --port=PORT Port to listen on (default: 3456)
142
155
  --verbose, -v Log all requests
143
156
 
package/dist/oauth.d.ts CHANGED
@@ -45,4 +45,5 @@ export declare function getStatus(): Promise<{
45
45
  status: 'healthy' | 'expiring' | 'expired' | 'none';
46
46
  expiresAt?: number;
47
47
  expiresIn?: string;
48
+ canRefresh?: boolean;
48
49
  }>;
package/dist/oauth.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Handles authorization, token exchange, storage, and auto-refresh.
6
6
  */
7
7
  import { randomBytes, createHash } from 'node:crypto';
8
- import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
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
11
  // Claude CLI's public OAuth client (PKCE, no secret needed)
@@ -58,7 +58,6 @@ async function saveCredentials(creds) {
58
58
  // Write atomically: write to temp file, then rename
59
59
  const tmpPath = `${path}.tmp.${Date.now()}`;
60
60
  await writeFile(tmpPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
61
- const { rename } = await import('node:fs/promises');
62
61
  await rename(tmpPath, path);
63
62
  // Set permissions (best-effort — no-op on Windows where mode is ignored)
64
63
  try {
@@ -105,10 +104,10 @@ export async function exchangeCode(code, codeVerifier) {
105
104
  redirect_uri: OAUTH_REDIRECT_URI,
106
105
  code_verifier: codeVerifier,
107
106
  }),
107
+ signal: AbortSignal.timeout(30000),
108
108
  });
109
109
  if (!res.ok) {
110
- const err = await res.text().catch(() => 'Unknown error');
111
- throw new Error(`Token exchange failed (${res.status}): ${err}`);
110
+ throw new Error(`Token exchange failed (${res.status}). Check your authorization code and try again.`);
112
111
  }
113
112
  const data = await res.json();
114
113
  const tokens = {
@@ -203,7 +202,9 @@ export async function getStatus() {
203
202
  const { expiresAt } = creds.claudeAiOauth;
204
203
  const now = Date.now();
205
204
  if (expiresAt < now) {
206
- return { authenticated: false, status: 'expired', expiresAt };
205
+ // Expired but has refresh token can be refreshed
206
+ const canRefresh = !!creds.claudeAiOauth.refreshToken;
207
+ return { authenticated: false, status: 'expired', expiresAt, canRefresh };
207
208
  }
208
209
  const ms = expiresAt - now;
209
210
  const hours = Math.floor(ms / 3600000);
package/dist/proxy.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  interface ProxyOptions {
11
11
  port?: number;
12
12
  verbose?: boolean;
13
+ model?: string;
13
14
  }
14
15
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
15
16
  export {};
package/dist/proxy.js CHANGED
@@ -39,6 +39,22 @@ function getOsName() {
39
39
  }
40
40
  // Persistent session ID per proxy lifetime (like Claude Code does per session)
41
41
  const SESSION_ID = randomUUID();
42
+ // Detect @anthropic-ai/sdk version from installed package
43
+ function detectSdkVersion() {
44
+ try {
45
+ const pkg = require('@anthropic-ai/sdk/package.json');
46
+ return pkg.version ?? '0.81.0';
47
+ }
48
+ catch {
49
+ return '0.81.0';
50
+ }
51
+ }
52
+ // Model shortcuts — users can pass short names
53
+ const MODEL_ALIASES = {
54
+ 'opus': 'claude-opus-4-6',
55
+ 'sonnet': 'claude-sonnet-4-6',
56
+ 'haiku': 'claude-haiku-4-5',
57
+ };
42
58
  function sanitizeError(err) {
43
59
  const msg = err instanceof Error ? err.message : String(err);
44
60
  // Never leak tokens in error messages
@@ -54,6 +70,8 @@ export async function startProxy(opts = {}) {
54
70
  process.exit(1);
55
71
  }
56
72
  const cliVersion = detectClaudeVersion();
73
+ const sdkVersion = detectSdkVersion();
74
+ const modelOverride = opts.model ? (MODEL_ALIASES[opts.model] ?? opts.model) : null;
57
75
  let requestCount = 0;
58
76
  let tokenCostEstimate = 0;
59
77
  const server = createServer(async (req, res) => {
@@ -109,7 +127,6 @@ export async function startProxy(opts = {}) {
109
127
  // Proxy to Anthropic
110
128
  try {
111
129
  const accessToken = await getAccessToken();
112
- requestCount++;
113
130
  // Read request body with size limit
114
131
  const chunks = [];
115
132
  let totalBytes = 0;
@@ -124,8 +141,19 @@ export async function startProxy(opts = {}) {
124
141
  chunks.push(buf);
125
142
  }
126
143
  const body = Buffer.concat(chunks);
144
+ // Override model in request body if --model flag was set
145
+ let finalBody = body.length > 0 ? body : undefined;
146
+ if (modelOverride && body.length > 0) {
147
+ try {
148
+ const parsed = JSON.parse(body.toString());
149
+ parsed.model = modelOverride;
150
+ finalBody = Buffer.from(JSON.stringify(parsed));
151
+ }
152
+ catch { /* not JSON, send as-is */ }
153
+ }
127
154
  if (verbose) {
128
- console.log(`[dario] #${requestCount} ${req.method} ${req.url}`);
155
+ const modelInfo = modelOverride ? ` (model: ${modelOverride})` : '';
156
+ console.log(`[dario] #${requestCount} ${req.method} ${req.url}${modelInfo}`);
129
157
  }
130
158
  // Build target URL from allowlist (no user input in URL construction)
131
159
  const targetUrl = targetBase;
@@ -158,7 +186,7 @@ export async function startProxy(opts = {}) {
158
186
  'x-stainless-arch': arch,
159
187
  'x-stainless-lang': 'js',
160
188
  'x-stainless-os': getOsName(),
161
- 'x-stainless-package-version': '0.81.0',
189
+ 'x-stainless-package-version': sdkVersion,
162
190
  'x-stainless-retry-count': '0',
163
191
  'x-stainless-runtime': 'node',
164
192
  'x-stainless-runtime-version': nodeVersion,
@@ -167,10 +195,8 @@ export async function startProxy(opts = {}) {
167
195
  const upstream = await fetch(targetUrl, {
168
196
  method: req.method ?? 'POST',
169
197
  headers,
170
- body: body.length > 0 ? body : undefined,
198
+ body: finalBody ? new Uint8Array(finalBody) : undefined,
171
199
  signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
172
- // @ts-expect-error — duplex needed for streaming
173
- duplex: 'half',
174
200
  });
175
201
  // Detect streaming from content-type (reliable) or body (fallback)
176
202
  const contentType = upstream.headers.get('content-type') ?? '';
@@ -186,6 +212,7 @@ export async function startProxy(opts = {}) {
186
212
  if (v)
187
213
  responseHeaders[h] = v;
188
214
  }
215
+ requestCount++;
189
216
  res.writeHead(upstream.status, responseHeaders);
190
217
  if (isStream && upstream.body) {
191
218
  // Stream SSE chunks through
@@ -229,8 +256,18 @@ export async function startProxy(opts = {}) {
229
256
  res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
230
257
  }
231
258
  });
259
+ server.on('error', (err) => {
260
+ if (err.code === 'EADDRINUSE') {
261
+ console.error(`[dario] Port ${port} is already in use. Is another dario proxy running?`);
262
+ }
263
+ else {
264
+ console.error(`[dario] Server error: ${err.message}`);
265
+ }
266
+ process.exit(1);
267
+ });
232
268
  server.listen(port, LOCALHOST, () => {
233
269
  const oauthLine = `OAuth: ${status.status} (expires in ${status.expiresIn})`;
270
+ const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
234
271
  console.log('');
235
272
  console.log(` dario — http://localhost:${port}`);
236
273
  console.log('');
@@ -241,6 +278,7 @@ export async function startProxy(opts = {}) {
241
278
  console.log(' ANTHROPIC_API_KEY=dario');
242
279
  console.log('');
243
280
  console.log(` ${oauthLine}`);
281
+ console.log(` ${modelLine}`);
244
282
  console.log('');
245
283
  });
246
284
  // Periodic token refresh (every 15 minutes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Use your Claude subscription as an API. Two commands, no API key. OAuth bridge for Claude Max/Pro.",
5
5
  "type": "module",
6
6
  "bin": {