@askalf/dario 3.38.5 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/analytics.d.ts +32 -3
- package/dist/analytics.js +48 -3
- package/dist/cc-template.js +18 -10
- package/dist/cli.js +111 -25
- package/dist/config-file.d.ts +157 -0
- package/dist/config-file.js +326 -0
- 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/README.md
CHANGED
|
@@ -23,13 +23,35 @@ You're already paying $20, $100, or $200 a month for Claude. Then Cursor wants a
|
|
|
23
23
|
```bash
|
|
24
24
|
npm install -g @askalf/dario
|
|
25
25
|
dario login # uses your existing Claude Code credentials
|
|
26
|
-
dario proxy
|
|
26
|
+
dario proxy # start the server (separate terminal or background)
|
|
27
27
|
export ANTHROPIC_BASE_URL=http://localhost:3456
|
|
28
28
|
export ANTHROPIC_API_KEY=dario
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
That's the whole setup. Every tool that honors those env vars now runs on your subscription.
|
|
32
32
|
|
|
33
|
+
**New in v4:** type `dario` (no args) in another terminal to open the interactive TUI — live request stream, per-model burn-rate, rate-limit utilization, and a config editor that writes to `~/.dario/config.json`. Migrating from v3? See [MIGRATION.md](MIGRATION.md).
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─ dario v4.0.0 ───────────────[ q quit · ? help · Tab next panel ]┐
|
|
37
|
+
│ Status Config ▎Analytics▎ Hits Accounts Backends │
|
|
38
|
+
├──────────────────────────────────────────────────────────────────┤
|
|
39
|
+
│ Analytics — last 60 min │
|
|
40
|
+
│ │
|
|
41
|
+
│ Requests: 247 (4.1/min) │
|
|
42
|
+
│ Tokens in: 142,830 │
|
|
43
|
+
│ Tokens out: 38,200 │
|
|
44
|
+
│ │
|
|
45
|
+
│ Per-model: │
|
|
46
|
+
│ opus-4-7 ████████████████████░ 72% │
|
|
47
|
+
│ sonnet-4-6 █████░░░░░░░░░░░░░░░░ 22% │
|
|
48
|
+
│ │
|
|
49
|
+
│ Rate-limit: │
|
|
50
|
+
│ 5h ████░░░░░░░░░░░░░░░░░░░░░░ 18% │
|
|
51
|
+
│ 7d ██░░░░░░░░░░░░░░░░░░░░░░░░ 8% │
|
|
52
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
33
55
|
---
|
|
34
56
|
|
|
35
57
|
## The money
|
package/dist/analytics.d.ts
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* Token analytics — per-request billing tracking, utilization trends,
|
|
3
3
|
* window exhaustion predictions, cost estimation.
|
|
4
4
|
*
|
|
5
|
-
* In-memory rolling window
|
|
6
|
-
*
|
|
5
|
+
* In-memory rolling window. Exposed via two endpoints on the running
|
|
6
|
+
* proxy:
|
|
7
|
+
*
|
|
8
|
+
* - GET /analytics — rolling summary (`AnalyticsSummary`)
|
|
9
|
+
* - GET /analytics/stream — Server-Sent Events of new `RequestRecord`s
|
|
10
|
+
* as they're appended. The v4 TUI's Hits
|
|
11
|
+
* tab subscribes here for the live request
|
|
12
|
+
* feed; non-TUI clients can `curl -N` it.
|
|
13
|
+
*
|
|
14
|
+
* Pre-v4 the class only emitted data when pool mode was active; v4
|
|
15
|
+
* promotes analytics to always-on so single-account users get the same
|
|
16
|
+
* UX. The EventEmitter mixin below makes the streaming endpoint cheap —
|
|
17
|
+
* each subscriber listens for `'record'` and writes one SSE frame.
|
|
7
18
|
*/
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
8
20
|
export interface RequestRecord {
|
|
9
21
|
timestamp: number;
|
|
10
22
|
account: string;
|
|
@@ -43,11 +55,28 @@ export type BillingBucket = 'subscription' | 'subscription_fallback' | 'extra_us
|
|
|
43
55
|
* billing bucket. Pure function; no state; safe to call from any context.
|
|
44
56
|
*/
|
|
45
57
|
export declare function billingBucketFromClaim(claim: string | null | undefined): BillingBucket;
|
|
46
|
-
export declare class Analytics {
|
|
58
|
+
export declare class Analytics extends EventEmitter {
|
|
47
59
|
private records;
|
|
48
60
|
private maxRecords;
|
|
49
61
|
constructor(maxRecords?: number);
|
|
62
|
+
/**
|
|
63
|
+
* Append a request record to the rolling window and fan it out to
|
|
64
|
+
* any `'record'` listeners (the SSE stream subscribers). Emit happens
|
|
65
|
+
* AFTER the push so a subscriber that re-queries `recent()` from
|
|
66
|
+
* inside its handler sees the new record.
|
|
67
|
+
*
|
|
68
|
+
* The emit is wrapped in try/catch so a misbehaving subscriber can't
|
|
69
|
+
* crash the proxy hot-path; errors land on stderr (visible in
|
|
70
|
+
* --verbose) but don't propagate.
|
|
71
|
+
*/
|
|
50
72
|
record(r: RequestRecord): void;
|
|
73
|
+
/**
|
|
74
|
+
* Return the most recent `n` records (newest last). Used by the SSE
|
|
75
|
+
* endpoint to send a backlog snapshot before the live tail starts,
|
|
76
|
+
* so a freshly-attached TUI sees the recent state instead of an
|
|
77
|
+
* empty list.
|
|
78
|
+
*/
|
|
79
|
+
recent(n?: number): RequestRecord[];
|
|
51
80
|
/** Parse usage from a non-streaming Anthropic response body. */
|
|
52
81
|
static parseUsage(body: Record<string, unknown>): {
|
|
53
82
|
inputTokens: number;
|
package/dist/analytics.js
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* Token analytics — per-request billing tracking, utilization trends,
|
|
3
3
|
* window exhaustion predictions, cost estimation.
|
|
4
4
|
*
|
|
5
|
-
* In-memory rolling window
|
|
6
|
-
*
|
|
5
|
+
* In-memory rolling window. Exposed via two endpoints on the running
|
|
6
|
+
* proxy:
|
|
7
|
+
*
|
|
8
|
+
* - GET /analytics — rolling summary (`AnalyticsSummary`)
|
|
9
|
+
* - GET /analytics/stream — Server-Sent Events of new `RequestRecord`s
|
|
10
|
+
* as they're appended. The v4 TUI's Hits
|
|
11
|
+
* tab subscribes here for the live request
|
|
12
|
+
* feed; non-TUI clients can `curl -N` it.
|
|
13
|
+
*
|
|
14
|
+
* Pre-v4 the class only emitted data when pool mode was active; v4
|
|
15
|
+
* promotes analytics to always-on so single-account users get the same
|
|
16
|
+
* UX. The EventEmitter mixin below makes the streaming endpoint cheap —
|
|
17
|
+
* each subscriber listens for `'record'` and writes one SSE frame.
|
|
7
18
|
*/
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
8
20
|
/**
|
|
9
21
|
* Map the raw `representative-claim` header value to a human-friendly
|
|
10
22
|
* billing bucket. Pure function; no state; safe to call from any context.
|
|
@@ -39,17 +51,50 @@ function estimateCost(record) {
|
|
|
39
51
|
(record.cacheReadTokens * p.cacheRead) +
|
|
40
52
|
(record.cacheCreateTokens * p.cacheCreate)) / 1_000_000;
|
|
41
53
|
}
|
|
42
|
-
export class Analytics {
|
|
54
|
+
export class Analytics extends EventEmitter {
|
|
43
55
|
records = [];
|
|
44
56
|
maxRecords;
|
|
45
57
|
constructor(maxRecords = 10_000) {
|
|
58
|
+
super();
|
|
59
|
+
// High default — the /analytics/stream SSE endpoint creates one
|
|
60
|
+
// listener per active subscriber, and Node warns at 10 by default.
|
|
61
|
+
// 100 is generous for the TUI use case (one process, ~5 tabs)
|
|
62
|
+
// without hiding genuine leaks.
|
|
63
|
+
this.setMaxListeners(100);
|
|
46
64
|
this.maxRecords = maxRecords;
|
|
47
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Append a request record to the rolling window and fan it out to
|
|
68
|
+
* any `'record'` listeners (the SSE stream subscribers). Emit happens
|
|
69
|
+
* AFTER the push so a subscriber that re-queries `recent()` from
|
|
70
|
+
* inside its handler sees the new record.
|
|
71
|
+
*
|
|
72
|
+
* The emit is wrapped in try/catch so a misbehaving subscriber can't
|
|
73
|
+
* crash the proxy hot-path; errors land on stderr (visible in
|
|
74
|
+
* --verbose) but don't propagate.
|
|
75
|
+
*/
|
|
48
76
|
record(r) {
|
|
49
77
|
this.records.push(r);
|
|
50
78
|
if (this.records.length > this.maxRecords) {
|
|
51
79
|
this.records = this.records.slice(-this.maxRecords);
|
|
52
80
|
}
|
|
81
|
+
try {
|
|
82
|
+
this.emit('record', r);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
// Subscriber threw — log + swallow. Not catastrophic; the record
|
|
86
|
+
// itself is already in the rolling window.
|
|
87
|
+
console.error('[dario] analytics subscriber threw:', err.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Return the most recent `n` records (newest last). Used by the SSE
|
|
92
|
+
* endpoint to send a backlog snapshot before the live tail starts,
|
|
93
|
+
* so a freshly-attached TUI sees the recent state instead of an
|
|
94
|
+
* empty list.
|
|
95
|
+
*/
|
|
96
|
+
recent(n = 100) {
|
|
97
|
+
return this.records.slice(-n);
|
|
53
98
|
}
|
|
54
99
|
/** Parse usage from a non-streaming Anthropic response body. */
|
|
55
100
|
static parseUsage(body) {
|
package/dist/cc-template.js
CHANGED
|
@@ -833,16 +833,24 @@ const TOOL_MAP = {
|
|
|
833
833
|
// • hybrid mode → dropped, so the model doesn't see a broken tool;
|
|
834
834
|
// • --preserve-tools → client's real schema flows through untouched
|
|
835
835
|
// (recommended for agents that depend on ask-user flows).
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
836
|
+
// Intentionally unmapped (CC v2.1.142): Anthropic removed TodoWrite /
|
|
837
|
+
// TodoRead from the CC tool catalog in favor of the Task* family
|
|
838
|
+
// (TaskCreate / TaskGet / TaskList / TaskOutput / TaskStop / TaskUpdate).
|
|
839
|
+
// The previous `todo_read`/`todo_write` → `TodoWrite` mappings now point
|
|
840
|
+
// at a destination tool that no longer exists in the bundled or live
|
|
841
|
+
// template, so the schema-contract test correctly fails for them.
|
|
842
|
+
//
|
|
843
|
+
// We drop the mappings rather than remap to Task* because the semantics
|
|
844
|
+
// diverge: TodoWrite replaced an entire flat todo list per call; Task*
|
|
845
|
+
// is single-task-by-ID. A `todo_write` → `TaskCreate` rewrite would
|
|
846
|
+
// silently truncate a list-write to creating only the first item. The
|
|
847
|
+
// unmapped-tool path handles legacy clients honestly:
|
|
848
|
+
// • default mode → round-robin to a fallback CC tool (lossy but the
|
|
849
|
+
// upstream accepts the request);
|
|
850
|
+
// • hybrid mode → dropped, so the model doesn't see a phantom tool;
|
|
851
|
+
// • --preserve-tools → client's real schema flows through untouched
|
|
852
|
+
// (recommended for clients that actually depend on todo semantics).
|
|
853
|
+
//
|
|
846
854
|
// Intentionally unmapped (dario#43): CC has no notebook-read tool, and
|
|
847
855
|
// routing a read to NotebookEdit with empty new_source either fails the
|
|
848
856
|
// schema (`new_source` required) or executes a destructive no-op edit.
|
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
|
+
};
|