@askalf/dario 1.0.8 → 1.1.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/dist/cli.js CHANGED
@@ -132,7 +132,10 @@ async function proxy() {
132
132
  process.exit(1);
133
133
  }
134
134
  const verbose = args.includes('--verbose') || args.includes('-v');
135
- await startProxy({ port, verbose });
135
+ const cliBackend = args.includes('--cli');
136
+ const modelArg = args.find(a => a.startsWith('--model='));
137
+ const model = modelArg ? modelArg.split('=')[1] : undefined;
138
+ await startProxy({ port, verbose, model, cliBackend });
136
139
  }
137
140
  async function help() {
138
141
  console.log(`
@@ -146,6 +149,10 @@ async function help() {
146
149
  dario logout Remove saved credentials
147
150
 
148
151
  Proxy options:
152
+ --model=MODEL Force a model for all requests
153
+ Shortcuts: opus, sonnet, haiku
154
+ Default: passthrough (client decides)
155
+ --cli Use Claude CLI as backend (bypasses rate limits)
149
156
  --port=PORT Port to listen on (default: 3456)
150
157
  --verbose, -v Log all requests
151
158
 
package/dist/proxy.d.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  interface ProxyOptions {
11
11
  port?: number;
12
12
  verbose?: boolean;
13
+ model?: string;
14
+ cliBackend?: boolean;
13
15
  }
14
16
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
15
17
  export {};
package/dist/proxy.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { createServer } from 'node:http';
11
11
  import { randomUUID } from 'node:crypto';
12
- import { execSync } from 'node:child_process';
12
+ import { execSync, spawn } from 'node:child_process';
13
13
  import { arch, platform, version as nodeVersion } from 'node:process';
14
14
  import { getAccessToken, getStatus } from './oauth.js';
15
15
  const ANTHROPIC_API = 'https://api.anthropic.com';
@@ -49,11 +49,106 @@ function detectSdkVersion() {
49
49
  return '0.81.0';
50
50
  }
51
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
+ };
52
58
  function sanitizeError(err) {
53
59
  const msg = err instanceof Error ? err.message : String(err);
54
60
  // Never leak tokens in error messages
55
61
  return msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]');
56
62
  }
63
+ /**
64
+ * CLI Backend: route requests through `claude --print` instead of direct API.
65
+ * This bypasses rate limiting because Claude Code's binary has priority routing.
66
+ */
67
+ async function handleViaCli(body, model, verbose) {
68
+ try {
69
+ const parsed = JSON.parse(body.toString());
70
+ // Extract the last user message as the prompt
71
+ const messages = parsed.messages ?? [];
72
+ const lastUser = [...messages].reverse().find(m => m.role === 'user');
73
+ if (!lastUser) {
74
+ return { status: 400, body: JSON.stringify({ error: 'No user message' }), contentType: 'application/json' };
75
+ }
76
+ const effectiveModel = model ?? parsed.model ?? 'claude-opus-4-6';
77
+ const prompt = typeof lastUser.content === 'string'
78
+ ? lastUser.content
79
+ : JSON.stringify(lastUser.content);
80
+ // Build claude --print command
81
+ const args = ['--print', '--model', effectiveModel];
82
+ // Build system prompt from messages context
83
+ let systemPrompt = parsed.system ?? '';
84
+ // Include conversation history as context
85
+ const history = messages.slice(0, -1);
86
+ if (history.length > 0) {
87
+ const historyText = history.map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n');
88
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\nConversation history:\n${historyText}` : `Conversation history:\n${historyText}`;
89
+ }
90
+ if (systemPrompt) {
91
+ args.push('--append-system-prompt', systemPrompt);
92
+ }
93
+ if (verbose) {
94
+ console.log(`[dario:cli] model=${effectiveModel} prompt=${prompt.substring(0, 60)}...`);
95
+ }
96
+ // Spawn claude --print
97
+ return new Promise((resolve) => {
98
+ const child = spawn('claude', args, {
99
+ stdio: ['pipe', 'pipe', 'pipe'],
100
+ timeout: 300_000,
101
+ });
102
+ let stdout = '';
103
+ let stderr = '';
104
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
105
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
106
+ child.stdin.write(prompt);
107
+ child.stdin.end();
108
+ child.on('close', (code) => {
109
+ if (code !== 0 || !stdout.trim()) {
110
+ resolve({
111
+ status: 502,
112
+ body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: stderr.substring(0, 200) || 'CLI backend failed' } }),
113
+ contentType: 'application/json',
114
+ });
115
+ return;
116
+ }
117
+ // Build a proper Messages API response
118
+ const text = stdout.trim();
119
+ const estimatedTokens = Math.ceil(text.length / 4);
120
+ const response = {
121
+ id: `msg_${randomUUID().replace(/-/g, '').substring(0, 24)}`,
122
+ type: 'message',
123
+ role: 'assistant',
124
+ model: effectiveModel,
125
+ content: [{ type: 'text', text }],
126
+ stop_reason: 'end_turn',
127
+ stop_sequence: null,
128
+ usage: {
129
+ input_tokens: Math.ceil(prompt.length / 4),
130
+ output_tokens: estimatedTokens,
131
+ },
132
+ };
133
+ resolve({ status: 200, body: JSON.stringify(response), contentType: 'application/json' });
134
+ });
135
+ child.on('error', (err) => {
136
+ resolve({
137
+ status: 502,
138
+ body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'Claude CLI not found. Install Claude Code first.' } }),
139
+ contentType: 'application/json',
140
+ });
141
+ });
142
+ });
143
+ }
144
+ catch (err) {
145
+ return {
146
+ status: 400,
147
+ body: JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid request body' } }),
148
+ contentType: 'application/json',
149
+ };
150
+ }
151
+ }
57
152
  export async function startProxy(opts = {}) {
58
153
  const port = opts.port ?? DEFAULT_PORT;
59
154
  const verbose = opts.verbose ?? false;
@@ -65,6 +160,8 @@ export async function startProxy(opts = {}) {
65
160
  }
66
161
  const cliVersion = detectClaudeVersion();
67
162
  const sdkVersion = detectSdkVersion();
163
+ const modelOverride = opts.model ? (MODEL_ALIASES[opts.model] ?? opts.model) : null;
164
+ const useCli = opts.cliBackend ?? false;
68
165
  let requestCount = 0;
69
166
  let tokenCostEstimate = 0;
70
167
  const server = createServer(async (req, res) => {
@@ -134,8 +231,30 @@ export async function startProxy(opts = {}) {
134
231
  chunks.push(buf);
135
232
  }
136
233
  const body = Buffer.concat(chunks);
234
+ // CLI backend mode: route through claude --print
235
+ if (useCli && rawPath === '/v1/messages' && req.method === 'POST' && body.length > 0) {
236
+ const cliResult = await handleViaCli(body, modelOverride, verbose);
237
+ requestCount++;
238
+ res.writeHead(cliResult.status, {
239
+ 'Content-Type': cliResult.contentType,
240
+ 'Access-Control-Allow-Origin': CORS_ORIGIN,
241
+ });
242
+ res.end(cliResult.body);
243
+ return;
244
+ }
245
+ // Override model in request body if --model flag was set
246
+ let finalBody = body.length > 0 ? body : undefined;
247
+ if (modelOverride && body.length > 0) {
248
+ try {
249
+ const parsed = JSON.parse(body.toString());
250
+ parsed.model = modelOverride;
251
+ finalBody = Buffer.from(JSON.stringify(parsed));
252
+ }
253
+ catch { /* not JSON, send as-is */ }
254
+ }
137
255
  if (verbose) {
138
- console.log(`[dario] #${requestCount} ${req.method} ${req.url}`);
256
+ const modelInfo = modelOverride ? ` (model: ${modelOverride})` : '';
257
+ console.log(`[dario] #${requestCount} ${req.method} ${req.url}${modelInfo}`);
139
258
  }
140
259
  // Build target URL from allowlist (no user input in URL construction)
141
260
  const targetUrl = targetBase;
@@ -177,10 +296,8 @@ export async function startProxy(opts = {}) {
177
296
  const upstream = await fetch(targetUrl, {
178
297
  method: req.method ?? 'POST',
179
298
  headers,
180
- body: body.length > 0 ? body : undefined,
299
+ body: finalBody ? new Uint8Array(finalBody) : undefined,
181
300
  signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
182
- // @ts-expect-error — duplex needed for streaming
183
- duplex: 'half',
184
301
  });
185
302
  // Detect streaming from content-type (reliable) or body (fallback)
186
303
  const contentType = upstream.headers.get('content-type') ?? '';
@@ -250,7 +367,8 @@ export async function startProxy(opts = {}) {
250
367
  process.exit(1);
251
368
  });
252
369
  server.listen(port, LOCALHOST, () => {
253
- const oauthLine = `OAuth: ${status.status} (expires in ${status.expiresIn})`;
370
+ const oauthLine = useCli ? 'Backend: Claude CLI (bypasses rate limits)' : `OAuth: ${status.status} (expires in ${status.expiresIn})`;
371
+ const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
254
372
  console.log('');
255
373
  console.log(` dario — http://localhost:${port}`);
256
374
  console.log('');
@@ -261,6 +379,7 @@ export async function startProxy(opts = {}) {
261
379
  console.log(' ANTHROPIC_API_KEY=dario');
262
380
  console.log('');
263
381
  console.log(` ${oauthLine}`);
382
+ console.log(` ${modelLine}`);
264
383
  console.log('');
265
384
  });
266
385
  // 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.8",
3
+ "version": "1.1.0",
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": {