@askalf/dario 3.16.0 → 3.19.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 +169 -169
- package/dist/accounts.d.ts +2 -0
- package/dist/accounts.js +54 -4
- package/dist/cc-template-data.json +1 -0
- package/dist/cc-template.d.ts +3 -0
- package/dist/cc-template.js +88 -33
- package/dist/cli.js +19 -0
- package/dist/doctor.d.ts +43 -0
- package/dist/doctor.js +208 -0
- package/dist/live-fingerprint.d.ts +137 -0
- package/dist/live-fingerprint.js +375 -9
- package/dist/openai-backend.js +24 -3
- package/dist/proxy.js +108 -16
- package/package.json +2 -2
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dario doctor — health report aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Runs every check we know how to run and returns a list of labelled
|
|
5
|
+
* results. The CLI passes the result list through `formatChecks` for
|
|
6
|
+
* display; `runChecks` is the I/O-heavy collector, `formatChecks` is a
|
|
7
|
+
* pure function the tests exercise directly.
|
|
8
|
+
*
|
|
9
|
+
* Keep `runChecks` defensive: a check that throws must not take the
|
|
10
|
+
* rest of the report down — every check is wrapped so a broken sub-
|
|
11
|
+
* system surfaces as `fail` instead of crashing the CLI.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { homedir, platform, arch, release } from 'node:os';
|
|
17
|
+
import { CC_TEMPLATE, } from './cc-template.js';
|
|
18
|
+
import { describeTemplate, detectDrift, checkCCCompat, findInstalledCC, SUPPORTED_CC_RANGE, CURRENT_SCHEMA_VERSION, } from './live-fingerprint.js';
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
/**
|
|
21
|
+
* Pretty-print a list of Check results as aligned ASCII. No color codes —
|
|
22
|
+
* Windows cmd / CI logs render plain text reliably; colors are a downside
|
|
23
|
+
* not an upside for a report that's often piped or pasted.
|
|
24
|
+
*/
|
|
25
|
+
export function formatChecks(checks) {
|
|
26
|
+
const prefix = {
|
|
27
|
+
ok: '[ OK ]',
|
|
28
|
+
warn: '[WARN]',
|
|
29
|
+
fail: '[FAIL]',
|
|
30
|
+
info: '[INFO]',
|
|
31
|
+
};
|
|
32
|
+
const labelWidth = checks.reduce((n, c) => Math.max(n, c.label.length), 0);
|
|
33
|
+
const lines = checks.map((c) => ` ${prefix[c.status]} ${c.label.padEnd(labelWidth)} ${c.detail}`);
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Derive a CLI exit code from a set of check results. Any `fail` → 1.
|
|
38
|
+
* `warn` alone does not fail — we don't want `dario doctor` to CI-fail
|
|
39
|
+
* a user's machine just because they're on an untested CC version.
|
|
40
|
+
*/
|
|
41
|
+
export function exitCodeFor(checks) {
|
|
42
|
+
return checks.some((c) => c.status === 'fail') ? 1 : 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run every available health check. Never throws — each check is
|
|
46
|
+
* individually try/caught so a broken subsystem (e.g. unreadable accounts
|
|
47
|
+
* dir) shows up as a `fail` row instead of crashing the CLI.
|
|
48
|
+
*
|
|
49
|
+
* The order is curated — more fundamental checks first (Node, dario
|
|
50
|
+
* version, platform) so a reader scanning the output top-down sees
|
|
51
|
+
* the environment before the subsystems.
|
|
52
|
+
*/
|
|
53
|
+
export async function runChecks() {
|
|
54
|
+
const checks = [];
|
|
55
|
+
// ---- dario version
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
58
|
+
checks.push({ status: 'info', label: 'dario', detail: `v${pkg.version}` });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
checks.push({ status: 'warn', label: 'dario', detail: 'package.json not readable — version unknown' });
|
|
62
|
+
}
|
|
63
|
+
// ---- Node
|
|
64
|
+
checks.push({
|
|
65
|
+
status: nodeStatus(),
|
|
66
|
+
label: 'Node',
|
|
67
|
+
detail: process.version,
|
|
68
|
+
});
|
|
69
|
+
// ---- Platform
|
|
70
|
+
checks.push({
|
|
71
|
+
status: 'info',
|
|
72
|
+
label: 'Platform',
|
|
73
|
+
detail: `${platform()} ${arch()} (${release()})`,
|
|
74
|
+
});
|
|
75
|
+
// ---- CC binary
|
|
76
|
+
const cc = safely(() => findInstalledCC(), { path: null, version: null });
|
|
77
|
+
if (cc.path && cc.version) {
|
|
78
|
+
const compat = checkCCCompat(cc.version);
|
|
79
|
+
const status = compat.status === 'ok' ? 'ok' :
|
|
80
|
+
compat.status === 'untested-above' ? 'warn' :
|
|
81
|
+
compat.status === 'below-min' ? 'fail' :
|
|
82
|
+
'warn';
|
|
83
|
+
checks.push({
|
|
84
|
+
status,
|
|
85
|
+
label: 'CC binary',
|
|
86
|
+
detail: `v${cc.version} at ${cc.path} (range: v${SUPPORTED_CC_RANGE.min} – v${SUPPORTED_CC_RANGE.maxTested})`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (cc.path) {
|
|
90
|
+
checks.push({
|
|
91
|
+
status: 'warn',
|
|
92
|
+
label: 'CC binary',
|
|
93
|
+
detail: `found at ${cc.path} but --version didn't parse — compat unchecked`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
checks.push({
|
|
98
|
+
status: 'warn',
|
|
99
|
+
label: 'CC binary',
|
|
100
|
+
detail: 'not on PATH — dario falls back to bundled template',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ---- Template source
|
|
104
|
+
try {
|
|
105
|
+
checks.push({
|
|
106
|
+
status: CC_TEMPLATE._source === 'live' ? 'ok' : 'info',
|
|
107
|
+
label: 'Template',
|
|
108
|
+
detail: `${describeTemplate(CC_TEMPLATE)} (schema v${CC_TEMPLATE._schemaVersion ?? '?'})`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
checks.push({ status: 'fail', label: 'Template', detail: `load failed: ${err.message}` });
|
|
113
|
+
}
|
|
114
|
+
// ---- Template drift
|
|
115
|
+
try {
|
|
116
|
+
const drift = detectDrift(CC_TEMPLATE);
|
|
117
|
+
const status = drift.installedVersion === null ? 'info' : drift.drifted ? 'warn' : 'ok';
|
|
118
|
+
checks.push({ status, label: 'Template drift', detail: drift.message });
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
checks.push({ status: 'warn', label: 'Template drift', detail: `check failed: ${err.message}` });
|
|
122
|
+
}
|
|
123
|
+
void CURRENT_SCHEMA_VERSION; // keep the import load-bearing for future schema checks
|
|
124
|
+
// ---- OAuth
|
|
125
|
+
try {
|
|
126
|
+
const { getStatus } = await import('./oauth.js');
|
|
127
|
+
const s = await getStatus();
|
|
128
|
+
if (!s.authenticated) {
|
|
129
|
+
checks.push({
|
|
130
|
+
status: s.status === 'expired' && s.canRefresh ? 'warn' : 'fail',
|
|
131
|
+
label: 'OAuth',
|
|
132
|
+
detail: s.status === 'none' ? 'not authenticated — run `dario login`' : s.status,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
checks.push({ status: 'ok', label: 'OAuth', detail: `${s.status} (expires in ${s.expiresIn})` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
checks.push({ status: 'warn', label: 'OAuth', detail: `check failed: ${err.message}` });
|
|
141
|
+
}
|
|
142
|
+
// ---- Account pool
|
|
143
|
+
try {
|
|
144
|
+
const { listAccountAliases, loadAllAccounts } = await import('./accounts.js');
|
|
145
|
+
const aliases = await listAccountAliases();
|
|
146
|
+
if (aliases.length === 0) {
|
|
147
|
+
checks.push({ status: 'info', label: 'Pool', detail: 'single-account mode (no pool configured)' });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const loaded = await loadAllAccounts();
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const expired = loaded.filter((a) => a.expiresAt <= now).length;
|
|
153
|
+
checks.push({
|
|
154
|
+
status: expired > 0 ? 'warn' : aliases.length >= 2 ? 'ok' : 'info',
|
|
155
|
+
label: 'Pool',
|
|
156
|
+
detail: `${aliases.length} account${aliases.length === 1 ? '' : 's'}` +
|
|
157
|
+
(expired > 0 ? `, ${expired} expired` : '') +
|
|
158
|
+
(aliases.length < 2 ? ' (pool activates at 2+)' : ''),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
checks.push({ status: 'warn', label: 'Pool', detail: `check failed: ${err.message}` });
|
|
164
|
+
}
|
|
165
|
+
// ---- Secondary backends
|
|
166
|
+
try {
|
|
167
|
+
const { listBackends } = await import('./openai-backend.js');
|
|
168
|
+
const backends = await listBackends();
|
|
169
|
+
checks.push({
|
|
170
|
+
status: 'info',
|
|
171
|
+
label: 'Backends',
|
|
172
|
+
detail: backends.length === 0
|
|
173
|
+
? 'none configured (Claude subscription is the only route)'
|
|
174
|
+
: `${backends.length} configured: ${backends.map((b) => b.name).join(', ')}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
checks.push({ status: 'warn', label: 'Backends', detail: `check failed: ${err.message}` });
|
|
179
|
+
}
|
|
180
|
+
// ---- ~/.dario dir
|
|
181
|
+
try {
|
|
182
|
+
const home = join(homedir(), '.dario');
|
|
183
|
+
checks.push({ status: 'info', label: 'Home', detail: home });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// never fails in practice — homedir() is always defined on supported platforms
|
|
187
|
+
}
|
|
188
|
+
return checks;
|
|
189
|
+
}
|
|
190
|
+
function nodeStatus() {
|
|
191
|
+
const m = /^v(\d+)\./.exec(process.version);
|
|
192
|
+
const major = m ? parseInt(m[1], 10) : 0;
|
|
193
|
+
// engines: >=18 (see package.json). 18/20 are current supported Node LTS
|
|
194
|
+
// lines — anything below 18 fails; above is ok.
|
|
195
|
+
if (major >= 18)
|
|
196
|
+
return 'ok';
|
|
197
|
+
if (major === 0)
|
|
198
|
+
return 'warn';
|
|
199
|
+
return 'fail';
|
|
200
|
+
}
|
|
201
|
+
function safely(fn, fallback) {
|
|
202
|
+
try {
|
|
203
|
+
return fn();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return fallback;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -84,10 +84,18 @@
|
|
|
84
84
|
* contributor — or dario maintainer six months from now — can pick up
|
|
85
85
|
* the right piece without re-deriving the threat model.
|
|
86
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* Cache-file schema version. Bump when `TemplateData` gains a required
|
|
89
|
+
* field or changes shape in a way that would make older caches produce
|
|
90
|
+
* wrong behavior if loaded verbatim. Mismatched caches are rejected at
|
|
91
|
+
* load time so the fallback + next background refresh write a fresh one.
|
|
92
|
+
*/
|
|
93
|
+
export declare const CURRENT_SCHEMA_VERSION = 2;
|
|
87
94
|
export interface TemplateData {
|
|
88
95
|
_version: string;
|
|
89
96
|
_captured: string;
|
|
90
97
|
_source?: 'bundled' | 'live';
|
|
98
|
+
_schemaVersion?: number;
|
|
91
99
|
agent_identity: string;
|
|
92
100
|
system_prompt: string;
|
|
93
101
|
tools: Array<{
|
|
@@ -104,6 +112,23 @@ export interface TemplateData {
|
|
|
104
112
|
* header ordering instead of Node's alphabetical default.
|
|
105
113
|
*/
|
|
106
114
|
header_order?: string[];
|
|
115
|
+
/**
|
|
116
|
+
* The `anthropic-beta` flag set CC sent on the captured request, verbatim.
|
|
117
|
+
* Schema v2 (v3.19). Previously the proxy path hardcoded this — bumping
|
|
118
|
+
* CC's beta list required a dario release. Now the shim and proxy both
|
|
119
|
+
* replay whatever the live capture recorded. Falls back to
|
|
120
|
+
* `'claude-code-20250219'` when undefined (bundled snapshots, older caches).
|
|
121
|
+
*/
|
|
122
|
+
anthropic_beta?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Selected static headers CC sent on the captured request. Scoped to
|
|
125
|
+
* fingerprint-relevant keys — values that CC sets identically on every
|
|
126
|
+
* request and that don't change per session (user-agent, anthropic-version,
|
|
127
|
+
* x-app, x-stainless-*). Excludes auth (authorization), body-framing
|
|
128
|
+
* (content-type, content-length, host), and session-scoped identifiers
|
|
129
|
+
* (x-claude-code-session-id, x-client-request-id). Schema v2.
|
|
130
|
+
*/
|
|
131
|
+
header_values?: Record<string, string>;
|
|
107
132
|
}
|
|
108
133
|
/**
|
|
109
134
|
* Load the template synchronously. Prefers the live cache (fresh capture
|
|
@@ -131,6 +156,8 @@ export declare function refreshLiveFingerprintAsync(options?: {
|
|
|
131
156
|
silent?: boolean;
|
|
132
157
|
timeoutMs?: number;
|
|
133
158
|
}): Promise<TemplateData | null>;
|
|
159
|
+
/** Test-only surface for `atomicWriteJson`. Production code uses `writeLiveCache`. */
|
|
160
|
+
export declare function _atomicWriteJsonForTest(targetPath: string, data: unknown): void;
|
|
134
161
|
interface CapturedRequest {
|
|
135
162
|
method: string;
|
|
136
163
|
path: string;
|
|
@@ -151,12 +178,122 @@ interface CapturedRequest {
|
|
|
151
178
|
* Returns null on timeout or spawn failure. Does not throw.
|
|
152
179
|
*/
|
|
153
180
|
export declare function captureLiveTemplateAsync(timeoutMs?: number): Promise<TemplateData | null>;
|
|
181
|
+
/**
|
|
182
|
+
* Locate the installed `claude` binary and its version. Thin public
|
|
183
|
+
* wrapper over `findClaudeBinary` + `probeInstalledCCVersion` — the
|
|
184
|
+
* doctor CLI and external callers use this to report install state
|
|
185
|
+
* without reaching into module-private helpers.
|
|
186
|
+
*/
|
|
187
|
+
export declare function findInstalledCC(): {
|
|
188
|
+
path: string | null;
|
|
189
|
+
version: string | null;
|
|
190
|
+
};
|
|
154
191
|
/**
|
|
155
192
|
* Given a captured /v1/messages request body, pull out the fields that
|
|
156
193
|
* matter for template replay: agent identity, system prompt, tool list,
|
|
157
194
|
* and CC version (from the billing header or user-agent).
|
|
158
195
|
*/
|
|
159
196
|
export declare function extractTemplate(captured: CapturedRequest): TemplateData | null;
|
|
197
|
+
/**
|
|
198
|
+
* Sync-probe `claude --version` and return the parsed version string, e.g.
|
|
199
|
+
* `"2.1.104"`. Memoized per-process — the binary is invoked at most once,
|
|
200
|
+
* subsequent calls return the cached result. Returns `null` if the binary
|
|
201
|
+
* isn't on PATH, or the probe failed / timed out, or the output didn't
|
|
202
|
+
* match the expected format.
|
|
203
|
+
*
|
|
204
|
+
* Used by `detectDrift` to compare the installed CC against the version
|
|
205
|
+
* recorded in the cache at capture time.
|
|
206
|
+
*/
|
|
207
|
+
export declare function probeInstalledCCVersion(): string | null;
|
|
208
|
+
/**
|
|
209
|
+
* Format how old a captured timestamp is, human-readable. `_captured` is
|
|
210
|
+
* an ISO string written by `extractTemplate` or the bundled snapshot.
|
|
211
|
+
* Falls back to `"unknown age"` if the timestamp doesn't parse.
|
|
212
|
+
*/
|
|
213
|
+
export declare function formatCaptureAge(capturedIso: string, now?: number): string;
|
|
214
|
+
/**
|
|
215
|
+
* One-line human summary of the active template — what source, which CC
|
|
216
|
+
* version captured it, and how old that capture is. Proxy and shim
|
|
217
|
+
* startup log this so users can tell at a glance whether they're on a
|
|
218
|
+
* fresh live capture or a stale bundled fallback.
|
|
219
|
+
*/
|
|
220
|
+
export declare function describeTemplate(t: TemplateData): string;
|
|
221
|
+
export interface DriftResult {
|
|
222
|
+
/** True when we can confirm the cache is from a different CC version than the one currently installed. */
|
|
223
|
+
drifted: boolean;
|
|
224
|
+
cachedVersion: string;
|
|
225
|
+
/** null when the probe couldn't run (no CC on PATH, timeout, parse fail). */
|
|
226
|
+
installedVersion: string | null;
|
|
227
|
+
/** Reason string — safe to log as-is. */
|
|
228
|
+
message: string;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Compare the loaded template's captured CC version against the version
|
|
232
|
+
* reported by `claude --version` on the current machine. Drifted caches
|
|
233
|
+
* are still usable — the shape is probably compatible — but the proxy
|
|
234
|
+
* should force-refresh ASAP so the next startup is back in sync.
|
|
235
|
+
*
|
|
236
|
+
* @param installedOverride test-only injection for unit tests; production
|
|
237
|
+
* callers pass nothing and the real binary probe runs.
|
|
238
|
+
*/
|
|
239
|
+
export declare function detectDrift(t: TemplateData, installedOverride?: string | null): DriftResult;
|
|
240
|
+
/**
|
|
241
|
+
* Reset the memoized `claude --version` probe. Test-only — production
|
|
242
|
+
* code should never need to clear the cache since the installed binary
|
|
243
|
+
* doesn't change mid-process.
|
|
244
|
+
*/
|
|
245
|
+
export declare function _resetInstalledVersionProbeForTest(): void;
|
|
246
|
+
/**
|
|
247
|
+
* The CC version range the current dario release has been exercised
|
|
248
|
+
* against. Update `maxTested` every time we validate against a new CC
|
|
249
|
+
* (ideally as part of the release checklist — the e2e test against the
|
|
250
|
+
* user's own CC is the ground-truth signal).
|
|
251
|
+
*
|
|
252
|
+
* - `min`: below this, dario's extractor hasn't been validated; proxy
|
|
253
|
+
* will still run but may mis-parse CC's request body.
|
|
254
|
+
* - `maxTested`: the newest CC version the current dario release has
|
|
255
|
+
* been exercised against. Above this, dario is *likely* fine (CC's
|
|
256
|
+
* request shape evolves slowly) but it's explicitly untested, so
|
|
257
|
+
* users get a soft warn and we get a signal to refresh the bundled
|
|
258
|
+
* snapshot + rerun e2e.
|
|
259
|
+
*/
|
|
260
|
+
export declare const SUPPORTED_CC_RANGE: {
|
|
261
|
+
readonly min: "1.0.0";
|
|
262
|
+
readonly maxTested: "2.1.104";
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
|
266
|
+
* zero if equal, positive if `a>b`. Handles suffixes like `-beta.1` or
|
|
267
|
+
* `.dev` by comparing the numeric prefix first and treating anything
|
|
268
|
+
* after as a tiebreaker (strings compared lexicographically; absence of
|
|
269
|
+
* suffix beats presence, matching semver's "release > prerelease").
|
|
270
|
+
*
|
|
271
|
+
* Intentionally minimal — dario's "zero runtime deps" policy rules out
|
|
272
|
+
* pulling `semver`. CC versions are well-formed `M.m.p[-suffix]` so we
|
|
273
|
+
* don't need the full spec.
|
|
274
|
+
*/
|
|
275
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
276
|
+
export type CompatStatus = 'ok' | 'untested-above' | 'below-min' | 'unknown';
|
|
277
|
+
export interface CompatResult {
|
|
278
|
+
status: CompatStatus;
|
|
279
|
+
installedVersion: string | null;
|
|
280
|
+
range: {
|
|
281
|
+
min: string;
|
|
282
|
+
maxTested: string;
|
|
283
|
+
};
|
|
284
|
+
message: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Check whether the installed CC version sits inside the supported range.
|
|
288
|
+
* Called at startup by the proxy; the result drives whether we emit a
|
|
289
|
+
* compatibility warning to the user.
|
|
290
|
+
*
|
|
291
|
+
* `unknown` is not a failure — it just means we couldn't probe (no CC on
|
|
292
|
+
* PATH, timeout, parse miss). Dario still runs on bundled template.
|
|
293
|
+
*
|
|
294
|
+
* @param installedOverride test-only injection; production callers pass nothing.
|
|
295
|
+
*/
|
|
296
|
+
export declare function checkCCCompat(installedOverride?: string | null): CompatResult;
|
|
160
297
|
/**
|
|
161
298
|
* Test hook: given a captured request object (from a mocked server or a
|
|
162
299
|
* synthetic fixture), run it through the same extraction path. Exposed so
|