@askalf/dario 3.38.6 → 4.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 +136 -25
- package/dist/analytics.d.ts +32 -3
- package/dist/analytics.js +48 -3
- package/dist/cc-template-data.json +36 -5
- package/dist/cli.js +111 -25
- package/dist/config-file.d.ts +157 -0
- package/dist/config-file.js +326 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.js +93 -23
- package/dist/tui/app.d.ts +96 -0
- package/dist/tui/app.js +178 -0
- package/dist/tui/input.d.ts +57 -0
- package/dist/tui/input.js +206 -0
- package/dist/tui/layout.d.ts +66 -0
- package/dist/tui/layout.js +152 -0
- package/dist/tui/proxy-client.d.ts +60 -0
- package/dist/tui/proxy-client.js +166 -0
- package/dist/tui/render.d.ts +178 -0
- package/dist/tui/render.js +246 -0
- package/dist/tui/tab.d.ts +89 -0
- package/dist/tui/tab.js +19 -0
- package/dist/tui/tabs/accounts.d.ts +32 -0
- package/dist/tui/tabs/accounts.js +110 -0
- package/dist/tui/tabs/analytics.d.ts +53 -0
- package/dist/tui/tabs/analytics.js +161 -0
- package/dist/tui/tabs/backends.d.ts +19 -0
- package/dist/tui/tabs/backends.js +77 -0
- package/dist/tui/tabs/config.d.ts +35 -0
- package/dist/tui/tabs/config.js +267 -0
- package/dist/tui/tabs/hits.d.ts +34 -0
- package/dist/tui/tabs/hits.js +223 -0
- package/dist/tui/tabs/status.d.ts +45 -0
- package/dist/tui/tabs/status.js +132 -0
- package/dist/tui/tui-app.d.ts +41 -0
- package/dist/tui/tui-app.js +217 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,28 @@ import { parseOutboundProxy, installOutboundProxyWrapper } from './outbound-prox
|
|
|
31
31
|
// `args` to read their own flags. Reading argv is harmless on import; only
|
|
32
32
|
// the handler dispatch at the bottom is gated behind the main-entry check.
|
|
33
33
|
const args = process.argv.slice(2);
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Default command when invoked with no args.
|
|
36
|
+
*
|
|
37
|
+
* v3.x: `dario` started the proxy (default = 'proxy').
|
|
38
|
+
* v4.0: `dario` opens the interactive TUI (default = 'tui').
|
|
39
|
+
*
|
|
40
|
+
* Migration: scripts that ran bare `dario` to launch the proxy need
|
|
41
|
+
* to switch to `dario proxy`. The TUI itself surfaces a "proxy
|
|
42
|
+
* unreachable" hint with the exact command if it doesn't see one
|
|
43
|
+
* running, so users discovering the change get pointed to the fix.
|
|
44
|
+
*
|
|
45
|
+
* `--no-tui` opt-out runs help instead (escape hatch for users who
|
|
46
|
+
* want the v3-style behavior without explicitly typing `proxy`, e.g.
|
|
47
|
+
* inside CI scripts that grep `dario` output).
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_COMMAND = args.includes('--no-tui') ? 'help' : 'tui';
|
|
50
|
+
// --no-tui is a meta-flag (controls DEFAULT_COMMAND above) not a command
|
|
51
|
+
// itself, so when it appears as args[0] we still resolve to the default.
|
|
52
|
+
// All other args pass through unchanged — `dario proxy`, `dario --help`,
|
|
53
|
+
// `dario --version` etc. still dispatch as expected.
|
|
54
|
+
const positionalArgs = args.filter((a) => a !== '--no-tui');
|
|
55
|
+
const command = positionalArgs[0] ?? DEFAULT_COMMAND;
|
|
35
56
|
async function login() {
|
|
36
57
|
console.log('');
|
|
37
58
|
console.log(' dario — Claude Login');
|
|
@@ -190,18 +211,33 @@ async function logout() {
|
|
|
190
211
|
}
|
|
191
212
|
}
|
|
192
213
|
async function proxy() {
|
|
214
|
+
// v4: load ~/.dario/config.json once at startup so file-stored values
|
|
215
|
+
// serve as defaults below where no CLI flag / env var supplies one.
|
|
216
|
+
// Precedence per M1: defaults < file < env < CLI. Missing-file is
|
|
217
|
+
// treated as "no file values" — the existing CLI/env paths see no
|
|
218
|
+
// change. Invalid file is logged but doesn't abort; we still start
|
|
219
|
+
// with whatever the env+flags provide.
|
|
220
|
+
const { loadConfig } = await import('./config-file.js');
|
|
221
|
+
const fileResult = loadConfig();
|
|
222
|
+
const fileCfg = fileResult.config;
|
|
223
|
+
if (fileResult.source === 'invalid') {
|
|
224
|
+
console.warn(`[dario] config file present but invalid (${fileResult.error}) — using defaults + env + flags only.`);
|
|
225
|
+
}
|
|
193
226
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
194
|
-
|
|
227
|
+
// Precedence: --port flag > DARIO_PORT env > config file > built-in default 3456
|
|
228
|
+
const portFromCli = portArg ? parseInt(portArg.split('=')[1], 10) : undefined;
|
|
229
|
+
const portFromEnv = process.env['DARIO_PORT'] ? parseInt(process.env['DARIO_PORT'], 10) : undefined;
|
|
230
|
+
const port = portFromCli ?? portFromEnv ?? fileCfg.port ?? 3456;
|
|
195
231
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
196
232
|
console.error('[dario] Invalid port. Must be 1-65535.');
|
|
197
233
|
process.exit(1);
|
|
198
234
|
}
|
|
199
|
-
// Bind address —
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// happens when the OS tries to bind.
|
|
235
|
+
// Bind address — --host > DARIO_HOST > config file > startProxy's
|
|
236
|
+
// built-in 127.0.0.1 default. The regex sanity-check rejects
|
|
237
|
+
// obviously bad shapes; real address validation happens at bind time.
|
|
203
238
|
const hostArg = args.find(a => a.startsWith('--host='));
|
|
204
|
-
const host = hostArg ? hostArg.split('=')[1]
|
|
239
|
+
const host = hostArg ? hostArg.split('=')[1]
|
|
240
|
+
: (process.env['DARIO_HOST'] || fileCfg.host || undefined);
|
|
205
241
|
if (host !== undefined && !/^[a-zA-Z0-9._:-]+$/.test(host)) {
|
|
206
242
|
console.error('[dario] Invalid --host. Must be an IP address or hostname.');
|
|
207
243
|
process.exit(1);
|
|
@@ -247,34 +283,33 @@ async function proxy() {
|
|
|
247
283
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
248
284
|
// --pace-min=MS / --pace-jitter=MS (v3.24, direction #6 — behavioral
|
|
249
285
|
// smoothing). Inter-request gap floor + optional uniform-random jitter.
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
const
|
|
286
|
+
// v4: ~/.dario/config.json's `pacing.{minMs,jitterMs}` is the fallback
|
|
287
|
+
// when no CLI flag is set, so the TUI's Config tab can persist edits.
|
|
288
|
+
// The pure calc lives in src/pacing.ts; flags+config just feed it.
|
|
289
|
+
const pacingMinMs = parsePositiveIntFlag('--pace-min=') ?? fileCfg.pacing?.minMs;
|
|
290
|
+
const pacingJitterMs = parsePositiveIntFlag('--pace-jitter=') ?? fileCfg.pacing?.jitterMs;
|
|
254
291
|
// --think-time-* / --session-start-* — behavioral smoothing extension.
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const sessionStartMinMs = parsePositiveIntFlag('--session-start-min=');
|
|
263
|
-
const sessionStartJitterMs = parsePositiveIntFlag('--session-start-jitter=');
|
|
292
|
+
// Same v4 precedence (flag > file > built-in default).
|
|
293
|
+
const thinkTimeBaseMs = parsePositiveIntFlag('--think-time-base=') ?? fileCfg.thinkTime?.baseMs;
|
|
294
|
+
const thinkTimePerTokenMs = parsePositiveIntFlag('--think-time-per-token=') ?? fileCfg.thinkTime?.perTokenMs;
|
|
295
|
+
const thinkTimeJitterMs = parsePositiveIntFlag('--think-time-jitter=') ?? fileCfg.thinkTime?.jitterMs;
|
|
296
|
+
const thinkTimeMaxMs = parsePositiveIntFlag('--think-time-max=') ?? fileCfg.thinkTime?.maxMs;
|
|
297
|
+
const sessionStartMinMs = parsePositiveIntFlag('--session-start-min=') ?? fileCfg.sessionStart?.minMs;
|
|
298
|
+
const sessionStartJitterMs = parsePositiveIntFlag('--session-start-jitter=') ?? fileCfg.sessionStart?.jitterMs;
|
|
264
299
|
// --stealth flips all three pacing layers (pace, think, session-start)
|
|
265
|
-
// into their behavioral-stealth presets
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// toggle stealth on and then tune individual axes.
|
|
300
|
+
// into their behavioral-stealth presets. Same v4 precedence (flag >
|
|
301
|
+
// env > file > false). Per-knob flags above still win even when
|
|
302
|
+
// stealth is on, so operators can flip on then tune individual axes.
|
|
269
303
|
const stealth = args.includes('--stealth')
|
|
270
304
|
|| parseBooleanEnv(process.env['DARIO_STEALTH'])
|
|
305
|
+
|| fileCfg.stealth
|
|
271
306
|
|| undefined;
|
|
272
307
|
// --drain-on-close (v3.25, direction #5). When set, a client
|
|
273
308
|
// disconnect no longer aborts the upstream SSE — dario keeps
|
|
274
309
|
// draining the stream to EOF so Anthropic sees the CC-shaped
|
|
275
310
|
// read-to-completion pattern. Costs tokens (the response is fully
|
|
276
311
|
// generated even if nobody reads it), so it's opt-in.
|
|
277
|
-
const drainOnClose = args.includes('--drain-on-close') || undefined;
|
|
312
|
+
const drainOnClose = args.includes('--drain-on-close') || fileCfg.drainOnClose || undefined;
|
|
278
313
|
// --session-* knobs (v3.28, direction #1). Control the single-account
|
|
279
314
|
// session-id lifecycle: idle threshold, jitter on that threshold, hard
|
|
280
315
|
// max-age, and whether to give each upstream client its own session.
|
|
@@ -1711,6 +1746,56 @@ async function usage() {
|
|
|
1711
1746
|
}
|
|
1712
1747
|
console.log('');
|
|
1713
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* `dario tui` (or `dario` with no args) — opens the interactive
|
|
1751
|
+
* terminal UI. v4 entry point.
|
|
1752
|
+
*
|
|
1753
|
+
* The TUI is a viewer/configurator; it expects a `dario proxy`
|
|
1754
|
+
* already running locally for live analytics. When no proxy is
|
|
1755
|
+
* reachable, each tab degrades gracefully — Status shows
|
|
1756
|
+
* "unreachable" with the start-command hint, Analytics + Hits show
|
|
1757
|
+
* the same. Accounts + Backends + Config don't need the proxy at
|
|
1758
|
+
* all (they read disk directly).
|
|
1759
|
+
*
|
|
1760
|
+
* Bails out early if stdin isn't a TTY — the TUI can't function in
|
|
1761
|
+
* a pipe / redirect. The error message points at `dario proxy` (the
|
|
1762
|
+
* non-interactive entry) for that case.
|
|
1763
|
+
*
|
|
1764
|
+
* --port=<n> target a non-default proxy port (default 3456)
|
|
1765
|
+
* --api-key=KEY authenticate against a DARIO_API_KEY-protected proxy
|
|
1766
|
+
*/
|
|
1767
|
+
async function tui() {
|
|
1768
|
+
if (!process.stdin.isTTY) {
|
|
1769
|
+
console.error('[dario] TUI requires an interactive terminal.');
|
|
1770
|
+
console.error(' Pipe / redirect detected on stdin.');
|
|
1771
|
+
console.error(' Run `dario proxy` for the non-interactive proxy server,');
|
|
1772
|
+
console.error(' or `dario --no-tui` to print help instead.');
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
const portArg = args.find((a) => a.startsWith('--port='));
|
|
1776
|
+
const port = portArg ? parseInt(portArg.split('=')[1], 10) : 3456;
|
|
1777
|
+
const apiKeyArg = args.find((a) => a.startsWith('--api-key='));
|
|
1778
|
+
const apiKey = apiKeyArg
|
|
1779
|
+
? apiKeyArg.split('=')[1]
|
|
1780
|
+
: (process.env['DARIO_API_KEY'] || undefined);
|
|
1781
|
+
const { startTuiApp } = await import('./tui/tui-app.js');
|
|
1782
|
+
await startTuiApp({
|
|
1783
|
+
version: pkgVersion(),
|
|
1784
|
+
proxyUrl: `http://127.0.0.1:${port}`,
|
|
1785
|
+
apiKey,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
function pkgVersion() {
|
|
1789
|
+
try {
|
|
1790
|
+
// Read from the same package.json the rest of the CLI uses.
|
|
1791
|
+
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
1792
|
+
const fs = require('node:fs');
|
|
1793
|
+
return JSON.parse(fs.readFileSync(pkgUrl, 'utf-8')).version || 'unknown';
|
|
1794
|
+
}
|
|
1795
|
+
catch {
|
|
1796
|
+
return 'unknown';
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1714
1799
|
// Main
|
|
1715
1800
|
const commands = {
|
|
1716
1801
|
login,
|
|
@@ -1727,6 +1812,7 @@ const commands = {
|
|
|
1727
1812
|
config,
|
|
1728
1813
|
upgrade,
|
|
1729
1814
|
usage,
|
|
1815
|
+
tui,
|
|
1730
1816
|
help,
|
|
1731
1817
|
version,
|
|
1732
1818
|
'--help': help,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file foundation — v4.
|
|
3
|
+
*
|
|
4
|
+
* Persists user-tunable settings to `~/.dario/config.json` so the TUI
|
|
5
|
+
* can read + write them without having to manage shell scripts. Establishes
|
|
6
|
+
* the precedence chain that every effective value passes through:
|
|
7
|
+
*
|
|
8
|
+
* defaults < config.json < env var < CLI flag
|
|
9
|
+
*
|
|
10
|
+
* Existing CLI flags + env vars continue to work unchanged. The config
|
|
11
|
+
* file is purely additive — a missing file resolves to defaults, exactly
|
|
12
|
+
* as v3 already behaved (since v3 had no config file).
|
|
13
|
+
*
|
|
14
|
+
* Atomic write: write to `config.json.tmp`, fsync, rename. Same primitive
|
|
15
|
+
* shape `atomicWriteJson` in src/live-fingerprint.ts uses for the
|
|
16
|
+
* captured CC template.
|
|
17
|
+
*
|
|
18
|
+
* Unknown keys in the loaded file are preserved (forward-compat for
|
|
19
|
+
* future schema versions); validation is best-effort, not strict — a
|
|
20
|
+
* corrupt or partial file falls back to defaults rather than aborting
|
|
21
|
+
* the process.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Bumped on any incompatible shape change. v4.0.0 ships schema v1. A
|
|
25
|
+
* future shape change would either add a new optional field (no bump)
|
|
26
|
+
* or rename / restructure (bump to v2 with a migration in `loadConfig`).
|
|
27
|
+
*/
|
|
28
|
+
export declare const CONFIG_SCHEMA_VERSION = 1;
|
|
29
|
+
/**
|
|
30
|
+
* Default `~/.dario/config.json` location. Override in tests via
|
|
31
|
+
* `loadConfig(path)` / `saveConfig(path, …)`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_CONFIG_PATH: string;
|
|
34
|
+
/**
|
|
35
|
+
* Every user-tunable setting. Grouped into sub-objects when the knobs
|
|
36
|
+
* cluster naturally (pacing, thinkTime, session, queue) so the TUI's
|
|
37
|
+
* Config tab can render each cluster as a folder without extra glue.
|
|
38
|
+
*
|
|
39
|
+
* Optional everywhere — a partially-populated config file is valid; the
|
|
40
|
+
* proxy fills in defaults for whatever's absent.
|
|
41
|
+
*/
|
|
42
|
+
export interface DarioConfig {
|
|
43
|
+
/** Schema version of this file. Required for forward-compat. */
|
|
44
|
+
version: number;
|
|
45
|
+
port?: number;
|
|
46
|
+
host?: string;
|
|
47
|
+
model?: string | null;
|
|
48
|
+
passthrough?: boolean;
|
|
49
|
+
preserveTools?: boolean;
|
|
50
|
+
hybridTools?: boolean;
|
|
51
|
+
mergeTools?: boolean;
|
|
52
|
+
noAutoDetect?: boolean;
|
|
53
|
+
strictTls?: boolean;
|
|
54
|
+
strictTemplate?: boolean;
|
|
55
|
+
noLiveCapture?: boolean;
|
|
56
|
+
drainOnClose?: boolean;
|
|
57
|
+
stealth?: boolean;
|
|
58
|
+
pacing?: {
|
|
59
|
+
minMs?: number;
|
|
60
|
+
jitterMs?: number;
|
|
61
|
+
};
|
|
62
|
+
thinkTime?: {
|
|
63
|
+
baseMs?: number;
|
|
64
|
+
perTokenMs?: number;
|
|
65
|
+
jitterMs?: number;
|
|
66
|
+
maxMs?: number;
|
|
67
|
+
};
|
|
68
|
+
sessionStart?: {
|
|
69
|
+
minMs?: number;
|
|
70
|
+
jitterMs?: number;
|
|
71
|
+
};
|
|
72
|
+
session?: {
|
|
73
|
+
idleRotateMs?: number;
|
|
74
|
+
rotateJitterMs?: number;
|
|
75
|
+
maxAgeMs?: number | null;
|
|
76
|
+
perClient?: boolean;
|
|
77
|
+
};
|
|
78
|
+
queue?: {
|
|
79
|
+
maxConcurrent?: number | null;
|
|
80
|
+
maxQueued?: number | null;
|
|
81
|
+
timeoutMs?: number | null;
|
|
82
|
+
};
|
|
83
|
+
effort?: string | null;
|
|
84
|
+
maxTokens?: number | 'client' | null;
|
|
85
|
+
passthroughBetas?: string[];
|
|
86
|
+
systemPrompt?: string | null;
|
|
87
|
+
preserveOrchestrationTags?: boolean;
|
|
88
|
+
logFile?: string | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Defaults match the v3.x CLI flag defaults exactly. Any value not
|
|
92
|
+
* specified in config.json resolves to its corresponding default here.
|
|
93
|
+
* Updates to a flag default MUST land here too so they stay in sync.
|
|
94
|
+
*/
|
|
95
|
+
export declare function defaultConfig(): DarioConfig;
|
|
96
|
+
/**
|
|
97
|
+
* Load the config file at `path` (default ~/.dario/config.json).
|
|
98
|
+
*
|
|
99
|
+
* Returns `{ config, source }` where `source` describes the load outcome
|
|
100
|
+
* for the caller's UI:
|
|
101
|
+
*
|
|
102
|
+
* - 'file' — successfully loaded
|
|
103
|
+
* - 'missing' — file doesn't exist; defaults returned (not an error)
|
|
104
|
+
* - 'invalid' — file exists but parse / shape check failed; defaults
|
|
105
|
+
* returned. The TUI surfaces this so the user knows
|
|
106
|
+
* their saved settings were ignored.
|
|
107
|
+
*
|
|
108
|
+
* The loaded shape is type-checked field-by-field: unknown keys pass
|
|
109
|
+
* through (forward-compat), known keys with wrong types are dropped.
|
|
110
|
+
* Strict validation would force a config migration on every shape
|
|
111
|
+
* tweak; loose-but-typed lets the file evolve without breaking older
|
|
112
|
+
* dario installs that haven't been restarted.
|
|
113
|
+
*/
|
|
114
|
+
export declare function loadConfig(path?: string): {
|
|
115
|
+
config: DarioConfig;
|
|
116
|
+
source: 'file' | 'missing' | 'invalid';
|
|
117
|
+
error?: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Atomically write `config` to `path`. Writes to `<path>.tmp`, then
|
|
121
|
+
* renames into place — guarantees a reader never observes a half-written
|
|
122
|
+
* file. Creates parent directories if missing.
|
|
123
|
+
*
|
|
124
|
+
* Throws on permission / disk failures (caller handles + surfaces to
|
|
125
|
+
* the TUI's status line). Does NOT throw on a no-op rewrite of the
|
|
126
|
+
* same content; that's a cheap idempotent path.
|
|
127
|
+
*/
|
|
128
|
+
export declare function saveConfig(path: string | undefined, config: DarioConfig): void;
|
|
129
|
+
/**
|
|
130
|
+
* Deep-merge `over` into `base`, preferring `over` values where defined.
|
|
131
|
+
* Nested objects are merged recursively; arrays and primitives are
|
|
132
|
+
* replaced wholesale (no array-element merge — that'd be surprising).
|
|
133
|
+
*
|
|
134
|
+
* `undefined` in `over` is treated as "absent" and falls through to
|
|
135
|
+
* the `base` value. `null` is a real value and overrides.
|
|
136
|
+
*/
|
|
137
|
+
export declare function mergeOver<T extends object>(base: T, over: Partial<T>): T;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the effective config: load file, layer env vars on top, layer
|
|
140
|
+
* CLI flags on top.
|
|
141
|
+
*
|
|
142
|
+
* defaults < config.json < env < cli
|
|
143
|
+
*
|
|
144
|
+
* `cliOverrides` and `envOverrides` are partial — only the keys the
|
|
145
|
+
* caller actually wants to override should be set. `undefined` keys
|
|
146
|
+
* are skipped, so the existing flag parsers in cli.ts can pass through
|
|
147
|
+
* their normalized output without filtering nulls.
|
|
148
|
+
*/
|
|
149
|
+
export declare function resolveConfig(opts: {
|
|
150
|
+
path?: string;
|
|
151
|
+
envOverrides?: Partial<DarioConfig>;
|
|
152
|
+
cliOverrides?: Partial<DarioConfig>;
|
|
153
|
+
}): {
|
|
154
|
+
config: DarioConfig;
|
|
155
|
+
source: 'file' | 'missing' | 'invalid';
|
|
156
|
+
error?: string;
|
|
157
|
+
};
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file foundation — v4.
|
|
3
|
+
*
|
|
4
|
+
* Persists user-tunable settings to `~/.dario/config.json` so the TUI
|
|
5
|
+
* can read + write them without having to manage shell scripts. Establishes
|
|
6
|
+
* the precedence chain that every effective value passes through:
|
|
7
|
+
*
|
|
8
|
+
* defaults < config.json < env var < CLI flag
|
|
9
|
+
*
|
|
10
|
+
* Existing CLI flags + env vars continue to work unchanged. The config
|
|
11
|
+
* file is purely additive — a missing file resolves to defaults, exactly
|
|
12
|
+
* as v3 already behaved (since v3 had no config file).
|
|
13
|
+
*
|
|
14
|
+
* Atomic write: write to `config.json.tmp`, fsync, rename. Same primitive
|
|
15
|
+
* shape `atomicWriteJson` in src/live-fingerprint.ts uses for the
|
|
16
|
+
* captured CC template.
|
|
17
|
+
*
|
|
18
|
+
* Unknown keys in the loaded file are preserved (forward-compat for
|
|
19
|
+
* future schema versions); validation is best-effort, not strict — a
|
|
20
|
+
* corrupt or partial file falls back to defaults rather than aborting
|
|
21
|
+
* the process.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
/**
|
|
27
|
+
* Bumped on any incompatible shape change. v4.0.0 ships schema v1. A
|
|
28
|
+
* future shape change would either add a new optional field (no bump)
|
|
29
|
+
* or rename / restructure (bump to v2 with a migration in `loadConfig`).
|
|
30
|
+
*/
|
|
31
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
32
|
+
/**
|
|
33
|
+
* Default `~/.dario/config.json` location. Override in tests via
|
|
34
|
+
* `loadConfig(path)` / `saveConfig(path, …)`.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_CONFIG_PATH = join(homedir(), '.dario', 'config.json');
|
|
37
|
+
/**
|
|
38
|
+
* Defaults match the v3.x CLI flag defaults exactly. Any value not
|
|
39
|
+
* specified in config.json resolves to its corresponding default here.
|
|
40
|
+
* Updates to a flag default MUST land here too so they stay in sync.
|
|
41
|
+
*/
|
|
42
|
+
export function defaultConfig() {
|
|
43
|
+
return {
|
|
44
|
+
version: CONFIG_SCHEMA_VERSION,
|
|
45
|
+
port: 3456,
|
|
46
|
+
host: '127.0.0.1',
|
|
47
|
+
model: null,
|
|
48
|
+
passthrough: false,
|
|
49
|
+
preserveTools: false,
|
|
50
|
+
hybridTools: false,
|
|
51
|
+
mergeTools: false,
|
|
52
|
+
noAutoDetect: false,
|
|
53
|
+
strictTls: false,
|
|
54
|
+
strictTemplate: false,
|
|
55
|
+
noLiveCapture: false,
|
|
56
|
+
drainOnClose: false,
|
|
57
|
+
stealth: false,
|
|
58
|
+
pacing: { minMs: 500, jitterMs: 0 },
|
|
59
|
+
thinkTime: { baseMs: 0, perTokenMs: 0, jitterMs: 0, maxMs: 30_000 },
|
|
60
|
+
sessionStart: { minMs: 0, jitterMs: 0 },
|
|
61
|
+
session: {
|
|
62
|
+
idleRotateMs: 900_000,
|
|
63
|
+
rotateJitterMs: 0,
|
|
64
|
+
maxAgeMs: null,
|
|
65
|
+
perClient: false,
|
|
66
|
+
},
|
|
67
|
+
queue: { maxConcurrent: null, maxQueued: null, timeoutMs: null },
|
|
68
|
+
effort: null,
|
|
69
|
+
maxTokens: null,
|
|
70
|
+
passthroughBetas: [],
|
|
71
|
+
systemPrompt: null,
|
|
72
|
+
preserveOrchestrationTags: false,
|
|
73
|
+
logFile: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load the config file at `path` (default ~/.dario/config.json).
|
|
78
|
+
*
|
|
79
|
+
* Returns `{ config, source }` where `source` describes the load outcome
|
|
80
|
+
* for the caller's UI:
|
|
81
|
+
*
|
|
82
|
+
* - 'file' — successfully loaded
|
|
83
|
+
* - 'missing' — file doesn't exist; defaults returned (not an error)
|
|
84
|
+
* - 'invalid' — file exists but parse / shape check failed; defaults
|
|
85
|
+
* returned. The TUI surfaces this so the user knows
|
|
86
|
+
* their saved settings were ignored.
|
|
87
|
+
*
|
|
88
|
+
* The loaded shape is type-checked field-by-field: unknown keys pass
|
|
89
|
+
* through (forward-compat), known keys with wrong types are dropped.
|
|
90
|
+
* Strict validation would force a config migration on every shape
|
|
91
|
+
* tweak; loose-but-typed lets the file evolve without breaking older
|
|
92
|
+
* dario installs that haven't been restarted.
|
|
93
|
+
*/
|
|
94
|
+
export function loadConfig(path = DEFAULT_CONFIG_PATH) {
|
|
95
|
+
if (!existsSync(path)) {
|
|
96
|
+
return { config: defaultConfig(), source: 'missing' };
|
|
97
|
+
}
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = readFileSync(path, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
config: defaultConfig(),
|
|
105
|
+
source: 'invalid',
|
|
106
|
+
error: `read failed: ${err.message}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return {
|
|
115
|
+
config: defaultConfig(),
|
|
116
|
+
source: 'invalid',
|
|
117
|
+
error: `JSON parse failed: ${err.message}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (!isPlainObject(parsed)) {
|
|
121
|
+
return {
|
|
122
|
+
config: defaultConfig(),
|
|
123
|
+
source: 'invalid',
|
|
124
|
+
error: `top-level value is not an object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Future schema bumps: dispatch on parsed.version here and run the
|
|
128
|
+
// appropriate migration. For now we accept any version field but
|
|
129
|
+
// pass the rest through field-by-field validation.
|
|
130
|
+
const typed = sanitize(parsed);
|
|
131
|
+
// Merge over defaults so callers always get a fully-populated shape.
|
|
132
|
+
return {
|
|
133
|
+
config: mergeOver(defaultConfig(), typed),
|
|
134
|
+
source: 'file',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Atomically write `config` to `path`. Writes to `<path>.tmp`, then
|
|
139
|
+
* renames into place — guarantees a reader never observes a half-written
|
|
140
|
+
* file. Creates parent directories if missing.
|
|
141
|
+
*
|
|
142
|
+
* Throws on permission / disk failures (caller handles + surfaces to
|
|
143
|
+
* the TUI's status line). Does NOT throw on a no-op rewrite of the
|
|
144
|
+
* same content; that's a cheap idempotent path.
|
|
145
|
+
*/
|
|
146
|
+
export function saveConfig(path = DEFAULT_CONFIG_PATH, config) {
|
|
147
|
+
const parent = dirname(path);
|
|
148
|
+
if (!existsSync(parent)) {
|
|
149
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
150
|
+
}
|
|
151
|
+
const json = JSON.stringify({ ...config, version: CONFIG_SCHEMA_VERSION }, null, 2) + '\n';
|
|
152
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(tmp, json, { mode: 0o600 });
|
|
155
|
+
renameSync(tmp, path);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Best-effort cleanup of the temp file so we don't leave debris.
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(tmp);
|
|
161
|
+
}
|
|
162
|
+
catch { /* ignore */ }
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Deep-merge `over` into `base`, preferring `over` values where defined.
|
|
168
|
+
* Nested objects are merged recursively; arrays and primitives are
|
|
169
|
+
* replaced wholesale (no array-element merge — that'd be surprising).
|
|
170
|
+
*
|
|
171
|
+
* `undefined` in `over` is treated as "absent" and falls through to
|
|
172
|
+
* the `base` value. `null` is a real value and overrides.
|
|
173
|
+
*/
|
|
174
|
+
export function mergeOver(base, over) {
|
|
175
|
+
const out = { ...base };
|
|
176
|
+
for (const [k, v] of Object.entries(over)) {
|
|
177
|
+
if (v === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
if (isPlainObject(v) && isPlainObject(out[k])) {
|
|
180
|
+
// Recurse into nested object groups (pacing, thinkTime, …) so a
|
|
181
|
+
// partial override on one sub-field doesn't wipe siblings.
|
|
182
|
+
out[k] = mergeOver(out[k], v);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
out[k] = v;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the effective config: load file, layer env vars on top, layer
|
|
192
|
+
* CLI flags on top.
|
|
193
|
+
*
|
|
194
|
+
* defaults < config.json < env < cli
|
|
195
|
+
*
|
|
196
|
+
* `cliOverrides` and `envOverrides` are partial — only the keys the
|
|
197
|
+
* caller actually wants to override should be set. `undefined` keys
|
|
198
|
+
* are skipped, so the existing flag parsers in cli.ts can pass through
|
|
199
|
+
* their normalized output without filtering nulls.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveConfig(opts) {
|
|
202
|
+
const fromFile = loadConfig(opts.path);
|
|
203
|
+
const withEnv = mergeOver(fromFile.config, opts.envOverrides ?? {});
|
|
204
|
+
const withCli = mergeOver(withEnv, opts.cliOverrides ?? {});
|
|
205
|
+
return { ...fromFile, config: withCli };
|
|
206
|
+
}
|
|
207
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
208
|
+
function isPlainObject(v) {
|
|
209
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Field-by-field type-check. Drops keys whose values don't match the
|
|
213
|
+
* expected type — better to silently fall back to a default than to
|
|
214
|
+
* abort startup on a stray manually-edited typo. Unknown top-level
|
|
215
|
+
* keys pass through unchanged (forward-compat for future fields).
|
|
216
|
+
*/
|
|
217
|
+
function sanitize(parsed) {
|
|
218
|
+
const out = { version: CONFIG_SCHEMA_VERSION };
|
|
219
|
+
const pickNumber = (k) => typeof parsed[k] === 'number' && Number.isFinite(parsed[k]) ? parsed[k] : undefined;
|
|
220
|
+
const pickBool = (k) => typeof parsed[k] === 'boolean' ? parsed[k] : undefined;
|
|
221
|
+
const pickString = (k) => typeof parsed[k] === 'string' ? parsed[k] : undefined;
|
|
222
|
+
const pickStringOrNull = (k) => {
|
|
223
|
+
if (parsed[k] === null)
|
|
224
|
+
return null;
|
|
225
|
+
if (typeof parsed[k] === 'string')
|
|
226
|
+
return parsed[k];
|
|
227
|
+
return undefined;
|
|
228
|
+
};
|
|
229
|
+
const pickNumberOrNull = (k) => {
|
|
230
|
+
if (parsed[k] === null)
|
|
231
|
+
return null;
|
|
232
|
+
if (typeof parsed[k] === 'number' && Number.isFinite(parsed[k]))
|
|
233
|
+
return parsed[k];
|
|
234
|
+
return undefined;
|
|
235
|
+
};
|
|
236
|
+
if (typeof parsed.version === 'number')
|
|
237
|
+
out.version = parsed.version;
|
|
238
|
+
if (pickNumber('port') !== undefined)
|
|
239
|
+
out.port = pickNumber('port');
|
|
240
|
+
if (pickString('host') !== undefined)
|
|
241
|
+
out.host = pickString('host');
|
|
242
|
+
const model = pickStringOrNull('model');
|
|
243
|
+
if (model !== undefined)
|
|
244
|
+
out.model = model;
|
|
245
|
+
for (const k of ['passthrough', 'preserveTools', 'hybridTools', 'mergeTools',
|
|
246
|
+
'noAutoDetect', 'strictTls', 'strictTemplate', 'noLiveCapture',
|
|
247
|
+
'drainOnClose', 'stealth', 'preserveOrchestrationTags']) {
|
|
248
|
+
const v = pickBool(k);
|
|
249
|
+
// Each `k` is a literal boolean-typed field on DarioConfig (verified
|
|
250
|
+
// by the `as const` tuple type above), so the assignment is sound
|
|
251
|
+
// — we route through `unknown` because TS can't narrow the union
|
|
252
|
+
// of literal keys to a single typed assignment at compile time.
|
|
253
|
+
if (v !== undefined)
|
|
254
|
+
out[k] = v;
|
|
255
|
+
}
|
|
256
|
+
// Nested groups — sanitize each, drop if not an object
|
|
257
|
+
if (isPlainObject(parsed.pacing)) {
|
|
258
|
+
out.pacing = {};
|
|
259
|
+
if (typeof parsed.pacing.minMs === 'number')
|
|
260
|
+
out.pacing.minMs = parsed.pacing.minMs;
|
|
261
|
+
if (typeof parsed.pacing.jitterMs === 'number')
|
|
262
|
+
out.pacing.jitterMs = parsed.pacing.jitterMs;
|
|
263
|
+
}
|
|
264
|
+
if (isPlainObject(parsed.thinkTime)) {
|
|
265
|
+
out.thinkTime = {};
|
|
266
|
+
for (const k of ['baseMs', 'perTokenMs', 'jitterMs', 'maxMs']) {
|
|
267
|
+
if (typeof parsed.thinkTime[k] === 'number') {
|
|
268
|
+
out.thinkTime[k] = parsed.thinkTime[k];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (isPlainObject(parsed.sessionStart)) {
|
|
273
|
+
out.sessionStart = {};
|
|
274
|
+
if (typeof parsed.sessionStart.minMs === 'number')
|
|
275
|
+
out.sessionStart.minMs = parsed.sessionStart.minMs;
|
|
276
|
+
if (typeof parsed.sessionStart.jitterMs === 'number')
|
|
277
|
+
out.sessionStart.jitterMs = parsed.sessionStart.jitterMs;
|
|
278
|
+
}
|
|
279
|
+
if (isPlainObject(parsed.session)) {
|
|
280
|
+
out.session = {};
|
|
281
|
+
if (typeof parsed.session.idleRotateMs === 'number')
|
|
282
|
+
out.session.idleRotateMs = parsed.session.idleRotateMs;
|
|
283
|
+
if (typeof parsed.session.rotateJitterMs === 'number')
|
|
284
|
+
out.session.rotateJitterMs = parsed.session.rotateJitterMs;
|
|
285
|
+
if (parsed.session.maxAgeMs === null || typeof parsed.session.maxAgeMs === 'number') {
|
|
286
|
+
out.session.maxAgeMs = parsed.session.maxAgeMs;
|
|
287
|
+
}
|
|
288
|
+
if (typeof parsed.session.perClient === 'boolean')
|
|
289
|
+
out.session.perClient = parsed.session.perClient;
|
|
290
|
+
}
|
|
291
|
+
if (isPlainObject(parsed.queue)) {
|
|
292
|
+
out.queue = {};
|
|
293
|
+
for (const k of ['maxConcurrent', 'maxQueued', 'timeoutMs']) {
|
|
294
|
+
const v = parsed.queue[k];
|
|
295
|
+
if (v === null || (typeof v === 'number' && Number.isFinite(v))) {
|
|
296
|
+
out.queue[k] = v;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const effort = pickStringOrNull('effort');
|
|
301
|
+
if (effort !== undefined)
|
|
302
|
+
out.effort = effort;
|
|
303
|
+
// maxTokens is special — it's a number, the string 'client', or null
|
|
304
|
+
if (parsed.maxTokens === null)
|
|
305
|
+
out.maxTokens = null;
|
|
306
|
+
else if (parsed.maxTokens === 'client')
|
|
307
|
+
out.maxTokens = 'client';
|
|
308
|
+
else {
|
|
309
|
+
const n = pickNumber('maxTokens');
|
|
310
|
+
if (n !== undefined)
|
|
311
|
+
out.maxTokens = n;
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(parsed.passthroughBetas)) {
|
|
314
|
+
out.passthroughBetas = parsed.passthroughBetas
|
|
315
|
+
.filter((x) => typeof x === 'string');
|
|
316
|
+
}
|
|
317
|
+
const sysPrompt = pickStringOrNull('systemPrompt');
|
|
318
|
+
if (sysPrompt !== undefined)
|
|
319
|
+
out.systemPrompt = sysPrompt;
|
|
320
|
+
const logFile = pickStringOrNull('logFile');
|
|
321
|
+
if (logFile !== undefined)
|
|
322
|
+
out.logFile = logFile;
|
|
323
|
+
// Silence unused-warning helper.
|
|
324
|
+
void pickNumberOrNull;
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
|
|
|
282
282
|
*/
|
|
283
283
|
export declare const SUPPORTED_CC_RANGE: {
|
|
284
284
|
readonly min: "1.0.0";
|
|
285
|
-
readonly maxTested: "2.1.
|
|
285
|
+
readonly maxTested: "2.1.143";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|