@timo972/cc-router 0.7.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/Dockerfile +42 -0
  3. package/LICENSE +21 -0
  4. package/README.md +716 -0
  5. package/accounts.example.json +25 -0
  6. package/dist/cli/cmd-accounts.js +248 -0
  7. package/dist/cli/cmd-client.js +612 -0
  8. package/dist/cli/cmd-configure.js +145 -0
  9. package/dist/cli/cmd-docker.js +140 -0
  10. package/dist/cli/cmd-logs.js +85 -0
  11. package/dist/cli/cmd-models.js +125 -0
  12. package/dist/cli/cmd-service.js +193 -0
  13. package/dist/cli/cmd-setup.js +501 -0
  14. package/dist/cli/cmd-start.js +318 -0
  15. package/dist/cli/cmd-status.js +177 -0
  16. package/dist/cli/cmd-stop.js +100 -0
  17. package/dist/cli/cmd-telemetry.js +58 -0
  18. package/dist/cli/cmd-update.js +37 -0
  19. package/dist/cli/index.js +59 -0
  20. package/dist/config/manager.js +262 -0
  21. package/dist/config/paths.js +21 -0
  22. package/dist/config/telemetry.js +64 -0
  23. package/dist/daemon/launcher.js +163 -0
  24. package/dist/daemon/pid.js +98 -0
  25. package/dist/daemon/service.js +260 -0
  26. package/dist/interceptor/mitmproxy-manager.js +616 -0
  27. package/dist/protocol/anthropic-to-openai.js +51 -0
  28. package/dist/protocol/anthropic-types.js +1 -0
  29. package/dist/protocol/model-ref.js +36 -0
  30. package/dist/protocol/model-routing-config.js +30 -0
  31. package/dist/protocol/openai-response-to-anthropic.js +20 -0
  32. package/dist/protocol/openai-responses-types.js +1 -0
  33. package/dist/protocol/openai-stream-to-anthropic.js +75 -0
  34. package/dist/protocol/openai-to-anthropic.js +61 -0
  35. package/dist/protocol/sse.js +17 -0
  36. package/dist/providers/model-discovery.js +71 -0
  37. package/dist/providers/openai/account-pool.js +11 -0
  38. package/dist/providers/openai/account-record.js +33 -0
  39. package/dist/providers/openai/codex-transport.js +36 -0
  40. package/dist/providers/openai/device-oauth.js +116 -0
  41. package/dist/providers/openai/token-refresher.js +56 -0
  42. package/dist/providers/route-selector.js +8 -0
  43. package/dist/providers/types.js +1 -0
  44. package/dist/proxy/account-deletion.js +44 -0
  45. package/dist/proxy/anthropic-proxy.js +26 -0
  46. package/dist/proxy/anthropic-routing.js +90 -0
  47. package/dist/proxy/lease-lifecycle.js +68 -0
  48. package/dist/proxy/logger.js +39 -0
  49. package/dist/proxy/messages-cross-route.js +179 -0
  50. package/dist/proxy/models-server.js +150 -0
  51. package/dist/proxy/provider-routing.js +14 -0
  52. package/dist/proxy/responses-server.js +91 -0
  53. package/dist/proxy/server.js +875 -0
  54. package/dist/proxy/session-router.js +171 -0
  55. package/dist/proxy/stats.js +25 -0
  56. package/dist/proxy/stream-lifecycle.js +83 -0
  57. package/dist/proxy/token-pool.js +407 -0
  58. package/dist/proxy/token-refresher.js +209 -0
  59. package/dist/proxy/types.js +29 -0
  60. package/dist/ui/Dashboard.js +640 -0
  61. package/dist/ui/accountsApi.js +48 -0
  62. package/dist/ui/modelsApi.js +47 -0
  63. package/dist/utils/claude-config.js +185 -0
  64. package/dist/utils/codex-config.js +62 -0
  65. package/dist/utils/network.js +16 -0
  66. package/dist/utils/platform.js +13 -0
  67. package/dist/utils/self-update.js +239 -0
  68. package/dist/utils/telemetry.js +88 -0
  69. package/dist/utils/token-extractor.js +95 -0
  70. package/dist/utils/token-validator.js +26 -0
  71. package/docker-compose.yml +63 -0
  72. package/litellm-config.yaml +44 -0
  73. package/package.json +69 -0
  74. package/src/interceptor/addon.py +78 -0
@@ -0,0 +1,640 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useState, useEffect, useCallback, useRef } from "react";
3
+ import { Box, Text, useInput, useApp } from "ink";
4
+ import { createAccountsApi } from "./accountsApi.js";
5
+ import { createModelsApi } from "./modelsApi.js";
6
+ const POLL_INTERVAL_MS = 2_000;
7
+ const LOG_VISIBLE = 20;
8
+ const MODEL_VISIBLE_ROWS = 16;
9
+ const EMPTY_RL = {
10
+ status: "unknown", fiveHourUtil: 0, fiveHourReset: 0,
11
+ sevenDayUtil: 0, sevenDayReset: 0, claim: "", plan: "",
12
+ requestsLimit: 0, lastUpdated: 0,
13
+ };
14
+ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
15
+ const { exit } = useApp();
16
+ const [data, setData] = useState(null);
17
+ const [connectError, setConnectError] = useState(null);
18
+ const [lastUpdate, setLastUpdate] = useState(0);
19
+ const [retryCount, setRetryCount] = useState(0);
20
+ const resolvedBase = baseUrl
21
+ ? baseUrl.replace(/\/+$/, "")
22
+ : `http://localhost:${port}`;
23
+ const api = React.useMemo(() => createAccountsApi(resolvedBase, authToken), [resolvedBase, authToken]);
24
+ const modelsApi = React.useMemo(() => createModelsApi(resolvedBase, authToken), [resolvedBase, authToken]);
25
+ // Only q to quit when no live data yet (no mode to cancel)
26
+ useInput((input, key) => {
27
+ if (!data && (input === "q" || key.escape))
28
+ exit();
29
+ });
30
+ useEffect(() => {
31
+ let cancelled = false;
32
+ const healthUrl = `${resolvedBase}/cc-router/health`;
33
+ const headers = authToken
34
+ ? { authorization: `Bearer ${authToken}` }
35
+ : {};
36
+ const poll = async () => {
37
+ try {
38
+ const res = await fetch(healthUrl, {
39
+ headers,
40
+ signal: AbortSignal.timeout(1_500),
41
+ });
42
+ if (cancelled)
43
+ return;
44
+ if (res.ok) {
45
+ setData(await res.json());
46
+ setConnectError(null);
47
+ setLastUpdate(Date.now());
48
+ setRetryCount(0);
49
+ }
50
+ else {
51
+ setConnectError(`Proxy returned HTTP ${res.status}`);
52
+ }
53
+ }
54
+ catch {
55
+ if (cancelled)
56
+ return;
57
+ setConnectError(`Cannot connect to ${resolvedBase}`);
58
+ setRetryCount(n => n + 1);
59
+ }
60
+ };
61
+ poll();
62
+ const timer = setInterval(poll, POLL_INTERVAL_MS);
63
+ return () => { cancelled = true; clearInterval(timer); };
64
+ }, [resolvedBase, authToken]);
65
+ if (connectError) {
66
+ return _jsx(ErrorScreen, { error: connectError, port: port, retries: retryCount });
67
+ }
68
+ if (!data) {
69
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u280B Connecting to ", resolvedBase, "..."] }) }));
70
+ }
71
+ return (_jsx(LiveDashboard, { data: data, port: port, baseUrl: resolvedBase, lastUpdate: lastUpdate, api: api, modelsApi: modelsApi, onIntent: onIntent }));
72
+ }
73
+ // ─── Error screen ─────────────────────────────────────────────────────────────
74
+ function ErrorScreen({ error, port, retries }) {
75
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, marginX: 2, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Is the proxy running? Start it with:" }), _jsx(Text, { color: "cyan", children: " cc-router start" })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", children: ["Retrying every ", POLL_INTERVAL_MS / 1000, "s"] }), retries > 0 && _jsxs(Text, { color: "gray", children: [" (attempt ", retries, ")"] }), _jsx(Text, { color: "gray", children: " \u00B7 [q] quit" })] })] }));
76
+ }
77
+ // ─── Live dashboard ───────────────────────────────────────────────────────────
78
+ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onIntent, }) {
79
+ const { exit } = useApp();
80
+ const healthyCount = data.accounts.filter(a => a.healthy).length;
81
+ const updatedAgo = Math.round((Date.now() - lastUpdate) / 1000);
82
+ const logs = data.recentLogs;
83
+ // ── Focus / mode ──────────────────────────────────────────────────────────
84
+ const [focus, setFocus] = useState("logs");
85
+ const [mode, setMode] = useState("view");
86
+ // Selected log by timestamp (existing)
87
+ const [selectedTs, setSelectedTs] = useState(null);
88
+ const selectedLogIndex = selectedTs !== null
89
+ ? Math.max(0, logs.findIndex(l => l.ts === selectedTs))
90
+ : 0;
91
+ // Selected account by id
92
+ const [selectedAccountId, setSelectedAccountId] = useState(null);
93
+ const selectedAccountIndex = selectedAccountId !== null
94
+ ? Math.max(0, data.accounts.findIndex(a => a.id === selectedAccountId))
95
+ : 0;
96
+ const selectedAccount = data.accounts[selectedAccountIndex] ?? null;
97
+ const selectedAccountIsAnthropic = selectedAccount?.provider !== "openai_subscription";
98
+ const [modelsStatus, setModelsStatus] = useState(null);
99
+ const [selectedModelId, setSelectedModelId] = useState(null);
100
+ const modelRows = modelsStatus?.models ?? [];
101
+ const selectedModelIndex = selectedModelId !== null
102
+ ? Math.max(0, modelRows.findIndex(m => m.id === selectedModelId))
103
+ : 0;
104
+ const selectedModel = modelRows[selectedModelIndex] ?? null;
105
+ // Inline text input state (for w / s keys)
106
+ const [editBuffer, setEditBuffer] = useState("");
107
+ // Transient banner (error or success, cleared after 4s).
108
+ // The timer handle is stored in a ref so new banners cancel the previous
109
+ // timeout and component unmount also clears it — otherwise a deferred
110
+ // setBanner can fire on an unmounted component after `n` exits Ink.
111
+ const [banner, setBanner] = useState(null);
112
+ const bannerTimerRef = useRef(null);
113
+ const showBanner = useCallback((text, color) => {
114
+ if (bannerTimerRef.current)
115
+ clearTimeout(bannerTimerRef.current);
116
+ setBanner({ text, color });
117
+ bannerTimerRef.current = setTimeout(() => {
118
+ setBanner(null);
119
+ bannerTimerRef.current = null;
120
+ }, 4_000);
121
+ }, []);
122
+ useEffect(() => () => {
123
+ if (bannerTimerRef.current)
124
+ clearTimeout(bannerTimerRef.current);
125
+ }, []);
126
+ // Normalize any thrown value to a displayable string — rejections from
127
+ // fetch/AbortSignal can be DOMException without .message, strings, or
128
+ // even undefined.
129
+ const errMsg = (err) => {
130
+ if (err instanceof Error && err.message)
131
+ return err.message;
132
+ const s = String(err ?? "");
133
+ return s || "unknown error";
134
+ };
135
+ // ── Async helpers (fire-and-forget with error → banner) ──────────────────
136
+ const doToggleEnabled = useCallback(async () => {
137
+ if (!selectedAccount)
138
+ return;
139
+ if (selectedAccount.provider === "openai_subscription") {
140
+ showBanner("OpenAI accounts are managed from the CLI", "yellow");
141
+ return;
142
+ }
143
+ const newValue = !(selectedAccount.enabled !== false);
144
+ try {
145
+ await api.patch(selectedAccount.id, { enabled: newValue });
146
+ showBanner(`${selectedAccount.id} → ${newValue ? "enabled" : "disabled"}`, newValue ? "green" : "yellow");
147
+ }
148
+ catch (err) {
149
+ showBanner(`Error: ${errMsg(err)}`, "red");
150
+ }
151
+ }, [selectedAccount, api, showBanner]);
152
+ const doToggleProvider = useCallback(async (provider) => {
153
+ const providerStatus = provider === "anthropic_subscription"
154
+ ? data.operational?.providers.anthropic
155
+ : data.operational?.providers.openai;
156
+ const label = provider === "anthropic_subscription" ? "Claude" : "OpenAI";
157
+ if (!providerStatus?.configured || providerStatus.accounts === 0) {
158
+ showBanner(`${label} accounts are not configured`, "yellow");
159
+ return;
160
+ }
161
+ const enabled = providerStatus.enabled < providerStatus.accounts;
162
+ try {
163
+ await api.setProviderEnabled(provider, enabled);
164
+ showBanner(`${label} accounts → ${enabled ? "enabled" : "disabled"}`, enabled ? "green" : "yellow");
165
+ }
166
+ catch (err) {
167
+ showBanner(`Error: ${errMsg(err)}`, "red");
168
+ }
169
+ }, [api, data.operational, showBanner]);
170
+ const doSetLimit = useCallback(async (field, value) => {
171
+ if (!selectedAccount)
172
+ return;
173
+ if (selectedAccount.provider === "openai_subscription") {
174
+ showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
175
+ return;
176
+ }
177
+ try {
178
+ await api.patch(selectedAccount.id, { [field]: value });
179
+ const label = field === "sessionLimitPercent" ? "5h cap" : "7d cap";
180
+ showBanner(`${selectedAccount.id} → ${label} = ${value}%`, "green");
181
+ }
182
+ catch (err) {
183
+ showBanner(`Error: ${errMsg(err)}`, "red");
184
+ }
185
+ }, [selectedAccount, api, showBanner]);
186
+ const doDelete = useCallback(async () => {
187
+ if (!selectedAccount)
188
+ return;
189
+ if (selectedAccount.provider === "openai_subscription") {
190
+ showBanner("Use cc-router accounts remove for OpenAI accounts", "yellow");
191
+ return;
192
+ }
193
+ try {
194
+ await api.remove(selectedAccount.id);
195
+ showBanner(`Removed ${selectedAccount.id}`, "yellow");
196
+ setSelectedAccountId(null);
197
+ }
198
+ catch (err) {
199
+ showBanner(`Error: ${errMsg(err)}`, "red");
200
+ }
201
+ }, [selectedAccount, api, showBanner]);
202
+ const doLoadModels = useCallback(async () => {
203
+ try {
204
+ const status = await modelsApi.list();
205
+ setModelsStatus(status);
206
+ setSelectedModelId(status.models[0]?.id ?? null);
207
+ setFocus("models");
208
+ showBanner(`Loaded ${status.models.length} models`, "green");
209
+ }
210
+ catch (err) {
211
+ showBanner(`Models error: ${errMsg(err)}`, "red");
212
+ }
213
+ }, [modelsApi, showBanner]);
214
+ const doSetSelectedModel = useCallback(async (provider) => {
215
+ if (!selectedModel)
216
+ return;
217
+ if (provider === "claude" && !selectedModel.id.startsWith("anthropic/")) {
218
+ showBanner("Select an anthropic/* model for Claude", "yellow");
219
+ return;
220
+ }
221
+ if (provider === "openai" && !selectedModel.id.startsWith("openai/")) {
222
+ showBanner("Select an openai/* model for OpenAI", "yellow");
223
+ return;
224
+ }
225
+ try {
226
+ const status = await modelsApi.setDefaults(provider === "claude"
227
+ ? { claudeModel: selectedModel.id }
228
+ : { openAIModel: selectedModel.id });
229
+ setModelsStatus(previous => ({
230
+ routing: status.routing,
231
+ models: status.models.length > 0 ? status.models : previous?.models ?? [],
232
+ }));
233
+ showBanner(`${provider === "claude" ? "Claude" : "OpenAI"} default → ${selectedModel.id}`, "green");
234
+ }
235
+ catch (err) {
236
+ showBanner(`Models error: ${errMsg(err)}`, "red");
237
+ }
238
+ }, [modelsApi, selectedModel, showBanner]);
239
+ // ── Keyboard handler ──────────────────────────────────────────────────────
240
+ useInput((input, key) => {
241
+ // ── Text editing mode (w / s) ───────────────────────────────────────
242
+ if (mode === "editSession" || mode === "editWeekly") {
243
+ if (key.escape) {
244
+ setMode("view");
245
+ setEditBuffer("");
246
+ return;
247
+ }
248
+ if (key.return) {
249
+ const parsed = parseInt(editBuffer, 10);
250
+ if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 100) {
251
+ const field = mode === "editSession" ? "sessionLimitPercent" : "weeklyLimitPercent";
252
+ void doSetLimit(field, parsed);
253
+ }
254
+ else {
255
+ showBanner("Invalid: enter a number 0–100", "red");
256
+ }
257
+ setMode("view");
258
+ setEditBuffer("");
259
+ return;
260
+ }
261
+ if (key.backspace || key.delete) {
262
+ setEditBuffer(b => b.slice(0, -1));
263
+ return;
264
+ }
265
+ if (/^[0-9]$/.test(input) && editBuffer.length < 3) {
266
+ setEditBuffer(b => b + input);
267
+ }
268
+ return;
269
+ }
270
+ // ── Confirm delete (y/n) ────────────────────────────────────────────
271
+ if (mode === "confirmDelete") {
272
+ if (input === "y" || input === "Y") {
273
+ void doDelete();
274
+ setMode("view");
275
+ }
276
+ else {
277
+ setMode("view");
278
+ showBanner("Delete cancelled", "gray");
279
+ }
280
+ return;
281
+ }
282
+ // ── Normal view mode ────────────────────────────────────────────────
283
+ // Always call exit() so Ink fully unmounts and releases stdin.
284
+ // The outer dashboardLoop reads `pendingIntent` after waitUntilExit().
285
+ if (input === "q") {
286
+ onIntent?.("quit");
287
+ exit();
288
+ return;
289
+ }
290
+ if (key.escape) {
291
+ if (focus === "accounts" || focus === "models") {
292
+ setFocus("logs");
293
+ return;
294
+ }
295
+ onIntent?.("quit");
296
+ exit();
297
+ return;
298
+ }
299
+ if (key.tab) {
300
+ setFocus(f => f === "logs" ? "accounts" : f === "accounts" ? "models" : "logs");
301
+ return;
302
+ }
303
+ // Navigation: ↑↓ move within the focused panel
304
+ if (focus === "logs") {
305
+ if (key.upArrow) {
306
+ const next = Math.max(0, selectedLogIndex - 1);
307
+ setSelectedTs(logs[next]?.ts ?? null);
308
+ }
309
+ if (key.downArrow) {
310
+ const next = Math.min(logs.length - 1, selectedLogIndex + 1);
311
+ setSelectedTs(logs[next]?.ts ?? null);
312
+ }
313
+ }
314
+ if (focus === "accounts") {
315
+ if (key.upArrow) {
316
+ const next = Math.max(0, selectedAccountIndex - 1);
317
+ setSelectedAccountId(data.accounts[next]?.id ?? null);
318
+ }
319
+ if (key.downArrow) {
320
+ const next = Math.min(data.accounts.length - 1, selectedAccountIndex + 1);
321
+ setSelectedAccountId(data.accounts[next]?.id ?? null);
322
+ }
323
+ // Account actions (only when focus = accounts)
324
+ if (input === "e") {
325
+ void doToggleEnabled();
326
+ return;
327
+ }
328
+ if (input === "a") {
329
+ void doToggleProvider("anthropic_subscription");
330
+ return;
331
+ }
332
+ if (input === "o") {
333
+ void doToggleProvider("openai_subscription");
334
+ return;
335
+ }
336
+ if (input === "w") {
337
+ if (!selectedAccountIsAnthropic) {
338
+ showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
339
+ return;
340
+ }
341
+ setMode("editWeekly");
342
+ setEditBuffer("");
343
+ return;
344
+ }
345
+ if (input === "s") {
346
+ if (!selectedAccountIsAnthropic) {
347
+ showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
348
+ return;
349
+ }
350
+ setMode("editSession");
351
+ setEditBuffer("");
352
+ return;
353
+ }
354
+ if (input === "d") {
355
+ if (!selectedAccountIsAnthropic) {
356
+ showBanner("Use cc-router accounts remove for OpenAI accounts", "yellow");
357
+ return;
358
+ }
359
+ setMode("confirmDelete");
360
+ return;
361
+ }
362
+ }
363
+ if (focus === "models") {
364
+ if (key.upArrow) {
365
+ const next = Math.max(0, selectedModelIndex - 1);
366
+ setSelectedModelId(modelRows[next]?.id ?? null);
367
+ }
368
+ if (key.downArrow) {
369
+ const next = Math.min(modelRows.length - 1, selectedModelIndex + 1);
370
+ setSelectedModelId(modelRows[next]?.id ?? null);
371
+ }
372
+ if (input === "r") {
373
+ void doLoadModels();
374
+ return;
375
+ }
376
+ if (input === "c") {
377
+ void doSetSelectedModel("claude");
378
+ return;
379
+ }
380
+ if (input === "o") {
381
+ void doSetSelectedModel("openai");
382
+ return;
383
+ }
384
+ }
385
+ if (input === "m") {
386
+ void doLoadModels();
387
+ return;
388
+ }
389
+ // n = add account — works regardless of focus.
390
+ // Requires an onIntent handler because the outer loop runs the OAuth
391
+ // flow after Ink unmounts; if none is wired, this key is a no-op.
392
+ if (input === "n") {
393
+ if (onIntent) {
394
+ onIntent("addAccount");
395
+ exit();
396
+ }
397
+ return;
398
+ }
399
+ });
400
+ const selectedLog = logs[selectedLogIndex] ?? null;
401
+ const visibleLogs = logs.slice(0, LOG_VISIBLE);
402
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: " CC-Router " }), _jsx(Text, { color: "gray", children: "\u00B7 " }), _jsx(Text, { color: "green", children: data.mode }), _jsxs(Text, { color: "gray", children: [" \u2192 ", data.target, " \u00B7 "] }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsxs(Text, { color: "gray", children: [" \u00B7 updated ", updatedAgo, "s ago \u00B7 [q] quit"] })] }), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models" }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === data.accounts.length ? "green" : "yellow", children: [healthyCount, "/", data.accounts.length, " healthy"] })] }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: focus === "accounts" ? "white" : "gray", children: "[Tab] focus [e] toggle [a] Claude all [o] OpenAI all [w] 7d cap [s] 5h cap [n] add [d] delete" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: data.accounts.map((a, i) => (_jsx(AccountRow, { account: a, selected: focus === "accounts" && i === selectedAccountIndex }, a.id))) })] }), mode === "editWeekly" && selectedAccount && (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 7d cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "editSession" && selectedAccount && (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 5h cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "confirmDelete" && selectedAccount && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: "red", bold: true, children: ["Delete \"", selectedAccount.id, "\"? [y] yes [n/Esc] cancel"] }) })), banner && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: banner.color, children: [" ", banner.text] }) })), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(TokenSummary, { cacheRead: data.totalCacheReadTokens, cacheCreated: data.totalCacheCreationTokens, uncached: data.totalInputTokens, output: data.totalOutputTokens ?? 0 })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleLogs.length === 0
403
+ ? _jsx(Text, { color: "gray", children: " No activity yet" })
404
+ : visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && i === selectedLogIndex }, `${log.ts}-${i}`))) })] }), focus === "logs" && selectedLog && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] }))] }));
405
+ }
406
+ function OperationsPanel({ operational, baseUrl, focus }) {
407
+ const authLabel = operational.auth.required ? "protected" : "open";
408
+ const authColor = operational.auth.required ? "green" : "yellow";
409
+ const claudeReady = operational.capabilities.anthropicMessages;
410
+ const openAIReady = operational.capabilities.openAIResponses;
411
+ const crossReady = operational.capabilities.crossProviderMessages;
412
+ const modelsReady = operational.capabilities.dynamicModels;
413
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " OPERATIONS " }), _jsx(Text, { color: "gray", children: "base " }), _jsx(Text, { color: "cyan", children: baseUrl }), _jsx(Text, { color: "gray", children: " \u00B7 auth " }), _jsx(Text, { color: authColor, children: authLabel }), _jsx(Text, { color: "gray", children: " \u00B7 models " }), _jsx(Text, { color: modelsReady ? "green" : "red", children: modelsReady ? "dynamic" : "off" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(ProviderBadge, { label: "Claude", status: operational.providers.anthropic, ready: claudeReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "OpenAI", status: operational.providers.openai, ready: openAIReady }), _jsx(Text, { color: "gray", children: " \u00B7 cross-route " }), _jsx(Text, { color: crossReady ? "green" : "gray", children: crossReady ? "ready" : "needs OpenAI" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "endpoints " }), _jsx(Text, { color: "white", children: operational.endpoints.messages }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.responses }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.models }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.accounts })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "routing " }), _jsxs(Text, { color: "white", children: ["claude=", operational.routing.anthropicDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.anthropicAliases.join(",") || "-", "]"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "white", children: ["openai=", operational.routing.openAIDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.openAIAliases.join(",") || "-", "]"] })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "models " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[m] list/select" }), _jsx(Text, { color: "gray", children: " change " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[c] Claude [o] OpenAI" })] })] }));
414
+ }
415
+ function ModelsPanel({ status, selectedIndex, focused, }) {
416
+ const models = status?.models ?? [];
417
+ const visible = getVisibleModelWindow(models, selectedIndex, MODEL_VISIBLE_ROWS);
418
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " MODELS " }), _jsx(Text, { color: "gray", children: "[m/r] refresh [\u2191/\u2193] select [c] Claude default [o] OpenAI default [Esc] logs" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "current " }), _jsxs(Text, { color: "white", children: ["claude=", status?.routing.anthropicDefaultModel ?? "default"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "white", children: ["openai=", status?.routing.openAIDefaultModel ?? "default"] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: status === null
419
+ ? _jsx(Text, { color: "gray", children: " Press [m] to load models from provider APIs" })
420
+ : models.length === 0
421
+ ? _jsx(Text, { color: "gray", children: " No models discovered" })
422
+ : (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: [" showing ", visible.start + 1, "-", visible.end, " of ", models.length] }), visible.rows.map((model, i) => (_jsx(ModelRow, { model: model, selected: focused && visible.start + i === selectedIndex, currentClaude: status.routing.anthropicDefaultModel, currentOpenAI: status.routing.openAIDefaultModel }, model.id)))] })) })] }));
423
+ }
424
+ export function getVisibleModelWindow(models, selectedIndex, maxRows = MODEL_VISIBLE_ROWS) {
425
+ if (models.length <= maxRows) {
426
+ return { rows: models, start: 0, end: models.length };
427
+ }
428
+ const selected = Math.max(0, Math.min(selectedIndex, models.length - 1));
429
+ const start = Math.max(0, Math.min(selected - maxRows + 1, models.length - maxRows));
430
+ const end = Math.min(models.length, start + maxRows);
431
+ return {
432
+ rows: models.slice(start, end),
433
+ start,
434
+ end,
435
+ };
436
+ }
437
+ function ModelRow({ model, selected, currentClaude, currentOpenAI, }) {
438
+ const id = model.id;
439
+ const upstream = id.replace(/^anthropic\//, "").replace(/^openai\//, "");
440
+ const isClaudeDefault = currentClaude === upstream || id === "claude/default";
441
+ const isOpenAIDefault = currentOpenAI === upstream || id === "openai/default";
442
+ const providerColor = id.startsWith("openai/") ? "cyan" : id.startsWith("anthropic/") ? "magenta" : "gray";
443
+ const marker = isClaudeDefault ? " Claude" : isOpenAIDefault ? " OpenAI" : "";
444
+ return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶" : " " }), _jsxs(Text, { color: providerColor, children: [" ", id] }), marker && _jsx(Text, { color: "green", children: marker })] }));
445
+ }
446
+ function ProviderBadge({ label, status, ready, }) {
447
+ const color = !status.configured ? "gray" : ready ? "green" : "yellow";
448
+ const text = status.configured
449
+ ? `${label} ${status.healthy}/${status.accounts} healthy`
450
+ : `${label} not configured`;
451
+ return _jsx(Text, { color: color, children: text });
452
+ }
453
+ // ─── Account row (two-line: status + utilization bars) ───────────────────────
454
+ function AccountRow({ account: a, selected }) {
455
+ const rl = a.rateLimits ?? EMPTY_RL;
456
+ const isLimited = rl.status === "rate_limited";
457
+ const isDisabled = a.enabled === false;
458
+ const dot = isDisabled ? "⊘" : isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
459
+ const dotColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
460
+ const statusLabel = isDisabled ? "OFF " : isLimited ? "LIMITED" : a.busy ? "busy " : a.healthy ? "ok " : "ERROR ";
461
+ const statusColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
462
+ const expiryLabel = a.expiresInMs > 0 ? formatMs(a.expiresInMs) : "EXPIRED";
463
+ const expiryColor = a.expiresInMs < 10 * 60 * 1000 ? "red"
464
+ : a.expiresInMs < 30 * 60 * 1000 ? "yellow"
465
+ : "white";
466
+ const providerTag = a.provider === "openai_subscription"
467
+ ? " [OpenAI]"
468
+ : rl.plan ? ` [${rl.plan}]` : "";
469
+ // User-defined caps hint
470
+ const s5 = a.sessionLimitPercent ?? 100;
471
+ const w7 = a.weeklyLimitPercent ?? 100;
472
+ const hasCaps = s5 < 100 || w7 < 100;
473
+ const capsHint = hasCaps
474
+ ? ` cap${s5 < 100 ? ` 5h≤${s5}%` : ""}${w7 < 100 ? ` 7d≤${w7}%` : ""}`
475
+ : "";
476
+ const pointer = selected ? "▶" : " ";
477
+ const nameColor = isDisabled ? "gray" : undefined;
478
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: pointer }), _jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { color: nameColor, dimColor: isDisabled, children: a.id.slice(0, 20).padEnd(20) }), _jsx(Text, { color: statusColor, children: statusLabel }), providerTag && _jsx(Text, { color: a.provider === "openai_subscription" ? "cyan" : "magenta", children: providerTag.padEnd(10) }), !providerTag && _jsx(Text, { children: "".padEnd(10) }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(5) }), _jsx(Text, { color: "gray", children: " err " }), _jsx(Text, { color: a.errorCount > 0 ? "red" : "gray", children: String(a.errorCount).padStart(3) }), _jsx(Text, { color: "gray", children: " tok " }), _jsx(Text, { color: expiryColor, children: expiryLabel.padEnd(8) }), _jsx(Text, { color: "gray", children: " last " }), _jsx(Text, { color: "gray", children: formatAgo(a.lastUsedMs) }), a.provider !== "openai_subscription" && (_jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] })), capsHint && _jsx(Text, { color: "yellow", children: capsHint })] }), rl.lastUpdated > 0 && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: rl.fiveHourUtil, resetTs: rl.fiveHourReset, isActive: rl.claim === "five_hour", cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d", util: rl.sevenDayUtil, resetTs: rl.sevenDayReset, isActive: rl.claim === "seven_day", cap: w7 })] }))] }));
479
+ }
480
+ // ─── Utilization bar ─────────────────────────────────────────────────────────
481
+ function UtilBar({ label, util, resetTs, isActive, cap }) {
482
+ const pct = Math.round(util * 100);
483
+ const BAR_W = 12;
484
+ const filled = Math.round(util * BAR_W);
485
+ const capPos = Math.round((cap / 100) * BAR_W);
486
+ const bar = "█".repeat(Math.min(filled, BAR_W)) + "░".repeat(Math.max(BAR_W - filled, 0));
487
+ const color = pct >= cap ? "red" : pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
488
+ const resetLabel = resetTs > 0 ? formatResetIn(resetTs) : "";
489
+ const capLabel = cap < 100 ? ` cap ${cap}%` : "";
490
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? "white" : "gray", bold: isActive, children: [label, " "] }), _jsx(Text, { color: color, children: bar }), _jsxs(Text, { color: color, children: [String(pct).padStart(4), "%"] }), capLabel && _jsx(Text, { color: "yellow", children: capLabel }), resetLabel && _jsxs(Text, { color: "gray", children: [" \u21BB", resetLabel] })] }));
491
+ }
492
+ function formatResetIn(unixSeconds) {
493
+ const diff = unixSeconds - Date.now() / 1000;
494
+ if (diff <= 0)
495
+ return "now";
496
+ const d = Math.floor(diff / 86400);
497
+ const h = Math.floor((diff % 86400) / 3600);
498
+ const m = Math.floor((diff % 3600) / 60);
499
+ if (d > 0)
500
+ return `${d}d${h}h`;
501
+ if (h > 0)
502
+ return `${h}h${m}m`;
503
+ return `${m}m`;
504
+ }
505
+ // ─── Log row ──────────────────────────────────────────────────────────────────
506
+ function LogRow({ log, selected }) {
507
+ const time = new Date(log.ts).toLocaleTimeString("en-GB", { hour12: false });
508
+ const isError = log.type === "error";
509
+ const isRefresh = log.type === "refresh";
510
+ const typeColor = isError ? "red" : isRefresh ? "yellow" : "gray";
511
+ const typeIcon = isError ? "✗" : isRefresh ? "↻" : "→";
512
+ const statusColor = log.statusCode === undefined ? undefined
513
+ : log.statusCode >= 500 ? "red"
514
+ : log.statusCode >= 400 ? "yellow"
515
+ : log.statusCode >= 200 ? "green"
516
+ : "gray";
517
+ const bg = selected ? "white" : undefined;
518
+ const fg = (c) => selected ? "black" : c;
519
+ const sourceLabel = log.source === "cli" ? "cli"
520
+ : log.source === "desktop" ? "dsk"
521
+ : log.source === "api" ? "api"
522
+ : " ";
523
+ const sourceColor = log.source === "cli" ? "blue"
524
+ : log.source === "desktop" ? "magenta"
525
+ : "gray";
526
+ // Per-request token stats
527
+ const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
528
+ const outputTok = log.outputTokens ?? 0;
529
+ const cacheHitPct = inputTok > 0 ? Math.round(((log.cacheReadTokens ?? 0) / inputTok) * 100) : null;
530
+ const cacheColor = cacheHitPct === null ? undefined
531
+ : cacheHitPct >= 70 ? "green"
532
+ : cacheHitPct >= 30 ? "yellow"
533
+ : "red";
534
+ return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: bg, color: fg(undefined), children: [selected ? "▶" : " ", " ", time, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [typeIcon, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(sourceColor), children: [sourceLabel, " "] }), _jsx(Text, { backgroundColor: bg, color: fg("cyan"), children: log.accountId.slice(0, 22).padEnd(22) }), log.method && log.path
535
+ ? _jsxs(Text, { backgroundColor: bg, color: fg("white"), children: [" ", log.method, " ", log.path.padEnd(14)] })
536
+ : _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [" ", log.type.padEnd(9)] }), log.statusCode !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg(statusColor), children: [" ", log.statusCode] })), log.durationMs !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.durationMs, "ms"] })), cacheHitPct !== null && (_jsxs(Text, { backgroundColor: bg, color: fg(cacheColor), children: [" \u2191", cacheHitPct, "%"] })), (inputTok > 0 || outputTok > 0) && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", fmtTok(inputTok), "\u2191 ", fmtTok(outputTok), "\u2193"] })), log.details && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.details] }))] }));
537
+ }
538
+ // ─── Detail panel ─────────────────────────────────────────────────────────────
539
+ function DetailPanel({ log }) {
540
+ const time = new Date(log.ts).toLocaleString("en-GB", {
541
+ hour12: false,
542
+ year: "numeric", month: "2-digit", day: "2-digit",
543
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
544
+ });
545
+ const isError = log.type === "error";
546
+ const statusLabel = log.statusCode === undefined ? "—"
547
+ : log.statusCode === 0 ? "connection error"
548
+ : `${log.statusCode} ${httpStatusText(log.statusCode)}`;
549
+ const statusColor = log.statusCode === undefined ? "gray"
550
+ : log.statusCode === 0 ? "red"
551
+ : log.statusCode >= 500 ? "red"
552
+ : log.statusCode >= 400 ? "yellow"
553
+ : "green";
554
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type }), _jsx(Field, { label: "Source", value: sourceFullLabel(log.source) })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
555
+ }
556
+ function Field({ label, value }) {
557
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
558
+ }
559
+ function FieldColored({ label, value, color }) {
560
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: color, children: value })] }));
561
+ }
562
+ // ─── Cache health badge (aggregated) ─────────────────────────────────────────
563
+ function CacheHealthBadge({ read, created, input }) {
564
+ const total = read + created + input;
565
+ if (total === 0)
566
+ return null;
567
+ const hitPct = Math.round((read / total) * 100);
568
+ const color = hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
569
+ const label = hitPct >= 70 ? "healthy" : hitPct >= 30 ? "fair" : "poor";
570
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "cache " }), _jsxs(Text, { color: color, children: [hitPct, "% hit "] }), _jsxs(Text, { color: "gray", children: ["(", label, ")"] })] }));
571
+ }
572
+ // ─── Cache breakdown (per-request detail) ────────────────────────────────────
573
+ function CacheBreakdown({ read, created, input, output }) {
574
+ const totalInput = read + created + input;
575
+ const hitPct = totalInput > 0 ? (read / totalInput) * 100 : 0;
576
+ const color = totalInput === 0 ? "gray" : hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
577
+ return (_jsxs(_Fragment, { children: [_jsx(FieldColored, { label: "Cache hit", value: totalInput > 0 ? `${fmtTok(read)} tok (${hitPct.toFixed(1)}%)` : "—", color: color }), _jsx(Field, { label: "Cache created", value: fmtTok(created) + " tok" }), _jsx(Field, { label: "Uncached", value: fmtTok(input) + " tok" }), _jsx(Field, { label: "Total input", value: fmtTok(totalInput) + " tok" }), _jsx(Field, { label: "Output", value: fmtTok(output) + " tok" }), _jsx(Field, { label: "Total", value: fmtTok(totalInput + output) + " tok" })] }));
578
+ }
579
+ // ─── Token summary (aggregated totals) ──────────────────────────────────────
580
+ function TokenSummary({ cacheRead, cacheCreated, uncached, output }) {
581
+ const totalInput = cacheRead + cacheCreated + uncached;
582
+ const totalAll = totalInput + output;
583
+ if (totalAll === 0)
584
+ return null;
585
+ return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "input " }), _jsx(Text, { color: "white", children: fmtTok(totalInput) }), _jsx(Text, { color: "gray", children: " (cached " }), _jsx(Text, { color: "green", children: fmtTok(cacheRead) }), _jsx(Text, { color: "gray", children: " + new " }), _jsx(Text, { color: "yellow", children: fmtTok(cacheCreated) }), _jsx(Text, { color: "gray", children: " + uncached " }), _jsx(Text, { color: "white", children: fmtTok(uncached) }), _jsx(Text, { color: "gray", children: ") \u00B7 output " }), _jsx(Text, { color: "white", children: fmtTok(output) }), _jsx(Text, { color: "gray", children: " \u00B7 total " }), _jsx(Text, { color: "cyan", bold: true, children: fmtTok(totalAll) })] }));
586
+ }
587
+ function fmtTok(n) {
588
+ if (n >= 1_000_000)
589
+ return `${(n / 1_000_000).toFixed(1)}M`;
590
+ if (n >= 1_000)
591
+ return `${(n / 1_000).toFixed(1)}k`;
592
+ return String(n);
593
+ }
594
+ // ─── Source label ─────────────────────────────────────────────────────────────
595
+ function sourceFullLabel(source) {
596
+ if (source === "cli")
597
+ return "Claude Code";
598
+ if (source === "desktop")
599
+ return "Claude Desktop";
600
+ if (source === "api")
601
+ return "API";
602
+ return "—";
603
+ }
604
+ // ─── HTTP status text ─────────────────────────────────────────────────────────
605
+ function httpStatusText(code) {
606
+ const map = {
607
+ 200: "OK", 201: "Created", 204: "No Content",
608
+ 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
609
+ 404: "Not Found", 429: "Too Many Requests",
610
+ 500: "Internal Server Error", 502: "Bad Gateway",
611
+ 503: "Service Unavailable", 529: "Overloaded",
612
+ };
613
+ return map[code] ?? "";
614
+ }
615
+ // ─── Formatters ───────────────────────────────────────────────────────────────
616
+ function formatUptime(seconds) {
617
+ const h = Math.floor(seconds / 3_600);
618
+ const m = Math.floor((seconds % 3_600) / 60);
619
+ const s = seconds % 60;
620
+ if (h > 0)
621
+ return `${h}h ${m}m`;
622
+ if (m > 0)
623
+ return `${m}m ${s}s`;
624
+ return `${s}s`;
625
+ }
626
+ function formatMs(ms) {
627
+ const totalMin = Math.round(ms / 60_000);
628
+ if (totalMin >= 60)
629
+ return `${Math.floor(totalMin / 60)}h ${totalMin % 60}m`;
630
+ return `${totalMin}m`;
631
+ }
632
+ function formatAgo(ts) {
633
+ if (!ts)
634
+ return "never";
635
+ const s = Math.round((Date.now() - ts) / 1_000);
636
+ if (s < 60)
637
+ return `${s}s ago`;
638
+ const m = Math.floor(s / 60);
639
+ return `${m}m ago`;
640
+ }