@askalf/dario 4.8.154 → 5.0.1
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 +93 -170
- package/dist/cc-template.d.ts +0 -2
- package/dist/cc-template.js +0 -2
- package/dist/cli.js +50 -124
- package/dist/config-report.js +16 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +17 -7
- package/dist/live-fingerprint.d.ts +8 -8
- package/dist/live-fingerprint.js +6 -6
- package/dist/proxy.d.ts +2 -11
- package/dist/proxy.js +247 -256
- package/dist/runtime-fingerprint.d.ts +5 -11
- package/dist/runtime-fingerprint.js +7 -13
- package/dist/session-rotation.d.ts +22 -10
- package/dist/session-rotation.js +30 -13
- package/package.json +3 -3
- package/dist/shim/host.d.ts +0 -86
- package/dist/shim/host.js +0 -196
- package/dist/shim/runtime.cjs +0 -327
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
101
|
-
'
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
114
|
+
* Resolve the outbound session id for a given account + client key at `now`.
|
|
110
115
|
*
|
|
111
|
-
* `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* otherwise
|
|
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;
|
package/dist/session-rotation.js
CHANGED
|
@@ -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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
103
|
+
* Resolve the outbound session id for a given account + client key at `now`.
|
|
99
104
|
*
|
|
100
|
-
* `
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* otherwise
|
|
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
|
|
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
|
-
|
|
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
|
|
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": "
|
|
3
|
+
"version": "5.0.1",
|
|
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/
|
|
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/
|
|
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",
|
package/dist/shim/host.d.ts
DELETED
|
@@ -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
|
-
}
|