@askalf/dario 4.8.153 → 5.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.
@@ -6,17 +6,11 @@
6
6
  * That ClientHello (JA3/JA4 hash) is what Anthropic's TLS-layer classifier
7
7
  * actually sees on the wire.
8
8
  *
9
- * Dario has two transports with different exposure to this axis:
10
- *
11
- * - **Shim mode** runs inside CC's own process (NODE_OPTIONS=--require),
12
- * so its outbound fetch rides on CC's TLS stack by construction.
13
- * Nothing to reconcile the shim is always TLS-matched to CC.
14
- *
15
- * - **Proxy mode** is a separate process holding its own TLS sessions
16
- * to api.anthropic.com. Anthropic sees the proxy's TLS fingerprint,
17
- * not the consumer client's. If the proxy runs under Node, the
18
- * ClientHello is OpenSSL-shaped — distinct from Bun's BoringSSL shape.
19
- * That's the JA3 gap this module flags.
9
+ * The proxy is a separate process holding its own TLS sessions to
10
+ * api.anthropic.com. Anthropic sees the proxy's TLS fingerprint, not the
11
+ * consumer client's. If the proxy runs under Node, the ClientHello is
12
+ * OpenSSL-shaped distinct from Bun's BoringSSL shape. That's the JA3
13
+ * gap this module flags.
20
14
  *
21
15
  * Mitigation today: dario auto-relaunches under Bun when Bun is on PATH
22
16
  * (see top of `src/cli.ts`). When Bun isn't available the auto-relaunch
@@ -6,17 +6,11 @@
6
6
  * That ClientHello (JA3/JA4 hash) is what Anthropic's TLS-layer classifier
7
7
  * actually sees on the wire.
8
8
  *
9
- * Dario has two transports with different exposure to this axis:
10
- *
11
- * - **Shim mode** runs inside CC's own process (NODE_OPTIONS=--require),
12
- * so its outbound fetch rides on CC's TLS stack by construction.
13
- * Nothing to reconcile the shim is always TLS-matched to CC.
14
- *
15
- * - **Proxy mode** is a separate process holding its own TLS sessions
16
- * to api.anthropic.com. Anthropic sees the proxy's TLS fingerprint,
17
- * not the consumer client's. If the proxy runs under Node, the
18
- * ClientHello is OpenSSL-shaped — distinct from Bun's BoringSSL shape.
19
- * That's the JA3 gap this module flags.
9
+ * The proxy is a separate process holding its own TLS sessions to
10
+ * api.anthropic.com. Anthropic sees the proxy's TLS fingerprint, not the
11
+ * consumer client's. If the proxy runs under Node, the ClientHello is
12
+ * OpenSSL-shaped distinct from Bun's BoringSSL shape. That's the JA3
13
+ * gap this module flags.
20
14
  *
21
15
  * Mitigation today: dario auto-relaunches under Bun when Bun is on PATH
22
16
  * (see top of `src/cli.ts`). When Bun isn't available the auto-relaunch
@@ -97,8 +91,8 @@ export function classifyRuntimeFingerprint(runningUnderBun, availableBunVersion,
97
91
  runtime: 'node',
98
92
  runtimeVersion: nodeVersion,
99
93
  detail: `Node ${nodeVersion} — Bun not installed; proxy-mode TLS fingerprint diverges from Claude Code`,
100
- hint: 'Install Bun (https://bun.sh) so dario can auto-relaunch under it, or use shim mode ' +
101
- '(`dario shim -- claude …`) which runs inside CC\'s own process and inherits its TLS stack.',
94
+ hint: 'Install Bun (https://bun.sh) so dario can auto-relaunch under it and its TLS ClientHello ' +
95
+ 'matches Claude Code\'s.',
102
96
  };
103
97
  }
104
98
  /**
@@ -39,9 +39,14 @@
39
39
  * timers, or UUID sources. The proxy injects a `() => string` id factory
40
40
  * (randomUUID) and `() => number` rng so both are swappable in tests.
41
41
  *
42
- * Pool mode is unaffected each account carries a stable identity.sessionId
43
- * for its lifetime, and the caller doesn't consult this registry. This
44
- * module only governs the single-account SESSION_ID slot.
42
+ * v5.0 (pool-as-primitive): this registry is now the ONE session-id mechanism.
43
+ * Every request carries an `accountKey` the selected pool account's alias
44
+ * (or a synthetic key in upstream-api-key mode) — so each account keeps its
45
+ * own idle/jitter/max-age lifecycle. The caller passes the account's minted
46
+ * `identity.sessionId` as a `seedId`, used only for that account's first
47
+ * session so the first request stays byte-identical to the pre-v5 stable id;
48
+ * idle/age rotation then mints fresh ids via `newId()`. `perClient` still
49
+ * sub-partitions each account's sessions by the caller's session header.
45
50
  */
46
51
  export interface SessionRotationConfig {
47
52
  /** Idle threshold in ms: if no traffic for this long, the session rotates on the next request. */
@@ -106,19 +111,26 @@ export declare class SessionRegistry {
106
111
  private readonly entries;
107
112
  constructor(cfg: SessionRotationConfig, newId: () => string, rng?: () => number, maxEntries?: number);
108
113
  /**
109
- * Resolve the outbound session id for a given client key at time `now`.
114
+ * Resolve the outbound session id for a given account + client key at `now`.
110
115
  *
111
- * `clientKey` is the caller-side session header when cfg.perClient is
112
- * true, and ignored (replaced with 'default') when perClient is false.
113
- * Callers pass the raw header value and let the registry decide —
114
- * otherwise flipping perClient at runtime would require threading
116
+ * `accountKey` partitions sessions by the selected pool account so each
117
+ * account rotates on its own idle timer. `clientKey` is the caller-side
118
+ * session header, added as a sub-partition only when cfg.perClient is true
119
+ * (ignored otherwise). Callers pass the raw header value and let the registry
120
+ * decide — otherwise flipping perClient at runtime would require threading
115
121
  * the decision to every call site.
116
122
  *
123
+ * `seedId`, when provided, is used as the session id only on the FIRST
124
+ * creation for a key (decision === 'rotate-new'): it lets the caller carry a
125
+ * pre-minted id (the account's identity.sessionId) into the registry so the
126
+ * account's first request is byte-identical to the pre-v5 stable id. Idle /
127
+ * age rotations ignore the seed and mint a fresh id via newId().
128
+ *
117
129
  * Updates lastUsedAt on the entry (whether kept or freshly minted),
118
130
  * and nudges the entry to the end of the insertion-order map so
119
131
  * eviction under maxEntries pressure is LRU.
120
132
  */
121
- getOrCreate(clientKey: string | undefined, now: number): RegistryResult;
133
+ getOrCreate(accountKey: string, clientKey: string | undefined, now: number, seedId?: string): RegistryResult;
122
134
  /**
123
135
  * Read the current id for a client key without touching lastUsedAt.
124
136
  *
@@ -126,7 +138,7 @@ export declare class SessionRegistry {
126
138
  * reflect the most recently assigned session id but must not count
127
139
  * as activity for rotation purposes. Returns undefined if no entry.
128
140
  */
129
- peek(clientKey: string | undefined): string | undefined;
141
+ peek(accountKey: string, clientKey: string | undefined): string | undefined;
130
142
  size(): number;
131
143
  clear(): void;
132
144
  private evictIfOverCap;
@@ -39,9 +39,14 @@
39
39
  * timers, or UUID sources. The proxy injects a `() => string` id factory
40
40
  * (randomUUID) and `() => number` rng so both are swappable in tests.
41
41
  *
42
- * Pool mode is unaffected each account carries a stable identity.sessionId
43
- * for its lifetime, and the caller doesn't consult this registry. This
44
- * module only governs the single-account SESSION_ID slot.
42
+ * v5.0 (pool-as-primitive): this registry is now the ONE session-id mechanism.
43
+ * Every request carries an `accountKey` the selected pool account's alias
44
+ * (or a synthetic key in upstream-api-key mode) — so each account keeps its
45
+ * own idle/jitter/max-age lifecycle. The caller passes the account's minted
46
+ * `identity.sessionId` as a `seedId`, used only for that account's first
47
+ * session so the first request stays byte-identical to the pre-v5 stable id;
48
+ * idle/age rotation then mints fresh ids via `newId()`. `perClient` still
49
+ * sub-partitions each account's sessions by the caller's session header.
45
50
  */
46
51
  /**
47
52
  * Pure decision: should the given entry be rotated at `now`?
@@ -95,20 +100,29 @@ export class SessionRegistry {
95
100
  this.maxEntries = maxEntries;
96
101
  }
97
102
  /**
98
- * Resolve the outbound session id for a given client key at time `now`.
103
+ * Resolve the outbound session id for a given account + client key at `now`.
99
104
  *
100
- * `clientKey` is the caller-side session header when cfg.perClient is
101
- * true, and ignored (replaced with 'default') when perClient is false.
102
- * Callers pass the raw header value and let the registry decide —
103
- * otherwise flipping perClient at runtime would require threading
105
+ * `accountKey` partitions sessions by the selected pool account so each
106
+ * account rotates on its own idle timer. `clientKey` is the caller-side
107
+ * session header, added as a sub-partition only when cfg.perClient is true
108
+ * (ignored otherwise). Callers pass the raw header value and let the registry
109
+ * decide — otherwise flipping perClient at runtime would require threading
104
110
  * the decision to every call site.
105
111
  *
112
+ * `seedId`, when provided, is used as the session id only on the FIRST
113
+ * creation for a key (decision === 'rotate-new'): it lets the caller carry a
114
+ * pre-minted id (the account's identity.sessionId) into the registry so the
115
+ * account's first request is byte-identical to the pre-v5 stable id. Idle /
116
+ * age rotations ignore the seed and mint a fresh id via newId().
117
+ *
106
118
  * Updates lastUsedAt on the entry (whether kept or freshly minted),
107
119
  * and nudges the entry to the end of the insertion-order map so
108
120
  * eviction under maxEntries pressure is LRU.
109
121
  */
110
- getOrCreate(clientKey, now) {
111
- const key = this.cfg.perClient ? (clientKey && clientKey.length > 0 ? clientKey : 'default') : 'default';
122
+ getOrCreate(accountKey, clientKey, now, seedId) {
123
+ const key = this.cfg.perClient
124
+ ? `${accountKey}\u0000${clientKey && clientKey.length > 0 ? clientKey : 'default'}`
125
+ : accountKey;
112
126
  const existing = this.entries.get(key);
113
127
  const decision = decideSessionRotation(existing, now, this.cfg);
114
128
  if (decision === 'keep' && existing) {
@@ -120,7 +134,8 @@ export class SessionRegistry {
120
134
  }
121
135
  const jitterOffset = this.cfg.jitterMs > 0 ? Math.floor(this.rng() * this.cfg.jitterMs) : 0;
122
136
  const entry = {
123
- upstreamSessionId: this.newId(),
137
+ // Seed only a brand-new key; a rotation (idle/age) always mints fresh.
138
+ upstreamSessionId: decision === 'rotate-new' && seedId ? seedId : this.newId(),
124
139
  createdAt: now,
125
140
  lastUsedAt: now,
126
141
  idleJitterOffsetMs: jitterOffset,
@@ -136,8 +151,10 @@ export class SessionRegistry {
136
151
  * reflect the most recently assigned session id but must not count
137
152
  * as activity for rotation purposes. Returns undefined if no entry.
138
153
  */
139
- peek(clientKey) {
140
- const key = this.cfg.perClient ? (clientKey && clientKey.length > 0 ? clientKey : 'default') : 'default';
154
+ peek(accountKey, clientKey) {
155
+ const key = this.cfg.perClient
156
+ ? `${accountKey}\u0000${clientKey && clientKey.length > 0 ? clientKey : 'default'}`
157
+ : accountKey;
141
158
  return this.entries.get(key)?.upstreamSessionId;
142
159
  }
143
160
  size() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.153",
3
+ "version": "5.0.0",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,9 +20,9 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
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/",
23
+ "build": "tsc && cp src/cc-template-data.json dist/",
24
24
  "test": "node --test --test-concurrency=8 test/all.test.mjs",
25
- "test:serial": "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/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
25
+ "test:serial": "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/live-fingerprint.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
26
26
  "audit": "npm audit --production --audit-level=high",
27
27
  "prepublishOnly": "npm run build",
28
28
  "start": "node dist/cli.js",
@@ -1,86 +0,0 @@
1
- /**
2
- * Shim host — dario-side of the shim transport.
3
- *
4
- * Runs `dario shim -- <cmd> [args...]`. Spawns the child with NODE_OPTIONS
5
- * pointing at the shim runtime, listens on a unix socket (or named pipe on
6
- * Windows) for billing relay events from the runtime, feeds them into the
7
- * Analytics class, and forwards the child's stdio to the host TTY so the
8
- * user experience is identical to running the command directly.
9
- *
10
- * Why a socket and not a file or stdout: the runtime patches `globalThis.fetch`
11
- * inside the *child*'s process — that child still owns its own stdout (the
12
- * user's TTY), and we don't want shim relay traffic interleaved with CC's
13
- * normal output. A unix socket gives us a clean side-channel, lets the host
14
- * keep accumulating analytics across the child's lifetime, and stays open if
15
- * the child re-execs (rare but possible with claude wrappers).
16
- *
17
- * See v3.12.0 CHANGELOG for the design rationale.
18
- */
19
- import { Analytics } from './../analytics.js';
20
- /**
21
- * Locate the shim runtime CJS file. In the published package it lives at
22
- * `dist/shim/runtime.cjs` next to this module's compiled output. In dev
23
- * (running via tsx from src/) it lives at `src/shim/runtime.cjs`.
24
- */
25
- export declare function locateShimRuntime(): string;
26
- interface RelayEvent {
27
- kind: 'request' | 'response';
28
- timestamp: number;
29
- bytes?: number;
30
- status?: number;
31
- claim?: string | null;
32
- overageUtil?: number | null;
33
- }
34
- /**
35
- * Process-priority levels accepted by the shim. Cross-platform via Node's
36
- * os.setPriority — same name on every OS, different underlying class:
37
- * - 'normal' : default, no change
38
- * - 'below-normal' : BELOW_NORMAL_PRIORITY_CLASS on Windows, nice +7 on POSIX
39
- * - 'low' : IDLE_PRIORITY_CLASS on Windows, nice +19 on POSIX
40
- *
41
- * Use case: when the dario user is RDP'd into the same machine that hosts
42
- * the claude CLI, claude can saturate ~4 cores during heavy tool work and
43
- * starve the kernel network IO threads. The result is a Windows-specific
44
- * cascade — RDP TCP socket writes return ERROR_SEM_TIMEOUT, sessions drop,
45
- * Defender notices the disruption and writes a config-change event ~12s
46
- * later. Lowering claude's scheduling priority lets the kernel preempt it
47
- * for IO threads without changing claude's behavior or throughput. Same
48
- * sustained CPU usage, no more drops. Documented in faq.md.
49
- *
50
- * (See dario#xxx — this lands as part of the same investigation that
51
- * turned up the wider Defender / vmswitch / NIC offload cleanup work.)
52
- */
53
- export type ShimPriority = 'normal' | 'below-normal' | 'low';
54
- export interface ShimHostOptions {
55
- /** Command to spawn (the user's claude binary, or any node-based CC wrapper). */
56
- command: string;
57
- /** Args passed through to the child. */
58
- args: string[];
59
- /** Override the template path the runtime reads. Defaults to ~/.dario/cc-template.live.json. */
60
- templatePath?: string;
61
- /** Print per-event lines to stderr. */
62
- verbose?: boolean;
63
- /**
64
- * Process priority for the spawned child. Defaults to 'normal' (no change).
65
- * Set to 'below-normal' when running claude on a machine you're RDP'd into,
66
- * so kernel network IO threads can preempt the heavy claude workload and
67
- * the RDP session doesn't drop on every tool burst. See ShimPriority.
68
- */
69
- priority?: ShimPriority;
70
- /** Optional Analytics sink. If omitted, a fresh instance is created. */
71
- analytics?: Analytics;
72
- }
73
- export interface ShimHostResult {
74
- exitCode: number;
75
- events: RelayEvent[];
76
- analytics: Analytics;
77
- }
78
- /**
79
- * Spawn the child command with the shim runtime injected, relay billing
80
- * events to Analytics, return when the child exits.
81
- *
82
- * Stdio is inherited so the user sees the child's output exactly as if they
83
- * had run it without the shim.
84
- */
85
- export declare function runShim(opts: ShimHostOptions): Promise<ShimHostResult>;
86
- export {};
package/dist/shim/host.js DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * Shim host — dario-side of the shim transport.
3
- *
4
- * Runs `dario shim -- <cmd> [args...]`. Spawns the child with NODE_OPTIONS
5
- * pointing at the shim runtime, listens on a unix socket (or named pipe on
6
- * Windows) for billing relay events from the runtime, feeds them into the
7
- * Analytics class, and forwards the child's stdio to the host TTY so the
8
- * user experience is identical to running the command directly.
9
- *
10
- * Why a socket and not a file or stdout: the runtime patches `globalThis.fetch`
11
- * inside the *child*'s process — that child still owns its own stdout (the
12
- * user's TTY), and we don't want shim relay traffic interleaved with CC's
13
- * normal output. A unix socket gives us a clean side-channel, lets the host
14
- * keep accumulating analytics across the child's lifetime, and stays open if
15
- * the child re-execs (rare but possible with claude wrappers).
16
- *
17
- * See v3.12.0 CHANGELOG for the design rationale.
18
- */
19
- import { createServer } from 'node:net';
20
- import { spawn } from 'node:child_process';
21
- import { mkdtempSync, existsSync } from 'node:fs';
22
- import { tmpdir, homedir, setPriority, constants as osConstants } from 'node:os';
23
- import { join, dirname } from 'node:path';
24
- import { fileURLToPath } from 'node:url';
25
- import { Analytics } from './../analytics.js';
26
- const __filename = fileURLToPath(import.meta.url);
27
- const __dirname = dirname(__filename);
28
- /**
29
- * Locate the shim runtime CJS file. In the published package it lives at
30
- * `dist/shim/runtime.cjs` next to this module's compiled output. In dev
31
- * (running via tsx from src/) it lives at `src/shim/runtime.cjs`.
32
- */
33
- export function locateShimRuntime() {
34
- const candidates = [
35
- join(__dirname, 'runtime.cjs'), // dist/shim/runtime.cjs (production)
36
- join(__dirname, '..', '..', 'src', 'shim', 'runtime.cjs'), // dev: from dist/shim → ../../src/shim
37
- join(__dirname, '..', 'src', 'shim', 'runtime.cjs'), // dev: from src/shim → ../src/shim (rare)
38
- ];
39
- for (const c of candidates) {
40
- if (existsSync(c))
41
- return c;
42
- }
43
- throw new Error(`shim runtime not found; checked: ${candidates.join(', ')}`);
44
- }
45
- function priorityValue(p) {
46
- switch (p) {
47
- case 'normal': return osConstants.priority.PRIORITY_NORMAL;
48
- case 'below-normal': return osConstants.priority.PRIORITY_BELOW_NORMAL;
49
- case 'low': return osConstants.priority.PRIORITY_LOW;
50
- }
51
- }
52
- /**
53
- * Pick a socket path: unix domain socket on POSIX, named pipe on Windows.
54
- * Both forms are accepted directly by net.createServer / net.connect.
55
- */
56
- function makeSockPath() {
57
- if (process.platform === 'win32') {
58
- return `\\\\.\\pipe\\dario-shim-${process.pid}-${Date.now()}`;
59
- }
60
- const dir = mkdtempSync(join(tmpdir(), 'dario-shim-'));
61
- return join(dir, 'sock');
62
- }
63
- /**
64
- * Build the child env. We *prepend* our --require to NODE_OPTIONS rather than
65
- * overwrite, so existing user NODE_OPTIONS (debuggers, source maps, tracers)
66
- * still apply. Quoting paths defends against spaces in the dario install dir.
67
- */
68
- function buildChildEnv(parentEnv, runtimePath, sockPath, templatePath, verbose) {
69
- const requireFlag = `--require=${JSON.stringify(runtimePath)}`;
70
- const existing = parentEnv.NODE_OPTIONS ?? '';
71
- const NODE_OPTIONS = existing ? `${requireFlag} ${existing}` : requireFlag;
72
- return {
73
- ...parentEnv,
74
- NODE_OPTIONS,
75
- DARIO_SHIM: '1',
76
- DARIO_SHIM_SOCK: sockPath,
77
- DARIO_SHIM_TEMPLATE: templatePath,
78
- ...(verbose ? { DARIO_SHIM_VERBOSE: '1' } : {}),
79
- };
80
- }
81
- /** Stream parser: relay events arrive as newline-delimited JSON over the socket. */
82
- function makeSocketHandler(events, analytics, verbose) {
83
- return (sock) => {
84
- let buf = '';
85
- sock.setEncoding('utf-8');
86
- sock.on('data', (chunk) => {
87
- buf += chunk;
88
- let nl;
89
- while ((nl = buf.indexOf('\n')) !== -1) {
90
- const line = buf.slice(0, nl);
91
- buf = buf.slice(nl + 1);
92
- if (!line)
93
- continue;
94
- try {
95
- const event = JSON.parse(line);
96
- events.push(event);
97
- if (event.kind === 'response') {
98
- // Synthesize a minimal RequestRecord so this surfaces in /analytics.
99
- // Token counts aren't available from the shim transport — the runtime
100
- // would need to parse the SSE stream to extract them, which we
101
- // explicitly chose not to do (it's expensive and intrusive). So this
102
- // is a request-count + claim-tracking record, not a token-cost record.
103
- analytics.record({
104
- timestamp: event.timestamp ?? Date.now(),
105
- account: 'shim',
106
- model: 'unknown',
107
- inputTokens: 0, outputTokens: 0,
108
- cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
109
- claim: event.claim ?? '',
110
- util5h: 0, util7d: 0,
111
- overageUtil: event.overageUtil ?? 0,
112
- latencyMs: 0,
113
- status: event.status ?? 0,
114
- isStream: false,
115
- isOpenAI: false,
116
- });
117
- }
118
- if (verbose) {
119
- process.stderr.write(`[dario shim] ${JSON.stringify(event)}\n`);
120
- }
121
- }
122
- catch {
123
- // Malformed line — drop silently. The runtime is best-effort.
124
- }
125
- }
126
- });
127
- };
128
- }
129
- /** Internal: stand up the socket server and resolve when it's listening. */
130
- function startSocketServer(sockPath, events, analytics, verbose) {
131
- return new Promise((resolve, reject) => {
132
- const server = createServer(makeSocketHandler(events, analytics, verbose));
133
- server.once('error', reject);
134
- server.listen(sockPath, () => resolve(server));
135
- });
136
- }
137
- /**
138
- * Spawn the child command with the shim runtime injected, relay billing
139
- * events to Analytics, return when the child exits.
140
- *
141
- * Stdio is inherited so the user sees the child's output exactly as if they
142
- * had run it without the shim.
143
- */
144
- export async function runShim(opts) {
145
- const runtimePath = locateShimRuntime();
146
- const sockPath = makeSockPath();
147
- const templatePath = opts.templatePath ?? join(homedir(), '.dario', 'cc-template.live.json');
148
- const verbose = opts.verbose ?? false;
149
- const analytics = opts.analytics ?? new Analytics();
150
- const events = [];
151
- const server = await startSocketServer(sockPath, events, analytics, verbose);
152
- let child;
153
- try {
154
- child = spawn(opts.command, opts.args, {
155
- stdio: 'inherit',
156
- env: buildChildEnv(process.env, runtimePath, sockPath, templatePath, verbose),
157
- });
158
- }
159
- catch (e) {
160
- server.close();
161
- throw e;
162
- }
163
- // Apply priority best-effort. setPriority can fail if the user lacks the
164
- // privilege to lower priorities below normal (rare on Windows / Linux for
165
- // the same user's process, but reported on locked-down corporate setups).
166
- // We log and continue — priority is a perf optimization, not a correctness
167
- // requirement. The child runs at default priority if the call fails.
168
- if (opts.priority && opts.priority !== 'normal') {
169
- try {
170
- if (child.pid !== undefined) {
171
- setPriority(child.pid, priorityValue(opts.priority));
172
- if (verbose) {
173
- process.stderr.write(`[dario shim] child PID ${child.pid} priority → ${opts.priority}\n`);
174
- }
175
- }
176
- }
177
- catch (err) {
178
- if (verbose) {
179
- process.stderr.write(`[dario shim] priority set failed (continuing at default): ${err.message}\n`);
180
- }
181
- }
182
- }
183
- const exitCode = await new Promise((resolve) => {
184
- child.on('exit', (code, signal) => {
185
- if (signal)
186
- resolve(128 + (signal === 'SIGTERM' ? 15 : 1));
187
- else
188
- resolve(code ?? 0);
189
- });
190
- child.on('error', () => resolve(1));
191
- });
192
- // Give any in-flight relay writes a brief window to land before tearing down.
193
- await new Promise((r) => setTimeout(r, 50));
194
- server.close();
195
- return { exitCode, events, analytics };
196
- }