@askalf/dario 3.19.2 → 3.19.4

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
@@ -316,12 +316,13 @@ The OpenAI-compat backend forwards tool definitions byte-for-byte and doesn't ne
316
316
  | Flag / env | Description | Default |
317
317
  |---|---|---|
318
318
  | `--passthrough` / `--thin` | Thin proxy for the Claude backend — OAuth swap only, no template injection | off |
319
- | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). | off |
320
- | `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). | off |
319
+ | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). Auto-enabled for Cline / Kilo Code / Roo Code and forks (dario#40 — detected via system-prompt fingerprint). | off (auto for text-tool clients) |
320
+ | `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). Overrides the text-tool auto-detect. | off |
321
321
  | `--model=<name>` | Force a model. Shortcuts (`opus`, `sonnet`, `haiku`), full IDs (`claude-opus-4-7`), or a **provider prefix** (`openai:gpt-4o`, `groq:llama-3.3-70b`, `claude:opus`, `local:qwen-coder`) to force the backend server-wide. See [Provider prefix](#provider-prefix). | passthrough |
322
322
  | `--port=<n>` | Port to listen on | `3456` |
323
323
  | `--host=<addr>` / `DARIO_HOST` | Bind address. Use `0.0.0.0` for LAN, or a specific IP (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
324
- | `--verbose` / `-v` | Log every request | off |
324
+ | `--verbose` / `-v` | Log every request (one line per request — method + path + billing bucket) | off |
325
+ | `--verbose=2` / `-vv` / `DARIO_LOG_BODIES=1` | Also dump the outbound request body (redacted: bearer tokens, `sk-ant-*` keys, JWTs stripped; capped at 8KB). For wire-level client-compat debugging (dario#40). | off |
325
326
  | `DARIO_API_KEY` | If set, all endpoints (except `/health`) require a matching `x-api-key` or `Authorization: Bearer` header. Required when `--host` binds non-loopback. | unset (open) |
326
327
  | `DARIO_CORS_ORIGIN` | Override browser CORS origin | `http://localhost:${port}` |
327
328
  | `DARIO_NO_BUN` | Disable automatic Bun relaunch | unset |
@@ -564,7 +565,7 @@ Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter,
564
565
  `dario doctor`. One command, one aggregated report — dario version, Node, platform, CC binary compat, template source + age + drift, OAuth status, pool state, backends, home dir. Exit code 1 if any check fails. Paste the output when you file an issue.
565
566
 
566
567
  **What happens when Anthropic rotates the OAuth config?**
567
- Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v3.json`, keyed by the CC binary fingerprint.
568
+ Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v4.json`, keyed by the CC binary fingerprint.
568
569
 
569
570
  **What happens when Anthropic changes the CC request template?**
570
571
  Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, and header insertion order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection (v3.17) forces a refresh when the installed CC version changes under dario.
@@ -28,9 +28,10 @@
28
28
  * "MANUAL_REDIRECT_URL" on platform.claude.com is only used when dario's
29
29
  * local HTTP server can't bind a port; dario never hits that path.)
30
30
  *
31
- * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v2.json so
32
- * startup only re-scans when the user upgrades Claude Code. The -v2 suffix
33
- * invalidates the v3.4.0-v3.4.2 caches that held the wrong (dev) client_id.
31
+ * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v4.json so
32
+ * startup only re-scans when the user upgrades Claude Code. The cache suffix
33
+ * is bumped each time scope handling or the fallback config changes, so
34
+ * upgrading dario picks up the new values without a manual cache clear.
34
35
  */
35
36
  export interface DetectedOAuthConfig {
36
37
  clientId: string;
@@ -28,9 +28,10 @@
28
28
  * "MANUAL_REDIRECT_URL" on platform.claude.com is only used when dario's
29
29
  * local HTTP server can't bind a port; dario never hits that path.)
30
30
  *
31
- * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v2.json so
32
- * startup only re-scans when the user upgrades Claude Code. The -v2 suffix
33
- * invalidates the v3.4.0-v3.4.2 caches that held the wrong (dev) client_id.
31
+ * Results are cached per-binary-hash at ~/.dario/cc-oauth-cache-v4.json so
32
+ * startup only re-scans when the user upgrades Claude Code. The cache suffix
33
+ * is bumped each time scope handling or the fallback config changes, so
34
+ * upgrading dario picks up the new values without a manual cache clear.
34
35
  */
35
36
  import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
36
37
  import { existsSync } from 'node:fs';
@@ -44,23 +45,25 @@ const FALLBACK = {
44
45
  clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
45
46
  authorizeUrl: 'https://claude.com/cai/oauth/authorize',
46
47
  tokenUrl: 'https://platform.claude.com/v1/oauth/token',
47
- // Scopes are the full `n36` union from the CC binary, which is the value
48
- // sent during a normal `claude login` (non-setup-token) flow. In CC's
49
- // source: `let D = f ? [TI] : n36` where `f = inferenceOnly` (true only
50
- // for `claude setup-token`). Normal interactive login uses the 6-scope
51
- // union including `org:create_api_key` even though that scope is named
52
- // "Console-only" by convention, CC's own login flow requests it up front.
53
- // Earlier dario versions (3.2.7 through 3.4.3) dropped `org:create_api_key`
54
- // from the list based on a misread of the name; the dev-only client_id
55
- // was lenient enough to accept the shorter list, the prod client_id is not.
56
- scopes: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
48
+ // Scopes match CC v2.1.107+ interactive login: the 5-scope user-only set.
49
+ // Between CC v2.1.104 and v2.1.107, Anthropic's authorize endpoint flipped
50
+ // its policy on `org:create_api_key` for this client_id the shorter list
51
+ // is now the only accepted one, and the 6-scope form returns "Invalid
52
+ // request format". CC's own binary dropped `org:create_api_key` from the
53
+ // `n36` union to match. Dario #42 (tetsuco, 2026-04-17) surfaced this as
54
+ // a fresh-login failure on macOS against CC v2.1.107.
55
+ //
56
+ // History: dario 3.2.7–3.4.3 once dropped this scope by mistake (misread
57
+ // of the "Console-only" name); 3.4.4 added it back after users hit auth
58
+ // failures with the dev client_id accepting it but prod rejecting. That
59
+ // situation has now inverted — prod rejects the longer list.
60
+ scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
57
61
  source: 'fallback',
58
62
  };
59
- // -v3 suffix invalidates v3.4.3 caches that were populated with the wrong
60
- // 4-scope list (the scanner's regex matched a help-message string literal
61
- // in the CC binary instead of the real scope array). See the scanner's
62
- // scope handling below for why scope detection is no longer attempted.
63
- const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v3.json');
63
+ // -v4 suffix invalidates v3.x caches populated with the 6-scope list that
64
+ // Anthropic now rejects (dario #42). On upgrade, users regenerate the cache
65
+ // with the new FALLBACK scopes automatically no manual clear required.
66
+ const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v4.json');
64
67
  function candidatePaths() {
65
68
  const home = homedir();
66
69
  if (platform() === 'win32') {
@@ -49,6 +49,43 @@ export declare const CC_AGENT_IDENTITY: string;
49
49
  */
50
50
  export declare function orderHeadersForOutbound(headers: Record<string, string>, overrideHeaderOrder?: string[] | undefined): Record<string, string> | Array<[string, string]>;
51
51
  export declare function scrubFrameworkIdentifiers(text: string): string;
52
+ /**
53
+ * Detect text-tool-protocol clients (Cline, Kilo Code, Roo Code and
54
+ * their forks) by fingerprinting the incoming system prompt.
55
+ *
56
+ * These clients ship their own XML-style tool invocation protocol in
57
+ * the system prompt (`<execute_command>`, `<replace_in_file>`,
58
+ * `<attempt_completion>`, …) and parse the model's output with a
59
+ * regex tuned to that exact shape. When dario's default mode
60
+ * substitutes CC's canonical tools into the `tools` array, the model
61
+ * correctly emits Anthropic's generic `<function_calls><invoke>`
62
+ * wrapper — which is well-formed for a CC-tool request but
63
+ * unparseable for a text-protocol client, so every edit surfaces as
64
+ * an error in the client UI even though the model produced a valid
65
+ * response (dario#40, reported by @ringge).
66
+ *
67
+ * The fix is preserve-tools behavior: skip the CC tool swap so the
68
+ * model sees the client's own schema and emits its native XML shape.
69
+ * Auto-detection saves users from having to discover the
70
+ * `--preserve-tools` flag exists; the flag is still honored as an
71
+ * explicit override and `--hybrid-tools` outranks detection.
72
+ *
73
+ * Detection must run BEFORE `scrubFrameworkIdentifiers` so brand
74
+ * names like "Cline" / "Roo" are still present. Tool-protocol
75
+ * markers are scrub-proof on their own.
76
+ *
77
+ * Returns the matched family (`cline` / `kilo` / `roo` / `cline-like`)
78
+ * or null when no text-tool protocol signature is present.
79
+ */
80
+ export declare function detectTextToolClient(systemText: string): string | null;
81
+ /**
82
+ * Flatten an Anthropic-shaped `system` field (string or array of text
83
+ * blocks) to a single joined string. Skips the billing-tag block so
84
+ * captured billing metadata isn't conflated with the operator's own
85
+ * prompt. Used both by the main request-build path (post-scrub) and
86
+ * by the early text-tool-client detector (pre-scrub).
87
+ */
88
+ export declare function extractSystemText(clientBody: Record<string, unknown>): string;
52
89
  /**
53
90
  * Client tool name → CC tool mapping with parameter translation.
54
91
  *
@@ -117,6 +154,7 @@ export declare function buildCCRequest(clientBody: Record<string, unknown>, bill
117
154
  body: Record<string, unknown>;
118
155
  toolMap: Map<string, ToolMapping>;
119
156
  unmappedTools: string[];
157
+ detectedClient?: string;
120
158
  };
121
159
  /**
122
160
  * Reverse-map CC tool calls in a non-streaming response back to the
@@ -116,6 +116,73 @@ export function scrubFrameworkIdentifiers(text) {
116
116
  }
117
117
  return result;
118
118
  }
119
+ /**
120
+ * Detect text-tool-protocol clients (Cline, Kilo Code, Roo Code and
121
+ * their forks) by fingerprinting the incoming system prompt.
122
+ *
123
+ * These clients ship their own XML-style tool invocation protocol in
124
+ * the system prompt (`<execute_command>`, `<replace_in_file>`,
125
+ * `<attempt_completion>`, …) and parse the model's output with a
126
+ * regex tuned to that exact shape. When dario's default mode
127
+ * substitutes CC's canonical tools into the `tools` array, the model
128
+ * correctly emits Anthropic's generic `<function_calls><invoke>`
129
+ * wrapper — which is well-formed for a CC-tool request but
130
+ * unparseable for a text-protocol client, so every edit surfaces as
131
+ * an error in the client UI even though the model produced a valid
132
+ * response (dario#40, reported by @ringge).
133
+ *
134
+ * The fix is preserve-tools behavior: skip the CC tool swap so the
135
+ * model sees the client's own schema and emits its native XML shape.
136
+ * Auto-detection saves users from having to discover the
137
+ * `--preserve-tools` flag exists; the flag is still honored as an
138
+ * explicit override and `--hybrid-tools` outranks detection.
139
+ *
140
+ * Detection must run BEFORE `scrubFrameworkIdentifiers` so brand
141
+ * names like "Cline" / "Roo" are still present. Tool-protocol
142
+ * markers are scrub-proof on their own.
143
+ *
144
+ * Returns the matched family (`cline` / `kilo` / `roo` / `cline-like`)
145
+ * or null when no text-tool protocol signature is present.
146
+ */
147
+ export function detectTextToolClient(systemText) {
148
+ if (!systemText)
149
+ return null;
150
+ if (/\bYou are Cline\b/.test(systemText))
151
+ return 'cline';
152
+ if (/\bYou are Kilo Code\b/.test(systemText))
153
+ return 'kilo';
154
+ if (/\bYou are Roo\b/.test(systemText))
155
+ return 'roo';
156
+ // Protocol-signature fallback — unique to the Cline family and its
157
+ // forks; survives a forked system prompt that edited the identity
158
+ // string out but kept the tool protocol intact.
159
+ if (/<attempt_completion>/.test(systemText))
160
+ return 'cline-like';
161
+ if (/<ask_followup_question>/.test(systemText))
162
+ return 'cline-like';
163
+ if (/<<<<<<< SEARCH\b/.test(systemText))
164
+ return 'cline-like';
165
+ return null;
166
+ }
167
+ /**
168
+ * Flatten an Anthropic-shaped `system` field (string or array of text
169
+ * blocks) to a single joined string. Skips the billing-tag block so
170
+ * captured billing metadata isn't conflated with the operator's own
171
+ * prompt. Used both by the main request-build path (post-scrub) and
172
+ * by the early text-tool-client detector (pre-scrub).
173
+ */
174
+ export function extractSystemText(clientBody) {
175
+ const sys = clientBody.system;
176
+ if (typeof sys === 'string')
177
+ return sys;
178
+ if (Array.isArray(sys)) {
179
+ return sys
180
+ .filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
181
+ .map(b => b.text)
182
+ .join('\n\n');
183
+ }
184
+ return '';
185
+ }
119
186
  /**
120
187
  * Map from client-declared field name (lowercase) to the RequestContext
121
188
  * key that supplies its value. A field declared on the client's tool
@@ -580,6 +647,16 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
580
647
  const messages = clientBody.messages || [];
581
648
  const clientTools = clientBody.tools;
582
649
  const stream = clientBody.stream ?? false;
650
+ // ── Detect text-tool-protocol clients up-front ──
651
+ // Cline / Kilo Code / Roo Code (and forks) ship an XML tool-invocation
652
+ // protocol in the system prompt. Peek at it before scrubbing so the
653
+ // brand name is still present, decide whether to auto-switch into
654
+ // preserve-tools behavior below. Explicit --hybrid-tools outranks the
655
+ // heuristic (operator opt-in wins). dario#40.
656
+ const rawSystemForDetection = extractSystemText(clientBody);
657
+ const detectedClient = detectTextToolClient(rawSystemForDetection) ?? undefined;
658
+ const autoPreserve = Boolean(detectedClient) && !opts.hybridTools;
659
+ const effectivePreserveTools = Boolean(opts.preserveTools) || autoPreserve;
583
660
  // ── Strip thinking from history ──
584
661
  for (const msg of messages) {
585
662
  if (msg.role === 'assistant' && Array.isArray(msg.content)) {
@@ -622,7 +699,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
622
699
  // the fingerprint risk on their own account.
623
700
  const activeToolMap = new Map();
624
701
  const unmappedTools = [];
625
- if (clientTools && !opts.preserveTools) {
702
+ if (clientTools && !effectivePreserveTools) {
626
703
  // Two passes so the unmapped-tool distributor can avoid colliding with
627
704
  // CC tools the client already uses directly. Without this, a client
628
705
  // sending both `WebSearch` and some unmapped tool like `memory_get`
@@ -705,7 +782,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
705
782
  }
706
783
  // ── Remap tool_use and tool_result references in message history ──
707
784
  // Skip in preserveTools mode — leave conversation history untouched.
708
- if (!opts.preserveTools) {
785
+ if (!effectivePreserveTools) {
709
786
  for (const msg of messages) {
710
787
  if (Array.isArray(msg.content)) {
711
788
  for (const block of msg.content) {
@@ -754,18 +831,11 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
754
831
  }
755
832
  }
756
833
  // ── Merge system prompt ──
757
- let systemText = '';
758
- const sys = clientBody.system;
759
- if (typeof sys === 'string') {
760
- systemText = sys;
761
- }
762
- else if (Array.isArray(sys)) {
763
- systemText = sys
764
- .filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
765
- .map(b => b.text)
766
- .join('\n\n');
767
- }
768
- systemText = scrubFrameworkIdentifiers(systemText);
834
+ // rawSystemForDetection holds the same text already used by the
835
+ // up-front detector above — reuse it here so we don't reparse the
836
+ // system array a second time per request. Scrub applies at this
837
+ // point so framework identifiers don't leak upstream.
838
+ let systemText = scrubFrameworkIdentifiers(rawSystemForDetection);
769
839
  // Also scrub framework identifiers from message content text blocks.
770
840
  // Clients often inject their product name into user/tool messages as well,
771
841
  // and the system-prompt-only scrub used to miss those.
@@ -815,7 +885,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
815
885
  // preserveTools mode: pass client tools through unchanged (better for real
816
886
  // agents with custom schemas, but loses the CC tool fingerprint).
817
887
  if (clientTools && clientTools.length > 0) {
818
- ccRequest.tools = opts.preserveTools ? clientTools : CC_TOOL_DEFINITIONS;
888
+ ccRequest.tools = effectivePreserveTools ? clientTools : CC_TOOL_DEFINITIONS;
819
889
  }
820
890
  // Metadata
821
891
  ccRequest.metadata = {
@@ -833,7 +903,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
833
903
  ccRequest.output_config = { effort: 'medium' };
834
904
  }
835
905
  ccRequest.stream = stream;
836
- return { body: ccRequest, toolMap: activeToolMap, unmappedTools };
906
+ return { body: ccRequest, toolMap: activeToolMap, unmappedTools, detectedClient };
837
907
  }
838
908
  /**
839
909
  * Build the CC-name → {clientName, mapping} reverse lookup used by both
package/dist/cli.js CHANGED
@@ -54,8 +54,31 @@ async function login() {
54
54
  await proxy();
55
55
  return;
56
56
  }
57
- console.log(' No Claude Code credentials found. Starting OAuth flow...');
58
- console.log('');
57
+ // Credentials exist but are expired try refresh before falling through
58
+ // to a fresh OAuth flow. Without this, dario silently burned every
59
+ // fresh-login attempt (surfaced by dario #42 when Anthropic's authorize
60
+ // endpoint started rejecting the 6-scope list and `dario login` kept
61
+ // reporting "No credentials found" even though refresh would have worked).
62
+ if (creds?.claudeAiOauth?.refreshToken) {
63
+ console.log(' Existing credentials expired — attempting token refresh...');
64
+ try {
65
+ const tokens = await refreshTokens();
66
+ const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 60000);
67
+ console.log(` Refresh successful! Token expires in ${expiresIn} minutes.`);
68
+ console.log('');
69
+ console.log(' Run `dario proxy` to start the API proxy.');
70
+ console.log('');
71
+ return;
72
+ }
73
+ catch (err) {
74
+ console.log(` Refresh failed (${sanitizeError(err)}). Starting fresh OAuth flow...`);
75
+ console.log('');
76
+ }
77
+ }
78
+ else {
79
+ console.log(' No Claude Code credentials found. Starting OAuth flow...');
80
+ console.log('');
81
+ }
59
82
  try {
60
83
  const tokens = await startAutoOAuthFlow();
61
84
  const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 60000);
@@ -137,7 +160,14 @@ async function proxy() {
137
160
  console.error('[dario] Invalid --host. Must be an IP address or hostname.');
138
161
  process.exit(1);
139
162
  }
140
- const verbose = args.includes('--verbose') || args.includes('-v');
163
+ // --verbose=2 / -vv / DARIO_LOG_BODIES=1 emit redacted request bodies
164
+ // on every POST. -v alone is unchanged (one-line per-request summary).
165
+ // dario#40 (ringge asked for a body-dump mode when debugging client
166
+ // compatibility without having to attach a MITM).
167
+ const verboseBodies = args.includes('-vv')
168
+ || args.includes('--verbose=2')
169
+ || process.env.DARIO_LOG_BODIES === '1';
170
+ const verbose = verboseBodies || args.includes('--verbose') || args.includes('-v');
141
171
  const passthrough = args.includes('--passthrough') || args.includes('--thin');
142
172
  const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
143
173
  const hybridTools = args.includes('--hybrid-tools') || args.includes('--context-inject');
@@ -147,7 +177,7 @@ async function proxy() {
147
177
  }
148
178
  const modelArg = args.find(a => a.startsWith('--model='));
149
179
  const model = modelArg ? modelArg.split('=')[1] : undefined;
150
- await startProxy({ port, host, verbose, model, passthrough, preserveTools, hybridTools });
180
+ await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools });
151
181
  }
152
182
  async function accounts() {
153
183
  const sub = args[1];
@@ -387,6 +417,8 @@ async function help() {
387
417
  --host=ADDRESS Address to bind to (default: 127.0.0.1)
388
418
  Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
389
419
  --verbose, -v Log all requests
420
+ --verbose=2, -vv Also dump redacted request bodies
421
+ (env: DARIO_LOG_BODIES=1)
390
422
 
391
423
  Quick start:
392
424
  dario login # auto-detects Claude Code credentials
package/dist/proxy.d.ts CHANGED
@@ -6,6 +6,7 @@ interface ProxyOptions {
6
6
  port?: number;
7
7
  host?: string;
8
8
  verbose?: boolean;
9
+ verboseBodies?: boolean;
9
10
  model?: string;
10
11
  passthrough?: boolean;
11
12
  preserveTools?: boolean;
package/dist/proxy.js CHANGED
@@ -363,6 +363,17 @@ export async function startProxy(opts = {}) {
363
363
  const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
364
364
  const verbose = opts.verbose ?? false;
365
365
  const passthrough = opts.passthrough ?? false;
366
+ // Text-tool-protocol client families that have already logged a
367
+ // "detected → auto-enabling preserve-tools" banner this session.
368
+ // Set once on first sighting per family so the startup log stays
369
+ // short even under heavy traffic. dario#40.
370
+ const detectedClientsLogged = new Set();
371
+ // Body-dump mode: set via --verbose=2 / -vv or DARIO_LOG_BODIES=1.
372
+ // When on, every request emits a redacted JSON body to stderr so
373
+ // operators can see exactly what dario forwards upstream. Default
374
+ // -v stays quiet because bodies can carry file content and tool
375
+ // output. Reported in dario#40 by @ringge.
376
+ const verboseBodies = Boolean(opts.verboseBodies) || process.env.DARIO_LOG_BODIES === '1';
366
377
  // Multi-provider backends (v3.6.0+). Loaded once at startup; the CLI
367
378
  // `dario backend add openai --key=…` writes to ~/.dario/backends/.
368
379
  // Routing: a GPT-family model arriving on /v1/chat/completions is
@@ -968,10 +979,22 @@ export async function startProxy(opts = {}) {
968
979
  const bodyIdentity = poolAccount
969
980
  ? poolAccount.identity
970
981
  : { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID };
971
- const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
982
+ const { body: ccBody, toolMap, detectedClient } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
972
983
  preserveTools: opts.preserveTools ?? false,
973
984
  hybridTools: opts.hybridTools ?? false,
974
985
  });
986
+ // Log the auto-preserve-tools switch once per text-tool
987
+ // client family. Skip when the operator already opted into
988
+ // --preserve-tools or --hybrid-tools — they know what they
989
+ // picked and don't need a "hey, we heuristically agree"
990
+ // line on every new client seen. dario#40.
991
+ if (detectedClient
992
+ && !opts.preserveTools
993
+ && !opts.hybridTools
994
+ && !detectedClientsLogged.has(detectedClient)) {
995
+ detectedClientsLogged.add(detectedClient);
996
+ console.log(`[dario] detected ${detectedClient}-style text-tool protocol — auto-enabling preserve-tools for this client (pass --hybrid-tools to override, --preserve-tools to silence)`);
997
+ }
975
998
  // Store tool map for response reverse-mapping
976
999
  ccToolMap = toolMap;
977
1000
  // Replace request body entirely with CC template
@@ -987,6 +1010,20 @@ export async function startProxy(opts = {}) {
987
1010
  const modelInfo = modelOverride ? ` (model: ${modelOverride})` : '';
988
1011
  console.log(`[dario] #${requestCount} ${req.method} ${urlPath}${modelInfo}`);
989
1012
  }
1013
+ // Body dump — -vv / DARIO_LOG_BODIES=1. Runs on the outbound
1014
+ // body after the template build so operators see what actually
1015
+ // lands on the wire. sanitizeError's redaction strips bearer
1016
+ // tokens, sk-ant-* keys, and JWT triples in case any leaked
1017
+ // into the body (e.g. user pasted a curl). 8KB cap because the
1018
+ // CC system prompt alone is 25KB and dumping it every request
1019
+ // buries the useful content. dario#40.
1020
+ if (verboseBodies && finalBody) {
1021
+ const rendered = finalBody.toString('utf8');
1022
+ const capped = rendered.length > 8192
1023
+ ? rendered.slice(0, 8192) + `\n[...truncated ${rendered.length - 8192} bytes]`
1024
+ : rendered;
1025
+ console.log(`[dario] #${requestCount} request body:\n${sanitizeError(capped)}`);
1026
+ }
990
1027
  // Beta headers
991
1028
  const clientBeta = req.headers['anthropic-beta'];
992
1029
  let beta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.19.2",
3
+ "version": "3.19.4",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",