@askalf/dario 3.16.0 → 3.19.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/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