tokmon 0.19.8 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,2700 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- PROVIDERS,
4
- PROVIDER_ORDER,
5
- accountsByProvider,
6
- buildAccounts,
7
- cacheDir,
8
- col,
9
- configLocation,
10
- currency,
11
- cursorModelSpend,
12
- detectProviders,
13
- flushDisk,
14
- generateAccountId,
15
- isValidTimezone,
16
- loadConfig,
17
- mergeTables,
18
- pickAccentColor,
19
- readJson,
20
- resolveTimezone,
21
- saveConfig,
22
- shortDate,
23
- systemTimezone,
24
- time,
25
- tokens
26
- } from "./chunk-UAPL47GL.js";
27
2
 
28
3
  // src/cli.tsx
29
4
  import { EventEmitter } from "events";
30
- import { render } from "ink";
31
- import { MouseProvider } from "@zenobius/ink-mouse";
32
-
33
- // src/glyphs.ts
34
- var GLYPHS_UNICODE = {
35
- spark: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"],
36
- barFull: "\u2501",
37
- barEmpty: "\u2500",
38
- rule: "\u2500",
39
- spinner: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
40
- dot: "\u25CF",
41
- dotSel: "\u25C9",
42
- radioOff: "\u25CB",
43
- dotAll: "\u2726",
44
- caretR: "\u25B8",
45
- caretL: "\u25C2",
46
- play: "\u25B6",
47
- arrowU: "\u2191",
48
- arrowD: "\u2193",
49
- arrowL: "\u2190",
50
- arrowR: "\u2192",
51
- shift: "\u21E7",
52
- vbar: "\u258C",
53
- treeMid: "\u251C\u2500",
54
- treeEnd: "\u2514\u2500",
55
- boxMark: "\u2502",
56
- check: "\u2713",
57
- warn: "\u26A0",
58
- ellipsis: "\u2026",
59
- middot: "\xB7",
60
- emDash: "\u2014",
61
- eur: "\u20AC",
62
- gbp: "\xA3",
63
- border: "round"
64
- };
65
- var GLYPHS_ASCII = {
66
- spark: [".", ":", "-", "=", "+", "*", "#", "@"],
67
- barFull: "#",
68
- barEmpty: "-",
69
- rule: "-",
70
- spinner: ["|", "/", "-", "\\"],
71
- dot: "*",
72
- dotSel: "*",
73
- radioOff: "o",
74
- dotAll: "+",
75
- caretR: ">",
76
- caretL: "<",
77
- play: ">",
78
- arrowU: "^",
79
- arrowD: "v",
80
- arrowL: "<",
81
- arrowR: ">",
82
- shift: "^",
83
- vbar: "|",
84
- treeMid: "+-",
85
- treeEnd: "`-",
86
- boxMark: "|",
87
- check: "x",
88
- warn: "!",
89
- ellipsis: "...",
90
- middot: "-",
91
- emDash: "-",
92
- eur: "EUR",
93
- gbp: "GBP",
94
- border: "classic"
95
- };
96
- function detectUnicode(env, isTTY, platform) {
97
- if (!isTTY) return false;
98
- if (env.TERM === "dumb") return false;
99
- if (platform === "win32") {
100
- return Boolean(env.WT_SESSION || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || /xterm/i.test(env.TERM ?? ""));
101
- }
102
- const loc = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
103
- if (loc && /\.(iso|latin|ascii|cp\d|koi|gbk|big5)/i.test(loc)) return false;
104
- if (/^(C|POSIX)$/i.test(loc)) return false;
105
- return true;
106
- }
107
- function resolveGlyphs(opts) {
108
- let ascii;
109
- if (opts.flag === "on") ascii = true;
110
- else if (opts.flag === "off") ascii = false;
111
- else {
112
- const e = (opts.env.TOKMON_ASCII ?? "").toLowerCase();
113
- if (/^(1|true|on|yes)$/.test(e)) ascii = true;
114
- else if (/^(0|false|off|no)$/.test(e)) ascii = false;
115
- else if (opts.config === "on") ascii = true;
116
- else if (opts.config === "off") ascii = false;
117
- else ascii = !detectUnicode(opts.env, opts.isTTY, opts.platform);
118
- }
119
- return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
120
- }
121
- var active = GLYPHS_UNICODE;
122
- function setGlyphs(set) {
123
- active = set;
124
- }
125
- function glyphs() {
126
- return active;
127
- }
128
-
129
- // src/app.tsx
130
- import { spawn } from "child_process";
131
- import { appendFileSync as appendFileSync2 } from "fs";
132
- import { useState as useState3, useEffect as useEffect3, useCallback, useRef as useRef2, useMemo } from "react";
133
- import { Box as Box7, Text as Text7, Transform, useInput, useStdout, useApp } from "ink";
134
- import { useMouse } from "@zenobius/ink-mouse";
135
-
136
- // src/peak.ts
137
- async function fetchPeak() {
138
- try {
139
- const res = await fetch("https://promoclock.co/api/status", {
140
- headers: { "Accept": "application/json", "User-Agent": "tokmon" },
141
- signal: AbortSignal.timeout(3e3)
142
- });
143
- if (!res.ok) return null;
144
- const data = await readJson(res);
145
- if (!data) return null;
146
- let state;
147
- if (data.isPeak === true || data.status === "peak") state = "peak";
148
- else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
149
- else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
150
- else return null;
151
- return {
152
- state,
153
- label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
154
- minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
155
- };
156
- } catch {
157
- return null;
158
- }
159
- }
160
-
161
- // src/snapshot.ts
162
- import { readFile, writeFile, mkdir, rename } from "fs/promises";
163
- import { join } from "path";
164
- function snapshotFile() {
165
- return join(cacheDir(), "dashboard-snapshot.json");
166
- }
167
- async function loadSnapshot() {
168
- try {
169
- const obj = JSON.parse(await readFile(snapshotFile(), "utf-8"));
170
- return obj && typeof obj === "object" ? obj : {};
171
- } catch {
172
- return {};
173
- }
174
- }
175
- var saveQueue = Promise.resolve();
176
- function saveSnapshot(stats) {
177
- const obj = {};
178
- for (const [id, s] of stats) {
179
- if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
180
- }
181
- saveQueue = saveQueue.then(async () => {
182
- try {
183
- const dir = cacheDir();
184
- await mkdir(dir, { recursive: true });
185
- const tmp = join(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
186
- await writeFile(tmp, JSON.stringify(obj));
187
- await rename(tmp, snapshotFile());
188
- } catch {
189
- }
190
- });
191
- }
192
-
193
- // src/ui/shared.tsx
194
- import { appendFileSync } from "fs";
195
- import { useEffect, useRef, useState } from "react";
196
- import { Box, Text } from "ink";
197
- import { useOnMouseClick } from "@zenobius/ink-mouse";
198
- import { jsx, jsxs } from "react/jsx-runtime";
199
- function truncateName(s, n) {
200
- const ell = glyphs().ellipsis;
201
- return s.length > n ? s.slice(0, n - ell.length) + ell : s;
202
- }
203
- function ClickableBox({ onClick, children, ...props }) {
204
- const ref = useRef(null);
205
- useOnMouseClick(ref, (clicked) => {
206
- if (clicked) onClick();
207
- });
208
- return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
209
- }
210
- var SGR_PRESS = /\x1b\[<(\d+);(\d+);(\d+)M/g;
211
- var linkHits = /* @__PURE__ */ new Set();
212
- function dispatchLinkClicks(chunk) {
213
- if (linkHits.size === 0) return;
214
- const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
215
- SGR_PRESS.lastIndex = 0;
216
- let m;
217
- while ((m = SGR_PRESS.exec(s)) !== null) {
218
- const code = Number(m[1]);
219
- if (code & 64 || code & 32) continue;
220
- const mx = Number(m[2]) - 1;
221
- const my = Number(m[3]) - 1;
222
- if (process.env.TOKMON_LINKDEBUG) {
223
- try {
224
- appendFileSync(process.env.TOKMON_LINKDEBUG, `DISPATCH code=${code} mx=${mx} my=${my} hits=${linkHits.size}
225
- `);
226
- } catch {
227
- }
228
- }
229
- for (const hit of [...linkHits]) {
230
- if (hit(mx, my)) break;
231
- }
232
- }
233
- }
234
- function nodeBox(node) {
235
- const yn = node?.yogaNode;
236
- if (!yn) return null;
237
- const l = yn.getComputedLayout();
238
- let left = l.left, top = l.top;
239
- let p = node?.parentNode;
240
- while (p) {
241
- const pn = p.yogaNode;
242
- if (!pn) break;
243
- const pl = pn.getComputedLayout();
244
- left += pl.left;
245
- top += pl.top;
246
- p = p.parentNode;
247
- }
248
- return { left, top, width: l.width, height: l.height };
249
- }
250
- function LinkBox({ onClick, children, ...props }) {
251
- const ref = useRef(null);
252
- const onClickRef = useRef(onClick);
253
- onClickRef.current = onClick;
254
- useEffect(() => {
255
- const hit = (mx, my) => {
256
- const box = nodeBox(ref.current);
257
- if (process.env.TOKMON_LINKDEBUG) {
258
- try {
259
- appendFileSync(process.env.TOKMON_LINKDEBUG, `HIT? mx=${mx} my=${my} box=${box ? `${box.left},${box.top} ${box.width}x${box.height}` : "null"}
260
- `);
261
- } catch {
262
- }
263
- }
264
- if (!box || box.width <= 0 || box.height <= 0) return false;
265
- if (mx >= box.left && mx < box.left + box.width && my >= box.top && my < box.top + box.height) {
266
- onClickRef.current();
267
- return true;
268
- }
269
- return false;
270
- };
271
- linkHits.add(hit);
272
- return () => {
273
- linkHits.delete(hit);
274
- };
275
- }, []);
276
- return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
277
- }
278
- function Spinner({ label }) {
279
- const frames = glyphs().spinner;
280
- const [i, setI] = useState(0);
281
- useEffect(() => {
282
- const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
283
- return () => clearInterval(id);
284
- }, []);
285
- return /* @__PURE__ */ jsxs(Box, { children: [
286
- /* @__PURE__ */ jsxs(Text, { color: "green", children: [
287
- frames[i],
288
- " "
289
- ] }),
290
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
291
- ] });
292
- }
293
- function TabBar({ tabs, active: active2, onSelect }) {
294
- return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active2 ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
295
- " ",
296
- t,
297
- " "
298
- ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
299
- " ",
300
- t,
301
- " "
302
- ] }) }, t)) });
303
- }
304
- function PeakBadge({ peak }) {
305
- const color = peak.state === "peak" ? "red" : "green";
306
- return /* @__PURE__ */ jsxs(Box, { children: [
307
- /* @__PURE__ */ jsxs(Text, { color, children: [
308
- glyphs().dot,
309
- " "
310
- ] }),
311
- /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
312
- peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
313
- " (",
314
- fmtMinutes(peak.minutesUntilChange),
315
- ")"
316
- ] })
317
- ] });
318
- }
319
- function fmtMinutes(mins) {
320
- if (mins < 60) return `${mins}m`;
321
- const h = Math.floor(mins / 60);
322
- const m = mins % 60;
323
- return m === 0 ? `${h}h` : `${h}h ${m}m`;
324
- }
325
- function currencySymbol(cur) {
326
- return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
327
- }
328
- function sparkline(values) {
329
- if (values.length === 0) return "";
330
- const spark = glyphs().spark;
331
- const max = Math.max(...values);
332
- if (max <= 0) return spark[0].repeat(values.length);
333
- return values.map((v) => {
334
- if (v <= 0) return spark[0];
335
- const idx = Math.min(spark.length - 1, 1 + Math.round(v / max * (spark.length - 2)));
336
- return spark[idx];
337
- }).join("");
338
- }
339
- function Bar({ pct, color, width = 24 }) {
340
- const filled = Math.max(0, Math.min(width, Math.round(pct / 100 * width)));
341
- return /* @__PURE__ */ jsxs(Text, { children: [
342
- /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
343
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
344
- ] });
345
- }
346
- function metricValueText(m) {
347
- if (m.format.kind === "dollars") {
348
- const sym = currencySymbol(m.format.currency);
349
- const used = `${sym}${m.used.toFixed(2)}`;
350
- return m.limit != null ? `${used} / ${sym}${m.limit.toFixed(2)}` : `${used}`;
351
- }
352
- if (m.format.kind === "count") {
353
- const suffix = m.format.suffix ? ` ${m.format.suffix}` : "";
354
- const used = `${Math.round(m.used)}${suffix}`;
355
- return m.limit != null ? `${used} / ${Math.round(m.limit)}` : used;
356
- }
357
- return `${Math.round(m.used)}%`;
358
- }
359
-
360
- // src/ui/dashboard.tsx
361
- import { Box as Box2, Text as Text2 } from "ink";
362
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
363
- var GAP = 2;
364
- var MIN_CARD = 56;
365
- var MIN_CARD_DENSE = 50;
366
- var CARD_H = { full: 14, compact: 12, mini: 8 };
367
- var VARIANT_ORDER = ["full", "compact", "mini"];
368
- var INDICATOR_ROWS = 1;
369
- var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
370
- function chooseLayout(content, budget, n, single, cols) {
371
- if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
372
- const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
373
- const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
374
- const maxCols = Math.max(1, Math.min(colCap, n));
375
- const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
376
- const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
377
- for (const variant of VARIANT_ORDER) {
378
- for (let nc = maxCols; nc >= 1; nc--) {
379
- if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
380
- const rows = Math.ceil(n / nc);
381
- if (gridHeight(rows, CARD_H[variant]) <= budget) {
382
- return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
383
- }
384
- }
385
- }
386
- let ncols = 1;
387
- for (let nc = maxCols; nc >= 1; nc--) {
388
- if (nc === 1 || cardWidthAt(nc) >= minWidthAt(nc)) {
389
- ncols = nc;
390
- break;
391
- }
392
- }
393
- const H = CARD_H.mini;
394
- const fitBudget = budget - INDICATOR_ROWS;
395
- const rowsThatFit = Math.max(1, Math.floor((fitBudget + 1) / (H + 1)));
396
- const cardsPerPage = Math.max(1, rowsThatFit * ncols);
397
- const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
398
- return { ncols, variant: "mini", cardsPerPage, pageCount };
399
- }
400
- function DashboardView({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
401
- if (groups.length === 0) {
402
- return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
403
- "No providers enabled ",
404
- glyphs().emDash,
405
- " press s to pick providers."
406
- ] });
407
- }
408
- let shown = groups;
409
- if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
410
- const single = focusId !== null || layout === "single";
411
- const content = Math.max(MIN_CARD, cols - 4);
412
- const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
413
- let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
414
- if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
415
- const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
416
- const visible = pageCount > 1 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
417
- return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
418
- /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant }) }, g.provider)) }),
419
- pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
420
- " ",
421
- glyphs().middot,
422
- " page ",
423
- pg + 1,
424
- "/",
425
- pageCount,
426
- " ",
427
- glyphs().middot,
428
- " scroll ",
429
- glyphs().arrowU,
430
- glyphs().arrowD
431
- ] })
432
- ] });
433
- }
434
- function ProviderCard({ provider, accounts, stats, width, variant }) {
435
- const meta = PROVIDERS[provider];
436
- const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
437
- const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
438
- const agg = meta.hasUsage && dashboards.length > 0 ? aggregate(dashboards) : null;
439
- const plan = items.map((i) => i.s?.billing?.plan).find(Boolean) ?? null;
440
- const activity = items.map((i) => i.s?.billing?.activity).find(Boolean) ?? null;
441
- const inner = width - 4;
442
- const barW = Math.max(10, Math.min(46, inner - 20));
443
- const hasSpark = !!agg && agg.series.some((v) => v > 0);
444
- const showBars = variant !== "mini";
445
- const showSpark = variant === "full";
446
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: glyphs().border, borderColor: meta.color, paddingX: 1, children: [
447
- /* @__PURE__ */ jsxs2(Box2, { children: [
448
- /* @__PURE__ */ jsxs2(Text2, { color: meta.color, children: [
449
- glyphs().dot,
450
- " "
451
- ] }),
452
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
453
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
454
- plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
455
- ] }),
456
- meta.hasUsage && (agg ? /* @__PURE__ */ jsxs2(Fragment, { children: [
457
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
458
- /* @__PURE__ */ jsx2(SummaryRow, { label: "Today", s: agg.today }),
459
- /* @__PURE__ */ jsx2(SummaryRow, { label: "This Week", s: agg.week }),
460
- /* @__PURE__ */ jsx2(SummaryRow, { label: "This Month", s: agg.month }),
461
- /* @__PURE__ */ jsx2(KpiLine, { agg })
462
- ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
463
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
464
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
465
- "Fetching usage",
466
- glyphs().ellipsis
467
- ] })
468
- ] })),
469
- meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
470
- meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
471
- /* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
472
- ] }),
473
- meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
474
- hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
475
- /* @__PURE__ */ jsx2(Rule, { inner }),
476
- /* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
477
- ] }),
478
- !meta.hasUsage && activity && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
479
- /* @__PURE__ */ jsx2(Rule, { inner }),
480
- /* @__PURE__ */ jsxs2(Box2, { children: [
481
- /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
482
- /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: sparkline(activity.series) }),
483
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
484
- ] })
485
- ] })
486
- ] });
487
- }
488
- function CompactBilling({ items }) {
489
- const billing = items.map((i) => i.s?.billing).find(Boolean);
490
- if (!billing) return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
491
- "Fetching",
492
- glyphs().ellipsis
493
- ] });
494
- if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
495
- const m = billing.metrics[0];
496
- if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
497
- return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
498
- }
499
- function Rule({ inner }) {
500
- return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: glyphs().rule.repeat(Math.max(0, inner)) });
501
- }
502
- function SummaryRow({ label, s }) {
503
- const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
504
- return /* @__PURE__ */ jsxs2(Box2, { children: [
505
- /* @__PURE__ */ jsx2(Box2, { width: 11, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: label }) }),
506
- /* @__PURE__ */ jsx2(Box2, { width: 11, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", wrap: "truncate", children: currency(s.cost) }) }),
507
- /* @__PURE__ */ jsx2(Box2, { width: 13, flexShrink: 0, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate", children: [
508
- tokens(s.tokens),
509
- " tok"
510
- ] }) }),
511
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: cachedPct > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate", children: [
512
- cachedPct,
513
- "% cached"
514
- ] }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
515
- ] });
516
- }
517
- function KpiLine({ agg }) {
518
- const hasBurn = agg.burnRate > 0;
519
- const hasSaved = agg.month.cacheSavings > 0;
520
- if (!hasBurn && !hasSaved) return null;
521
- return /* @__PURE__ */ jsxs2(Box2, { children: [
522
- hasBurn && /* @__PURE__ */ jsxs2(Fragment, { children: [
523
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Burn " }),
524
- /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
525
- currency(agg.burnRate),
526
- "/hr"
527
- ] })
528
- ] }),
529
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
530
- hasSaved && /* @__PURE__ */ jsxs2(Fragment, { children: [
531
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Cache saved " }),
532
- /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
533
- currency(agg.month.cacheSavings),
534
- "/mo"
535
- ] })
536
- ] })
537
- ] });
538
- }
539
- function LimitsBlock({ items, barW }) {
540
- const showName = items.length > 1;
541
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
542
- const billing = s?.billing;
543
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
544
- showName && /* @__PURE__ */ jsxs2(Box2, { children: [
545
- /* @__PURE__ */ jsxs2(Text2, { color: account.color, children: [
546
- glyphs().dot,
547
- " "
548
- ] }),
549
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
550
- ] }),
551
- !billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
552
- "Fetching",
553
- glyphs().ellipsis
554
- ] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW }, `${m.label}${i}`))
555
- ] }, account.id);
556
- }) });
557
- }
558
- function MetricRow({ m, color, barW }) {
559
- if (m.format.kind === "percent") {
560
- const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
561
- return /* @__PURE__ */ jsxs2(Box2, { children: [
562
- /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
563
- /* @__PURE__ */ jsx2(Bar, { pct: m.used, color: barColor, width: barW }),
564
- /* @__PURE__ */ jsx2(Box2, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
565
- Math.round(m.used),
566
- "%"
567
- ] }) }),
568
- /* @__PURE__ */ jsx2(Box2, { width: 8, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.resetsAt }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
569
- ] });
570
- }
571
- return /* @__PURE__ */ jsxs2(Box2, { children: [
572
- /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate", children: m.label }) }),
573
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) })
574
- ] });
575
- }
576
- function SparkFooter({ series, month, color }) {
577
- return /* @__PURE__ */ jsxs2(Box2, { children: [
578
- /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
579
- /* @__PURE__ */ jsx2(Text2, { color, children: sparkline(series) }),
580
- /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
581
- currency(month),
582
- " mo"
583
- ] }) })
584
- ] });
585
- }
586
- function aggregate(list) {
587
- const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
588
- const z = { today: zero(), week: zero(), month: zero(), burnRate: 0, series: [] };
589
- for (const d of list) {
590
- for (const k of ["today", "week", "month"]) {
591
- z[k].cost += d[k].cost;
592
- z[k].tokens += d[k].tokens;
593
- z[k].cacheRead += d[k].cacheRead;
594
- z[k].cacheSavings += d[k].cacheSavings;
595
- }
596
- z.burnRate += d.burnRate;
597
- d.series.forEach((v, i) => {
598
- z.series[i] = (z.series[i] ?? 0) + v;
599
- });
600
- }
601
- return z;
602
- }
603
- function TotalsRow({ groups, stats, cols }) {
604
- const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
605
- const t = zero(), w = zero(), m = zero();
606
- for (const g of groups) {
607
- if (!PROVIDERS[g.provider].hasUsage) continue;
608
- for (const a of g.accounts) {
609
- const d = stats.get(a.id)?.dashboard;
610
- if (!d) continue;
611
- t.cost += d.today.cost;
612
- t.tokens += d.today.tokens;
613
- w.cost += d.week.cost;
614
- w.tokens += d.week.tokens;
615
- m.cost += d.month.cost;
616
- m.tokens += d.month.tokens;
617
- }
618
- }
619
- const inner = cols - 4;
620
- const dot = glyphs().middot;
621
- const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok)`;
622
- const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}`;
623
- const tight = `${glyphs().dotAll} ${currency(t.cost)} ${dot} ${currency(w.cost)} ${dot} ${currency(m.cost)}`;
624
- const text = full.length <= inner ? full : noTok.length <= inner ? noTok : tight;
625
- return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
626
- }
627
-
628
- // src/ui/table.tsx
629
- import { Box as Box3, Text as Text3 } from "ink";
630
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
631
- var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
632
- function TableProviderBar({ providers, active: active2, onSelect }) {
633
- return /* @__PURE__ */ jsxs3(Box3, { children: [
634
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
635
- providers.map((p) => {
636
- const meta = PROVIDERS[p];
637
- return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active2 ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
638
- " ",
639
- meta.name,
640
- " "
641
- ] }) : /* @__PURE__ */ jsxs3(Text3, { color: meta.color, dimColor: true, children: [
642
- " ",
643
- meta.name,
644
- " "
645
- ] }) }, p);
646
- }),
647
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " p/P switch" })
648
- ] });
649
- }
650
- function ControlBar({ views, period, sort, search, searching, showPeriod }) {
651
- return /* @__PURE__ */ jsxs3(Box3, { children: [
652
- showPeriod && /* @__PURE__ */ jsxs3(Fragment2, { children: [
653
- views.map((v, i) => /* @__PURE__ */ jsx3(Box3, { marginRight: 2, children: i === period ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
654
- "[",
655
- v,
656
- "]"
657
- ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: v }) }, v)),
658
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " })
659
- ] }),
660
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "sort " }),
661
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: sort }),
662
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
663
- " o cycle ",
664
- glyphs().middot,
665
- " "
666
- ] }),
667
- searching ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
668
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/" }),
669
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: search }),
670
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: glyphs().vbar })
671
- ] }) : search ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
672
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "filter " }),
673
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: search }),
674
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
675
- " (/ edit ",
676
- glyphs().middot,
677
- " esc clear)"
678
- ] })
679
- ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/ filter" })
680
- ] });
681
- }
682
- function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
683
- if (rows.length === 0) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No usage in this period (or filtered out)." });
684
- const wide = cols > 90;
685
- const base = wide ? { label: 12, input: 10, output: 10, cc: 14, cr: 12, total: 11, cost: 13 } : { label: 8, input: 7, output: 7, cc: 7, cr: 8, total: 0, cost: 11 };
686
- const fixed = base.label + base.input + base.output + base.cc + base.cr + base.total + base.cost;
687
- const available = cols - fixed - 6;
688
- const W = { ...base, models: Math.max(wide ? 22 : 14, available) };
689
- const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
690
- const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
691
- for (const r of rows) {
692
- totals.input += r.input;
693
- totals.output += r.output;
694
- totals.cacheCreate += r.cacheCreate;
695
- totals.cacheRead += r.cacheRead;
696
- totals.cost += r.cost;
697
- }
698
- const clampedCursor = Math.min(cursor, rows.length - 1);
699
- const scrollStart = Math.max(0, Math.min(clampedCursor - Math.floor(maxRows / 2), rows.length - maxRows));
700
- const visible = rows.slice(scrollStart, scrollStart + maxRows);
701
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
702
- /* @__PURE__ */ jsxs3(Text3, { children: [
703
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
704
- " ",
705
- col("Date", W.label, "left")
706
- ] }),
707
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Models", W.models, "left") }),
708
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Input", W.input) }),
709
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Output", W.output) }),
710
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(wide ? "Cache Create" : "CchCrt", W.cc) }),
711
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(wide ? "Cache Read" : "CchRd", W.cr) }),
712
- W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Total", W.total) }),
713
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) })
714
- ] }),
715
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
716
- visible.map((r, vi) => {
717
- const idx = scrollStart + vi;
718
- const selected = idx === clampedCursor;
719
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
720
- /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
721
- /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "cyan", children: [
722
- selected ? `${glyphs().caretR} ` : " ",
723
- col(fmtLabel(r.label), W.label, "left")
724
- ] }),
725
- /* @__PURE__ */ jsx3(Text3, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
726
- /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.input), W.input) }),
727
- /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.output), W.output) }),
728
- /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.cacheCreate), W.cc) }),
729
- /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.cacheRead), W.cr) }),
730
- W.total > 0 && /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.total), W.total) }),
731
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.cost), W.cost) })
732
- ] }) }),
733
- idx === expanded && /* @__PURE__ */ jsx3(RowDetail, { row: r, indent: W.label + 2 })
734
- ] }, r.label);
735
- }),
736
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
737
- /* @__PURE__ */ jsxs3(Text3, { children: [
738
- /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
739
- " ",
740
- col("Total", W.label, "left")
741
- ] }),
742
- /* @__PURE__ */ jsx3(Text3, { children: col("", W.models, "left") }),
743
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
744
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
745
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
746
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
747
- W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
748
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
749
- ] }),
750
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
751
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
752
- glyphs().arrowU,
753
- glyphs().arrowD,
754
- " navigate ",
755
- glyphs().middot,
756
- " Enter detail ",
757
- glyphs().middot,
758
- " o sort ",
759
- glyphs().middot,
760
- " g/G top/bottom ",
761
- glyphs().middot,
762
- " ",
763
- clampedCursor + 1,
764
- "/",
765
- rows.length
766
- ] })
767
- ] });
768
- }
769
- function RowDetail({ row, indent }) {
770
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
771
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
772
- i === row.breakdown.length - 1 ? glyphs().treeEnd : glyphs().treeMid,
773
- " "
774
- ] }),
775
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(m.name, 16, "left") }),
776
- /* @__PURE__ */ jsxs3(Text3, { children: [
777
- col(tokens(m.input), 8),
778
- " in "
779
- ] }),
780
- /* @__PURE__ */ jsxs3(Text3, { children: [
781
- col(tokens(m.output), 8),
782
- " out "
783
- ] }),
784
- /* @__PURE__ */ jsxs3(Text3, { children: [
785
- col(tokens(m.cacheCreate), 8),
786
- " cc "
787
- ] }),
788
- /* @__PURE__ */ jsxs3(Text3, { children: [
789
- col(tokens(m.cacheRead), 9),
790
- " cr "
791
- ] }),
792
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: currency(m.cost) })
793
- ] }, m.name)) });
794
- }
795
- function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
796
- if (rows.length === 0) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No Cursor spend recorded locally." });
797
- const total = rows.reduce((a, r) => a + r.usd, 0);
798
- const totalAmt = rows.reduce((a, r) => a + r.requests, 0);
799
- const clamped = Math.min(cursor, rows.length - 1);
800
- const scrollStart = Math.max(0, Math.min(clamped - Math.floor(maxRows / 2), rows.length - maxRows));
801
- const visible = rows.slice(scrollStart, scrollStart + maxRows);
802
- const W = { model: 34, cost: 12, amount: 12, share: 8 };
803
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
804
- /* @__PURE__ */ jsxs3(Text3, { children: [
805
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
806
- " ",
807
- col("Model", W.model, "left")
808
- ] }),
809
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) }),
810
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Amount", W.amount) }),
811
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Share", W.share) })
812
- ] }),
813
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
814
- visible.map((r, vi) => {
815
- const idx = scrollStart + vi;
816
- const selected = idx === clamped;
817
- const share = total > 0 ? r.usd / total * 100 : 0;
818
- return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
819
- /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "magenta", children: [
820
- selected ? `${glyphs().caretR} ` : " ",
821
- col(r.name, W.model, "left")
822
- ] }),
823
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
824
- /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.requests), W.amount) }),
825
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
826
- ] }) }, r.name);
827
- }),
828
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
829
- /* @__PURE__ */ jsxs3(Text3, { children: [
830
- /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
831
- " ",
832
- col("Total", W.model, "left")
833
- ] }),
834
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellowBright", children: col(currency(total), W.cost) }),
835
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totalAmt), W.amount) }),
836
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col("100%", W.share) })
837
- ] }),
838
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
839
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
840
- "local spend by model (composerData) ",
841
- glyphs().middot,
842
- " est. API-equivalent ",
843
- glyphs().middot,
844
- " ",
845
- clamped + 1,
846
- "/",
847
- rows.length
848
- ] })
849
- ] });
850
- }
851
- function fmtLabel(label) {
852
- if (label.length === 7 && label[4] === "-") {
853
- return `${MONTHS[Number(label.slice(5, 7))]} '${label.slice(2, 4)}`;
854
- }
855
- return shortDate(label);
856
- }
857
-
858
- // src/ui/onboarding.tsx
859
- import { Box as Box4, Text as Text4 } from "ink";
860
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
861
- function Onboarding({ items, cursor, onToggle, onConfirm, heading = "Welcome to tokmon", subheading = "Pick the tools you want to track. You can change this anytime in settings." }) {
862
- const anyEnabled = items.some((it) => it.enabled);
863
- const startIdx = items.length;
864
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
865
- /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: heading }) }),
866
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: subheading }),
867
- /* @__PURE__ */ jsx4(Box4, { height: 1 }),
868
- items.map((it, i) => {
869
- const selected = cursor === i;
870
- const box = it.enabled ? `[${glyphs().check}]` : "[ ]";
871
- return /* @__PURE__ */ jsxs4(ClickableBox, { onClick: () => onToggle(i), children: [
872
- /* @__PURE__ */ jsxs4(Text4, { color: selected ? "green" : void 0, children: [
873
- selected ? glyphs().caretR : " ",
874
- " "
875
- ] }),
876
- /* @__PURE__ */ jsx4(Text4, { bold: it.enabled, color: it.enabled ? it.color : void 0, dimColor: !it.enabled, children: box }),
877
- /* @__PURE__ */ jsxs4(Text4, { color: it.color, children: [
878
- " ",
879
- glyphs().dot,
880
- " "
881
- ] }),
882
- /* @__PURE__ */ jsx4(Box4, { width: 13, flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, wrap: "truncate", children: it.name }) }),
883
- it.detected ? /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: "installed" }) : it.enabled ? /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "manual" }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "not found" })
884
- ] }, it.id);
885
- }),
886
- /* @__PURE__ */ jsx4(Box4, { height: 1 }),
887
- /* @__PURE__ */ jsxs4(ClickableBox, { onClick: onConfirm, children: [
888
- /* @__PURE__ */ jsxs4(Text4, { color: cursor === startIdx ? "green" : void 0, children: [
889
- cursor === startIdx ? glyphs().caretR : " ",
890
- " "
891
- ] }),
892
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? `${glyphs().play} Start tokmon` : `${glyphs().play} Start (nothing selected)` })
893
- ] }),
894
- /* @__PURE__ */ jsx4(Box4, { height: 1 }),
895
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
896
- glyphs().arrowU,
897
- glyphs().arrowD,
898
- " move ",
899
- glyphs().middot,
900
- " space toggle ",
901
- glyphs().middot,
902
- " enter start ",
903
- glyphs().middot,
904
- " q quit"
905
- ] })
906
- ] });
907
- }
908
-
909
- // src/ui/loading.tsx
910
- import { useState as useState2, useEffect as useEffect2 } from "react";
911
- import { Box as Box5, Text as Text5 } from "ink";
912
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
913
- function accountReady(s, providerId) {
914
- if (!s) return false;
915
- const p = PROVIDERS[providerId];
916
- if (p.hasBilling && s.billing?.error) return true;
917
- if (p.hasUsage && !s.dashboard) return false;
918
- if (p.hasBilling && !s.billing) return false;
919
- return true;
920
- }
921
- function groupTodayCost(items) {
922
- return items.reduce((sum, s) => {
923
- const d = s?.dashboard;
924
- return sum + (d?.today.cost ?? 0);
925
- }, 0);
926
- }
927
- function headlineFor(group, items) {
928
- const meta = PROVIDERS[group.provider];
929
- if (meta.hasUsage) return `${currency(groupTodayCost(items))} today`;
930
- const billing = items.map((s) => s?.billing).find(Boolean);
931
- if (!billing) return "no data";
932
- if (billing.error) return billing.error;
933
- const m = billing.metrics[0];
934
- if (m) return metricValueText(m);
935
- return billing.plan ?? "no data";
936
- }
937
- var STAGGER_FRAMES = 2;
938
- function LoadingView({ groups, stats, cols, rows }) {
939
- const sp = glyphs().spinner;
940
- const [frame, setFrame] = useState2(0);
941
- useEffect2(() => {
942
- const id = setInterval(() => setFrame((f) => f + 1), 80);
943
- return () => clearInterval(id);
944
- }, []);
945
- const nameW = Math.min(13, groups.reduce((w, g) => Math.max(w, PROVIDERS[g.provider].name.length), 0));
946
- const readyCount = groups.filter((g) => g.accounts.every((a) => accountReady(stats.get(a.id), g.provider))).length;
947
- const maxRows = Math.max(1, rows - 7);
948
- const visible = groups.slice(0, maxRows);
949
- const hidden = groups.length - visible.length;
950
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
951
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "greenBright", children: [
952
- glyphs().dotSel,
953
- " tokmon"
954
- ] }),
955
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
956
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
957
- "Detecting installed tools",
958
- glyphs().ellipsis
959
- ] }),
960
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
961
- " ",
962
- groups.length,
963
- " found"
964
- ] })
965
- ] }),
966
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
967
- visible.map((g, i) => {
968
- const meta = PROVIDERS[g.provider];
969
- const items = g.accounts.map((a) => stats.get(a.id));
970
- const ready = g.accounts.every((a) => accountReady(stats.get(a.id), g.provider));
971
- const errored = items.some((s) => !!s?.billing?.error);
972
- const revealed = frame >= i * STAGGER_FRAMES;
973
- const name = truncateName(meta.name, nameW);
974
- const namePad = " ".repeat(Math.max(0, nameW - name.length));
975
- if (!revealed) {
976
- return /* @__PURE__ */ jsxs5(Box5, { children: [
977
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
978
- glyphs().dot,
979
- " "
980
- ] }),
981
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
982
- name,
983
- namePad
984
- ] })
985
- ] }, g.provider);
986
- }
987
- return /* @__PURE__ */ jsxs5(Box5, { children: [
988
- /* @__PURE__ */ jsxs5(Text5, { color: meta.color, children: [
989
- glyphs().dot,
990
- " "
991
- ] }),
992
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: meta.color, children: name }),
993
- /* @__PURE__ */ jsx5(Text5, { children: namePad }),
994
- /* @__PURE__ */ jsx5(Text5, { children: " " }),
995
- errored ? /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
996
- glyphs().warn,
997
- " "
998
- ] }) : ready ? /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
999
- glyphs().check,
1000
- " "
1001
- ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
1002
- sp[frame % sp.length],
1003
- " "
1004
- ] }),
1005
- errored ? /* @__PURE__ */ jsx5(Text5, { color: "red", children: headlineFor(g, items) }) : ready ? /* @__PURE__ */ jsx5(Text5, { children: headlineFor(g, items) }) : /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1006
- "loading",
1007
- glyphs().ellipsis
1008
- ] })
1009
- ] }, g.provider);
1010
- }),
1011
- hidden > 0 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1012
- "+",
1013
- hidden,
1014
- " more"
1015
- ] })
1016
- ] }),
1017
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1018
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1019
- "loading ",
1020
- readyCount,
1021
- " / ",
1022
- groups.length
1023
- ] }),
1024
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1025
- " ",
1026
- glyphs().middot,
1027
- " W opens the web dashboard once loaded"
1028
- ] })
1029
- ] })
1030
- ] });
1031
- }
1032
-
1033
- // src/ui/settings.tsx
1034
- import { Box as Box6, Text as Text6 } from "ink";
1035
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1036
- var GENERAL_ROWS = 6;
1037
- var PROVIDER_ROWS_START = GENERAL_ROWS;
1038
- var ACCOUNT_ROWS_START = GENERAL_ROWS + PROVIDER_ORDER.length;
1039
- var FORM_FIELDS = ["provider", "name", "homeDir", "color"];
1040
- var COLOR_PALETTE = [
1041
- "cyan",
1042
- "magenta",
1043
- "green",
1044
- "yellow",
1045
- "blue",
1046
- "red",
1047
- "cyanBright",
1048
- "magentaBright",
1049
- "greenBright"
1050
- ];
1051
- function SettingsView({
1052
- config: config2,
1053
- cursor,
1054
- tzEdit,
1055
- tzError,
1056
- resolvedTz,
1057
- accountForm,
1058
- activeAccountId
1059
- }) {
1060
- if (accountForm) return /* @__PURE__ */ jsx6(AccountFormView, { form: accountForm, accounts: config2.accounts });
1061
- const editingTz = tzEdit !== null;
1062
- const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
1063
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
1064
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Settings" }),
1065
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: configLocation() }),
1066
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1067
- /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "General" }),
1068
- /* @__PURE__ */ jsxs6(Row, { cursor, idx: 0, label: "Refresh interval", children: [
1069
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1070
- glyphs().caretL,
1071
- " "
1072
- ] }),
1073
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
1074
- config2.interval,
1075
- "s"
1076
- ] }),
1077
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1078
- " ",
1079
- glyphs().caretR
1080
- ] })
1081
- ] }),
1082
- /* @__PURE__ */ jsxs6(Row, { cursor, idx: 1, label: "Billing poll", children: [
1083
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1084
- glyphs().caretL,
1085
- " "
1086
- ] }),
1087
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
1088
- config2.billingInterval,
1089
- "m"
1090
- ] }),
1091
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1092
- " ",
1093
- glyphs().caretR
1094
- ] })
1095
- ] }),
1096
- /* @__PURE__ */ jsx6(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx6(Text6, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" }) }),
1097
- /* @__PURE__ */ jsx6(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
1098
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[" }),
1099
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: tzEdit }),
1100
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "_" }),
1101
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "]" })
1102
- ] }) : /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: tzDisplay }) }),
1103
- cursor === 3 && tzError && /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
1104
- " ",
1105
- tzError
1106
- ] }),
1107
- /* @__PURE__ */ jsxs6(Row, { cursor, idx: 4, label: "Dashboard", children: [
1108
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1109
- glyphs().caretL,
1110
- " "
1111
- ] }),
1112
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
1113
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1114
- " ",
1115
- glyphs().caretR
1116
- ] })
1117
- ] }),
1118
- /* @__PURE__ */ jsxs6(Row, { cursor, idx: 5, label: "Default focus", children: [
1119
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1120
- glyphs().caretL,
1121
- " "
1122
- ] }),
1123
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
1124
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1125
- " ",
1126
- glyphs().caretR
1127
- ] })
1128
- ] }),
1129
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1130
- /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Providers" }),
1131
- PROVIDER_ORDER.map((pid, i) => {
1132
- const idx = PROVIDER_ROWS_START + i;
1133
- const selected = cursor === idx;
1134
- const enabled = !config2.disabledProviders.includes(pid);
1135
- const p = PROVIDERS[pid];
1136
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1137
- /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
1138
- selected ? glyphs().caretR : " ",
1139
- " "
1140
- ] }),
1141
- /* @__PURE__ */ jsx6(Text6, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? `[${glyphs().check}]` : "[ ]" }),
1142
- /* @__PURE__ */ jsxs6(Text6, { color: p.color, children: [
1143
- " ",
1144
- glyphs().dot,
1145
- " "
1146
- ] }),
1147
- /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { bold: selected, children: p.name }) }),
1148
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: enabled ? "tracking" : "off" })
1149
- ] }, pid);
1150
- }),
1151
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1152
- /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Accounts" }),
1153
- config2.accounts.length === 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1154
- " none configured ",
1155
- glyphs().emDash,
1156
- " enabled providers track automatically"
1157
- ] }),
1158
- config2.accounts.map((acc, i) => {
1159
- const idx = ACCOUNT_ROWS_START + i;
1160
- const selected = cursor === idx;
1161
- const isActive = acc.id === activeAccountId;
1162
- const provider = PROVIDERS[acc.providerId];
1163
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1164
- /* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
1165
- selected ? glyphs().caretR : " ",
1166
- " "
1167
- ] }),
1168
- /* @__PURE__ */ jsxs6(Text6, { color: acc.color || provider.color, children: [
1169
- isActive ? glyphs().dot : glyphs().radioOff,
1170
- " "
1171
- ] }),
1172
- /* @__PURE__ */ jsx6(Box6, { width: 16, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: truncateName(acc.name, 15) }) }),
1173
- /* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { color: provider.color, children: provider.name }) }),
1174
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: truncateName(acc.homeDir, 24) })
1175
- ] }, acc.id);
1176
- }),
1177
- /* @__PURE__ */ jsxs6(Box6, { children: [
1178
- /* @__PURE__ */ jsxs6(Text6, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
1179
- cursor === ACCOUNT_ROWS_START + config2.accounts.length ? glyphs().caretR : " ",
1180
- " "
1181
- ] }),
1182
- /* @__PURE__ */ jsx6(Text6, { color: "greenBright", children: "+ " }),
1183
- /* @__PURE__ */ jsx6(Text6, { children: "Add account" })
1184
- ] }),
1185
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1186
- editingTz ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1187
- "type IANA name (e.g. Europe/London) ",
1188
- glyphs().middot,
1189
- " empty = System ",
1190
- glyphs().middot,
1191
- " Enter save ",
1192
- glyphs().middot,
1193
- " Esc cancel"
1194
- ] }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1195
- glyphs().arrowU,
1196
- glyphs().arrowD,
1197
- " select ",
1198
- glyphs().middot,
1199
- " space toggle provider ",
1200
- glyphs().middot,
1201
- " s/Esc close"
1202
- ] }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1203
- glyphs().arrowU,
1204
- glyphs().arrowD,
1205
- " select ",
1206
- glyphs().middot,
1207
- " ",
1208
- glyphs().shift,
1209
- glyphs().arrowU,
1210
- glyphs().arrowD,
1211
- " reorder ",
1212
- glyphs().middot,
1213
- " Enter edit ",
1214
- glyphs().middot,
1215
- " space activate ",
1216
- glyphs().middot,
1217
- " d delete ",
1218
- glyphs().middot,
1219
- " s/Esc close"
1220
- ] }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1221
- glyphs().arrowU,
1222
- glyphs().arrowD,
1223
- " select ",
1224
- glyphs().middot,
1225
- " Enter add account ",
1226
- glyphs().middot,
1227
- " s/Esc close"
1228
- ] }) : /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1229
- glyphs().arrowU,
1230
- glyphs().arrowD,
1231
- " select ",
1232
- glyphs().arrowL,
1233
- glyphs().arrowR,
1234
- " adjust Enter edit s/Esc close"
1235
- ] })
1236
- ] });
1237
- }
1238
- function Row({ cursor, idx, label, children }) {
1239
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1240
- /* @__PURE__ */ jsxs6(Text6, { color: cursor === idx ? "green" : void 0, children: [
1241
- cursor === idx ? glyphs().caretR : " ",
1242
- " "
1243
- ] }),
1244
- /* @__PURE__ */ jsx6(Box6, { width: 20, children: /* @__PURE__ */ jsx6(Text6, { children: label }) }),
1245
- children
1246
- ] });
1247
- }
1248
- function AccountFormView({ form, accounts }) {
1249
- const previewId = form.mode === "add" ? generateAccountId(form.name || "account", accounts) : form.editingId ?? "";
1250
- const accent = form.color;
1251
- const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
1252
- const step = stepIndex[form.field];
1253
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
1254
- /* @__PURE__ */ jsxs6(Box6, { children: [
1255
- /* @__PURE__ */ jsx6(Text6, { color: accent, bold: true, children: glyphs().vbar }),
1256
- /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
1257
- " ",
1258
- form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
1259
- ] }),
1260
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1261
- " step ",
1262
- step,
1263
- " of 4"
1264
- ] })
1265
- ] }),
1266
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Stepper, { active: form.field, accent }) }),
1267
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", borderStyle: glyphs().border, borderColor: accent, paddingX: 2, paddingY: 1, children: [
1268
- /* @__PURE__ */ jsx6(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
1269
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1270
- /* @__PURE__ */ jsx6(
1271
- FormField,
1272
- {
1273
- label: "Name",
1274
- hint: "display name for this account",
1275
- value: form.name,
1276
- focused: form.field === "name",
1277
- accent,
1278
- placeholder: "e.g. Work, Personal"
1279
- }
1280
- ),
1281
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1282
- /* @__PURE__ */ jsx6(
1283
- FormField,
1284
- {
1285
- label: "Home directory",
1286
- hint: `path containing the tool's data dir ${glyphs().middot} ~ for default`,
1287
- value: form.homeDir,
1288
- focused: form.field === "homeDir",
1289
- accent,
1290
- placeholder: "~/work",
1291
- mono: true
1292
- }
1293
- ),
1294
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1295
- /* @__PURE__ */ jsx6(ColorField, { value: form.color, focused: form.field === "color" }),
1296
- /* @__PURE__ */ jsx6(Box6, { height: 1 }),
1297
- /* @__PURE__ */ jsxs6(Box6, { children: [
1298
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1299
- "id ",
1300
- glyphs().boxMark,
1301
- " "
1302
- ] }),
1303
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: accent, children: previewId || "account" }),
1304
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1305
- " ",
1306
- glyphs().boxMark,
1307
- " auto-generated from name"
1308
- ] })
1309
- ] })
1310
- ] }),
1311
- form.error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
1312
- glyphs().warn,
1313
- " ",
1314
- form.error
1315
- ] }) }),
1316
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1317
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1318
- "tab/",
1319
- glyphs().arrowU,
1320
- glyphs().arrowD,
1321
- " "
1322
- ] }),
1323
- /* @__PURE__ */ jsx6(Text6, { children: "switch field" }),
1324
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1325
- " ",
1326
- glyphs().middot,
1327
- " "
1328
- ] }),
1329
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "enter " }),
1330
- /* @__PURE__ */ jsx6(Text6, { children: form.field === "color" ? "save" : "next" }),
1331
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1332
- " ",
1333
- glyphs().middot,
1334
- " "
1335
- ] }),
1336
- (form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1337
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1338
- glyphs().arrowL,
1339
- glyphs().arrowR,
1340
- " "
1341
- ] }),
1342
- /* @__PURE__ */ jsx6(Text6, { children: form.field === "provider" ? "pick provider" : "pick color" }),
1343
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1344
- " ",
1345
- glyphs().middot,
1346
- " "
1347
- ] })
1348
- ] }),
1349
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "esc " }),
1350
- /* @__PURE__ */ jsx6(Text6, { children: "cancel" })
1351
- ] })
1352
- ] });
1353
- }
1354
- function Stepper({ active: active2, accent }) {
1355
- const steps = [
1356
- { id: "provider", label: "Provider" },
1357
- { id: "name", label: "Name" },
1358
- { id: "homeDir", label: "Home" },
1359
- { id: "color", label: "Color" }
1360
- ];
1361
- const activeIdx = steps.findIndex((s) => s.id === active2);
1362
- return /* @__PURE__ */ jsx6(Box6, { children: steps.map((s, i) => {
1363
- const done = i < activeIdx;
1364
- const cur = i === activeIdx;
1365
- const dot = done ? glyphs().dot : cur ? glyphs().dotSel : glyphs().radioOff;
1366
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1367
- /* @__PURE__ */ jsxs6(Text6, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
1368
- dot,
1369
- " "
1370
- ] }),
1371
- /* @__PURE__ */ jsx6(Text6, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
1372
- i < steps.length - 1 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1373
- " ",
1374
- glyphs().rule,
1375
- " "
1376
- ] })
1377
- ] }, s.id);
1378
- }) });
1379
- }
1380
- function ProviderField({ value, focused }) {
1381
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1382
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
1383
- focused ? glyphs().caretR : " ",
1384
- " Provider"
1385
- ] }) }),
1386
- /* @__PURE__ */ jsxs6(Box6, { children: [
1387
- /* @__PURE__ */ jsxs6(Text6, { children: [
1388
- " ",
1389
- focused ? glyphs().vbar : " ",
1390
- " "
1391
- ] }),
1392
- PROVIDER_ORDER.map((pid) => {
1393
- const selected = pid === value;
1394
- const p = PROVIDERS[pid];
1395
- return /* @__PURE__ */ jsx6(Box6, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: p.color, children: [
1396
- "[",
1397
- p.name,
1398
- "]"
1399
- ] }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: p.name }) }, pid);
1400
- })
1401
- ] }),
1402
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " which tool this account tracks" }) })
1403
- ] });
1404
- }
1405
- function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
1406
- const isPlaceholder = value === "";
1407
- const display = isPlaceholder ? placeholder : value;
1408
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1409
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
1410
- focused ? glyphs().caretR : " ",
1411
- " ",
1412
- label
1413
- ] }) }),
1414
- /* @__PURE__ */ jsxs6(Box6, { children: [
1415
- /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, children: [
1416
- " ",
1417
- focused ? glyphs().vbar : " ",
1418
- " "
1419
- ] }),
1420
- /* @__PURE__ */ jsx6(
1421
- Text6,
1422
- {
1423
- bold: focused && !isPlaceholder,
1424
- color: focused && !isPlaceholder ? accent : void 0,
1425
- dimColor: isPlaceholder,
1426
- italic: mono && isPlaceholder,
1427
- children: display
1428
- }
1429
- ),
1430
- focused && /* @__PURE__ */ jsx6(Text6, { color: accent, children: glyphs().vbar })
1431
- ] }),
1432
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1433
- " ",
1434
- hint
1435
- ] }) })
1436
- ] });
1437
- }
1438
- function ColorField({ value, focused }) {
1439
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1440
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
1441
- focused ? glyphs().caretR : " ",
1442
- " Accent color"
1443
- ] }) }),
1444
- /* @__PURE__ */ jsxs6(Box6, { children: [
1445
- /* @__PURE__ */ jsxs6(Text6, { children: [
1446
- " ",
1447
- focused ? glyphs().vbar : " ",
1448
- " "
1449
- ] }),
1450
- COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx6(Box6, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: c, children: [
1451
- "[",
1452
- glyphs().dot,
1453
- "]"
1454
- ] }) : /* @__PURE__ */ jsxs6(Text6, { color: c, dimColor: !focused, children: [
1455
- " ",
1456
- glyphs().dot
1457
- ] }) }, c))
1458
- ] }),
1459
- /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
1460
- ] });
1461
- }
1462
-
1463
- // src/app.tsx
1464
- import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1465
- var TABS = ["Dashboard", "Table"];
1466
- var VIEWS = ["Daily", "Weekly", "Monthly"];
1467
- var SORTS = [
1468
- { label: "date", dir: "up" },
1469
- { label: "date", dir: "down" },
1470
- { label: "cost", dir: "up" },
1471
- { label: "cost", dir: "down" }
1472
- ];
1473
- var CURSOR_SORTS = [
1474
- { label: "cost", dir: "down" },
1475
- { label: "amount", dir: "down" },
1476
- { label: "model", dir: null }
1477
- ];
1478
- var IS_TTY = process.stdin.isTTY === true;
1479
- var REPO_URL = "https://github.com/DavidIlie/tokmon";
1480
- var SITE_URL = "https://davidilie.com";
1481
- var IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === "Apple_Terminal";
1482
- function detectHyperlinks(env, isTTY) {
1483
- const force = env.FORCE_HYPERLINK;
1484
- if (force != null && force !== "") return force !== "0" && force.toLowerCase() !== "false";
1485
- if (!isTTY || env.TERM === "dumb" || env.NO_HYPERLINK) return false;
1486
- if (env.WT_SESSION || env.ConEmuANSI === "ON" || env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
1487
- if (env.KONSOLE_VERSION || env.TERMINAL_EMULATOR === "JetBrains-JediTerm") return true;
1488
- if (env.VTE_VERSION && Number(env.VTE_VERSION) >= 5e3) return true;
1489
- const tp = env.TERM_PROGRAM;
1490
- if (tp) {
1491
- const [maj, min] = (env.TERM_PROGRAM_VERSION ?? "").split(".").map((n) => Number(n) || 0);
1492
- if (tp === "iTerm.app") return maj > 3 || maj === 3 && min >= 1;
1493
- if (tp === "vscode" || tp === "WezTerm" || tp === "ghostty" || tp === "Hyper" || tp === "Tabby" || tp === "rio") return true;
1494
- }
1495
- return false;
1496
- }
1497
- var HYPERLINKS = detectHyperlinks(process.env, process.stdout.isTTY === true);
1498
- function openUrl(url) {
1499
- if (process.env.TOKMON_OPENLOG) {
1500
- try {
1501
- appendFileSync2(process.env.TOKMON_OPENLOG, url + "\n");
1502
- } catch {
1503
- }
1504
- return;
1505
- }
1506
- try {
1507
- if (process.platform === "darwin") spawn("open", [url], { stdio: "ignore", detached: true }).unref();
1508
- else if (process.platform === "win32") spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
1509
- else spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
1510
- } catch {
1511
- }
1512
- }
1513
- function osc8(text, url) {
1514
- if (!HYPERLINKS) return text;
1515
- return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
1516
- }
1517
- var DEBOUNCE_MS = 300;
1518
- var LOADER_GRACE_MS = 600;
1519
- var LOADER_MAX_MS = 8e3;
1520
- var LOADER_MIN_VISIBLE_MS = 700;
1521
- var DEFAULT_CONFIG = {
1522
- interval: 2,
1523
- billingInterval: 5,
1524
- clearScreen: true,
1525
- timezone: null,
1526
- accounts: [],
1527
- activeAccountId: null,
1528
- disabledProviders: [],
1529
- onboarded: false,
1530
- dashboardLayout: "grid",
1531
- defaultFocus: "all",
1532
- ascii: "auto",
1533
- knownProviders: []
1534
- };
1535
- function applyStartup(c, cliInterval) {
1536
- if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
1537
- if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
1538
- return c;
1539
- }
1540
- function useTerminalSize(settleMs = 90) {
1541
- const { stdout } = useStdout();
1542
- const read = () => ({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
1543
- const [size, setSize] = useState3(read);
1544
- const [live, setLive] = useState3(read);
1545
- const [resizing, setResizing] = useState3(false);
1546
- useEffect3(() => {
1547
- if (!stdout) return;
1548
- let t;
1549
- const now = () => ({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
1550
- const settle = () => {
1551
- setSize(now());
1552
- setResizing(false);
1553
- };
1554
- const onResize = () => {
1555
- setLive(now());
1556
- setResizing(true);
1557
- if (t) clearTimeout(t);
1558
- t = setTimeout(settle, settleMs);
1559
- };
1560
- stdout.on("resize", onResize);
1561
- return () => {
1562
- if (t) clearTimeout(t);
1563
- stdout.off("resize", onResize);
1564
- };
1565
- }, [stdout, settleMs]);
1566
- return { cols: size.cols, rows: size.rows, resizing, live };
1567
- }
1568
- function ResizingView({ cols, rows }) {
1569
- return /* @__PURE__ */ jsx7(Box7, { width: cols, height: rows, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1570
- glyphs().dotSel,
1571
- " resizing\u2026 ",
1572
- /* @__PURE__ */ jsx7(Text7, { color: "greenBright", children: cols }),
1573
- "\xD7",
1574
- /* @__PURE__ */ jsx7(Text7, { color: "greenBright", children: rows })
1575
- ] }) });
1576
- }
1577
- function App({ interval: cliInterval, initialConfig }) {
1578
- const [config2, setConfig] = useState3(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
1579
- const [detected, setDetected] = useState3([]);
1580
- const [stats, setStats] = useState3(/* @__PURE__ */ new Map());
1581
- const [peak, setPeak] = useState3(null);
1582
- const [table, setTable] = useState3(null);
1583
- const [tableLoading, setTableLoading] = useState3(false);
1584
- const [error, setError] = useState3(null);
1585
- const [updated, setUpdated] = useState3(/* @__PURE__ */ new Date());
1586
- const [tab, setTab] = useState3(0);
1587
- const [view, setView] = useState3(0);
1588
- const [cursor, setCursor] = useState3(0);
1589
- const [expanded, setExpanded] = useState3(-1);
1590
- const [sort, setSort] = useState3(1);
1591
- const [tableProvider, setTableProvider] = useState3(null);
1592
- const [search, setSearch] = useState3("");
1593
- const [searchMode, setSearchMode] = useState3(false);
1594
- const [cursorRows, setCursorRows] = useState3(null);
1595
- const [showSettings, setShowSettings] = useState3(false);
1596
- const [settingsCursor, setSettingsCursor] = useState3(0);
1597
- const [tzEdit, setTzEdit] = useState3(null);
1598
- const [tzError, setTzError] = useState3(null);
1599
- const [accountForm, setAccountForm] = useState3(null);
1600
- const [onboardSel, setOnboardSel] = useState3(null);
1601
- const [onboardCursor, setOnboardCursor] = useState3(0);
1602
- const [dashPage, setDashPage] = useState3(0);
1603
- const [debouncePassed, setDebouncePassed] = useState3(false);
1604
- const [graceHold, setGraceHold] = useState3(false);
1605
- const [loaderShownAt, setLoaderShownAt] = useState3(null);
1606
- const loaderDone = useRef2(false);
1607
- const prevShowPicker = useRef2(false);
1608
- const { exit } = useApp();
1609
- const { cols, rows, resizing, live } = useTerminalSize();
1610
- const webRef = useRef2(null);
1611
- const webBusyRef = useRef2(false);
1612
- const webCooldownRef = useRef2(0);
1613
- const [webUrl, setWebUrl] = useState3(null);
1614
- const [webStatus, setWebStatus] = useState3("off");
1615
- useEffect3(() => () => {
1616
- void webRef.current?.stop();
1617
- }, []);
1618
- const cfg = config2 ?? DEFAULT_CONFIG;
1619
- const interval2 = cliInterval ?? cfg.interval * 1e3;
1620
- const billingMs = cfg.billingInterval * 6e4;
1621
- const tz = resolveTimezone(cfg.timezone);
1622
- const configReady = config2 !== null;
1623
- const accounts = useMemo(() => buildAccounts(cfg, detected), [cfg, detected]);
1624
- const accountsRef = useRef2([]);
1625
- accountsRef.current = accounts;
1626
- const rowCountRef = useRef2(0);
1627
- const dashPageCountRef = useRef2(1);
1628
- const seededRef = useRef2(false);
1629
- const accountsKey = accounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join("|");
1630
- const slots = accounts.length > 1 ? [{ id: null, name: "All", color: "whiteBright" }, ...accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }))] : accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }));
1631
- const activeSlotIdx = (() => {
1632
- if (cfg.activeAccountId === null) return 0;
1633
- const i = slots.findIndex((s) => s.id === cfg.activeAccountId);
1634
- return i < 0 ? 0 : i;
1635
- })();
1636
- const focusId = slots[activeSlotIdx]?.id ?? null;
1637
- const visibleAccounts = focusId === null ? accounts : accounts.filter((a) => a.id === focusId);
1638
- const groups = accountsByProvider(visibleAccounts);
1639
- const TOO_SMALL = cols < 40 || rows < 12;
1640
- const allGroups = accountsByProvider(accounts);
1641
- const allReady = accounts.length > 0 && accounts.every((a) => accountReady(stats.get(a.id), a.providerId));
1642
- const hasStrip = slots.length > 1;
1643
- const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
1644
- const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
1645
- const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(1, cols - 4 - 7))) : 0;
1646
- const headerRows = cols < 70 ? 2 : 1;
1647
- const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2 + 2;
1648
- const gridBudget = Math.max(1, rows - CHROME);
1649
- const dashLayout = useMemo(() => chooseLayout(
1650
- Math.max(56, cols - 4),
1651
- gridBudget,
1652
- groups.length,
1653
- focusId !== null || cfg.dashboardLayout === "single",
1654
- cols
1655
- ), [cols, gridBudget, groups.length, focusId, cfg.dashboardLayout]);
1656
- const dashPageCount = dashLayout.pageCount;
1657
- const dashPaginated = dashPageCount > 1;
1658
- dashPageCountRef.current = dashPageCount;
1659
- const tableProvs = accountsByProvider(accounts).map((g) => g.provider);
1660
- const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
1661
- const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
1662
- const tableAccounts = effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [];
1663
- const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
1664
- const needsOnboarding = configReady && !cfg.onboarded;
1665
- const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
1666
- const showPicker = needsOnboarding || newProviders.length > 0;
1667
- const minVisibleHold = loaderShownAt !== null && Date.now() - loaderShownAt < LOADER_MIN_VISIBLE_MS;
1668
- const showLoader = configReady && !showPicker && !showSettings && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDone.current;
1669
- const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
1670
- const onboardEnabled = onboardSel ?? detected;
1671
- const onboardItems = pickerProviders.map((pid) => ({
1672
- id: pid,
1673
- name: PROVIDERS[pid].name,
1674
- color: PROVIDERS[pid].color,
1675
- detected: detected.includes(pid),
1676
- enabled: onboardEnabled.includes(pid)
1677
- }));
1678
- useEffect3(() => {
1679
- const wasPicker = prevShowPicker.current;
1680
- prevShowPicker.current = showPicker;
1681
- if (wasPicker && !showPicker) {
1682
- loaderDone.current = false;
1683
- setDebouncePassed(false);
1684
- setGraceHold(false);
1685
- setLoaderShownAt(null);
1686
- }
1687
- }, [showPicker]);
1688
- useEffect3(() => {
1689
- if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
1690
- }, [showLoader, loaderShownAt]);
1691
- useEffect3(() => {
1692
- if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
1693
- detectProviders().then(setDetected);
1694
- }, []);
1695
- useEffect3(() => {
1696
- if (seededRef.current || !configReady || showPicker || accounts.length === 0) return;
1697
- seededRef.current = true;
1698
- loadSnapshot().then((snap) => {
1699
- setStats((prev) => {
1700
- if (prev.size > 0) return prev;
1701
- const next = new Map(prev);
1702
- for (const acc of accountsRef.current) {
1703
- const s = snap[acc.id];
1704
- if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
1705
- }
1706
- return next;
1707
- });
1708
- });
1709
- }, [configReady, showPicker, accountsKey]);
1710
- useEffect3(() => {
1711
- if (stats.size === 0) return;
1712
- const t = setTimeout(() => saveSnapshot(stats), 500);
1713
- return () => clearTimeout(t);
1714
- }, [stats]);
1715
- useEffect3(() => {
1716
- if (!configReady || showPicker || accounts.length === 0) return;
1717
- if (allReady || loaderDone.current) return;
1718
- const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
1719
- const deadline = setTimeout(() => {
1720
- loaderDone.current = true;
1721
- setDebouncePassed(false);
1722
- }, LOADER_MAX_MS);
1723
- return () => {
1724
- clearTimeout(debounce);
1725
- clearTimeout(deadline);
1726
- };
1727
- }, [configReady, showPicker, accountsKey]);
1728
- useEffect3(() => {
1729
- if (!allReady || loaderDone.current) return;
1730
- if (loaderShownAt === null) {
1731
- loaderDone.current = true;
1732
- return;
1733
- }
1734
- setGraceHold(true);
1735
- const minRemaining = Math.max(0, LOADER_MIN_VISIBLE_MS - (Date.now() - loaderShownAt));
1736
- const hold = Math.max(LOADER_GRACE_MS, minRemaining);
1737
- const t = setTimeout(() => {
1738
- loaderDone.current = true;
1739
- setGraceHold(false);
1740
- }, hold);
1741
- return () => clearTimeout(t);
1742
- }, [allReady]);
1743
- useEffect3(() => {
1744
- if (!configReady || showPicker) return;
1745
- let active2 = true;
1746
- let timer;
1747
- const load = async () => {
1748
- try {
1749
- await Promise.all(accountsRef.current.map(async (acc) => {
1750
- const provider = PROVIDERS[acc.providerId];
1751
- if (!provider.hasUsage || !provider.fetchSummary) return;
1752
- try {
1753
- const dashboard = await provider.fetchSummary(acc, tz);
1754
- if (active2) setStats((prev) => upsert(prev, acc, { dashboard }));
1755
- } catch {
1756
- }
1757
- }));
1758
- if (active2) {
1759
- setError(null);
1760
- setUpdated(/* @__PURE__ */ new Date());
1761
- }
1762
- } finally {
1763
- if (active2) timer = setTimeout(load, interval2);
1764
- }
1765
- };
1766
- load();
1767
- return () => {
1768
- active2 = false;
1769
- clearTimeout(timer);
1770
- };
1771
- }, [interval2, tz, configReady, showPicker, accountsKey]);
1772
- useEffect3(() => {
1773
- if (!configReady || showPicker) return;
1774
- let active2 = true;
1775
- let timer;
1776
- const load = async () => {
1777
- try {
1778
- const peakP = accountsRef.current.some((a) => a.providerId === "claude") ? fetchPeak() : Promise.resolve(null);
1779
- await Promise.all(accountsRef.current.map(async (acc) => {
1780
- const provider = PROVIDERS[acc.providerId];
1781
- if (!provider.hasBilling || !provider.fetchBilling) return;
1782
- try {
1783
- const billing = await provider.fetchBilling(acc);
1784
- if (active2) setStats((prev) => upsert(prev, acc, { billing }));
1785
- } catch {
1786
- }
1787
- }));
1788
- const p = await peakP;
1789
- if (active2 && p) setPeak(p);
1790
- } finally {
1791
- if (active2) timer = setTimeout(load, billingMs);
1792
- }
1793
- };
1794
- load();
1795
- return () => {
1796
- active2 = false;
1797
- clearTimeout(timer);
1798
- };
1799
- }, [billingMs, configReady, showPicker, accountsKey]);
1800
- const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
1801
- useEffect3(() => {
1802
- setTable(null);
1803
- setCursorRows(null);
1804
- setCursor(0);
1805
- setExpanded(-1);
1806
- setSort(tableIsCursor ? 0 : 1);
1807
- }, [tableKey]);
1808
- useEffect3(() => {
1809
- if (tab !== 1 || !effTableProvider) return;
1810
- let active2 = true;
1811
- let timer;
1812
- const fetchOnce = async () => {
1813
- try {
1814
- if (tableIsCursor) {
1815
- const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
1816
- if (active2) setCursorRows(s?.models ?? []);
1817
- } else {
1818
- const r = await fetchScopeTable(tableAccounts, tz);
1819
- if (active2) setTable(r);
1820
- }
1821
- } catch {
1822
- }
1823
- };
1824
- const run = async () => {
1825
- setTableLoading(true);
1826
- await fetchOnce();
1827
- if (!active2) return;
1828
- setTableLoading(false);
1829
- const loop = async () => {
1830
- await fetchOnce();
1831
- if (active2) timer = setTimeout(loop, Math.max(interval2, 1e4));
1832
- };
1833
- timer = setTimeout(loop, Math.max(interval2, 1e4));
1834
- };
1835
- run();
1836
- return () => {
1837
- active2 = false;
1838
- clearTimeout(timer);
1839
- };
1840
- }, [tab, tableKey, interval2]);
1841
- useEffect3(() => {
1842
- setCursor(0);
1843
- setExpanded(-1);
1844
- }, [search]);
1845
- useEffect3(() => {
1846
- setDashPage((p) => Math.min(p, dashPageCount - 1));
1847
- }, [dashPageCount]);
1848
- const resetView = useCallback(() => {
1849
- setCursor(0);
1850
- setExpanded(-1);
1851
- }, []);
1852
- const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
1853
- const mouse = useMouse();
1854
- useEffect3(() => {
1855
- if (!IS_TTY) return;
1856
- mouse.enable();
1857
- const onScroll = (_pos, dir) => {
1858
- const up = dir === "scrollup";
1859
- if (tab === 1) {
1860
- setCursor((c) => up ? Math.max(0, c - 3) : c + 3);
1861
- } else if (tab === 0 && dashPageCountRef.current > 1) {
1862
- setDashPage((p) => up ? Math.max(0, p - 1) : Math.min(dashPageCountRef.current - 1, p + 1));
1863
- }
1864
- };
1865
- mouse.events.on("scroll", onScroll);
1866
- const onData = (d) => dispatchLinkClicks(d);
1867
- process.stdin.on("data", onData);
1868
- return () => {
1869
- mouse.events.off("scroll", onScroll);
1870
- process.stdin.off("data", onData);
1871
- };
1872
- }, [tab]);
1873
- function updateConfig(fn) {
1874
- setConfig((prev) => {
1875
- const next = fn(prev ?? DEFAULT_CONFIG);
1876
- saveConfig(next);
1877
- return next;
1878
- });
1879
- }
1880
- function toggleOnboard(i) {
1881
- if (i < 0 || i >= pickerProviders.length) return;
1882
- const pid = pickerProviders[i];
1883
- setOnboardSel((prev) => {
1884
- const base = prev ?? detected;
1885
- return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
1886
- });
1887
- }
1888
- function toggleProvider(pid) {
1889
- updateConfig((c) => ({
1890
- ...c,
1891
- knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
1892
- disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
1893
- }));
1894
- }
1895
- function confirmOnboarding() {
1896
- const enabled = onboardEnabled;
1897
- updateConfig((c) => {
1898
- if (!c.onboarded) {
1899
- return {
1900
- ...c,
1901
- disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
1902
- knownProviders: [...PROVIDER_ORDER],
1903
- onboarded: true
1904
- };
1905
- }
1906
- const newlyDisabled = pickerProviders.filter((p) => !enabled.includes(p));
1907
- return {
1908
- ...c,
1909
- disabledProviders: [.../* @__PURE__ */ new Set([...c.disabledProviders, ...newlyDisabled])],
1910
- knownProviders: [.../* @__PURE__ */ new Set([...c.knownProviders, ...pickerProviders])]
1911
- };
1912
- });
1913
- setOnboardSel(null);
1914
- setOnboardCursor(0);
1915
- }
1916
- function cycleAccount(dir) {
1917
- if (slots.length <= 1) return;
1918
- const next = (activeSlotIdx + dir + slots.length) % slots.length;
1919
- updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
1920
- resetView();
1921
- }
1922
- function cycleTableProvider(dir) {
1923
- if (tableProvs.length <= 1) return;
1924
- const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
1925
- setTableProvider(tableProvs[(cur + dir + tableProvs.length) % tableProvs.length]);
1926
- setCursor(0);
1927
- setExpanded(-1);
1928
- setSearch("");
1929
- setSearchMode(false);
1930
- }
1931
- function openAddAccount() {
1932
- const providerId = detected[0] ?? "claude";
1933
- setAccountForm({
1934
- mode: "add",
1935
- field: "provider",
1936
- providerId,
1937
- name: "",
1938
- homeDir: "~",
1939
- color: pickAccentColor(cfg.accounts),
1940
- editingId: null,
1941
- error: null
1942
- });
1943
- }
1944
- function openEditAccount(acc) {
1945
- setAccountForm({
1946
- mode: "edit",
1947
- field: "provider",
1948
- providerId: acc.providerId,
1949
- name: acc.name,
1950
- homeDir: acc.homeDir,
1951
- color: acc.color || PROVIDERS[acc.providerId].color,
1952
- editingId: acc.id,
1953
- error: null
1954
- });
1955
- }
1956
- function commitAccountForm() {
1957
- if (!accountForm) return;
1958
- const name = accountForm.name.trim();
1959
- const homeDir = accountForm.homeDir.trim() || "~";
1960
- if (!name) {
1961
- setAccountForm({ ...accountForm, error: "Name required", field: "name" });
1962
- return;
1963
- }
1964
- updateConfig((c) => {
1965
- if (accountForm.mode === "add") {
1966
- const id = generateAccountId(name, c.accounts);
1967
- const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
1968
- return { ...c, accounts: [...c.accounts, account] };
1969
- }
1970
- return {
1971
- ...c,
1972
- accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
1973
- };
1974
- });
1975
- setAccountForm(null);
1976
- }
1977
- function cycleFormField(dir) {
1978
- setAccountForm((f) => {
1979
- if (!f) return f;
1980
- const i = FORM_FIELDS.indexOf(f.field);
1981
- return { ...f, field: FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length] };
1982
- });
1983
- }
1984
- function cycleProvider(dir) {
1985
- setAccountForm((f) => {
1986
- if (!f) return f;
1987
- const i = PROVIDER_ORDER.indexOf(f.providerId);
1988
- return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
1989
- });
1990
- }
1991
- function cycleColor(dir) {
1992
- setAccountForm((f) => {
1993
- if (!f) return f;
1994
- const i = COLOR_PALETTE.indexOf(f.color);
1995
- const idx = i < 0 ? 0 : i;
1996
- return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
1997
- });
1998
- }
1999
- function deleteAccount(id) {
2000
- updateConfig((c) => ({
2001
- ...c,
2002
- accounts: c.accounts.filter((a) => a.id !== id),
2003
- activeAccountId: c.activeAccountId === id ? null : c.activeAccountId
2004
- }));
2005
- }
2006
- function moveAccount(idx, dir) {
2007
- updateConfig((c) => {
2008
- const next = [...c.accounts];
2009
- const target = idx + dir;
2010
- if (target < 0 || target >= next.length) return c;
2011
- [next[idx], next[target]] = [next[target], next[idx]];
2012
- return { ...c, accounts: next };
2013
- });
2014
- setSettingsCursor((c) => Math.max(ACCOUNT_ROWS_START, Math.min(ACCOUNT_ROWS_START + cfg.accounts.length - 1, c + dir)));
2015
- }
2016
- const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
2017
- async function toggleWeb() {
2018
- if (webBusyRef.current || Date.now() < webCooldownRef.current) return;
2019
- webBusyRef.current = true;
2020
- try {
2021
- if (webRef.current) {
2022
- setWebStatus("stopping");
2023
- const ctrl = webRef.current;
2024
- webRef.current = null;
2025
- await ctrl.stop();
2026
- setWebUrl(null);
2027
- setWebStatus("off");
2028
- } else {
2029
- setWebStatus("starting");
2030
- const { startWebServer } = await import("./server-VMB5ZLZC.js");
2031
- const ctrl = await startWebServer({ config: cfg, log: false });
2032
- webRef.current = ctrl;
2033
- setWebUrl(ctrl.url);
2034
- setWebStatus("on");
2035
- openUrl(ctrl.url);
2036
- }
2037
- } catch {
2038
- setWebStatus(webRef.current ? "on" : "off");
2039
- } finally {
2040
- webBusyRef.current = false;
2041
- webCooldownRef.current = Date.now() + 600;
2042
- }
2043
- }
2044
- useInput((input, key) => {
2045
- if (showPicker) {
2046
- if (input === "q") {
2047
- exit();
2048
- return;
2049
- }
2050
- const startIdx = pickerProviders.length;
2051
- if (key.upArrow) {
2052
- setOnboardCursor((c) => Math.max(0, c - 1));
2053
- return;
2054
- }
2055
- if (key.downArrow) {
2056
- setOnboardCursor((c) => Math.min(startIdx, c + 1));
2057
- return;
2058
- }
2059
- if (input === " ") {
2060
- toggleOnboard(onboardCursor);
2061
- return;
2062
- }
2063
- if (key.return) {
2064
- if (onboardCursor === startIdx) confirmOnboarding();
2065
- else toggleOnboard(onboardCursor);
2066
- return;
2067
- }
2068
- return;
2069
- }
2070
- if (showSettings && accountForm) {
2071
- if (key.escape) {
2072
- setAccountForm(null);
2073
- return;
2074
- }
2075
- if (key.tab) {
2076
- cycleFormField(key.shift ? -1 : 1);
2077
- return;
2078
- }
2079
- if (key.upArrow) {
2080
- cycleFormField(-1);
2081
- return;
2082
- }
2083
- if (key.downArrow) {
2084
- cycleFormField(1);
2085
- return;
2086
- }
2087
- if (accountForm.field === "provider") {
2088
- if (key.leftArrow) {
2089
- cycleProvider(-1);
2090
- return;
2091
- }
2092
- if (key.rightArrow) {
2093
- cycleProvider(1);
2094
- return;
2095
- }
2096
- if (key.return) {
2097
- setAccountForm((f) => f && { ...f, field: "name" });
2098
- return;
2099
- }
2100
- return;
2101
- }
2102
- if (accountForm.field === "color") {
2103
- if (key.leftArrow) {
2104
- cycleColor(-1);
2105
- return;
2106
- }
2107
- if (key.rightArrow) {
2108
- cycleColor(1);
2109
- return;
2110
- }
2111
- if (key.return) {
2112
- commitAccountForm();
2113
- return;
2114
- }
2115
- return;
2116
- }
2117
- if (key.return) {
2118
- setAccountForm((f) => f && { ...f, field: f.field === "name" ? "homeDir" : "color" });
2119
- return;
2120
- }
2121
- if (key.backspace || key.delete) {
2122
- setAccountForm((f) => {
2123
- if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2124
- return { ...f, [f.field]: f[f.field].slice(0, -1), error: null };
2125
- });
2126
- return;
2127
- }
2128
- if (input && !key.ctrl && !key.meta) {
2129
- setAccountForm((f) => {
2130
- if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2131
- return { ...f, [f.field]: f[f.field] + input, error: null };
2132
- });
2133
- }
2134
- return;
2135
- }
2136
- if (showSettings && tzEdit !== null) {
2137
- if (key.escape) {
2138
- setTzEdit(null);
2139
- setTzError(null);
2140
- return;
2141
- }
2142
- if (key.return) {
2143
- const val = tzEdit.trim();
2144
- if (val === "" || val.toLowerCase() === "system") {
2145
- updateConfig((c) => ({ ...c, timezone: null }));
2146
- setTzEdit(null);
2147
- setTzError(null);
2148
- } else if (isValidTimezone(val)) {
2149
- updateConfig((c) => ({ ...c, timezone: val }));
2150
- setTzEdit(null);
2151
- setTzError(null);
2152
- } else {
2153
- setTzError(`Invalid: ${val}`);
2154
- }
2155
- return;
2156
- }
2157
- if (key.backspace || key.delete) {
2158
- setTzEdit((s) => (s ?? "").slice(0, -1));
2159
- setTzError(null);
2160
- return;
2161
- }
2162
- if (input && !key.ctrl && !key.meta) {
2163
- setTzEdit((s) => (s ?? "") + input);
2164
- setTzError(null);
2165
- }
2166
- return;
2167
- }
2168
- if (tab === 1 && searchMode) {
2169
- if (key.return || key.escape) {
2170
- setSearchMode(false);
2171
- if (key.escape) setSearch("");
2172
- return;
2173
- }
2174
- if (key.backspace || key.delete) {
2175
- setSearch((s) => s.slice(0, -1));
2176
- return;
2177
- }
2178
- if (input && !key.ctrl && !key.meta) {
2179
- setSearch((s) => s + input);
2180
- }
2181
- return;
2182
- }
2183
- if (input === "q") {
2184
- exit();
2185
- return;
2186
- }
2187
- if (input === "O") {
2188
- openUrl(REPO_URL);
2189
- return;
2190
- }
2191
- if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2192
- if (showLoader || !configReady) return;
2193
- void toggleWeb();
2194
- return;
2195
- }
2196
- if (showSettings) {
2197
- if (key.escape || input === "s") {
2198
- setShowSettings(false);
2199
- return;
2200
- }
2201
- const accIdxNav = settingsCursor - ACCOUNT_ROWS_START;
2202
- const onAccountRow = accIdxNav >= 0 && accIdxNav < cfg.accounts.length;
2203
- if (onAccountRow && key.shift && (key.upArrow || key.downArrow)) {
2204
- moveAccount(accIdxNav, key.upArrow ? -1 : 1);
2205
- return;
2206
- }
2207
- if (key.upArrow) {
2208
- setSettingsCursor((c) => Math.max(0, c - 1));
2209
- return;
2210
- }
2211
- if (key.downArrow) {
2212
- setSettingsCursor((c) => Math.min(totalSettingsRows - 1, c + 1));
2213
- return;
2214
- }
2215
- if (settingsCursor === 0) {
2216
- if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
2217
- if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
2218
- return;
2219
- }
2220
- if (settingsCursor === 1) {
2221
- if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
2222
- if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
2223
- return;
2224
- }
2225
- if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
2226
- updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
2227
- return;
2228
- }
2229
- if (settingsCursor === 3) {
2230
- if (key.return) {
2231
- setTzEdit(cfg.timezone ?? "");
2232
- setTzError(null);
2233
- }
2234
- if (key.leftArrow || key.rightArrow) updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
2235
- return;
2236
- }
2237
- if (settingsCursor === 4 && (key.leftArrow || key.rightArrow || key.return)) {
2238
- updateConfig((c) => ({ ...c, dashboardLayout: c.dashboardLayout === "grid" ? "single" : "grid" }));
2239
- return;
2240
- }
2241
- if (settingsCursor === 5 && (key.leftArrow || key.rightArrow || key.return)) {
2242
- updateConfig((c) => ({ ...c, defaultFocus: c.defaultFocus === "all" ? "last" : "all" }));
2243
- return;
2244
- }
2245
- const provIdx = settingsCursor - PROVIDER_ROWS_START;
2246
- if (provIdx >= 0 && provIdx < PROVIDER_ORDER.length) {
2247
- if (input === " " || key.return || key.leftArrow || key.rightArrow) toggleProvider(PROVIDER_ORDER[provIdx]);
2248
- return;
2249
- }
2250
- const accIdx = settingsCursor - ACCOUNT_ROWS_START;
2251
- if (accIdx >= 0 && accIdx < cfg.accounts.length) {
2252
- const acc = cfg.accounts[accIdx];
2253
- if (key.return) {
2254
- openEditAccount(acc);
2255
- return;
2256
- }
2257
- if (input === "d" || input === "x") {
2258
- deleteAccount(acc.id);
2259
- return;
2260
- }
2261
- if (input === " ") {
2262
- updateConfig((c) => ({ ...c, activeAccountId: acc.id }));
2263
- return;
2264
- }
2265
- return;
2266
- }
2267
- if (accIdx === cfg.accounts.length && key.return) {
2268
- openAddAccount();
2269
- }
2270
- return;
2271
- }
2272
- if (input === "s") {
2273
- setShowSettings(true);
2274
- setSettingsCursor(0);
2275
- return;
2276
- }
2277
- if (input === "a") {
2278
- cycleAccount(1);
2279
- return;
2280
- }
2281
- if (input === "A") {
2282
- cycleAccount(-1);
2283
- return;
2284
- }
2285
- if (key.tab) {
2286
- setTab((t) => (t + 1) % TABS.length);
2287
- resetView();
2288
- return;
2289
- }
2290
- if (input && /^[0-9]$/.test(input) && slots.length > 1) {
2291
- const target = slots[parseInt(input, 10)];
2292
- if (target) {
2293
- updateConfig((c) => ({ ...c, activeAccountId: target.id }));
2294
- resetView();
2295
- }
2296
- return;
2297
- }
2298
- if (tab === 0 && dashPaginated) {
2299
- if (input === "]" || key.downArrow || key.pageDown) {
2300
- setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
2301
- return;
2302
- }
2303
- if (input === "[" || key.upArrow || key.pageUp) {
2304
- setDashPage((p) => Math.max(0, p - 1));
2305
- return;
2306
- }
2307
- }
2308
- if (tab === 1) {
2309
- if (input === "p") {
2310
- cycleTableProvider(1);
2311
- return;
2312
- }
2313
- if (input === "P") {
2314
- cycleTableProvider(-1);
2315
- return;
2316
- }
2317
- if (input === "/") {
2318
- setSearchMode(true);
2319
- return;
2320
- }
2321
- if (key.escape) {
2322
- if (search) setSearch("");
2323
- else setExpanded(-1);
2324
- return;
2325
- }
2326
- if (input === "o") {
2327
- setSort((s) => (s + 1) % SORTS_FOR.length);
2328
- resetView();
2329
- return;
2330
- }
2331
- if (!tableIsCursor) {
2332
- if (input === "d") {
2333
- setView(0);
2334
- resetView();
2335
- return;
2336
- }
2337
- if (input === "w") {
2338
- setView(1);
2339
- resetView();
2340
- return;
2341
- }
2342
- if (input === "m") {
2343
- setView(2);
2344
- resetView();
2345
- return;
2346
- }
2347
- if (key.leftArrow) {
2348
- setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
2349
- resetView();
2350
- return;
2351
- }
2352
- if (key.rightArrow) {
2353
- setView((v) => (v + 1) % VIEWS.length);
2354
- resetView();
2355
- return;
2356
- }
2357
- if (key.return) {
2358
- setExpanded((e) => e === cursor ? -1 : cursor);
2359
- return;
2360
- }
2361
- }
2362
- } else {
2363
- if (key.leftArrow || key.rightArrow) {
2364
- setTab((t) => (t + 1) % TABS.length);
2365
- resetView();
2366
- return;
2367
- }
2368
- }
2369
- if (key.upArrow) {
2370
- setCursor((c) => Math.max(0, c - 1));
2371
- return;
2372
- }
2373
- if (key.downArrow) {
2374
- setCursor((c) => clampRow(c + 1));
2375
- return;
2376
- }
2377
- if (key.pageDown || input === "G") {
2378
- setCursor((c) => clampRow(input === "G" ? rowCountRef.current - 1 : c + Math.max(1, rows - 12)));
2379
- return;
2380
- }
2381
- if (key.pageUp || input === "g") {
2382
- setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
2383
- return;
2384
- }
2385
- }, { isActive: IS_TTY });
2386
- if (error) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) });
2387
- if (!config2) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading..." }) });
2388
- if (resizing) return /* @__PURE__ */ jsx7(ResizingView, { cols: live.cols, rows: live.rows });
2389
- if (showPicker) {
2390
- return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx7(
2391
- Onboarding,
2392
- {
2393
- items: onboardItems,
2394
- cursor: onboardCursor,
2395
- onToggle: toggleOnboard,
2396
- onConfirm: confirmOnboarding,
2397
- heading: needsOnboarding ? "Welcome to tokmon" : "New providers detected",
2398
- subheading: needsOnboarding ? "Pick the tools you want to track. You can change this anytime in settings." : "tokmon found these installed since you last set up. Pick which to track."
2399
- }
2400
- ) });
2401
- }
2402
- if (showLoader) {
2403
- return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx7(LoadingView, { groups: allGroups, stats, cols, rows }) });
2404
- }
2405
- if (TOO_SMALL && !showSettings) {
2406
- return /* @__PURE__ */ jsx7(TinyFallback, { groups, stats, rows, cols });
2407
- }
2408
- const tokenRows = sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort);
2409
- const cursorTableRows = sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort);
2410
- rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
2411
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
2412
- /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
2413
- /* @__PURE__ */ jsxs7(Box7, { children: [
2414
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
2415
- glyphs().dotSel,
2416
- " tokmon"
2417
- ] }),
2418
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2419
- " ",
2420
- glyphs().middot,
2421
- " every ",
2422
- cfg.interval,
2423
- "s"
2424
- ] })
2425
- ] }),
2426
- /* @__PURE__ */ jsxs7(Box7, { children: [
2427
- webStatus !== "off" && /* @__PURE__ */ jsxs7(Fragment4, { children: [
2428
- /* @__PURE__ */ jsx7(WebStatus, { status: webStatus, url: webUrl }),
2429
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2430
- " ",
2431
- glyphs().middot,
2432
- " "
2433
- ] })
2434
- ] }),
2435
- peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
2436
- /* @__PURE__ */ jsx7(PeakBadge, { peak }),
2437
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2438
- " ",
2439
- glyphs().middot,
2440
- " "
2441
- ] })
2442
- ] }),
2443
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: time(updated, tz) })
2444
- ] })
2445
- ] }),
2446
- showSettings ? /* @__PURE__ */ jsx7(
2447
- SettingsView,
2448
- {
2449
- config: cfg,
2450
- cursor: settingsCursor,
2451
- tzEdit,
2452
- tzError,
2453
- resolvedTz: tz,
2454
- accountForm,
2455
- activeAccountId: cfg.activeAccountId
2456
- }
2457
- ) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
2458
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, marginBottom: 1, children: [
2459
- /* @__PURE__ */ jsx7(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
2460
- setTab(i);
2461
- resetView();
2462
- } }),
2463
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2464
- " Tab/",
2465
- glyphs().arrowL,
2466
- glyphs().arrowR
2467
- ] })
2468
- ] }),
2469
- tab === 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
2470
- /* @__PURE__ */ jsx7(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage }),
2471
- slots.length > 1 && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
2472
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "focus " }),
2473
- /* @__PURE__ */ jsx7(
2474
- AccountStrip,
2475
- {
2476
- slots,
2477
- activeIdx: activeSlotIdx,
2478
- onSelect: (i) => {
2479
- updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
2480
- resetView();
2481
- }
2482
- }
2483
- )
2484
- ] }),
2485
- /* @__PURE__ */ jsx7(TotalsRow, { groups, stats, cols })
2486
- ] }),
2487
- tab === 1 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
2488
- tableProvs.length > 0 && /* @__PURE__ */ jsx7(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
2489
- setTableProvider(p);
2490
- setCursor(0);
2491
- setExpanded(-1);
2492
- setSearch("");
2493
- setSearchMode(false);
2494
- } }),
2495
- /* @__PURE__ */ jsx7(Box7, { height: 1 }),
2496
- /* @__PURE__ */ jsx7(
2497
- ControlBar,
2498
- {
2499
- views: VIEWS,
2500
- period: view,
2501
- sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
2502
- search,
2503
- searching: searchMode,
2504
- showPeriod: !tableIsCursor
2505
- }
2506
- ),
2507
- /* @__PURE__ */ jsx7(Box7, { height: 1 }),
2508
- !effTableProvider ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2509
- "No providers enabled ",
2510
- glyphs().emDash,
2511
- " press s to pick providers."
2512
- ] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx7(
2513
- CursorSpendTable,
2514
- {
2515
- rows: cursorTableRows,
2516
- cursor,
2517
- maxRows: Math.max(1, rows - 16),
2518
- onRowClick: (idx) => setCursor(idx)
2519
- }
2520
- ) : /* @__PURE__ */ jsx7(
2521
- TokenTable,
2522
- {
2523
- rows: tokenRows,
2524
- cursor,
2525
- expanded,
2526
- maxRows: Math.max(1, rows - 16),
2527
- cols,
2528
- onRowClick: (idx) => {
2529
- if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
2530
- else setCursor(idx);
2531
- }
2532
- }
2533
- )
2534
- ] })
2535
- ] }),
2536
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols, webOn: webStatus === "on" || webStatus === "starting" })
2537
- ] });
2538
- }
2539
- function upsert(prev, account, patch) {
2540
- const next = new Map(prev);
2541
- const cur = next.get(account.id) ?? { account, dashboard: null, billing: null };
2542
- next.set(account.id, { ...cur, account, ...patch });
2543
- return next;
2544
- }
2545
- async function fetchScopeTable(scope, tz) {
2546
- const tables = await Promise.all(scope.map(async (acc) => {
2547
- const provider = PROVIDERS[acc.providerId];
2548
- if (!provider.fetchTable) return null;
2549
- try {
2550
- return await provider.fetchTable(acc, tz);
2551
- } catch {
2552
- return null;
2553
- }
2554
- }));
2555
- const valid = tables.filter((t) => t !== null);
2556
- if (valid.length === 0) return { daily: [], weekly: [], monthly: [] };
2557
- if (valid.length === 1) return valid[0];
2558
- return mergeTables(valid);
2559
- }
2560
- function sortLabel(entry) {
2561
- if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
2562
- if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
2563
- return entry.label;
2564
- }
2565
- function sortRows(rows, sortIdx) {
2566
- const sorted = [...rows];
2567
- switch (sortIdx % SORTS.length) {
2568
- case 0:
2569
- return sorted.sort((a, b) => a.label.localeCompare(b.label));
2570
- case 1:
2571
- return sorted.sort((a, b) => b.label.localeCompare(a.label));
2572
- case 2:
2573
- return sorted.sort((a, b) => a.cost - b.cost);
2574
- case 3:
2575
- return sorted.sort((a, b) => b.cost - a.cost);
2576
- default:
2577
- return sorted;
2578
- }
2579
- }
2580
- function filterTokenRows(rows, q) {
2581
- if (!q) return rows;
2582
- const s = q.toLowerCase();
2583
- return rows.filter((r) => r.label.toLowerCase().includes(s) || r.models.some((m) => m.toLowerCase().includes(s)));
2584
- }
2585
- function filterCursorRows(rows, q) {
2586
- if (!q) return rows;
2587
- const s = q.toLowerCase();
2588
- return rows.filter((r) => r.name.toLowerCase().includes(s));
2589
- }
2590
- function sortCursorRows(rows, sortIdx) {
2591
- const out = [...rows];
2592
- switch (sortIdx % CURSOR_SORTS.length) {
2593
- case 1:
2594
- return out.sort((a, b) => b.requests - a.requests);
2595
- case 2:
2596
- return out.sort((a, b) => a.name.localeCompare(b.name));
2597
- default:
2598
- return out.sort((a, b) => b.usd - a.usd);
2599
- }
2600
- }
2601
- function AccountStrip({ slots, activeIdx, onSelect }) {
2602
- return /* @__PURE__ */ jsx7(Box7, { flexWrap: "wrap", children: slots.map((s, i) => {
2603
- const active2 = i === activeIdx;
2604
- const dot = s.id === null ? glyphs().dotAll : glyphs().dot;
2605
- const label = truncateName(s.name, 16);
2606
- return /* @__PURE__ */ jsxs7(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
2607
- /* @__PURE__ */ jsx7(Text7, { dimColor: !active2, children: i }),
2608
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
2609
- /* @__PURE__ */ jsx7(Text7, { color: s.color, bold: active2, dimColor: !active2, children: dot }),
2610
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
2611
- active2 ? /* @__PURE__ */ jsx7(Text7, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: label })
2612
- ] }, s.id ?? "__all__");
2613
- }) });
2614
- }
2615
- function WebStatus({ status, url }) {
2616
- if (status === "starting") return /* @__PURE__ */ jsx7(Spinner, { label: "web starting" });
2617
- if (status === "stopping") return /* @__PURE__ */ jsx7(Spinner, { label: "web stopping" });
2618
- if (status === "on") return /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
2619
- glyphs().dot,
2620
- " web :",
2621
- url ? url.split(":").pop() : ""
2622
- ] });
2623
- return null;
2624
- }
2625
- function Footer({ hasAccounts, paginated, cols, webOn }) {
2626
- const inner = cols - 4;
2627
- const BASE = "by David Ilie (davidilie.com) \xB7 O=repo W=web s=settings q=quit".length;
2628
- const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
2629
- const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
2630
- const JUMP = "0-9=jump a/A=cycle ".length;
2631
- const PAGE = "scroll=page ".length;
2632
- const showOpt = IS_APPLE_TERMINAL && inner >= BASE + OPT;
2633
- const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
2634
- const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
2635
- return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
2636
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
2637
- /* @__PURE__ */ jsx7(LinkBox, { onClick: () => openUrl(REPO_URL), children: /* @__PURE__ */ jsx7(Transform, { transform: (s) => osc8(s, REPO_URL), children: /* @__PURE__ */ jsx7(Text7, { underline: true, children: "David Ilie" }) }) }),
2638
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (" }),
2639
- /* @__PURE__ */ jsx7(LinkBox, { onClick: () => openUrl(SITE_URL), children: /* @__PURE__ */ jsx7(Transform, { transform: (s) => osc8(s, SITE_URL), children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", underline: true, children: "davidilie.com" }) }) }),
2640
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2641
- ") ",
2642
- glyphs().middot,
2643
- " O=repo ",
2644
- webOn ? "W=stop" : "W=web",
2645
- " s=settings "
2646
- ] }),
2647
- showOpt && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: optHint }),
2648
- showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
2649
- showPage && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "scroll=page " }),
2650
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "q=quit" })
2651
- ] });
2652
- }
2653
- function TinyFallback({ groups, stats, rows, cols }) {
2654
- const maxLines = Math.max(1, rows - 4);
2655
- const visible = groups.slice(0, maxLines);
2656
- const hidden = groups.length - visible.length;
2657
- const w = Math.max(8, cols - 2);
2658
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
2659
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
2660
- glyphs().dotSel,
2661
- " tokmon"
2662
- ] }),
2663
- groups.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2664
- "No providers ",
2665
- glyphs().emDash,
2666
- " s=settings"
2667
- ] }) : visible.map((g) => /* @__PURE__ */ jsx7(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
2668
- hidden > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2669
- "+",
2670
- hidden,
2671
- " more (enlarge terminal)"
2672
- ] }),
2673
- /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
2674
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "s=settings q=quit" })
2675
- ] });
2676
- }
2677
- function TinyRow({ provider, accounts, stats, width }) {
2678
- const meta = PROVIDERS[provider];
2679
- const dashboards = accounts.map((a) => stats.get(a.id)?.dashboard).filter(Boolean);
2680
- const billings = accounts.map((a) => stats.get(a.id)?.billing).filter(Boolean);
2681
- const todayCost = dashboards.reduce((sum, d) => sum + (d?.today.cost ?? 0), 0);
2682
- const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
2683
- const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
2684
- const name = truncateName(meta.name, Math.max(4, width - 18));
2685
- return /* @__PURE__ */ jsxs7(Box7, { width, children: [
2686
- /* @__PURE__ */ jsxs7(Text7, { color: meta.color, children: [
2687
- glyphs().dot,
2688
- " "
2689
- ] }),
2690
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: meta.color, children: name }),
2691
- /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
2692
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: detail })
2693
- ] });
2694
- }
2695
-
2696
- // src/cli.tsx
2697
- import { jsx as jsx8 } from "react/jsx-runtime";
2698
5
  process.on("unhandledRejection", () => {
2699
6
  });
2700
7
  EventEmitter.defaultMaxListeners = 100;
@@ -2706,9 +13,14 @@ process.emitWarning = ((warning, ...rest) => {
2706
13
  });
2707
14
  var args = process.argv.slice(2);
2708
15
  var subcommand = args[0]?.toLowerCase();
16
+ if (subcommand === "__daemon") {
17
+ const { runDaemon } = await import("./daemon-OBQJO6D4.js");
18
+ await runDaemon(args.slice(1), { foreground: false });
19
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
20
+ }
2709
21
  if (subcommand === "serve" || subcommand === "web") {
2710
- const { startWeb } = await import("./web-DYAEMCAE.js");
2711
- await startWeb(args.slice(1));
22
+ const { runDaemon } = await import("./daemon-OBQJO6D4.js");
23
+ await runDaemon(args.slice(1), { foreground: true });
2712
24
  process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
2713
25
  }
2714
26
  var interval;
@@ -2742,18 +54,11 @@ for (let i = 0; i < args.length; i++) {
2742
54
  process.exit(0);
2743
55
  }
2744
56
  }
57
+ var { loadConfig } = await import("./config-C6Z65JUP.js");
58
+ var { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
59
+ var { attachOrSpawn } = await import("./daemon-handle-ZHECQZ6Q.js");
2745
60
  var config = await loadConfig();
2746
- var altScreen = config.clearScreen && process.stdout.isTTY === true;
2747
- var leaveAltScreen = () => {
2748
- try {
2749
- process.stdout.write("\x1B[?1049l");
2750
- } catch {
2751
- }
2752
- };
2753
- if (altScreen) {
2754
- process.stdout.write("\x1B[?1049h\x1B[H");
2755
- process.once("exit", leaveAltScreen);
2756
- }
61
+ var isTTY = process.stdout.isTTY === true;
2757
62
  setGlyphs(resolveGlyphs({
2758
63
  flag: asciiFlag,
2759
64
  env: process.env,
@@ -2761,7 +66,7 @@ setGlyphs(resolveGlyphs({
2761
66
  isTTY: !!process.stdout.isTTY,
2762
67
  platform: process.platform
2763
68
  }));
2764
- var { waitUntilExit } = render(/* @__PURE__ */ jsx8(MouseProvider, { children: /* @__PURE__ */ jsx8(App, { interval, initialConfig: config }) }));
2765
- await waitUntilExit();
2766
- await flushDisk();
2767
- if (altScreen) leaveAltScreen();
69
+ var daemon = await attachOrSpawn();
70
+ var mode = daemon.kind === "spawned" ? "connected" : "degraded";
71
+ var { bootstrapInk } = await import("./bootstrap-ink-AO3QA5BH.js");
72
+ await bootstrapInk({ interval, config, daemon, mode });