@askalf/dario 4.0.1 → 4.1.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 +50 -5
- package/dist/cli.js +128 -1
- package/dist/config-file.d.ts +26 -0
- package/dist/config-file.js +23 -0
- package/dist/notify.d.ts +48 -0
- package/dist/notify.js +120 -0
- package/dist/overage-guard.d.ts +102 -0
- package/dist/overage-guard.js +189 -0
- package/dist/proxy.d.ts +14 -0
- package/dist/proxy.js +106 -1
- package/dist/tui/proxy-client.d.ts +44 -1
- package/dist/tui/proxy-client.js +66 -2
- package/dist/tui/tabs/analytics.js +13 -0
- package/dist/tui/tabs/config.js +35 -0
- package/dist/tui/tabs/hits.d.ts +14 -0
- package/dist/tui/tabs/hits.js +54 -4
- package/dist/tui/tabs/status.d.ts +14 -0
- package/dist/tui/tabs/status.js +109 -3
- package/package.json +1 -1
package/dist/tui/tabs/hits.js
CHANGED
|
@@ -32,13 +32,28 @@ export const HitsTab = {
|
|
|
32
32
|
label: 'Hits',
|
|
33
33
|
hotkey: 'h',
|
|
34
34
|
initialState() {
|
|
35
|
-
return { buffer: [], selectedIdx: -1, subscribed: false, connectionError: null };
|
|
35
|
+
return { buffer: [], selectedIdx: -1, subscribed: false, connectionError: null, halt: null };
|
|
36
36
|
},
|
|
37
37
|
onMount(_state, ctx) {
|
|
38
38
|
// Subscribe to the live stream. Each record is prepended-conceptually
|
|
39
39
|
// (we push to the array and render in reverse, which keeps the
|
|
40
40
|
// buffer's mutation simple — Array.push is O(1) while unshift is O(n)).
|
|
41
|
-
|
|
41
|
+
//
|
|
42
|
+
// The same stream carries named events for overage-halt / -resume
|
|
43
|
+
// (v4.1, dario#288). The SSE event type is the second argument; we
|
|
44
|
+
// route on it.
|
|
45
|
+
const close = ctx.client.subscribeAnalyticsStream((payload, eventType) => {
|
|
46
|
+
if (eventType === 'overage_halt' || eventType === 'overage_warn') {
|
|
47
|
+
const state = payload;
|
|
48
|
+
ctx.setState((s) => ({ ...s, halt: state }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (eventType === 'overage_resume') {
|
|
52
|
+
ctx.setState((s) => ({ ...s, halt: null }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Default ('message') = RequestRecord
|
|
56
|
+
const record = payload;
|
|
42
57
|
ctx.setState((s) => {
|
|
43
58
|
const next = {
|
|
44
59
|
...s,
|
|
@@ -123,6 +138,16 @@ export const HitsTab = {
|
|
|
123
138
|
const colIn = 8, colOut = 7, colLat = 7, colStatus = 5;
|
|
124
139
|
lines.push(' ' + brand('Hits') +
|
|
125
140
|
dim(` ${state.buffer.length} buffered · ${state.subscribed ? fg('green', 'live') : fg('yellow', 'disconnected')}`));
|
|
141
|
+
// ── Overage-halt banner (v4.1, dario#288) ──────────────────
|
|
142
|
+
// Pinned at the top so it's always visible while scrolling the buffer.
|
|
143
|
+
if (state.halt) {
|
|
144
|
+
const since = formatTimestamp(state.halt.since);
|
|
145
|
+
const cooldown = formatRemaining(state.halt.cooldownUntil - Date.now());
|
|
146
|
+
const line1 = ` ${fg('red', '⚠ HALTED')} overage detected at ${since} on ${state.halt.request.model} (account=${state.halt.request.account})`;
|
|
147
|
+
const line2 = ` ${dim('→ New /v1/messages requests return 503 until')} ${fg('cyan', 'R')} ${dim('here, or')} ${fg('cyan', 'dario resume')}${dim(' from any shell. Auto-resume in')} ${cooldown}${dim('.')}`;
|
|
148
|
+
lines.push(line1);
|
|
149
|
+
lines.push(line2);
|
|
150
|
+
}
|
|
126
151
|
lines.push('');
|
|
127
152
|
// Header row (aligned with data rows)
|
|
128
153
|
lines.push(' ' + dim(pad('time', colTime) +
|
|
@@ -133,7 +158,10 @@ export const HitsTab = {
|
|
|
133
158
|
pad('st', colStatus)));
|
|
134
159
|
for (let i = startIdx; i < endIdx; i++) {
|
|
135
160
|
const r = newestFirst[i];
|
|
136
|
-
const
|
|
161
|
+
const isOverage = r.claim === 'overage';
|
|
162
|
+
const marker = i === state.selectedIdx ? fg('cyan', '▎')
|
|
163
|
+
: isOverage ? fg('red', '!')
|
|
164
|
+
: ' ';
|
|
137
165
|
const row = marker + ' ' +
|
|
138
166
|
pad(formatTime(r.timestamp), colTime) +
|
|
139
167
|
pad(shortenModel(r.model), colModel) +
|
|
@@ -141,7 +169,16 @@ export const HitsTab = {
|
|
|
141
169
|
pad(formatTokens(r.outputTokens), colOut) +
|
|
142
170
|
pad(formatLatency(r.latencyMs), colLat) +
|
|
143
171
|
pad(formatStatus(r.status), colStatus);
|
|
144
|
-
|
|
172
|
+
// Overage rows render in red even when unselected; selection still
|
|
173
|
+
// wins via the inverse() wrapper so the user can drill into one.
|
|
174
|
+
let styled;
|
|
175
|
+
if (i === state.selectedIdx)
|
|
176
|
+
styled = inverse(truncate(row, w - 2));
|
|
177
|
+
else if (isOverage)
|
|
178
|
+
styled = fg('red', truncate(row, w - 2));
|
|
179
|
+
else
|
|
180
|
+
styled = truncate(row, w - 2);
|
|
181
|
+
lines.push(styled);
|
|
145
182
|
}
|
|
146
183
|
// Scroll hint
|
|
147
184
|
if (newestFirst.length > listRows) {
|
|
@@ -221,3 +258,16 @@ function tokenBreakdown(r) {
|
|
|
221
258
|
parts.push(`thinking ${r.thinkingTokens}`);
|
|
222
259
|
return parts.join(' / ');
|
|
223
260
|
}
|
|
261
|
+
function formatTimestamp(ts) {
|
|
262
|
+
const d = new Date(ts);
|
|
263
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
264
|
+
}
|
|
265
|
+
function formatRemaining(ms) {
|
|
266
|
+
if (ms <= 0)
|
|
267
|
+
return fg('yellow', 'now');
|
|
268
|
+
const s = Math.floor(ms / 1000);
|
|
269
|
+
if (s < 60)
|
|
270
|
+
return `${s}s`;
|
|
271
|
+
const m = Math.floor(s / 60);
|
|
272
|
+
return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
273
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* └─────────────────────────────────────────────────┘
|
|
21
21
|
*/
|
|
22
22
|
import type { Tab, TabContext } from '../tab.js';
|
|
23
|
+
import type { OverageGuardStatus } from '../proxy-client.js';
|
|
23
24
|
export interface StatusState {
|
|
24
25
|
loading: boolean;
|
|
25
26
|
/** Proxy /health response, or null if unreachable. */
|
|
@@ -31,6 +32,12 @@ export interface StatusState {
|
|
|
31
32
|
} | null;
|
|
32
33
|
/** Config-file load source: file | missing | invalid. */
|
|
33
34
|
configSource: 'file' | 'missing' | 'invalid' | null;
|
|
35
|
+
/** Overage-guard state from /admin/resume — null if unreachable. */
|
|
36
|
+
overageGuard: OverageGuardStatus | null;
|
|
37
|
+
/** Transient: did we just attempt a manual resume? */
|
|
38
|
+
resumePending: boolean;
|
|
39
|
+
resumeMessage: string | null;
|
|
40
|
+
resumeKind: 'success' | 'info' | 'error' | null;
|
|
34
41
|
/** Last refresh timestamp (ms). */
|
|
35
42
|
lastRefreshAt: number;
|
|
36
43
|
/** Error from the last refresh attempt, if any. */
|
|
@@ -43,3 +50,10 @@ export declare const StatusTab: Tab<StatusState>;
|
|
|
43
50
|
* 'r' without re-running the full onMount flow.
|
|
44
51
|
*/
|
|
45
52
|
export declare function refreshStatus(ctx: TabContext): Promise<StatusState>;
|
|
53
|
+
/**
|
|
54
|
+
* Fire the manual-resume POST and update state. Called by TuiApp when the
|
|
55
|
+
* Status tab returns a state with resumePending=true (the `R` key path).
|
|
56
|
+
* Lives here next to refreshStatus so all status-tab-side-effects sit
|
|
57
|
+
* together.
|
|
58
|
+
*/
|
|
59
|
+
export declare function performResume(ctx: TabContext<StatusState>): Promise<Partial<StatusState>>;
|
package/dist/tui/tabs/status.js
CHANGED
|
@@ -30,6 +30,10 @@ export const StatusTab = {
|
|
|
30
30
|
loading: true,
|
|
31
31
|
health: null,
|
|
32
32
|
configSource: null,
|
|
33
|
+
overageGuard: null,
|
|
34
|
+
resumePending: false,
|
|
35
|
+
resumeMessage: null,
|
|
36
|
+
resumeKind: null,
|
|
33
37
|
lastRefreshAt: 0,
|
|
34
38
|
error: null,
|
|
35
39
|
};
|
|
@@ -37,12 +41,40 @@ export const StatusTab = {
|
|
|
37
41
|
async onMount(_state, ctx) {
|
|
38
42
|
return refreshStatus(ctx);
|
|
39
43
|
},
|
|
44
|
+
onTick(state, ctx) {
|
|
45
|
+
// Drive the async side-effects that onKey can't fire directly.
|
|
46
|
+
if (state.resumePending) {
|
|
47
|
+
void performResume(ctx).then((delta) => ctx.setState(delta));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Poll the overage-guard state every 2s so the halt countdown stays
|
|
51
|
+
// current without the user having to press `r`. Cheap GET; the proxy
|
|
52
|
+
// is on loopback. Skip when the rest of the status is still loading.
|
|
53
|
+
if (!state.loading && state.overageGuard !== null && state.overageGuard.halted) {
|
|
54
|
+
// While halted, refresh every 2s so the countdown updates.
|
|
55
|
+
const since = Date.now() - state.lastRefreshAt;
|
|
56
|
+
if (since >= 2000) {
|
|
57
|
+
void ctx.client.getOverageGuard().then((g) => {
|
|
58
|
+
if (g)
|
|
59
|
+
ctx.setState({ overageGuard: g, lastRefreshAt: Date.now() });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
40
64
|
onKey(state, key) {
|
|
41
|
-
// `r` triggers a manual refresh
|
|
42
|
-
// onMount again. The parent watches for a sentinel state.
|
|
65
|
+
// `r` triggers a manual refresh — signal the parent to call onMount again.
|
|
43
66
|
if (key.name === 'printable' && key.ch === 'r' && !key.ctrl) {
|
|
44
67
|
return { ...state, loading: true };
|
|
45
68
|
}
|
|
69
|
+
// `R` (shift-r) resumes the overage-guard halt state when one is active.
|
|
70
|
+
// Returning a sentinel state with resumePending=true signals the parent
|
|
71
|
+
// to fire the async resume() call.
|
|
72
|
+
if (key.name === 'printable' && key.ch === 'R' && !key.ctrl) {
|
|
73
|
+
if (state.overageGuard?.halted) {
|
|
74
|
+
return { ...state, resumePending: true, resumeMessage: 'Resuming…', resumeKind: 'info' };
|
|
75
|
+
}
|
|
76
|
+
return { ...state, resumeMessage: 'Nothing to resume — proxy is not halted.', resumeKind: 'info' };
|
|
77
|
+
}
|
|
46
78
|
return undefined;
|
|
47
79
|
},
|
|
48
80
|
render(state, dim_) {
|
|
@@ -75,12 +107,52 @@ export const StatusTab = {
|
|
|
75
107
|
: dim('not loaded');
|
|
76
108
|
lines.push(' ' + renderKvRow('Source', sourceLabel, w - 4));
|
|
77
109
|
lines.push('');
|
|
110
|
+
// ── Overage-guard section (v4.1, dario#288) ────────────────
|
|
111
|
+
if (state.overageGuard) {
|
|
112
|
+
lines.push(' ' + brand('Overage-guard'));
|
|
113
|
+
if (state.overageGuard.halted && state.overageGuard.state) {
|
|
114
|
+
const s = state.overageGuard.state;
|
|
115
|
+
const remainingMs = Math.max(0, s.cooldownUntil - Date.now());
|
|
116
|
+
const remaining = formatDuration(remainingMs);
|
|
117
|
+
// Red banner header — this is the loud surface when halted
|
|
118
|
+
lines.push(' ' + fg('red', '⚠ HALTED') + ' ' + dim(`overage detected ${formatAgo(s.since)} ago`));
|
|
119
|
+
lines.push(' ' + renderKvRow('Request', `${s.request.model} ${dim('account=' + s.request.account)}`, w - 4));
|
|
120
|
+
lines.push(' ' + renderKvRow('Cause', `representative-claim = ${fg('red', s.request.claim)}`, w - 4));
|
|
121
|
+
lines.push(' ' + renderKvRow('Auto-resume in', remaining === '0s' ? fg('yellow', 'now (cooldown elapsed)') : remaining, w - 4));
|
|
122
|
+
lines.push(' ' + renderKvRow('Manual resume', `press ${fg('cyan', 'R')} here, or ${fg('cyan', 'dario resume')} from any shell`, w - 4));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push(' ' + renderKvRow('State', fg('green', 'normal'), w - 4));
|
|
126
|
+
const cfg = state.overageGuard.config;
|
|
127
|
+
lines.push(' ' + renderKvRow('Mode', `${cfg.enabled ? fg('green', 'enabled') : fg('yellow', 'disabled')} ${dim(`behavior=${cfg.behavior} cooldown=${formatDuration(cfg.cooldownMs)}`)}`, w - 4));
|
|
128
|
+
}
|
|
129
|
+
if (state.resumeMessage) {
|
|
130
|
+
const c = state.resumeKind === 'error' ? 'red' : state.resumeKind === 'success' ? 'green' : 'cyan';
|
|
131
|
+
lines.push(' ' + fg(c, state.resumeMessage));
|
|
132
|
+
}
|
|
133
|
+
lines.push('');
|
|
134
|
+
}
|
|
78
135
|
// ── Footer hint ────────────────────────────────────────────
|
|
79
136
|
lines.push('');
|
|
80
|
-
|
|
137
|
+
const resumeHint = state.overageGuard?.halted ? ` · ${fg('cyan', 'R')} resume` : '';
|
|
138
|
+
lines.push(' ' + dim(`Last refresh: ${formatAgo(state.lastRefreshAt)}. ${fg('cyan', 'r')} refresh${resumeHint}.`));
|
|
81
139
|
return lines.join('\n');
|
|
82
140
|
},
|
|
83
141
|
};
|
|
142
|
+
function formatDuration(ms) {
|
|
143
|
+
if (ms <= 0)
|
|
144
|
+
return '0s';
|
|
145
|
+
const s = Math.floor(ms / 1000);
|
|
146
|
+
if (s < 60)
|
|
147
|
+
return `${s}s`;
|
|
148
|
+
const m = Math.floor(s / 60);
|
|
149
|
+
const rs = s % 60;
|
|
150
|
+
if (m < 60)
|
|
151
|
+
return rs > 0 ? `${m}m ${rs}s` : `${m}m`;
|
|
152
|
+
const h = Math.floor(m / 60);
|
|
153
|
+
const rm = m % 60;
|
|
154
|
+
return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
|
|
155
|
+
}
|
|
84
156
|
/**
|
|
85
157
|
* Refresh the Status tab's data — probe /health, load config file
|
|
86
158
|
* metadata. Exported separately so the parent can re-invoke on key
|
|
@@ -98,14 +170,48 @@ export async function refreshStatus(ctx) {
|
|
|
98
170
|
catch (e) {
|
|
99
171
|
error = e.message;
|
|
100
172
|
}
|
|
173
|
+
// Overage-guard state — best-effort; never throws (proxy-client wraps the
|
|
174
|
+
// GET in try/catch and returns null). Surface as 'unknown' when null.
|
|
175
|
+
const overageGuard = await ctx.client.getOverageGuard();
|
|
101
176
|
return {
|
|
102
177
|
loading: false,
|
|
103
178
|
health,
|
|
104
179
|
configSource: fileResult.source,
|
|
180
|
+
overageGuard,
|
|
181
|
+
resumePending: false,
|
|
182
|
+
resumeMessage: null,
|
|
183
|
+
resumeKind: null,
|
|
105
184
|
lastRefreshAt: Date.now(),
|
|
106
185
|
error,
|
|
107
186
|
};
|
|
108
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Fire the manual-resume POST and update state. Called by TuiApp when the
|
|
190
|
+
* Status tab returns a state with resumePending=true (the `R` key path).
|
|
191
|
+
* Lives here next to refreshStatus so all status-tab-side-effects sit
|
|
192
|
+
* together.
|
|
193
|
+
*/
|
|
194
|
+
export async function performResume(ctx) {
|
|
195
|
+
try {
|
|
196
|
+
const result = await ctx.client.resume();
|
|
197
|
+
const refreshed = await ctx.client.getOverageGuard();
|
|
198
|
+
return {
|
|
199
|
+
overageGuard: refreshed,
|
|
200
|
+
resumePending: false,
|
|
201
|
+
resumeMessage: result.wasHalted
|
|
202
|
+
? `Resumed at ${result.resumedAt}.`
|
|
203
|
+
: 'Already running normally — no-op.',
|
|
204
|
+
resumeKind: 'success',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
return {
|
|
209
|
+
resumePending: false,
|
|
210
|
+
resumeMessage: `Resume failed: ${e.message}`,
|
|
211
|
+
resumeKind: 'error',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
109
215
|
function formatOauth(label, expiresIn) {
|
|
110
216
|
if (label === 'healthy') {
|
|
111
217
|
return fg('green', expiresIn ? `healthy (expires in ${expiresIn})` : 'healthy');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|