@askalf/dario 3.2.7 → 3.4.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 +2 -0
- package/dist/cc-oauth-detect.d.ts +47 -0
- package/dist/cc-oauth-detect.js +232 -0
- package/dist/cc-template.d.ts +2 -0
- package/dist/cc-template.js +30 -26
- package/dist/cli.js +3 -1
- package/dist/oauth.js +21 -14
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +40 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,8 @@ Opus, Sonnet, Haiku — all models, streaming, tool use. **Zero dependencies.**
|
|
|
74
74
|
</tr>
|
|
75
75
|
</table>
|
|
76
76
|
|
|
77
|
+
> **Need more than a proxy?** Dario solves the API access problem. If you need a full agent fleet — desktop control, browser automation, scheduling, custom tools, persistent memory — check out the [askalf platform](https://askalf.org). Same team, different execution model that solves the proxy ceiling entirely.
|
|
78
|
+
|
|
77
79
|
---
|
|
78
80
|
|
|
79
81
|
## Why dario
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC OAuth Auto-Detection
|
|
3
|
+
*
|
|
4
|
+
* Scans the installed Claude Code binary to extract its OAuth configuration
|
|
5
|
+
* (client_id, authorize URL, token URL, scopes). Eliminates the need to
|
|
6
|
+
* hardcode values that Anthropic rotates between CC releases.
|
|
7
|
+
*
|
|
8
|
+
* CC ships two OAuth client configurations in one binary:
|
|
9
|
+
*
|
|
10
|
+
* 1. LOCAL flow — used when the OAuth client owns the callback
|
|
11
|
+
* (i.e. runs an HTTP server on localhost). This is what dario does.
|
|
12
|
+
* Identified by OAUTH_FILE_SUFFIX:"-local-oauth" next to the CLIENT_ID.
|
|
13
|
+
*
|
|
14
|
+
* 2. PLATFORM flow — used when the callback is hosted at
|
|
15
|
+
* platform.claude.com/oauth/code/callback. Different CLIENT_ID.
|
|
16
|
+
* Not applicable to dario.
|
|
17
|
+
*
|
|
18
|
+
* We scan for the LOCAL block and extract its config.
|
|
19
|
+
*
|
|
20
|
+
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache.json so
|
|
21
|
+
* startup only re-scans when the user upgrades Claude Code.
|
|
22
|
+
*/
|
|
23
|
+
export interface DetectedOAuthConfig {
|
|
24
|
+
clientId: string;
|
|
25
|
+
authorizeUrl: string;
|
|
26
|
+
tokenUrl: string;
|
|
27
|
+
scopes: string;
|
|
28
|
+
source: 'detected' | 'cached' | 'fallback';
|
|
29
|
+
ccPath?: string;
|
|
30
|
+
ccHash?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scan binary bytes for the LOCAL-oauth OAuth block.
|
|
34
|
+
* Uses Buffer.indexOf to locate anchor strings, then slices a small
|
|
35
|
+
* window of context to run regexes on. This avoids converting the
|
|
36
|
+
* whole binary to a JS string.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scanBinaryForOAuthConfig(buf: Buffer): Omit<DetectedOAuthConfig, 'source' | 'ccPath' | 'ccHash'> | null;
|
|
39
|
+
/**
|
|
40
|
+
* Get the OAuth config for dario to use. Scans the installed CC binary
|
|
41
|
+
* on first call, caches to disk, and memoizes in-process for subsequent
|
|
42
|
+
* calls. If no binary is found or scanning fails, falls back to the
|
|
43
|
+
* known-good v2.1.104 values.
|
|
44
|
+
*/
|
|
45
|
+
export declare function detectCCOAuthConfig(): Promise<DetectedOAuthConfig>;
|
|
46
|
+
/** Test-only: reset in-process memoization. */
|
|
47
|
+
export declare function _resetDetectorCache(): void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC OAuth Auto-Detection
|
|
3
|
+
*
|
|
4
|
+
* Scans the installed Claude Code binary to extract its OAuth configuration
|
|
5
|
+
* (client_id, authorize URL, token URL, scopes). Eliminates the need to
|
|
6
|
+
* hardcode values that Anthropic rotates between CC releases.
|
|
7
|
+
*
|
|
8
|
+
* CC ships two OAuth client configurations in one binary:
|
|
9
|
+
*
|
|
10
|
+
* 1. LOCAL flow — used when the OAuth client owns the callback
|
|
11
|
+
* (i.e. runs an HTTP server on localhost). This is what dario does.
|
|
12
|
+
* Identified by OAUTH_FILE_SUFFIX:"-local-oauth" next to the CLIENT_ID.
|
|
13
|
+
*
|
|
14
|
+
* 2. PLATFORM flow — used when the callback is hosted at
|
|
15
|
+
* platform.claude.com/oauth/code/callback. Different CLIENT_ID.
|
|
16
|
+
* Not applicable to dario.
|
|
17
|
+
*
|
|
18
|
+
* We scan for the LOCAL block and extract its config.
|
|
19
|
+
*
|
|
20
|
+
* Results are cached per-binary-hash at ~/.dario/cc-oauth-cache.json so
|
|
21
|
+
* startup only re-scans when the user upgrades Claude Code.
|
|
22
|
+
*/
|
|
23
|
+
import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
|
|
24
|
+
import { existsSync } from 'node:fs';
|
|
25
|
+
import { homedir, platform } from 'node:os';
|
|
26
|
+
import { join, dirname } from 'node:path';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
// Last-resort fallback if CC binary can't be found or scanned.
|
|
29
|
+
// These values are the known-good v2.1.104 local-oauth flow.
|
|
30
|
+
const FALLBACK = {
|
|
31
|
+
clientId: '22422756-60c9-4084-8eb7-27705fd5cf9a',
|
|
32
|
+
authorizeUrl: 'https://claude.com/cai/oauth/authorize',
|
|
33
|
+
tokenUrl: 'https://platform.claude.com/v1/oauth/token',
|
|
34
|
+
scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers',
|
|
35
|
+
source: 'fallback',
|
|
36
|
+
};
|
|
37
|
+
const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache.json');
|
|
38
|
+
function candidatePaths() {
|
|
39
|
+
const home = homedir();
|
|
40
|
+
if (platform() === 'win32') {
|
|
41
|
+
return [
|
|
42
|
+
join(home, '.local', 'bin', 'claude.exe'),
|
|
43
|
+
join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
44
|
+
join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
|
|
45
|
+
join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
46
|
+
join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
join(home, '.local', 'bin', 'claude'),
|
|
51
|
+
'/usr/local/bin/claude',
|
|
52
|
+
'/opt/homebrew/bin/claude',
|
|
53
|
+
'/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
54
|
+
'/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.mjs',
|
|
55
|
+
'/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
|
|
56
|
+
join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
57
|
+
join(home, '.claude', 'local', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.mjs'),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function findCCBinary() {
|
|
61
|
+
const override = process.env['DARIO_CC_PATH'];
|
|
62
|
+
if (override && existsSync(override))
|
|
63
|
+
return override;
|
|
64
|
+
for (const p of candidatePaths()) {
|
|
65
|
+
if (existsSync(p))
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fast fingerprint of a binary for caching. We hash the first 64KB plus
|
|
72
|
+
* size+mtime — this discriminates CC versions without reading GBs off disk.
|
|
73
|
+
*/
|
|
74
|
+
async function fingerprintBinary(path) {
|
|
75
|
+
const st = await stat(path);
|
|
76
|
+
const fh = await openFile(path, 'r');
|
|
77
|
+
try {
|
|
78
|
+
const buf = Buffer.alloc(Math.min(65536, st.size));
|
|
79
|
+
await fh.read(buf, 0, buf.length, 0);
|
|
80
|
+
const h = createHash('sha256');
|
|
81
|
+
h.update(buf);
|
|
82
|
+
h.update(String(st.size));
|
|
83
|
+
h.update(String(st.mtimeMs));
|
|
84
|
+
return h.digest('hex').slice(0, 16);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await fh.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Scan binary bytes for the LOCAL-oauth OAuth block.
|
|
92
|
+
* Uses Buffer.indexOf to locate anchor strings, then slices a small
|
|
93
|
+
* window of context to run regexes on. This avoids converting the
|
|
94
|
+
* whole binary to a JS string.
|
|
95
|
+
*/
|
|
96
|
+
export function scanBinaryForOAuthConfig(buf) {
|
|
97
|
+
// Anchor: `OAUTH_FILE_SUFFIX:"-local-oauth"` — this is the config-block
|
|
98
|
+
// occurrence, not the switch-case string literal. The switch-case produces
|
|
99
|
+
// just `-local-oauth` bytes, but the config object serializes as
|
|
100
|
+
// `OAUTH_FILE_SUFFIX:"-local-oauth"` with the key+quote prefix, which is
|
|
101
|
+
// stable across minified CC builds.
|
|
102
|
+
const anchor = Buffer.from('OAUTH_FILE_SUFFIX:"-local-oauth"');
|
|
103
|
+
let anchorIdx = buf.indexOf(anchor);
|
|
104
|
+
// Fallback anchor — some builds may tokenize differently.
|
|
105
|
+
if (anchorIdx === -1) {
|
|
106
|
+
const looseAnchor = Buffer.from('"-local-oauth"');
|
|
107
|
+
anchorIdx = buf.indexOf(looseAnchor);
|
|
108
|
+
}
|
|
109
|
+
if (anchorIdx === -1)
|
|
110
|
+
return null;
|
|
111
|
+
// The CLIENT_ID sits within a few hundred bytes BEFORE the anchor
|
|
112
|
+
// (in the same config object). Extract a window around it.
|
|
113
|
+
const windowStart = Math.max(0, anchorIdx - 1024);
|
|
114
|
+
const windowEnd = Math.min(buf.length, anchorIdx + 64);
|
|
115
|
+
const localBlock = buf.slice(windowStart, windowEnd).toString('latin1');
|
|
116
|
+
// Pick the CLIENT_ID that's CLOSEST to the anchor (last occurrence in window).
|
|
117
|
+
const cidRegex = /CLIENT_ID\s*:\s*"([0-9a-f-]{36})"/gi;
|
|
118
|
+
let lastCid = null;
|
|
119
|
+
let m;
|
|
120
|
+
while ((m = cidRegex.exec(localBlock)) !== null) {
|
|
121
|
+
if (m[1])
|
|
122
|
+
lastCid = m[1];
|
|
123
|
+
}
|
|
124
|
+
if (!lastCid)
|
|
125
|
+
return null;
|
|
126
|
+
const clientId = lastCid;
|
|
127
|
+
// Authorize URL: CLAUDE_AI_AUTHORIZE_URL appears once in the binary.
|
|
128
|
+
const authAnchor = Buffer.from('CLAUDE_AI_AUTHORIZE_URL');
|
|
129
|
+
const authIdx = buf.indexOf(authAnchor);
|
|
130
|
+
let authorizeUrl = FALLBACK.authorizeUrl;
|
|
131
|
+
if (authIdx !== -1) {
|
|
132
|
+
const w = buf.slice(authIdx, Math.min(buf.length, authIdx + 256)).toString('latin1');
|
|
133
|
+
const m = /CLAUDE_AI_AUTHORIZE_URL\s*:\s*"([^"]+)"/.exec(w);
|
|
134
|
+
if (m && m[1])
|
|
135
|
+
authorizeUrl = m[1];
|
|
136
|
+
}
|
|
137
|
+
// Token URL: TOKEN_URL — look for the one under platform.claude.com/.../oauth/token
|
|
138
|
+
const tokenAnchor = Buffer.from('TOKEN_URL');
|
|
139
|
+
let searchFrom = 0;
|
|
140
|
+
let tokenUrl = FALLBACK.tokenUrl;
|
|
141
|
+
while (searchFrom < buf.length) {
|
|
142
|
+
const idx = buf.indexOf(tokenAnchor, searchFrom);
|
|
143
|
+
if (idx === -1)
|
|
144
|
+
break;
|
|
145
|
+
const w = buf.slice(idx, Math.min(buf.length, idx + 128)).toString('latin1');
|
|
146
|
+
const m = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(w);
|
|
147
|
+
if (m && m[1]) {
|
|
148
|
+
tokenUrl = m[1];
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
searchFrom = idx + tokenAnchor.length;
|
|
152
|
+
}
|
|
153
|
+
// Scopes: contiguous quoted string of "user:X user:Y user:Z ..."
|
|
154
|
+
// Search for an anchor like "user:profile " which is the first scope.
|
|
155
|
+
const scopeAnchor = Buffer.from('"user:profile ');
|
|
156
|
+
let scopes = FALLBACK.scopes;
|
|
157
|
+
const scopeIdx = buf.indexOf(scopeAnchor);
|
|
158
|
+
if (scopeIdx !== -1) {
|
|
159
|
+
const w = buf.slice(scopeIdx, Math.min(buf.length, scopeIdx + 512)).toString('latin1');
|
|
160
|
+
const m = /"(user:profile(?:\s+user:[a-z_:]+)+)"/.exec(w);
|
|
161
|
+
if (m && m[1])
|
|
162
|
+
scopes = m[1];
|
|
163
|
+
}
|
|
164
|
+
return { clientId, authorizeUrl, tokenUrl, scopes };
|
|
165
|
+
}
|
|
166
|
+
async function loadCache() {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile(CACHE_PATH, 'utf-8');
|
|
169
|
+
const parsed = JSON.parse(raw);
|
|
170
|
+
if (parsed?.hash && parsed?.config?.clientId) {
|
|
171
|
+
return { hash: parsed.hash, config: parsed.config };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch { /* no cache */ }
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
async function saveCache(hash, config) {
|
|
178
|
+
try {
|
|
179
|
+
await mkdir(dirname(CACHE_PATH), { recursive: true });
|
|
180
|
+
await writeFile(CACHE_PATH, JSON.stringify({ hash, config, savedAt: Date.now() }, null, 2));
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore cache write errors */ }
|
|
183
|
+
}
|
|
184
|
+
let memoized = null;
|
|
185
|
+
/**
|
|
186
|
+
* Get the OAuth config for dario to use. Scans the installed CC binary
|
|
187
|
+
* on first call, caches to disk, and memoizes in-process for subsequent
|
|
188
|
+
* calls. If no binary is found or scanning fails, falls back to the
|
|
189
|
+
* known-good v2.1.104 values.
|
|
190
|
+
*/
|
|
191
|
+
export async function detectCCOAuthConfig() {
|
|
192
|
+
if (memoized)
|
|
193
|
+
return memoized;
|
|
194
|
+
try {
|
|
195
|
+
const ccPath = findCCBinary();
|
|
196
|
+
if (!ccPath) {
|
|
197
|
+
memoized = FALLBACK;
|
|
198
|
+
return memoized;
|
|
199
|
+
}
|
|
200
|
+
const hash = await fingerprintBinary(ccPath);
|
|
201
|
+
// Check cache
|
|
202
|
+
const cached = await loadCache();
|
|
203
|
+
if (cached && cached.hash === hash) {
|
|
204
|
+
memoized = { ...cached.config, source: 'cached', ccPath, ccHash: hash };
|
|
205
|
+
return memoized;
|
|
206
|
+
}
|
|
207
|
+
// Read binary and scan
|
|
208
|
+
const buf = await readFile(ccPath);
|
|
209
|
+
const scanned = scanBinaryForOAuthConfig(buf);
|
|
210
|
+
if (!scanned) {
|
|
211
|
+
memoized = { ...FALLBACK, ccPath, ccHash: hash };
|
|
212
|
+
return memoized;
|
|
213
|
+
}
|
|
214
|
+
const detected = {
|
|
215
|
+
...scanned,
|
|
216
|
+
source: 'detected',
|
|
217
|
+
ccPath,
|
|
218
|
+
ccHash: hash,
|
|
219
|
+
};
|
|
220
|
+
await saveCache(hash, detected);
|
|
221
|
+
memoized = detected;
|
|
222
|
+
return memoized;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
memoized = FALLBACK;
|
|
226
|
+
return memoized;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Test-only: reset in-process memoization. */
|
|
230
|
+
export function _resetDetectorCache() {
|
|
231
|
+
memoized = null;
|
|
232
|
+
}
|
package/dist/cc-template.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare function buildCCRequest(clientBody: Record<string, unknown>, bill
|
|
|
33
33
|
deviceId: string;
|
|
34
34
|
accountUuid: string;
|
|
35
35
|
sessionId: string;
|
|
36
|
+
}, opts?: {
|
|
37
|
+
preserveTools?: boolean;
|
|
36
38
|
}): {
|
|
37
39
|
body: Record<string, unknown>;
|
|
38
40
|
toolMap: Map<string, ToolMapping>;
|
package/dist/cc-template.js
CHANGED
|
@@ -52,7 +52,7 @@ const TOOL_MAP = {
|
|
|
52
52
|
* Replaces the entire request structure — tools, fields, ordering — with
|
|
53
53
|
* what real CC sends. Only the conversation content is preserved.
|
|
54
54
|
*/
|
|
55
|
-
export function buildCCRequest(clientBody, billingTag, cache1h, identity) {
|
|
55
|
+
export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts = {}) {
|
|
56
56
|
const model = clientBody.model || 'claude-sonnet-4-6';
|
|
57
57
|
const isHaiku = model.toLowerCase().includes('haiku');
|
|
58
58
|
const messages = clientBody.messages || [];
|
|
@@ -71,9 +71,14 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
// ── Build tool mapping ──
|
|
74
|
+
// In preserveTools mode, skip the tool name/arg rewriting entirely.
|
|
75
|
+
// Tool routing in real agents requires bidirectional schema fidelity that
|
|
76
|
+
// lossy forward-only translation can't provide. Users with custom tool
|
|
77
|
+
// schemas should use preserveTools to keep their tools as-is and accept
|
|
78
|
+
// the fingerprint risk on their own account.
|
|
74
79
|
const activeToolMap = new Map();
|
|
75
80
|
const unmappedTools = [];
|
|
76
|
-
if (clientTools) {
|
|
81
|
+
if (clientTools && !opts.preserveTools) {
|
|
77
82
|
for (const tool of clientTools) {
|
|
78
83
|
const name = (tool.name || '').toLowerCase();
|
|
79
84
|
const mapping = TOOL_MAP[name];
|
|
@@ -105,30 +110,27 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity) {
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
// ── Remap tool_use and tool_result references in message history ──
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
// Skip in preserveTools mode — leave conversation history untouched.
|
|
114
|
+
if (!opts.preserveTools) {
|
|
115
|
+
for (const msg of messages) {
|
|
116
|
+
if (Array.isArray(msg.content)) {
|
|
117
|
+
for (const block of msg.content) {
|
|
118
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
119
|
+
const mapping = activeToolMap.get(block.name);
|
|
120
|
+
if (mapping) {
|
|
121
|
+
block.name = mapping.ccTool;
|
|
122
|
+
if (mapping.translateArgs && block.input) {
|
|
123
|
+
block.input = mapping.translateArgs(block.input);
|
|
124
|
+
}
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
|
-
//
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Remove non-standard fields clients may add
|
|
129
|
-
for (const key of Object.keys(block)) {
|
|
130
|
-
if (!['type', 'tool_use_id', 'content', 'is_error'].includes(key)) {
|
|
131
|
-
delete block[key];
|
|
127
|
+
// Strip any client-specific fields from tool_result blocks that CC wouldn't send
|
|
128
|
+
if (block.type === 'tool_result') {
|
|
129
|
+
// Remove non-standard fields clients may add
|
|
130
|
+
for (const key of Object.keys(block)) {
|
|
131
|
+
if (!['type', 'tool_use_id', 'content', 'is_error'].includes(key)) {
|
|
132
|
+
delete block[key];
|
|
133
|
+
}
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
}
|
|
@@ -199,9 +201,11 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity) {
|
|
|
199
201
|
{ type: 'text', text: fullSystemPrompt, cache_control: cache1h },
|
|
200
202
|
],
|
|
201
203
|
};
|
|
202
|
-
// Tools come before metadata in CC's key order
|
|
204
|
+
// Tools come before metadata in CC's key order.
|
|
205
|
+
// preserveTools mode: pass client tools through unchanged (better for real
|
|
206
|
+
// agents with custom schemas, but loses the CC tool fingerprint).
|
|
203
207
|
if (clientTools && clientTools.length > 0) {
|
|
204
|
-
ccRequest.tools = CC_TOOL_DEFINITIONS;
|
|
208
|
+
ccRequest.tools = opts.preserveTools ? clientTools : CC_TOOL_DEFINITIONS;
|
|
205
209
|
}
|
|
206
210
|
// Metadata
|
|
207
211
|
ccRequest.metadata = {
|
package/dist/cli.js
CHANGED
|
@@ -128,9 +128,10 @@ async function proxy() {
|
|
|
128
128
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
129
129
|
const cliBackend = args.includes('--cli');
|
|
130
130
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
131
|
+
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
131
132
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
132
133
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
133
|
-
await startProxy({ port, verbose, model, cliBackend, passthrough });
|
|
134
|
+
await startProxy({ port, verbose, model, cliBackend, passthrough, preserveTools });
|
|
134
135
|
}
|
|
135
136
|
async function help() {
|
|
136
137
|
console.log(`
|
|
@@ -150,6 +151,7 @@ async function help() {
|
|
|
150
151
|
Default: passthrough (client decides)
|
|
151
152
|
--cli Use Claude CLI as backend (bypasses rate limits)
|
|
152
153
|
--passthrough Thin proxy — OAuth swap only, no injection
|
|
154
|
+
--preserve-tools Keep client tool schemas (for agents with custom tools)
|
|
153
155
|
--port=PORT Port to listen on (default: 3456)
|
|
154
156
|
--verbose, -v Log all requests
|
|
155
157
|
|
package/dist/oauth.js
CHANGED
|
@@ -8,13 +8,17 @@ import { randomBytes, createHash } from 'node:crypto';
|
|
|
8
8
|
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
11
|
+
import { detectCCOAuthConfig } from './cc-oauth-detect.js';
|
|
12
|
+
// OAuth config is auto-detected at runtime from the installed Claude Code
|
|
13
|
+
// binary. This eliminates the "Anthropic rotated the client_id again" class
|
|
14
|
+
// of bugs — dario stays in sync with whatever CC version the user has
|
|
15
|
+
// installed, forever. See cc-oauth-detect.ts for the scanner.
|
|
16
|
+
//
|
|
17
|
+
// Hardcoded fallbacks live in cc-oauth-detect.ts and are the known-good
|
|
18
|
+
// CC v2.1.104 local-oauth flow values.
|
|
19
|
+
async function getOAuthConfig() {
|
|
20
|
+
return detectCCOAuthConfig();
|
|
21
|
+
}
|
|
18
22
|
// Refresh 30 min before expiry
|
|
19
23
|
const REFRESH_BUFFER_MS = 30 * 60 * 1000;
|
|
20
24
|
// After a failed refresh, don't retry for 60s to avoid spam
|
|
@@ -116,17 +120,18 @@ export async function startAutoOAuthFlow() {
|
|
|
116
120
|
server.listen(0, 'localhost', async () => {
|
|
117
121
|
const addr = server.address();
|
|
118
122
|
port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
123
|
+
const cfg = await getOAuthConfig();
|
|
119
124
|
const params = new URLSearchParams({
|
|
120
125
|
code: 'true',
|
|
121
|
-
client_id:
|
|
126
|
+
client_id: cfg.clientId,
|
|
122
127
|
response_type: 'code',
|
|
123
128
|
redirect_uri: `http://localhost:${port}/callback`,
|
|
124
|
-
scope:
|
|
129
|
+
scope: cfg.scopes,
|
|
125
130
|
code_challenge: codeChallenge,
|
|
126
131
|
code_challenge_method: 'S256',
|
|
127
132
|
state,
|
|
128
133
|
});
|
|
129
|
-
const authUrl = `${
|
|
134
|
+
const authUrl = `${cfg.authorizeUrl}?${params.toString()}`;
|
|
130
135
|
// Open browser
|
|
131
136
|
console.log(' Opening browser to sign in...');
|
|
132
137
|
console.log(` If the browser didn't open, visit: ${authUrl}`);
|
|
@@ -152,12 +157,13 @@ export async function startAutoOAuthFlow() {
|
|
|
152
157
|
* Exchange code using the localhost redirect URI.
|
|
153
158
|
*/
|
|
154
159
|
async function exchangeCodeWithRedirect(code, codeVerifier, state, port) {
|
|
155
|
-
const
|
|
160
|
+
const cfg = await getOAuthConfig();
|
|
161
|
+
const res = await fetch(cfg.tokenUrl, {
|
|
156
162
|
method: 'POST',
|
|
157
163
|
headers: { 'Content-Type': 'application/json' },
|
|
158
164
|
body: JSON.stringify({
|
|
159
165
|
grant_type: 'authorization_code',
|
|
160
|
-
client_id:
|
|
166
|
+
client_id: cfg.clientId,
|
|
161
167
|
code,
|
|
162
168
|
redirect_uri: `http://localhost:${port}/callback`,
|
|
163
169
|
code_verifier: codeVerifier,
|
|
@@ -201,16 +207,17 @@ async function doRefreshTokens() {
|
|
|
201
207
|
throw new Error('No refresh token available. Run `dario login` first.');
|
|
202
208
|
}
|
|
203
209
|
const oauth = creds.claudeAiOauth;
|
|
210
|
+
const cfg = await getOAuthConfig();
|
|
204
211
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
205
212
|
if (attempt > 0)
|
|
206
213
|
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
207
|
-
const res = await fetch(
|
|
214
|
+
const res = await fetch(cfg.tokenUrl, {
|
|
208
215
|
method: 'POST',
|
|
209
216
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
210
217
|
body: new URLSearchParams({
|
|
211
218
|
grant_type: 'refresh_token',
|
|
212
219
|
refresh_token: oauth.refreshToken,
|
|
213
|
-
client_id:
|
|
220
|
+
client_id: cfg.clientId,
|
|
214
221
|
}),
|
|
215
222
|
signal: AbortSignal.timeout(15000),
|
|
216
223
|
});
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -705,7 +705,7 @@ export async function startProxy(opts = {}) {
|
|
|
705
705
|
const fullVersion = `${cliVersion}.${buildTag}`;
|
|
706
706
|
const billingTag = `x-anthropic-billing-header: cc_version=${fullVersion}; cc_entrypoint=cli; cch=${cch};`;
|
|
707
707
|
const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
|
|
708
|
-
const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID });
|
|
708
|
+
const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID }, { preserveTools: opts.preserveTools ?? false });
|
|
709
709
|
// Store tool map for response reverse-mapping
|
|
710
710
|
ccToolMap = toolMap;
|
|
711
711
|
// Replace request body entirely with CC template
|
|
@@ -767,12 +767,22 @@ export async function startProxy(opts = {}) {
|
|
|
767
767
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
768
768
|
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
769
769
|
});
|
|
770
|
-
// Auto-retry without context-1m if it triggers a long-context billing error
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
770
|
+
// Auto-retry without context-1m if it triggers a long-context billing error.
|
|
771
|
+
// Anthropic returns this as either 400 ("long context beta is not yet available
|
|
772
|
+
// for this subscription") or 429 ("Extra usage is required for long context
|
|
773
|
+
// requests") depending on the endpoint — we handle both.
|
|
774
|
+
//
|
|
775
|
+
// Note: `upstream.text()` consumes the body, so once we peek we MUST
|
|
776
|
+
// handle the response here (can't fall through to the normal forwarder).
|
|
777
|
+
let peekedBody = null;
|
|
778
|
+
if ((upstream.status === 400 || upstream.status === 429) && !passthrough) {
|
|
779
|
+
peekedBody = await upstream.text().catch(() => '');
|
|
780
|
+
const isLongContextError = peekedBody.includes('long context')
|
|
781
|
+
|| peekedBody.includes('Extra usage is required')
|
|
782
|
+
|| peekedBody.includes('long_context');
|
|
783
|
+
if (isLongContextError) {
|
|
774
784
|
if (verbose)
|
|
775
|
-
console.log(`[dario] #${requestCount} context-1m rejected — retrying without it`);
|
|
785
|
+
console.log(`[dario] #${requestCount} context-1m rejected (${upstream.status}) — retrying without it`);
|
|
776
786
|
const reducedBeta = beta.replace(',context-1m-2025-08-07', '').replace('context-1m-2025-08-07,', '');
|
|
777
787
|
const retryHeaders = { ...headers, 'anthropic-beta': reducedBeta };
|
|
778
788
|
const retry = await fetch(targetBase, {
|
|
@@ -781,13 +791,13 @@ export async function startProxy(opts = {}) {
|
|
|
781
791
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
782
792
|
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
783
793
|
});
|
|
784
|
-
// Use the retry response from here on
|
|
794
|
+
// Use the retry response from here on — peeked body is now stale
|
|
785
795
|
upstream = retry;
|
|
796
|
+
peekedBody = null;
|
|
786
797
|
}
|
|
787
|
-
else {
|
|
788
|
-
// Not a context-1m issue —
|
|
789
|
-
|
|
790
|
-
const enriched = enrich429(peekBody, upstream.headers);
|
|
798
|
+
else if (upstream.status === 429) {
|
|
799
|
+
// Not a context-1m issue — return enriched 429 directly
|
|
800
|
+
const enriched = enrich429(peekedBody, upstream.headers);
|
|
791
801
|
if (!(cliAvailable && !useCli)) {
|
|
792
802
|
const responseHeaders = {
|
|
793
803
|
'Content-Type': 'application/json',
|
|
@@ -804,7 +814,25 @@ export async function startProxy(opts = {}) {
|
|
|
804
814
|
res.end(enriched);
|
|
805
815
|
return;
|
|
806
816
|
}
|
|
807
|
-
// Fall through to CLI fallback below
|
|
817
|
+
// Fall through to CLI fallback below — need to re-handle 429 with
|
|
818
|
+
// already-consumed body; stash it for the fallback path.
|
|
819
|
+
}
|
|
820
|
+
else if (upstream.status === 400) {
|
|
821
|
+
// Non-long-context 400 — forward upstream error directly.
|
|
822
|
+
// The body is already consumed, so we write it straight out.
|
|
823
|
+
const responseHeaders = {
|
|
824
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
825
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
826
|
+
...SECURITY_HEADERS,
|
|
827
|
+
};
|
|
828
|
+
for (const [key, value] of upstream.headers.entries()) {
|
|
829
|
+
if (key === 'request-id')
|
|
830
|
+
responseHeaders[key] = value;
|
|
831
|
+
}
|
|
832
|
+
requestCount++;
|
|
833
|
+
res.writeHead(400, responseHeaders);
|
|
834
|
+
res.end(peekedBody);
|
|
835
|
+
return;
|
|
808
836
|
}
|
|
809
837
|
}
|
|
810
838
|
// Enrich 429 errors with rate limit details from headers (Anthropic only returns "Error")
|