tokmon 0.12.6 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +102 -51
  2. package/dist/cli.js +2588 -1262
  3. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -5,46 +5,104 @@ import { render } from "ink";
5
5
  import { MouseProvider } from "@zenobius/ink-mouse";
6
6
 
7
7
  // src/config.ts
8
- import { readFile, writeFile, mkdir } from "fs/promises";
9
- import { join } from "path";
8
+ import { readFile, writeFile, mkdir, rename } from "fs/promises";
9
+ import { join, isAbsolute } from "path";
10
10
  import { homedir } from "os";
11
+ function envDir(name) {
12
+ const v = process.env[name];
13
+ return v && v.trim() && isAbsolute(v.trim()) ? v.trim() : void 0;
14
+ }
11
15
  var DEFAULTS = {
12
16
  interval: 2,
13
17
  billingInterval: 5,
14
18
  clearScreen: true,
15
19
  timezone: null,
16
20
  accounts: [],
17
- activeAccountId: null
21
+ activeAccountId: null,
22
+ disabledProviders: [],
23
+ onboarded: false,
24
+ dashboardLayout: "grid",
25
+ defaultFocus: "all"
18
26
  };
19
27
  var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
20
28
  function configDir() {
21
29
  if (process.platform === "win32") {
22
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tokmon");
30
+ return join(envDir("APPDATA") ?? join(homedir(), "AppData", "Roaming"), "tokmon");
23
31
  }
24
- const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
25
- return join(xdg, "tokmon");
32
+ return join(envDir("XDG_CONFIG_HOME") ?? join(homedir(), ".config"), "tokmon");
26
33
  }
27
34
  function configLocation() {
28
35
  return join(configDir(), "config.json");
29
36
  }
37
+ function cacheDir() {
38
+ if (process.platform === "win32") {
39
+ return join(envDir("LOCALAPPDATA") ?? envDir("APPDATA") ?? join(homedir(), "AppData", "Local"), "tokmon", "cache");
40
+ }
41
+ if (process.platform === "darwin") {
42
+ return join(homedir(), "Library", "Caches", "tokmon");
43
+ }
44
+ return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
45
+ }
46
+ var PROVIDER_IDS = ["claude", "codex", "cursor"];
47
+ function clampNum(v, fallback, min) {
48
+ return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
49
+ }
30
50
  async function loadConfig() {
51
+ let raw;
52
+ try {
53
+ raw = await readFile(configLocation(), "utf-8");
54
+ } catch {
55
+ return { ...DEFAULTS };
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(raw);
60
+ } catch {
61
+ try {
62
+ await writeFile(configLocation() + ".bak", raw);
63
+ } catch {
64
+ }
65
+ return { ...DEFAULTS };
66
+ }
31
67
  try {
32
- const raw = await readFile(configLocation(), "utf-8");
33
- const parsed = JSON.parse(raw);
68
+ const accounts = (Array.isArray(parsed.accounts) ? parsed.accounts : []).map((a) => ({ ...a, providerId: a.providerId ?? "claude" })).filter((a) => typeof a?.id === "string" && typeof a?.name === "string" && PROVIDER_IDS.includes(a.providerId));
34
69
  return {
35
70
  ...DEFAULTS,
36
71
  ...parsed,
37
- accounts: Array.isArray(parsed.accounts) ? parsed.accounts : [],
38
- activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null
72
+ // Coerce the numeric/boolean knobs: a hand-edited `"interval": "fast"` or
73
+ // a negative value would otherwise reach setTimeout as NaN → a 0ms tight
74
+ // poll loop. Clamp to sane minimums.
75
+ interval: clampNum(parsed.interval, DEFAULTS.interval, 1),
76
+ billingInterval: clampNum(parsed.billingInterval, DEFAULTS.billingInterval, 1),
77
+ clearScreen: typeof parsed.clearScreen === "boolean" ? parsed.clearScreen : DEFAULTS.clearScreen,
78
+ timezone: typeof parsed.timezone === "string" && parsed.timezone.trim() ? parsed.timezone : null,
79
+ accounts,
80
+ activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null,
81
+ disabledProviders: (Array.isArray(parsed.disabledProviders) ? parsed.disabledProviders : []).filter((p) => PROVIDER_IDS.includes(p)),
82
+ // Only skip onboarding when it was explicitly completed. Configs that
83
+ // predate the flag (even ones with a legacy account) still get the
84
+ // provider picker once, so existing users can opt into Codex/Cursor.
85
+ onboarded: parsed.onboarded === true,
86
+ dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
87
+ defaultFocus: parsed.defaultFocus === "last" ? "last" : "all"
39
88
  };
40
89
  } catch {
41
90
  return { ...DEFAULTS };
42
91
  }
43
92
  }
44
- async function saveConfig(config2) {
45
- const dir = configDir();
46
- await mkdir(dir, { recursive: true });
47
- await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
93
+ var saveQueue = Promise.resolve();
94
+ function saveConfig(config2) {
95
+ saveQueue = saveQueue.then(async () => {
96
+ try {
97
+ const dir = configDir();
98
+ await mkdir(dir, { recursive: true });
99
+ const tmp = join(dir, `config.json.${process.pid}.tmp`);
100
+ await writeFile(tmp, JSON.stringify(config2, null, 2) + "\n");
101
+ await rename(tmp, configLocation());
102
+ } catch {
103
+ }
104
+ });
105
+ return saveQueue;
48
106
  }
49
107
  function slugify(value) {
50
108
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
@@ -68,22 +126,14 @@ function pickAccentColor(existing) {
68
126
  }
69
127
  function expandHome(p) {
70
128
  if (!p) return homedir();
71
- if (p === "~" || p === "~/") return homedir();
72
- if (p.startsWith("~/")) return join(homedir(), p.slice(2));
129
+ if (p === "~" || p === "~/" || p === "~\\") return homedir();
130
+ if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
73
131
  return p;
74
132
  }
75
133
 
76
- // src/app.tsx
77
- import { useState, useEffect, useCallback, useRef } from "react";
78
- import { Box, Text, useInput, useStdout, useApp } from "ink";
79
- import { useMouse, useOnMouseClick } from "@zenobius/ink-mouse";
80
-
81
- // src/data.ts
82
- import { readdir, stat as fsStat } from "fs/promises";
83
- import { createReadStream } from "fs";
84
- import { createInterface } from "readline";
134
+ // src/providers/usage-core.ts
135
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
85
136
  import { join as join2 } from "path";
86
- import { homedir as homedir2 } from "os";
87
137
 
88
138
  // src/tz.ts
89
139
  function systemTimezone() {
@@ -194,123 +244,167 @@ function weekKey(ts, tz) {
194
244
  return dayKey(startOfWeek(ts, tz), tz);
195
245
  }
196
246
 
197
- // src/data.ts
198
- var PRICING = {
199
- "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
200
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
201
- "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 }
202
- };
203
- var FALLBACK = PRICING["claude-opus-4"];
204
- var fileCache = /* @__PURE__ */ new Map();
205
- function getClaudeDirs(homeDir) {
206
- if (homeDir) {
207
- return [
208
- join2(homeDir, ".claude", "projects"),
209
- join2(homeDir, ".config", "claude", "projects")
210
- ];
211
- }
212
- const home = homedir2();
213
- const dirs = [join2(home, ".claude", "projects")];
214
- if (process.env.XDG_CONFIG_HOME) {
215
- dirs.push(join2(process.env.XDG_CONFIG_HOME, "claude", "projects"));
216
- } else if (process.platform !== "win32") {
217
- dirs.push(join2(home, ".config", "claude", "projects"));
218
- }
219
- if (process.env.APPDATA) {
220
- dirs.push(join2(process.env.APPDATA, "claude", "projects"));
221
- }
222
- if (process.env.CLAUDE_CONFIG_DIR) {
223
- for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
224
- dirs.push(join2(p.trim(), "projects"));
247
+ // src/providers/usage-core.ts
248
+ var SPARK_DAYS = 14;
249
+ var DAY_MS = 864e5;
250
+ var CACHE_VERSION = 4;
251
+ var STABLE_AGE_MS = 5 * 6e4;
252
+ var PRUNE_AGE_MS = 200 * DAY_MS;
253
+ var memCache = /* @__PURE__ */ new Map();
254
+ var diskLoaded = false;
255
+ var dirty = false;
256
+ var flushTimer = null;
257
+ function cacheFile() {
258
+ return join2(cacheDir(), `usage-v${CACHE_VERSION}.json`);
259
+ }
260
+ function encode(mtimeMs, size, entries) {
261
+ const mods = [];
262
+ const idx = /* @__PURE__ */ new Map();
263
+ const rows = entries.map((e) => {
264
+ let mi = idx.get(e.model);
265
+ if (mi === void 0) {
266
+ mi = mods.length;
267
+ mods.push(e.model);
268
+ idx.set(e.model, mi);
225
269
  }
270
+ return [e.ts, mi, e.input, e.output, e.cacheCreate, e.cacheRead, e.cost, e.cacheSavings, e.id ?? 0];
271
+ });
272
+ return { m: mtimeMs, s: size, mods, rows };
273
+ }
274
+ function decode(s) {
275
+ const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
276
+ const out = [];
277
+ for (const r of s.rows) {
278
+ if (!Array.isArray(r) || r.length < 8) continue;
279
+ const ts = r[0];
280
+ if (typeof ts !== "number" || !Number.isFinite(ts)) continue;
281
+ const mi = r[1];
282
+ out.push({
283
+ ts,
284
+ model: typeof mi === "number" && typeof s.mods[mi] === "string" ? s.mods[mi] : "unknown",
285
+ input: num(r[2]),
286
+ output: num(r[3]),
287
+ cacheCreate: num(r[4]),
288
+ cacheRead: num(r[5]),
289
+ cost: num(r[6]),
290
+ cacheSavings: num(r[7]),
291
+ id: typeof r[8] === "string" ? r[8] : void 0
292
+ });
226
293
  }
227
- return dirs;
294
+ return out;
228
295
  }
229
- function priceFor(model) {
230
- for (const [prefix, p] of Object.entries(PRICING)) {
231
- if (model.startsWith(prefix)) return p;
296
+ async function ensureDiskLoaded() {
297
+ if (diskLoaded) return;
298
+ diskLoaded = true;
299
+ try {
300
+ const obj = JSON.parse(await readFile2(cacheFile(), "utf-8"));
301
+ for (const [path, s] of Object.entries(obj)) {
302
+ if (s && typeof s.m === "number" && Array.isArray(s.rows) && Array.isArray(s.mods)) {
303
+ memCache.set(path, { mtimeMs: s.m, size: typeof s.s === "number" ? s.s : -1, entries: decode(s) });
304
+ }
305
+ }
306
+ } catch {
232
307
  }
233
- return FALLBACK;
234
308
  }
235
- function costOf(model, u) {
236
- const p = priceFor(model);
237
- return (u.input_tokens ?? 0) * p.i + (u.output_tokens ?? 0) * p.o + (u.cache_creation_input_tokens ?? 0) * p.cc + (u.cache_read_input_tokens ?? 0) * p.cr;
238
- }
239
- function shortModel(model) {
240
- return model.replace("claude-", "").replace(/-\d{8}$/, "");
241
- }
242
- async function parseFile(path, since) {
243
- const entries = [];
244
- const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
245
- for await (const line of rl) {
246
- if (!line.includes('"usage"')) continue;
247
- try {
248
- const obj = JSON.parse(line);
249
- if (obj.type !== "assistant" || !obj.message?.usage) continue;
250
- const ts = new Date(obj.timestamp ?? 0).getTime();
251
- if (ts < since) continue;
252
- const u = obj.message.usage;
253
- entries.push({
254
- ts,
255
- msgId: obj.message.id ?? "",
256
- model: obj.message.model ?? "unknown",
257
- cost: costOf(obj.message.model ?? "", u),
258
- input: u.input_tokens ?? 0,
259
- output: u.output_tokens ?? 0,
260
- cacheCreate: u.cache_creation_input_tokens ?? 0,
261
- cacheRead: u.cache_read_input_tokens ?? 0
262
- });
263
- } catch {
309
+ async function flushDisk() {
310
+ if (!dirty) return;
311
+ const now = Date.now();
312
+ const obj = {};
313
+ for (const [path, v] of memCache) {
314
+ if (now - v.mtimeMs > STABLE_AGE_MS && now - v.mtimeMs < PRUNE_AGE_MS) {
315
+ obj[path] = encode(v.mtimeMs, v.size, v.entries);
264
316
  }
265
317
  }
266
- return entries;
318
+ try {
319
+ await mkdir2(cacheDir(), { recursive: true });
320
+ const tmp = `${cacheFile()}.${process.pid}.tmp`;
321
+ await writeFile2(tmp, JSON.stringify(obj));
322
+ await rename2(tmp, cacheFile());
323
+ dirty = false;
324
+ } catch {
325
+ }
267
326
  }
268
- async function loadEntries(since, homeDir) {
327
+ function scheduleFlush() {
328
+ if (flushTimer) return;
329
+ flushTimer = setTimeout(() => {
330
+ flushTimer = null;
331
+ void flushDisk();
332
+ }, 4e3);
333
+ flushTimer.unref?.();
334
+ }
335
+ async function mapLimit(items, limit, fn) {
336
+ let i = 0;
337
+ const worker = async () => {
338
+ while (i < items.length) await fn(items[i++]);
339
+ };
340
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
341
+ }
342
+ async function loadCachedEntries(files, parse, since) {
343
+ await ensureDiskLoaded();
269
344
  const chunks = [];
270
- const seen = /* @__PURE__ */ new Set();
271
- for (const dir of getClaudeDirs(homeDir)) {
272
- let listing;
345
+ await mapLimit(files, 8, async (f) => {
273
346
  try {
274
- listing = await readdir(dir, { recursive: true });
347
+ let c = memCache.get(f.path);
348
+ if (!c || c.mtimeMs !== f.mtimeMs || c.size !== f.size) {
349
+ const entries = await parse(f.path);
350
+ c = { mtimeMs: f.mtimeMs, size: f.size, entries };
351
+ memCache.set(f.path, c);
352
+ if (Date.now() - f.mtimeMs > STABLE_AGE_MS) dirty = true;
353
+ }
354
+ chunks.push(c.entries);
275
355
  } catch {
276
- continue;
277
356
  }
278
- const files = listing.filter((f) => f.endsWith(".jsonl"));
279
- await Promise.all(files.map(async (f) => {
280
- const path = join2(dir, f);
281
- if (seen.has(path)) return;
282
- seen.add(path);
283
- try {
284
- const s = await fsStat(path);
285
- if (s.mtimeMs < since) return;
286
- const cached = fileCache.get(path);
287
- if (cached && cached.mtimeMs === s.mtimeMs) {
288
- chunks.push(cached.data);
289
- return;
290
- }
291
- const data = await parseFile(path, since);
292
- fileCache.set(path, { mtimeMs: s.mtimeMs, data });
293
- chunks.push(data);
294
- } catch {
295
- }
296
- }));
297
- }
298
- const all = chunks.flat();
299
- const seenIds = /* @__PURE__ */ new Set();
300
- return all.filter((e) => {
301
- if (!e.msgId) return true;
302
- if (seenIds.has(e.msgId)) return false;
303
- seenIds.add(e.msgId);
304
- return true;
305
357
  });
358
+ if (dirty) scheduleFlush();
359
+ return dedupe(chunks.flat().filter((e) => e.ts >= since));
360
+ }
361
+ function safeNum(v) {
362
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
363
+ }
364
+ function dedupe(entries) {
365
+ const seen = /* @__PURE__ */ new Set();
366
+ const out = [];
367
+ for (const e of entries) {
368
+ const k = e.id ?? `${e.ts} ${e.model} ${e.input} ${e.output} ${e.cacheCreate} ${e.cacheRead}`;
369
+ if (seen.has(k)) continue;
370
+ seen.add(k);
371
+ out.push(e);
372
+ }
373
+ return out;
306
374
  }
307
- function sum(entries) {
308
- let cost = 0, tokens2 = 0;
375
+ function summarize(entries, tz) {
376
+ const now = Date.now();
377
+ const todayStart = startOfDay(now, tz);
378
+ const weekStart = startOfWeek(now, tz);
379
+ const monthStart = startOfMonth(now, tz);
380
+ const today = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
381
+ const week = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
382
+ const month = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
383
+ const byDay = /* @__PURE__ */ new Map();
384
+ let oldestToday = now;
385
+ let hadToday = false;
386
+ const add = (s, e) => {
387
+ s.cost += e.cost;
388
+ s.tokens += e.input + e.output + e.cacheCreate + e.cacheRead;
389
+ s.cacheRead += e.cacheRead;
390
+ s.cacheSavings += e.cacheSavings;
391
+ };
309
392
  for (const e of entries) {
310
- cost += e.cost;
311
- tokens2 += e.input + e.output + e.cacheCreate + e.cacheRead;
393
+ if (e.ts >= monthStart) add(month, e);
394
+ if (e.ts >= weekStart) add(week, e);
395
+ if (e.ts >= todayStart) {
396
+ add(today, e);
397
+ hadToday = true;
398
+ if (e.ts < oldestToday) oldestToday = e.ts;
399
+ }
400
+ const dk = dayKey(e.ts, tz);
401
+ byDay.set(dk, (byDay.get(dk) ?? 0) + e.cost);
312
402
  }
313
- return { cost, tokens: tokens2 };
403
+ const hrs = Math.max((now - oldestToday) / 36e5, 1 / 60);
404
+ const burnRate = hadToday ? today.cost / hrs : 0;
405
+ const series = [];
406
+ for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(dayKey(now - i * DAY_MS, tz)) ?? 0);
407
+ return { today, week, month, burnRate, series };
314
408
  }
315
409
  function groupBy(entries, keyFn) {
316
410
  const groups = /* @__PURE__ */ new Map();
@@ -330,8 +424,7 @@ function groupBy(entries, keyFn) {
330
424
  cacheCreate += e.cacheCreate;
331
425
  cacheRead += e.cacheRead;
332
426
  cost += e.cost;
333
- const name = shortModel(e.model);
334
- const m = byModel.get(name);
427
+ const m = byModel.get(e.model);
335
428
  if (m) {
336
429
  m.input += e.input;
337
430
  m.output += e.output;
@@ -339,8 +432,8 @@ function groupBy(entries, keyFn) {
339
432
  m.cacheRead += e.cacheRead;
340
433
  m.cost += e.cost;
341
434
  } else {
342
- byModel.set(name, {
343
- name,
435
+ byModel.set(e.model, {
436
+ name: e.model,
344
437
  input: e.input,
345
438
  output: e.output,
346
439
  cacheCreate: e.cacheCreate,
@@ -363,145 +456,81 @@ function groupBy(entries, keyFn) {
363
456
  }
364
457
  return rows.sort((a, b) => a.label.localeCompare(b.label));
365
458
  }
366
- async function fetchDashboard(tz, homeDir) {
367
- const now = Date.now();
368
- const monthStart = startOfMonth(now, tz);
369
- const todayStart = startOfDay(now, tz);
370
- const weekStart = startOfWeek(now, tz);
371
- const entries = await loadEntries(monthStart, homeDir);
372
- const todayEntries = entries.filter((e) => e.ts >= todayStart);
373
- let burnRate = 0;
374
- if (todayEntries.length > 0) {
375
- let oldest = todayEntries[0].ts;
376
- let totalCost = 0;
377
- for (const e of todayEntries) {
378
- if (e.ts < oldest) oldest = e.ts;
379
- totalCost += e.cost;
380
- }
381
- const hrs = (now - oldest) / 36e5;
382
- if (hrs > 0) burnRate = totalCost / hrs;
383
- }
384
- return {
385
- today: sum(todayEntries),
386
- week: sum(entries.filter((e) => e.ts >= weekStart)),
387
- month: sum(entries.filter((e) => e.ts >= monthStart)),
388
- burnRate
389
- };
390
- }
391
- async function fetchTable(tz, homeDir) {
392
- const lookback = monthsAgoStart(Date.now(), 6, tz);
393
- const entries = await loadEntries(lookback, homeDir);
459
+ function tabulate(entries, tz) {
394
460
  return {
395
461
  daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
396
462
  weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
397
463
  monthly: groupBy(entries, (e) => monthKey(e.ts, tz))
398
464
  };
399
465
  }
400
-
401
- // src/billing.ts
402
- import { execFile as execFileCb } from "child_process";
403
- import { readFile as readFile2 } from "fs/promises";
404
- import { join as join3 } from "path";
405
- import { homedir as homedir3 } from "os";
406
- import { promisify } from "util";
407
- var execFile = promisify(execFileCb);
408
- function credentialsFilePath(homeDir) {
409
- if (homeDir) return join3(homeDir, ".claude", ".credentials.json");
410
- const base = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
411
- return join3(base, ".credentials.json");
412
- }
413
- async function readCredentialsFile(homeDir) {
414
- try {
415
- const raw = await readFile2(credentialsFilePath(homeDir), "utf-8");
416
- const creds = JSON.parse(raw);
417
- return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
418
- } catch {
419
- return null;
420
- }
421
- }
422
- async function readMacKeychain() {
423
- try {
424
- const { stdout } = await execFile("security", [
425
- "find-generic-password",
426
- "-s",
427
- "Claude Code-credentials",
428
- "-w"
429
- ], { timeout: 5e3 });
430
- const creds = JSON.parse(stdout.trim());
431
- return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
432
- } catch {
433
- return null;
434
- }
435
- }
436
- async function getAccessToken(homeDir) {
437
- const isDefault = !homeDir || homeDir === homedir3();
438
- if (isDefault) {
439
- if (process.platform === "darwin") {
440
- const token = await readMacKeychain();
441
- if (token) return token;
466
+ function mergeRows(groups) {
467
+ const byLabel = /* @__PURE__ */ new Map();
468
+ for (const rows of groups) {
469
+ for (const r of rows) {
470
+ const ex = byLabel.get(r.label);
471
+ if (!ex) {
472
+ byLabel.set(r.label, { ...r, models: [...r.models], breakdown: r.breakdown.map((m) => ({ ...m })) });
473
+ continue;
474
+ }
475
+ ex.input += r.input;
476
+ ex.output += r.output;
477
+ ex.cacheCreate += r.cacheCreate;
478
+ ex.cacheRead += r.cacheRead;
479
+ ex.total += r.total;
480
+ ex.cost += r.cost;
481
+ const bd = new Map(ex.breakdown.map((m) => [m.name, m]));
482
+ for (const m of r.breakdown) {
483
+ const e = bd.get(m.name);
484
+ if (e) {
485
+ e.input += m.input;
486
+ e.output += m.output;
487
+ e.cacheCreate += m.cacheCreate;
488
+ e.cacheRead += m.cacheRead;
489
+ e.cost += m.cost;
490
+ } else {
491
+ bd.set(m.name, { ...m });
492
+ }
493
+ }
494
+ ex.breakdown = [...bd.values()].sort((a, b) => b.cost - a.cost);
495
+ ex.models = [...bd.keys()].sort();
442
496
  }
443
- return readCredentialsFile(homeDir);
444
497
  }
445
- return readCredentialsFile(homeDir);
498
+ return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
446
499
  }
447
- var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, peak: null, error: null };
448
- async function fetchBilling(homeDir) {
449
- const token = await getAccessToken(homeDir);
450
- if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
451
- const [usageRes, peak] = await Promise.all([
452
- fetchUsage(token),
453
- fetchPeakStatus()
454
- ]);
455
- if ("error" in usageRes) return { ...EMPTY, peak, error: usageRes.error };
456
- return { ...usageRes.data, peak, error: null };
500
+ function mergeTables(list) {
501
+ return {
502
+ daily: mergeRows(list.map((t) => t.daily)),
503
+ weekly: mergeRows(list.map((t) => t.weekly)),
504
+ monthly: mergeRows(list.map((t) => t.monthly))
505
+ };
457
506
  }
458
- async function fetchUsage(token) {
507
+
508
+ // src/app.tsx
509
+ import { useState as useState2, useEffect as useEffect2, useCallback, useRef as useRef2, useMemo } from "react";
510
+ import { Box as Box6, Text as Text6, useInput, useStdout, useApp } from "ink";
511
+ import { useMouse } from "@zenobius/ink-mouse";
512
+
513
+ // src/http.ts
514
+ async function readJson(res) {
515
+ const type = (res.headers.get("content-type") ?? "").toLowerCase();
516
+ if (type && !type.includes("json")) return null;
459
517
  try {
460
- const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
461
- headers: {
462
- "Authorization": `Bearer ${token}`,
463
- "anthropic-beta": "oauth-2025-04-20",
464
- "User-Agent": "tokmon"
465
- },
466
- signal: AbortSignal.timeout(1e4)
467
- });
468
- if (res.status === 429) return { error: "Rate limited \u2014 retrying next poll" };
469
- if (res.status === 401) return { error: "Token expired \u2014 restart Claude Code" };
470
- if (!res.ok) return { error: `API ${res.status}` };
471
- const data = await res.json();
472
- return {
473
- data: {
474
- session: data.five_hour ? {
475
- utilization: data.five_hour.utilization,
476
- resetsAt: formatReset(data.five_hour.resets_at)
477
- } : null,
478
- weekly: data.seven_day ? {
479
- utilization: data.seven_day.utilization,
480
- resetsAt: formatReset(data.seven_day.resets_at)
481
- } : null,
482
- sonnet: data.seven_day_sonnet ? {
483
- utilization: data.seven_day_sonnet.utilization,
484
- resetsAt: formatReset(data.seven_day_sonnet.resets_at)
485
- } : null,
486
- extraUsage: data.extra_usage?.is_enabled ? {
487
- limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
488
- used: (data.extra_usage.used_credits ?? 0) / 100,
489
- currency: data.extra_usage.currency ?? "USD"
490
- } : null
491
- }
492
- };
518
+ return await res.json();
493
519
  } catch {
494
- return { error: "Network error" };
520
+ return null;
495
521
  }
496
522
  }
497
- async function fetchPeakStatus() {
523
+
524
+ // src/peak.ts
525
+ async function fetchPeak() {
498
526
  try {
499
527
  const res = await fetch("https://promoclock.co/api/status", {
500
528
  headers: { "Accept": "application/json", "User-Agent": "tokmon" },
501
529
  signal: AbortSignal.timeout(3e3)
502
530
  });
503
531
  if (!res.ok) return null;
504
- const data = await res.json();
532
+ const data = await readJson(res);
533
+ if (!data) return null;
505
534
  let state;
506
535
  if (data.isPeak === true || data.status === "peak") state = "peak";
507
536
  else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
@@ -516,33 +545,173 @@ async function fetchPeakStatus() {
516
545
  return null;
517
546
  }
518
547
  }
519
- function formatReset(iso) {
520
- const d = new Date(iso);
521
- const now = /* @__PURE__ */ new Date();
522
- const diff = d.getTime() - now.getTime();
523
- if (diff <= 0) return "now";
524
- const mins = Math.round(diff / 6e4);
525
- if (mins < 60) return `${mins}m`;
526
- const hrs = Math.floor(mins / 60);
527
- const m = mins % 60;
528
- if (hrs < 24) return `${hrs}h ${m}m`;
529
- const days = Math.floor(hrs / 24);
530
- const h = hrs % 24;
531
- return `${days}d ${h}h`;
548
+
549
+ // src/providers/claude/usage.ts
550
+ import { readdir, stat as fsStat, access } from "fs/promises";
551
+ import { createReadStream } from "fs";
552
+ import { createInterface } from "readline";
553
+ import { join as join3, isAbsolute as isAbsolute2 } from "path";
554
+ import { homedir as homedir2 } from "os";
555
+ var PRICING = {
556
+ "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
557
+ "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
558
+ "claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
559
+ "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
560
+ "claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
561
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
562
+ "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
563
+ "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
564
+ };
565
+ var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
566
+ var FALLBACK = PRICING["claude-opus-4"];
567
+ function claudeConfigDirs(homeDir) {
568
+ if (homeDir) {
569
+ return [join3(homeDir, ".claude"), join3(homeDir, ".config", "claude")];
570
+ }
571
+ const home = homedir2();
572
+ const dirs = [join3(home, ".claude")];
573
+ const xdg = envDir("XDG_CONFIG_HOME");
574
+ if (xdg) {
575
+ dirs.push(join3(xdg, "claude"));
576
+ } else if (process.platform !== "win32") {
577
+ dirs.push(join3(home, ".config", "claude"));
578
+ }
579
+ const appData = envDir("APPDATA");
580
+ if (appData) dirs.push(join3(appData, "claude"));
581
+ if (process.env.CLAUDE_CONFIG_DIR) {
582
+ for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
583
+ const t = p.trim();
584
+ if (t && isAbsolute2(t)) dirs.push(t);
585
+ }
586
+ }
587
+ return [...new Set(dirs)];
588
+ }
589
+ function getClaudeDirs(homeDir) {
590
+ return claudeConfigDirs(homeDir).map((d) => join3(d, "projects"));
591
+ }
592
+ async function detectClaude(homeDir) {
593
+ for (const dir of getClaudeDirs(homeDir)) {
594
+ try {
595
+ await access(dir);
596
+ return true;
597
+ } catch {
598
+ }
599
+ }
600
+ return false;
601
+ }
602
+ function priceFor(model) {
603
+ for (const key of PRICE_KEYS) {
604
+ if (model.startsWith(key)) return PRICING[key];
605
+ }
606
+ return FALLBACK;
607
+ }
608
+ function costOf(model, u) {
609
+ const p = priceFor(model);
610
+ return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + safeNum(u.cache_creation_input_tokens) * p.cc + safeNum(u.cache_read_input_tokens) * p.cr;
611
+ }
612
+ function shortModel(model) {
613
+ return model.replace("claude-", "").replace(/-\d{8}$/, "");
614
+ }
615
+ async function parseFile(path) {
616
+ const entries = [];
617
+ const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
618
+ for await (const line of rl) {
619
+ if (!line.includes('"usage"')) continue;
620
+ try {
621
+ const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
622
+ if (obj.type !== "assistant" || !obj.message?.usage) continue;
623
+ const ts = new Date(obj.timestamp ?? 0).getTime();
624
+ if (!Number.isFinite(ts)) continue;
625
+ const u = obj.message.usage;
626
+ const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
627
+ const input = safeNum(u.input_tokens);
628
+ const output = safeNum(u.output_tokens);
629
+ const cacheCreate = safeNum(u.cache_creation_input_tokens);
630
+ const cacheRead = safeNum(u.cache_read_input_tokens);
631
+ if (input + output + cacheCreate + cacheRead === 0) continue;
632
+ const p = priceFor(model);
633
+ const msgId = obj.message?.id;
634
+ entries.push({
635
+ // Claude logs a message's usage repeatedly and copies it into resumed
636
+ // sessions — dedup by message id (+ requestId when present).
637
+ id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
638
+ ts,
639
+ model: shortModel(model),
640
+ cost: costOf(model, u),
641
+ input,
642
+ output,
643
+ cacheCreate,
644
+ cacheRead,
645
+ cacheSavings: cacheRead * (p.i - p.cr)
646
+ });
647
+ } catch {
648
+ }
649
+ }
650
+ return entries;
651
+ }
652
+ async function loadEntries(since, homeDir) {
653
+ const files = [];
654
+ const seen = /* @__PURE__ */ new Set();
655
+ const seenIno = /* @__PURE__ */ new Set();
656
+ for (const dir of getClaudeDirs(homeDir)) {
657
+ let listing;
658
+ try {
659
+ listing = await readdir(dir, { recursive: true });
660
+ } catch {
661
+ continue;
662
+ }
663
+ for (const f of listing) {
664
+ if (!f.endsWith(".jsonl")) continue;
665
+ const path = join3(dir, f);
666
+ if (seen.has(path)) continue;
667
+ seen.add(path);
668
+ try {
669
+ const s = await fsStat(path);
670
+ if (s.mtimeMs < since) continue;
671
+ if (s.ino && process.platform !== "win32") {
672
+ const idn = `${s.dev}:${s.ino}`;
673
+ if (seenIno.has(idn)) continue;
674
+ seenIno.add(idn);
675
+ }
676
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
677
+ } catch {
678
+ }
679
+ }
680
+ }
681
+ return loadCachedEntries(files, parseFile, since);
682
+ }
683
+ async function claudeDashboard(tz, homeDir) {
684
+ const now = Date.now();
685
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
686
+ const entries = await loadEntries(since, homeDir);
687
+ return summarize(entries, tz);
688
+ }
689
+ async function claudeTable(tz, homeDir) {
690
+ const entries = await loadEntries(monthsAgoStart(Date.now(), 6, tz), homeDir);
691
+ return tabulate(entries, tz);
532
692
  }
533
693
 
694
+ // src/providers/claude/billing.ts
695
+ import { execFile as execFileCb } from "child_process";
696
+ import { readFile as readFile3 } from "fs/promises";
697
+ import { join as join4 } from "path";
698
+ import { homedir as homedir3 } from "os";
699
+ import { promisify } from "util";
700
+
534
701
  // src/format.ts
535
702
  function currency(value) {
703
+ if (!Number.isFinite(value) || value <= 0) return "$0.00";
536
704
  if (value >= 1e4) {
537
705
  return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
538
706
  }
539
707
  return `$${value.toFixed(2)}`;
540
708
  }
541
709
  function tokens(value) {
542
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
543
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
544
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
545
- return String(value);
710
+ const v = Number.isFinite(value) && value > 0 ? value : 0;
711
+ if (v >= 1e9) return `${(v / 1e9).toFixed(1)}B`;
712
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
713
+ if (v >= 1e3) return `${(v / 1e3).toFixed(1)}K`;
714
+ return String(Math.floor(v));
546
715
  }
547
716
  function time(date, tz) {
548
717
  return date.toLocaleTimeString(void 0, {
@@ -562,194 +731,1862 @@ function col(s, w, align = "right") {
562
731
  const spaces = " ".repeat(w - s.length);
563
732
  return align === "right" ? spaces + s : s + spaces;
564
733
  }
565
-
566
- // src/app.tsx
567
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
568
- var TABS = ["Dashboard", "Table"];
569
- var VIEWS = ["Daily", "Weekly", "Monthly"];
570
- var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
571
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
734
+ function resetIn(iso) {
735
+ const diff = new Date(iso).getTime() - Date.now();
736
+ if (!Number.isFinite(diff) || diff <= 0) return "now";
737
+ const mins = Math.round(diff / 6e4);
738
+ if (mins < 60) return `${mins}m`;
739
+ const hrs = Math.floor(mins / 60);
740
+ const m = mins % 60;
741
+ if (hrs < 24) return `${hrs}h ${m}m`;
742
+ const days = Math.floor(hrs / 24);
743
+ const h = hrs % 24;
744
+ return `${days}d ${h}h`;
745
+ }
746
+
747
+ // src/providers/claude/billing.ts
748
+ var execFile = promisify(execFileCb);
749
+ async function readCredentialsFile(homeDir) {
750
+ for (const dir of claudeConfigDirs(homeDir)) {
751
+ try {
752
+ const creds = JSON.parse(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
753
+ const token = creds?.claudeAiOauth?.accessToken ?? creds?.accessToken;
754
+ if (token) return token;
755
+ } catch {
756
+ }
757
+ }
758
+ return null;
759
+ }
760
+ async function readMacKeychain() {
761
+ try {
762
+ const { stdout } = await execFile("security", [
763
+ "find-generic-password",
764
+ "-s",
765
+ "Claude Code-credentials",
766
+ "-w"
767
+ ], { timeout: 5e3 });
768
+ const creds = JSON.parse(stdout.trim());
769
+ return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
770
+ } catch {
771
+ return null;
772
+ }
773
+ }
774
+ async function getAccessToken(homeDir) {
775
+ const isDefault = !homeDir || homeDir === homedir3();
776
+ if (isDefault && process.platform === "darwin") {
777
+ const token = await readMacKeychain();
778
+ if (token) return token;
779
+ }
780
+ return readCredentialsFile(homeDir);
781
+ }
782
+ var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
783
+ async function claudeBilling(account) {
784
+ const token = await getAccessToken(account.homeDir);
785
+ if (!token) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
786
+ try {
787
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
788
+ headers: {
789
+ "Authorization": `Bearer ${token}`,
790
+ "anthropic-beta": "oauth-2025-04-20",
791
+ "User-Agent": "tokmon"
792
+ },
793
+ signal: AbortSignal.timeout(1e4)
794
+ });
795
+ if (res.status === 429) return { plan: null, metrics: [], error: "Rate limited \u2014 retrying next poll" };
796
+ if (res.status === 401) return { plan: null, metrics: [], error: "Token expired \u2014 restart Claude Code" };
797
+ if (!res.ok) return { plan: null, metrics: [], error: `API ${res.status}` };
798
+ const data = await readJson(res);
799
+ if (!data) return { plan: null, metrics: [], error: "Unexpected API response" };
800
+ const metrics = [];
801
+ if (data.five_hour) {
802
+ metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
803
+ }
804
+ if (data.seven_day) {
805
+ metrics.push({ ...pct(data.seven_day.utilization, resetIn(data.seven_day.resets_at)), label: "Week" });
806
+ }
807
+ if (data.seven_day_sonnet) {
808
+ metrics.push({ ...pct(data.seven_day_sonnet.utilization), label: "Sonnet" });
809
+ }
810
+ if (data.extra_usage?.is_enabled) {
811
+ metrics.push({
812
+ label: "Extra",
813
+ used: (data.extra_usage.used_credits ?? 0) / 100,
814
+ limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
815
+ format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
816
+ });
817
+ }
818
+ return { plan: null, metrics, error: null };
819
+ } catch {
820
+ return { plan: null, metrics: [], error: "Network error" };
821
+ }
822
+ }
823
+
824
+ // src/providers/claude/index.ts
825
+ var claudeProvider = {
826
+ id: "claude",
827
+ name: "Claude",
828
+ color: "green",
829
+ hasUsage: true,
830
+ hasBilling: true,
831
+ detect: (homeDir) => detectClaude(homeDir),
832
+ fetchSummary: (account, tz) => claudeDashboard(tz, account.homeDir),
833
+ fetchTable: (account, tz) => claudeTable(tz, account.homeDir),
834
+ fetchBilling: (account) => claudeBilling(account)
835
+ };
836
+
837
+ // src/providers/codex/usage.ts
838
+ import { readdir as readdir2, stat as fsStat2, access as access2 } from "fs/promises";
839
+ import { createReadStream as createReadStream2 } from "fs";
840
+ import { createInterface as createInterface2 } from "readline";
841
+ import { join as join5 } from "path";
842
+ import { homedir as homedir4 } from "os";
843
+ var PRICING2 = {
844
+ "gpt-5-codex": { in: 125e-8, cr: 125e-9, out: 1e-5 },
845
+ "gpt-5-mini": { in: 25e-8, cr: 25e-9, out: 2e-6 },
846
+ "gpt-5-nano": { in: 5e-8, cr: 5e-9, out: 4e-7 },
847
+ "gpt-5": { in: 125e-8, cr: 125e-9, out: 1e-5 },
848
+ "o4-mini": { in: 11e-7, cr: 275e-9, out: 44e-7 }
849
+ };
850
+ var FALLBACK2 = PRICING2["gpt-5-codex"];
851
+ var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
852
+ function codexHomes(homeDir) {
853
+ if (homeDir) return [join5(homeDir, ".codex")];
854
+ const homes = [];
855
+ const codexHome = envDir("CODEX_HOME");
856
+ if (codexHome) homes.push(codexHome);
857
+ homes.push(join5(homedir4(), ".codex"));
858
+ homes.push(join5(homedir4(), ".config", "codex"));
859
+ return [...new Set(homes)];
860
+ }
861
+ async function detectCodex(homeDir) {
862
+ for (const home of codexHomes(homeDir)) {
863
+ try {
864
+ await access2(join5(home, "sessions"));
865
+ return true;
866
+ } catch {
867
+ }
868
+ }
869
+ return false;
870
+ }
871
+ function priceFor2(model) {
872
+ const m = model.toLowerCase();
873
+ for (const key of PRICE_KEYS2) {
874
+ if (m.startsWith(key) || m.includes(key)) return PRICING2[key];
875
+ }
876
+ return FALLBACK2;
877
+ }
878
+ function extractModel(obj) {
879
+ const p = obj?.payload ?? obj;
880
+ return p?.model || p?.collaboration_mode?.settings?.model || p?.model_slug || p?.config?.model || p?.info?.model || null;
881
+ }
882
+ function subtractClamped(cur, prev) {
883
+ const sub = (a, b) => Math.max(0, (a ?? 0) - (b ?? 0));
884
+ return {
885
+ input_tokens: sub(cur.input_tokens, prev?.input_tokens),
886
+ cached_input_tokens: sub(cur.cached_input_tokens, prev?.cached_input_tokens),
887
+ output_tokens: sub(cur.output_tokens, prev?.output_tokens),
888
+ reasoning_output_tokens: sub(cur.reasoning_output_tokens, prev?.reasoning_output_tokens)
889
+ };
890
+ }
891
+ async function parseFile2(path) {
892
+ const entries = [];
893
+ let model = "gpt-5-codex";
894
+ let prevTotal = null;
895
+ const rl = createInterface2({ input: createReadStream2(path), crlfDelay: Infinity });
896
+ for await (const rawLine of rl) {
897
+ if (!rawLine.includes("token_count") && !rawLine.includes("turn_context")) continue;
898
+ try {
899
+ const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
900
+ const obj = JSON.parse(line);
901
+ const payloadType = obj?.payload?.type ?? obj?.type;
902
+ if (payloadType === "turn_context") {
903
+ const m = extractModel(obj);
904
+ if (typeof m === "string" && m.trim()) model = m;
905
+ continue;
906
+ }
907
+ if (payloadType !== "token_count") continue;
908
+ const info = obj?.payload?.info;
909
+ const total = info?.total_token_usage;
910
+ let d = info?.last_token_usage;
911
+ if (!d && total) {
912
+ const reset = !!prevTotal && (total.input_tokens ?? 0) < (prevTotal.input_tokens ?? 0);
913
+ d = reset ? total : subtractClamped(total, prevTotal);
914
+ }
915
+ if (total) prevTotal = total;
916
+ if (!d) continue;
917
+ const ts = new Date(obj.timestamp ?? obj?.payload?.timestamp ?? 0).getTime();
918
+ if (!Number.isFinite(ts)) continue;
919
+ const inputTotal = safeNum(d.input_tokens);
920
+ const cached = Math.min(safeNum(d.cached_input_tokens), inputTotal);
921
+ const input = inputTotal - cached;
922
+ const output = safeNum(d.output_tokens);
923
+ if (input + output + cached === 0) continue;
924
+ const p = priceFor2(model);
925
+ entries.push({
926
+ ts,
927
+ model,
928
+ cost: input * p.in + cached * p.cr + output * p.out,
929
+ input,
930
+ output,
931
+ cacheCreate: 0,
932
+ cacheRead: cached,
933
+ cacheSavings: cached * (p.in - p.cr)
934
+ });
935
+ } catch {
936
+ }
937
+ }
938
+ return entries;
939
+ }
940
+ async function loadEntries2(since, homeDir) {
941
+ const files = [];
942
+ const seen = /* @__PURE__ */ new Set();
943
+ const seenIno = /* @__PURE__ */ new Set();
944
+ for (const home of codexHomes(homeDir)) {
945
+ const dir = join5(home, "sessions");
946
+ let listing;
947
+ try {
948
+ listing = await readdir2(dir, { recursive: true });
949
+ } catch {
950
+ continue;
951
+ }
952
+ for (const f of listing) {
953
+ if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
954
+ const path = join5(dir, f);
955
+ if (seen.has(path)) continue;
956
+ seen.add(path);
957
+ try {
958
+ const s = await fsStat2(path);
959
+ if (s.mtimeMs < since) continue;
960
+ if (s.ino && process.platform !== "win32") {
961
+ const idn = `${s.dev}:${s.ino}`;
962
+ if (seenIno.has(idn)) continue;
963
+ seenIno.add(idn);
964
+ }
965
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
966
+ } catch {
967
+ }
968
+ }
969
+ }
970
+ return loadCachedEntries(files, parseFile2, since);
971
+ }
972
+ async function codexDashboard(tz, homeDir) {
973
+ const now = Date.now();
974
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
975
+ const entries = await loadEntries2(since, homeDir);
976
+ return summarize(entries, tz);
977
+ }
978
+ async function codexTable(tz, homeDir) {
979
+ const entries = await loadEntries2(monthsAgoStart(Date.now(), 6, tz), homeDir);
980
+ return tabulate(entries, tz);
981
+ }
982
+
983
+ // src/providers/codex/billing.ts
984
+ import { execFile as execFileCb2 } from "child_process";
985
+ import { readFile as readFile4, readdir as readdir3, stat as fsStat3 } from "fs/promises";
986
+ import { createReadStream as createReadStream3 } from "fs";
987
+ import { createInterface as createInterface3 } from "readline";
988
+ import { join as join6 } from "path";
989
+ import { promisify as promisify2 } from "util";
990
+ var execFile2 = promisify2(execFileCb2);
991
+ var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
992
+ var CREDIT_USD_RATE = 0.04;
993
+ async function readAuthFile(home) {
994
+ try {
995
+ const raw = await readFile4(join6(home, "auth.json"), "utf-8");
996
+ const auth = JSON.parse(raw);
997
+ const accessToken = auth?.tokens?.access_token;
998
+ if (!accessToken) return null;
999
+ return { accessToken, accountId: auth?.tokens?.account_id };
1000
+ } catch {
1001
+ return null;
1002
+ }
1003
+ }
1004
+ async function readKeychainAuth() {
1005
+ try {
1006
+ const { stdout } = await execFile2("security", [
1007
+ "find-generic-password",
1008
+ "-s",
1009
+ "Codex Auth",
1010
+ "-w"
1011
+ ], { timeout: 5e3 });
1012
+ const auth = JSON.parse(stdout.trim());
1013
+ const accessToken = auth?.tokens?.access_token;
1014
+ if (!accessToken) return null;
1015
+ return { accessToken, accountId: auth?.tokens?.account_id };
1016
+ } catch {
1017
+ return null;
1018
+ }
1019
+ }
1020
+ async function getAuth(homeDir) {
1021
+ for (const home of codexHomes(homeDir)) {
1022
+ const auth = await readAuthFile(home);
1023
+ if (auth) return auth;
1024
+ }
1025
+ if (process.platform === "darwin") return readKeychainAuth();
1026
+ return null;
1027
+ }
1028
+ function planLabel(planType) {
1029
+ if (typeof planType !== "string" || !planType.trim()) return null;
1030
+ const p = planType.trim().toLowerCase();
1031
+ if (p === "prolite") return "Pro 5x";
1032
+ if (p === "pro") return "Pro 20x";
1033
+ return planType.charAt(0).toUpperCase() + planType.slice(1);
1034
+ }
1035
+ function isoOrNull(ms) {
1036
+ return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
1037
+ }
1038
+ function resetFrom(window) {
1039
+ if (!window) return null;
1040
+ let iso = null;
1041
+ if (typeof window.reset_at === "number") iso = isoOrNull(window.reset_at * 1e3);
1042
+ else if (typeof window.resets_at === "number") iso = isoOrNull(window.resets_at * 1e3);
1043
+ else if (typeof window.reset_after_seconds === "number") iso = isoOrNull(Date.now() + window.reset_after_seconds * 1e3);
1044
+ return iso ? resetIn(iso) : null;
1045
+ }
1046
+ function percentMetric(label, used, resets, primary) {
1047
+ return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: resets, primary };
1048
+ }
1049
+ async function liveBilling(auth) {
1050
+ try {
1051
+ const headers = {
1052
+ "Authorization": `Bearer ${auth.accessToken}`,
1053
+ "Accept": "application/json",
1054
+ "User-Agent": "tokmon"
1055
+ };
1056
+ if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
1057
+ const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(1e4) });
1058
+ if (!res.ok) return null;
1059
+ const data = await readJson(res);
1060
+ if (!data) return null;
1061
+ const metrics = [];
1062
+ const rl = data.rate_limit ?? null;
1063
+ const primary = rl?.primary_window ?? null;
1064
+ const secondary = rl?.secondary_window ?? null;
1065
+ const headerPct = (name) => {
1066
+ const h = res.headers.get(name);
1067
+ if (h === null || h.trim() === "") return void 0;
1068
+ const n = Number(h);
1069
+ return Number.isFinite(n) ? n : void 0;
1070
+ };
1071
+ const primaryPct = headerPct("x-codex-primary-used-percent") ?? primary?.used_percent;
1072
+ const secondaryPct = headerPct("x-codex-secondary-used-percent") ?? secondary?.used_percent;
1073
+ if (typeof primaryPct === "number") metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1074
+ if (typeof secondaryPct === "number") metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1075
+ const balance = data?.credits?.balance;
1076
+ if (typeof balance === "number" && balance >= 0) {
1077
+ metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1078
+ }
1079
+ if (metrics.length === 0) return null;
1080
+ return { plan: planLabel(data.plan_type), metrics, error: null };
1081
+ } catch {
1082
+ return null;
1083
+ }
1084
+ }
1085
+ async function newestRolloutFile(homeDir) {
1086
+ let best = null;
1087
+ for (const home of codexHomes(homeDir)) {
1088
+ const dir = join6(home, "sessions");
1089
+ let listing;
1090
+ try {
1091
+ listing = await readdir3(dir, { recursive: true });
1092
+ } catch {
1093
+ continue;
1094
+ }
1095
+ for (const f of listing) {
1096
+ if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1097
+ const path = join6(dir, f);
1098
+ try {
1099
+ const s = await fsStat3(path);
1100
+ if (!best || s.mtimeMs > best.mtime) best = { path, mtime: s.mtimeMs };
1101
+ } catch {
1102
+ }
1103
+ }
1104
+ }
1105
+ return best?.path ?? null;
1106
+ }
1107
+ async function snapshotBilling(homeDir) {
1108
+ const path = await newestRolloutFile(homeDir);
1109
+ if (!path) return null;
1110
+ let last = null;
1111
+ try {
1112
+ const rl = createInterface3({ input: createReadStream3(path), crlfDelay: Infinity });
1113
+ for await (const line of rl) {
1114
+ if (!line.includes("rate_limits")) continue;
1115
+ try {
1116
+ const obj = JSON.parse(line);
1117
+ if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
1118
+ } catch {
1119
+ }
1120
+ }
1121
+ } catch {
1122
+ return null;
1123
+ }
1124
+ if (!last) return null;
1125
+ const metrics = [];
1126
+ if (typeof last.primary?.used_percent === "number") {
1127
+ metrics.push(percentMetric("5h", last.primary.used_percent, resetFrom(last.primary), true));
1128
+ }
1129
+ if (typeof last.secondary?.used_percent === "number") {
1130
+ metrics.push(percentMetric("Week", last.secondary.used_percent, resetFrom(last.secondary)));
1131
+ }
1132
+ const balance = last?.credits?.balance;
1133
+ if (typeof balance === "number" && balance >= 0) {
1134
+ metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1135
+ }
1136
+ if (metrics.length === 0) return null;
1137
+ return { plan: planLabel(last.plan_type), metrics, error: null };
1138
+ }
1139
+ async function codexBilling(account) {
1140
+ const auth = await getAuth(account.homeDir);
1141
+ if (auth) {
1142
+ const live = await liveBilling(auth);
1143
+ if (live) return live;
1144
+ }
1145
+ const snap = await snapshotBilling(account.homeDir);
1146
+ if (snap) return snap;
1147
+ return {
1148
+ plan: null,
1149
+ metrics: [],
1150
+ error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex"
1151
+ };
1152
+ }
1153
+
1154
+ // src/providers/codex/index.ts
1155
+ var codexProvider = {
1156
+ id: "codex",
1157
+ name: "Codex",
1158
+ color: "cyan",
1159
+ hasUsage: true,
1160
+ hasBilling: true,
1161
+ detect: (homeDir) => detectCodex(homeDir),
1162
+ fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
1163
+ fetchTable: (account, tz) => codexTable(tz, account.homeDir),
1164
+ fetchBilling: (account) => codexBilling(account)
1165
+ };
1166
+
1167
+ // src/providers/cursor/billing.ts
1168
+ import { access as access3 } from "fs/promises";
1169
+ import { join as join8 } from "path";
1170
+ import { homedir as homedir6 } from "os";
1171
+
1172
+ // src/providers/cursor/activity.ts
1173
+ import { join as join7 } from "path";
1174
+ import { homedir as homedir5 } from "os";
1175
+
1176
+ // src/providers/cursor/sqlite.ts
1177
+ import { execFile as execFileCb3 } from "child_process";
1178
+ import { promisify as promisify3 } from "util";
1179
+ var execFile3 = promisify3(execFileCb3);
1180
+ async function runSqlite(db, sql, extraArgs = []) {
1181
+ try {
1182
+ const { stdout } = await execFile3(
1183
+ "sqlite3",
1184
+ ["-readonly", "-cmd", "PRAGMA busy_timeout=1500;", ...extraArgs, db, sql],
1185
+ { timeout: 1e4, maxBuffer: 8 << 20 }
1186
+ );
1187
+ return { status: "ok", stdout };
1188
+ } catch (e) {
1189
+ const err = e;
1190
+ if (err?.code === "ENOENT") return { status: "missing", stdout: "" };
1191
+ const msg = String(err?.stderr ?? err?.message ?? "");
1192
+ if (/database is (locked|busy)/i.test(msg)) return { status: "locked", stdout: "" };
1193
+ if (/no such function|unknown option|no such table/i.test(msg)) return { status: "old", stdout: "" };
1194
+ return { status: "error", stdout: "" };
1195
+ }
1196
+ }
1197
+ function sqliteStatusMessage(status) {
1198
+ switch (status) {
1199
+ case "missing":
1200
+ return "sqlite3 CLI not found \u2014 install it to read Cursor";
1201
+ case "old":
1202
+ return "sqlite3 too old (needs JSON support)";
1203
+ case "locked":
1204
+ return "Cursor DB busy \u2014 retrying next poll";
1205
+ default:
1206
+ return "Cursor data unavailable";
1207
+ }
1208
+ }
1209
+
1210
+ // src/providers/cursor/activity.ts
1211
+ var DAY_MS2 = 864e5;
1212
+ function trackingDb(homeDir) {
1213
+ return join7(homeDir ?? homedir5(), ".cursor", "ai-tracking", "ai-code-tracking.db");
1214
+ }
1215
+ function localDayKey(ms) {
1216
+ const d = new Date(ms);
1217
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1218
+ }
1219
+ async function cursorActivity(homeDir) {
1220
+ const db = trackingDb(homeDir);
1221
+ try {
1222
+ const now = Date.now();
1223
+ const res = await runSqlite(
1224
+ db,
1225
+ `SELECT date(createdAt/1000,'unixepoch','localtime') d, count(*) c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
1226
+ );
1227
+ if (res.status !== "ok") return null;
1228
+ const daily = res.stdout.trim();
1229
+ const byDay = /* @__PURE__ */ new Map();
1230
+ let month = 0;
1231
+ for (const line of daily.split("\n")) {
1232
+ if (!line) continue;
1233
+ const [d, c] = line.split("|");
1234
+ const n = Number(c) || 0;
1235
+ byDay.set(d, n);
1236
+ month += n;
1237
+ }
1238
+ const series = [];
1239
+ for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(localDayKey(now - i * DAY_MS2)) ?? 0);
1240
+ if (month === 0 && series.every((v) => v === 0)) return null;
1241
+ return { series, summary: `${tokens(month)} lines` };
1242
+ } catch {
1243
+ return null;
1244
+ }
1245
+ }
1246
+
1247
+ // src/providers/cursor/composer.ts
1248
+ async function cursorModelSpend(homeDir) {
1249
+ const db = cursorStateDb(homeDir);
1250
+ const sql = "SELECT mk.key, sum(json_extract(mk.value,'$.costInCents')), sum(json_extract(mk.value,'$.amount')) FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY 2 DESC;";
1251
+ const res = await runSqlite(db, sql, ["-separator", " "]);
1252
+ if (res.status !== "ok") return null;
1253
+ const models = [];
1254
+ let total = 0;
1255
+ for (const line of res.stdout.trim().split("\n")) {
1256
+ if (!line) continue;
1257
+ const [name, cents, amt] = line.split(" ");
1258
+ const usd = (Number(cents) || 0) / 100;
1259
+ if (usd <= 0) continue;
1260
+ models.push({ name, usd, requests: Number(amt) || 0 });
1261
+ total += usd;
1262
+ }
1263
+ if (total <= 0) return null;
1264
+ return { total, models };
1265
+ }
1266
+
1267
+ // src/providers/cursor/billing.ts
1268
+ var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
1269
+ var USAGE_URL2 = `${BASE}/GetCurrentPeriodUsage`;
1270
+ var PLAN_URL = `${BASE}/GetPlanInfo`;
1271
+ function cursorStateDb(homeDir) {
1272
+ const base = homeDir ?? homedir6();
1273
+ const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
1274
+ if (process.platform === "darwin") {
1275
+ return join8(base, "Library", "Application Support", ...tail);
1276
+ }
1277
+ if (process.platform === "win32") {
1278
+ const roaming = homeDir ? join8(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join8(base, "AppData", "Roaming");
1279
+ return join8(roaming, ...tail);
1280
+ }
1281
+ const cfg = homeDir ? join8(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join8(base, ".config");
1282
+ return join8(cfg, ...tail);
1283
+ }
1284
+ async function detectCursor(homeDir) {
1285
+ try {
1286
+ await access3(cursorStateDb(homeDir));
1287
+ return true;
1288
+ } catch {
1289
+ return false;
1290
+ }
1291
+ }
1292
+ async function readState(db, key) {
1293
+ const safe = key.replace(/'/g, "''");
1294
+ const r = await runSqlite(db, `SELECT value FROM ItemTable WHERE key='${safe}' LIMIT 1;`);
1295
+ return { value: r.status === "ok" ? r.stdout.trim() || null : null, status: r.status };
1296
+ }
1297
+ async function connectPost(url, token) {
1298
+ try {
1299
+ const res = await fetch(url, {
1300
+ method: "POST",
1301
+ headers: {
1302
+ "Authorization": `Bearer ${token}`,
1303
+ "Content-Type": "application/json",
1304
+ "Connect-Protocol-Version": "1",
1305
+ "User-Agent": "tokmon"
1306
+ },
1307
+ body: "{}",
1308
+ signal: AbortSignal.timeout(1e4)
1309
+ });
1310
+ if (!res.ok) return { __status: res.status };
1311
+ return await readJson(res);
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1316
+ var dollars = (cents) => cents / 100;
1317
+ async function cursorBilling(account) {
1318
+ const [core, activity, spend] = await Promise.all([
1319
+ cursorBillingCore(account),
1320
+ cursorActivity(account.homeDir),
1321
+ cursorModelSpend(account.homeDir)
1322
+ ]);
1323
+ let merged = activity;
1324
+ if (spend) {
1325
+ const lines = activity?.summary ?? "";
1326
+ const spendLabel = `$${Math.round(spend.total)} all-time`;
1327
+ merged = {
1328
+ series: activity?.series ?? [],
1329
+ summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
1330
+ };
1331
+ }
1332
+ return { ...core, activity: merged };
1333
+ }
1334
+ async function cursorBillingCore(account) {
1335
+ const db = cursorStateDb(account.homeDir);
1336
+ const [tokenRes, membershipRes] = await Promise.all([
1337
+ readState(db, "cursorAuth/accessToken"),
1338
+ readState(db, "cursorAuth/stripeMembershipType")
1339
+ ]);
1340
+ const token = tokenRes.value;
1341
+ const membership = membershipRes.value;
1342
+ const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
1343
+ if (!token) {
1344
+ const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
1345
+ return { plan: planFallback, metrics: [], error };
1346
+ }
1347
+ const [usage, planInfo] = await Promise.all([
1348
+ connectPost(USAGE_URL2, token),
1349
+ connectPost(PLAN_URL, token)
1350
+ ]);
1351
+ if (!usage || usage.__status) {
1352
+ const expired = usage?.__status === 401 || usage?.__status === 403;
1353
+ return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error" };
1354
+ }
1355
+ const planName = planInfo?.planInfo?.planName ?? planFallback;
1356
+ const price = planInfo?.planInfo?.price;
1357
+ const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
1358
+ const pu = usage.planUsage ?? {};
1359
+ const metrics = [];
1360
+ const rawEnd = usage.billingCycleEnd;
1361
+ const endMs = typeof rawEnd === "string" && rawEnd.trim() ? Number(rawEnd) : NaN;
1362
+ const resets = Number.isFinite(endMs) && endMs > 0 && endMs <= 864e13 ? resetIn(new Date(endMs).toISOString()) : null;
1363
+ if (typeof pu.totalPercentUsed === "number" && typeof pu.limit === "number") {
1364
+ metrics.push({
1365
+ label: "Usage",
1366
+ used: pu.totalPercentUsed,
1367
+ limit: 100,
1368
+ format: { kind: "percent" },
1369
+ resetsAt: resets,
1370
+ primary: true
1371
+ });
1372
+ const spentCents = typeof pu.totalSpend === "number" ? pu.totalSpend : pu.limit - (pu.remaining ?? 0);
1373
+ metrics.push({
1374
+ label: "Spend",
1375
+ used: dollars(spentCents),
1376
+ limit: dollars(pu.limit),
1377
+ format: { kind: "dollars" }
1378
+ });
1379
+ }
1380
+ if (pu.autoPercentUsed) {
1381
+ metrics.push({ label: "Auto", used: pu.autoPercentUsed, limit: 100, format: { kind: "percent" } });
1382
+ }
1383
+ if (pu.apiPercentUsed) {
1384
+ metrics.push({ label: "API", used: pu.apiPercentUsed, limit: 100, format: { kind: "percent" } });
1385
+ }
1386
+ const su = usage.spendLimitUsage;
1387
+ if (su) {
1388
+ const limitCents = su.individualLimit ?? su.pooledLimit ?? 0;
1389
+ const remainingCents = su.individualRemaining ?? su.pooledRemaining ?? 0;
1390
+ if (limitCents > 0) {
1391
+ metrics.push({
1392
+ label: "On-demand",
1393
+ used: dollars(limitCents - remainingCents),
1394
+ limit: dollars(limitCents),
1395
+ format: { kind: "dollars" }
1396
+ });
1397
+ }
1398
+ }
1399
+ if (metrics.length === 0) {
1400
+ return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data" };
1401
+ }
1402
+ return { plan, metrics, error: null };
1403
+ }
1404
+
1405
+ // src/providers/cursor/index.ts
1406
+ var cursorProvider = {
1407
+ id: "cursor",
1408
+ name: "Cursor",
1409
+ color: "magenta",
1410
+ hasUsage: false,
1411
+ // Cursor exposes spend/limits, not a token history
1412
+ hasBilling: true,
1413
+ detect: (homeDir) => detectCursor(homeDir),
1414
+ fetchBilling: (account) => cursorBilling(account)
1415
+ };
1416
+
1417
+ // src/providers/detect.ts
1418
+ import { accessSync, constants } from "fs";
1419
+ import { join as join9, delimiter, isAbsolute as isAbsolute3 } from "path";
1420
+ import { homedir as homedir7 } from "os";
1421
+ function searchDirs() {
1422
+ const home = homedir7();
1423
+ const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
1424
+ const extra = process.platform === "win32" ? [
1425
+ process.env.APPDATA && join9(process.env.APPDATA, "npm"),
1426
+ process.env.LOCALAPPDATA && join9(process.env.LOCALAPPDATA, "pnpm"),
1427
+ join9(home, "scoop", "shims")
1428
+ ] : [
1429
+ "/opt/homebrew/bin",
1430
+ "/usr/local/bin",
1431
+ "/usr/bin",
1432
+ "/bin",
1433
+ "/opt/local/bin",
1434
+ join9(home, ".local", "bin"),
1435
+ join9(home, "bin"),
1436
+ join9(home, ".npm-global", "bin"),
1437
+ join9(home, ".bun", "bin"),
1438
+ join9(home, ".local", "share", "pnpm")
1439
+ ];
1440
+ return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
1441
+ }
1442
+ function isExec(p) {
1443
+ try {
1444
+ accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
1445
+ return true;
1446
+ } catch {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ function onPath(names) {
1451
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()).concat("") : [""];
1452
+ for (const dir of searchDirs()) {
1453
+ for (const n of names) {
1454
+ for (const e of exts) {
1455
+ if (isExec(join9(dir, n + e))) return true;
1456
+ }
1457
+ }
1458
+ }
1459
+ return false;
1460
+ }
1461
+ function anyExists(paths) {
1462
+ return paths.some((p) => !!p && isExec(p));
1463
+ }
1464
+ function installSignals(id) {
1465
+ const home = homedir7();
1466
+ const pf = process.env.ProgramFiles;
1467
+ const pf86 = process.env["ProgramFiles(x86)"];
1468
+ const lad = process.env.LOCALAPPDATA;
1469
+ switch (id) {
1470
+ case "claude":
1471
+ return onPath(["claude"]) || anyExists([
1472
+ "/Applications/Claude.app",
1473
+ join9(home, "Applications", "Claude.app"),
1474
+ lad && join9(lad, "Programs", "claude", "Claude.exe")
1475
+ ]);
1476
+ case "codex": {
1477
+ const bin = process.env.CODEX_BIN;
1478
+ if (bin && isAbsolute3(bin) && isExec(bin)) return true;
1479
+ return onPath(["codex"]);
1480
+ }
1481
+ case "cursor":
1482
+ return onPath(["cursor", "cursor-agent"]) || anyExists([
1483
+ "/Applications/Cursor.app",
1484
+ join9(home, "Applications", "Cursor.app"),
1485
+ lad && join9(lad, "Programs", "cursor", "Cursor.exe"),
1486
+ pf && join9(pf, "Cursor", "Cursor.exe"),
1487
+ pf86 && join9(pf86, "Cursor", "Cursor.exe"),
1488
+ "/opt/Cursor/cursor",
1489
+ "/usr/share/cursor/cursor",
1490
+ "/usr/bin/cursor"
1491
+ ]);
1492
+ default:
1493
+ return false;
1494
+ }
1495
+ }
1496
+
1497
+ // src/providers/index.ts
1498
+ var PROVIDER_ORDER = ["claude", "codex", "cursor"];
1499
+ var PROVIDERS = {
1500
+ claude: claudeProvider,
1501
+ codex: codexProvider,
1502
+ cursor: cursorProvider
1503
+ };
1504
+ var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
1505
+ async function detectProviders() {
1506
+ const found = await Promise.all(
1507
+ PROVIDER_ORDER.map(async (id) => {
1508
+ try {
1509
+ if (installSignals(id)) return id;
1510
+ return await PROVIDERS[id].detect() ? id : null;
1511
+ } catch {
1512
+ return null;
1513
+ }
1514
+ })
1515
+ );
1516
+ return found.filter((id) => id !== null);
1517
+ }
1518
+
1519
+ // src/accounts.ts
1520
+ function buildAccounts(config2, detected) {
1521
+ const out = [];
1522
+ for (const pid of PROVIDER_ORDER) {
1523
+ if (config2.disabledProviders.includes(pid)) continue;
1524
+ const provider = PROVIDERS[pid];
1525
+ const configured = config2.accounts.filter((a) => a.providerId === pid);
1526
+ if (configured.length > 0) {
1527
+ for (const a of configured) {
1528
+ out.push({
1529
+ id: a.id,
1530
+ providerId: pid,
1531
+ name: a.name,
1532
+ color: a.color || provider.color,
1533
+ homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
1534
+ });
1535
+ }
1536
+ } else if (detected.includes(pid)) {
1537
+ out.push({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
1538
+ }
1539
+ }
1540
+ return out;
1541
+ }
1542
+ function accountsByProvider(accounts) {
1543
+ const groups = [];
1544
+ for (const pid of PROVIDER_ORDER) {
1545
+ const list = accounts.filter((a) => a.providerId === pid);
1546
+ if (list.length > 0) groups.push({ provider: pid, accounts: list });
1547
+ }
1548
+ return groups;
1549
+ }
1550
+
1551
+ // src/ui/shared.tsx
1552
+ import { useState, useEffect, useRef } from "react";
1553
+ import { Box, Text } from "ink";
1554
+ import { useOnMouseClick } from "@zenobius/ink-mouse";
1555
+ import { jsx, jsxs } from "react/jsx-runtime";
1556
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1557
+ function truncateName(s, n) {
1558
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
1559
+ }
1560
+ function ClickableBox({ onClick, children, ...props }) {
1561
+ const ref = useRef(null);
1562
+ useOnMouseClick(ref, (clicked) => {
1563
+ if (clicked) onClick();
1564
+ });
1565
+ return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
1566
+ }
1567
+ function Spinner({ label }) {
1568
+ const [i, setI] = useState(0);
1569
+ useEffect(() => {
1570
+ const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
1571
+ return () => clearInterval(id);
1572
+ }, []);
1573
+ return /* @__PURE__ */ jsxs(Box, { children: [
1574
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1575
+ SPINNER_FRAMES[i],
1576
+ " "
1577
+ ] }),
1578
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
1579
+ ] });
1580
+ }
1581
+ function TabBar({ tabs, active, onSelect }) {
1582
+ return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
1583
+ " ",
1584
+ t,
1585
+ " "
1586
+ ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1587
+ " ",
1588
+ t,
1589
+ " "
1590
+ ] }) }, t)) });
1591
+ }
1592
+ function PeakBadge({ peak }) {
1593
+ const color = peak.state === "peak" ? "red" : "green";
1594
+ return /* @__PURE__ */ jsxs(Box, { children: [
1595
+ /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
1596
+ /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
1597
+ peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1598
+ " (",
1599
+ fmtMinutes(peak.minutesUntilChange),
1600
+ ")"
1601
+ ] })
1602
+ ] });
1603
+ }
1604
+ function fmtMinutes(mins) {
1605
+ if (mins < 60) return `${mins}m`;
1606
+ const h = Math.floor(mins / 60);
1607
+ const m = mins % 60;
1608
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
1609
+ }
1610
+ function currencySymbol(cur) {
1611
+ return cur === "EUR" ? "\u20AC" : cur === "GBP" ? "\xA3" : "$";
1612
+ }
1613
+ var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
1614
+ function sparkline(values) {
1615
+ if (values.length === 0) return "";
1616
+ const max = Math.max(...values);
1617
+ if (max <= 0) return SPARK[0].repeat(values.length);
1618
+ return values.map((v) => {
1619
+ if (v <= 0) return SPARK[0];
1620
+ const idx = Math.min(SPARK.length - 1, 1 + Math.round(v / max * (SPARK.length - 2)));
1621
+ return SPARK[idx];
1622
+ }).join("");
1623
+ }
1624
+ function Bar({ pct: pct2, color, width = 24 }) {
1625
+ const filled = Math.max(0, Math.min(width, Math.round(pct2 / 100 * width)));
1626
+ return /* @__PURE__ */ jsxs(Text, { children: [
1627
+ /* @__PURE__ */ jsx(Text, { color, children: "\u2501".repeat(filled) }),
1628
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
1629
+ ] });
1630
+ }
1631
+ function metricValueText(m) {
1632
+ if (m.format.kind === "dollars") {
1633
+ const sym = currencySymbol(m.format.currency);
1634
+ const used = `${sym}${m.used.toFixed(2)}`;
1635
+ return m.limit != null ? `${used} / ${sym}${m.limit.toFixed(2)}` : `${used}`;
1636
+ }
1637
+ if (m.format.kind === "count") {
1638
+ const suffix = m.format.suffix ? ` ${m.format.suffix}` : "";
1639
+ const used = `${Math.round(m.used)}${suffix}`;
1640
+ return m.limit != null ? `${used} / ${Math.round(m.limit)}` : used;
1641
+ }
1642
+ return `${Math.round(m.used)}%`;
1643
+ }
1644
+
1645
+ // src/ui/dashboard.tsx
1646
+ import { Box as Box2, Text as Text2 } from "ink";
1647
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1648
+ var GAP = 2;
1649
+ var MIN_CARD = 56;
1650
+ function DashboardView({ groups, stats, cols, focusId, layout }) {
1651
+ if (groups.length === 0) {
1652
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No providers enabled \u2014 press s to pick providers." });
1653
+ }
1654
+ let shown = groups;
1655
+ if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
1656
+ const content = Math.max(MIN_CARD, cols - 4);
1657
+ const single = focusId !== null || layout === "single";
1658
+ const auto = Math.max(1, Math.min(2, Math.floor((content + GAP) / (MIN_CARD + GAP))));
1659
+ const ncols = single ? 1 : Math.min(auto, shown.length);
1660
+ const cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
1661
+ return /* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: shown.map((g) => /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW }, g.provider)) });
1662
+ }
1663
+ function ProviderCard({ provider, accounts, stats, width }) {
1664
+ const meta = PROVIDERS[provider];
1665
+ const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
1666
+ const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
1667
+ const agg = meta.hasUsage && dashboards.length > 0 ? aggregate(dashboards) : null;
1668
+ const plan = items.map((i) => i.s?.billing?.plan).find(Boolean) ?? null;
1669
+ const activity = items.map((i) => i.s?.billing?.activity).find(Boolean) ?? null;
1670
+ const inner = width - 4;
1671
+ const barW = Math.max(10, Math.min(46, inner - 20));
1672
+ const hasSpark = !!agg && agg.series.some((v) => v > 0);
1673
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: "round", borderColor: meta.color, paddingX: 1, children: [
1674
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1675
+ /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: "\u25CF " }),
1676
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
1677
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1678
+ plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
1679
+ ] }),
1680
+ meta.hasUsage && (agg ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1681
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
1682
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "Today", s: agg.today }),
1683
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "This Week", s: agg.week }),
1684
+ /* @__PURE__ */ jsx2(SummaryRow, { label: "This Month", s: agg.month }),
1685
+ /* @__PURE__ */ jsx2(KpiLine, { agg })
1686
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1687
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
1688
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Fetching usage\u2026" })
1689
+ ] })),
1690
+ meta.hasBilling && /* @__PURE__ */ jsxs2(Fragment, { children: [
1691
+ meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
1692
+ /* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
1693
+ ] }),
1694
+ hasSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
1695
+ /* @__PURE__ */ jsx2(Rule, { inner }),
1696
+ /* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
1697
+ ] }),
1698
+ !meta.hasUsage && activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
1699
+ /* @__PURE__ */ jsx2(Rule, { inner }),
1700
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1701
+ /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
1702
+ /* @__PURE__ */ jsx2(Text2, { color: meta.color, children: sparkline(activity.series) }),
1703
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
1704
+ ] })
1705
+ ] }),
1706
+ !meta.hasUsage && !activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
1707
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1708
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Billing only \u2014 no local history" })
1709
+ ] })
1710
+ ] });
1711
+ }
1712
+ function Rule({ inner }) {
1713
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(Math.max(0, inner)) });
1714
+ }
1715
+ function SummaryRow({ label, s }) {
1716
+ const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
1717
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1718
+ /* @__PURE__ */ jsx2(Box2, { width: 11, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: label }) }),
1719
+ /* @__PURE__ */ jsx2(Box2, { width: 11, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: currency(s.cost) }) }),
1720
+ /* @__PURE__ */ jsx2(Box2, { width: 13, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1721
+ tokens(s.tokens),
1722
+ " tok"
1723
+ ] }) }),
1724
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: cachedPct > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1725
+ cachedPct,
1726
+ "% cached"
1727
+ ] }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
1728
+ ] });
1729
+ }
1730
+ function KpiLine({ agg }) {
1731
+ const hasBurn = agg.burnRate > 0;
1732
+ const hasSaved = agg.month.cacheSavings > 0;
1733
+ if (!hasBurn && !hasSaved) return null;
1734
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1735
+ hasBurn && /* @__PURE__ */ jsxs2(Fragment, { children: [
1736
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Burn " }),
1737
+ /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
1738
+ currency(agg.burnRate),
1739
+ "/hr"
1740
+ ] })
1741
+ ] }),
1742
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
1743
+ hasSaved && /* @__PURE__ */ jsxs2(Fragment, { children: [
1744
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Cache saved " }),
1745
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
1746
+ currency(agg.month.cacheSavings),
1747
+ "/mo"
1748
+ ] })
1749
+ ] })
1750
+ ] });
1751
+ }
1752
+ function LimitsBlock({ items, barW }) {
1753
+ const showName = items.length > 1;
1754
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
1755
+ const billing = s?.billing;
1756
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
1757
+ showName && /* @__PURE__ */ jsxs2(Box2, { children: [
1758
+ /* @__PURE__ */ jsx2(Text2, { color: account.color, children: "\u25CF " }),
1759
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
1760
+ ] }),
1761
+ !billing ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Fetching\u2026" }) : 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}`))
1762
+ ] }, account.id);
1763
+ }) });
1764
+ }
1765
+ function MetricRow({ m, color, barW }) {
1766
+ if (m.format.kind === "percent") {
1767
+ const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
1768
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1769
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.label }) }),
1770
+ /* @__PURE__ */ jsx2(Bar, { pct: m.used, color: barColor, width: barW }),
1771
+ /* @__PURE__ */ jsx2(Box2, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
1772
+ Math.round(m.used),
1773
+ "%"
1774
+ ] }) }),
1775
+ /* @__PURE__ */ jsx2(Box2, { width: 8, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.resetsAt }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
1776
+ ] });
1777
+ }
1778
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1779
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: m.label }) }),
1780
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) })
1781
+ ] });
1782
+ }
1783
+ function SparkFooter({ series, month, color }) {
1784
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1785
+ /* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
1786
+ /* @__PURE__ */ jsx2(Text2, { color, children: sparkline(series) }),
1787
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1788
+ currency(month),
1789
+ " mo"
1790
+ ] }) })
1791
+ ] });
1792
+ }
1793
+ function aggregate(list) {
1794
+ const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
1795
+ const z = { today: zero(), week: zero(), month: zero(), burnRate: 0, series: [] };
1796
+ for (const d of list) {
1797
+ for (const k of ["today", "week", "month"]) {
1798
+ z[k].cost += d[k].cost;
1799
+ z[k].tokens += d[k].tokens;
1800
+ z[k].cacheRead += d[k].cacheRead;
1801
+ z[k].cacheSavings += d[k].cacheSavings;
1802
+ }
1803
+ z.burnRate += d.burnRate;
1804
+ d.series.forEach((v, i) => {
1805
+ z.series[i] = (z.series[i] ?? 0) + v;
1806
+ });
1807
+ }
1808
+ return z;
1809
+ }
1810
+
1811
+ // src/ui/table.tsx
1812
+ import { Box as Box3, Text as Text3 } from "ink";
1813
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
572
1814
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1815
+ function TableProviderBar({ providers, active, onSelect }) {
1816
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1817
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
1818
+ providers.map((p) => {
1819
+ const meta = PROVIDERS[p];
1820
+ return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
1821
+ " ",
1822
+ meta.name,
1823
+ " "
1824
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { color: meta.color, dimColor: true, children: [
1825
+ " ",
1826
+ meta.name,
1827
+ " "
1828
+ ] }) }, p);
1829
+ }),
1830
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " p/P switch" })
1831
+ ] });
1832
+ }
1833
+ function ControlBar({ views, period, sort, search, searching, showPeriod }) {
1834
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1835
+ showPeriod && /* @__PURE__ */ jsxs3(Fragment2, { children: [
1836
+ views.map((v, i) => /* @__PURE__ */ jsx3(Box3, { marginRight: 2, children: i === period ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
1837
+ "[",
1838
+ v,
1839
+ "]"
1840
+ ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: v }) }, v)),
1841
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " })
1842
+ ] }),
1843
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "sort " }),
1844
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: sort }),
1845
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " o cycle \xB7 " }),
1846
+ searching ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1847
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/" }),
1848
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: search }),
1849
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u258F" })
1850
+ ] }) : search ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1851
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "filter " }),
1852
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: search }),
1853
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (/ edit \xB7 esc clear)" })
1854
+ ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/ filter" })
1855
+ ] });
1856
+ }
1857
+ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
1858
+ if (rows.length === 0) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No usage in this period (or filtered out)." });
1859
+ const wide = cols > 90;
1860
+ 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 };
1861
+ const fixed = base.label + base.input + base.output + base.cc + base.cr + base.total + base.cost;
1862
+ const available = cols - fixed - 6;
1863
+ const W = { ...base, models: Math.max(wide ? 22 : 14, available) };
1864
+ const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
1865
+ const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
1866
+ for (const r of rows) {
1867
+ totals.input += r.input;
1868
+ totals.output += r.output;
1869
+ totals.cacheCreate += r.cacheCreate;
1870
+ totals.cacheRead += r.cacheRead;
1871
+ totals.cost += r.cost;
1872
+ }
1873
+ const clampedCursor = Math.min(cursor, rows.length - 1);
1874
+ const scrollStart = Math.max(0, Math.min(clampedCursor - Math.floor(maxRows / 2), rows.length - maxRows));
1875
+ const visible = rows.slice(scrollStart, scrollStart + maxRows);
1876
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1877
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1878
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
1879
+ " ",
1880
+ col("Date", W.label, "left")
1881
+ ] }),
1882
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Models", W.models, "left") }),
1883
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Input", W.input) }),
1884
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Output", W.output) }),
1885
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(wide ? "Cache Create" : "CchCrt", W.cc) }),
1886
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(wide ? "Cache Read" : "CchRd", W.cr) }),
1887
+ W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Total", W.total) }),
1888
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) })
1889
+ ] }),
1890
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
1891
+ visible.map((r, vi) => {
1892
+ const idx = scrollStart + vi;
1893
+ const selected = idx === clampedCursor;
1894
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1895
+ /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1896
+ /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "cyan", children: [
1897
+ selected ? "\u25B8 " : " ",
1898
+ col(fmtLabel(r.label), W.label, "left")
1899
+ ] }),
1900
+ /* @__PURE__ */ jsx3(Text3, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
1901
+ /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.input), W.input) }),
1902
+ /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.output), W.output) }),
1903
+ /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.cacheCreate), W.cc) }),
1904
+ /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.cacheRead), W.cr) }),
1905
+ W.total > 0 && /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.total), W.total) }),
1906
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.cost), W.cost) })
1907
+ ] }) }),
1908
+ idx === expanded && /* @__PURE__ */ jsx3(RowDetail, { row: r, indent: W.label + 2 })
1909
+ ] }, r.label);
1910
+ }),
1911
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
1912
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1913
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1914
+ " ",
1915
+ col("Total", W.label, "left")
1916
+ ] }),
1917
+ /* @__PURE__ */ jsx3(Text3, { children: col("", W.models, "left") }),
1918
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
1919
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
1920
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
1921
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
1922
+ W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
1923
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
1924
+ ] }),
1925
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
1926
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1927
+ "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
1928
+ clampedCursor + 1,
1929
+ "/",
1930
+ rows.length
1931
+ ] })
1932
+ ] });
1933
+ }
1934
+ function RowDetail({ row, indent }) {
1935
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
1936
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1937
+ i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500",
1938
+ " "
1939
+ ] }),
1940
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col(m.name, 16, "left") }),
1941
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1942
+ col(tokens(m.input), 8),
1943
+ " in "
1944
+ ] }),
1945
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1946
+ col(tokens(m.output), 8),
1947
+ " out "
1948
+ ] }),
1949
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1950
+ col(tokens(m.cacheCreate), 8),
1951
+ " cc "
1952
+ ] }),
1953
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1954
+ col(tokens(m.cacheRead), 9),
1955
+ " cr "
1956
+ ] }),
1957
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: currency(m.cost) })
1958
+ ] }, m.name)) });
1959
+ }
1960
+ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
1961
+ if (rows.length === 0) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No Cursor spend recorded locally." });
1962
+ const total = rows.reduce((a, r) => a + r.usd, 0);
1963
+ const totalAmt = rows.reduce((a, r) => a + r.requests, 0);
1964
+ const clamped = Math.min(cursor, rows.length - 1);
1965
+ const scrollStart = Math.max(0, Math.min(clamped - Math.floor(maxRows / 2), rows.length - maxRows));
1966
+ const visible = rows.slice(scrollStart, scrollStart + maxRows);
1967
+ const W = { model: 34, cost: 12, amount: 12, share: 8 };
1968
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1969
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1970
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
1971
+ " ",
1972
+ col("Model", W.model, "left")
1973
+ ] }),
1974
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) }),
1975
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Amount", W.amount) }),
1976
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Share", W.share) })
1977
+ ] }),
1978
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
1979
+ visible.map((r, vi) => {
1980
+ const idx = scrollStart + vi;
1981
+ const selected = idx === clamped;
1982
+ const share = total > 0 ? r.usd / total * 100 : 0;
1983
+ return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
1984
+ /* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "magenta", children: [
1985
+ selected ? "\u25B8 " : " ",
1986
+ col(r.name, W.model, "left")
1987
+ ] }),
1988
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
1989
+ /* @__PURE__ */ jsx3(Text3, { children: col(tokens(r.requests), W.amount) }),
1990
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
1991
+ ] }) }, r.name);
1992
+ }),
1993
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(W.model + W.cost + W.amount + W.share + 2) }),
1994
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1995
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
1996
+ " ",
1997
+ col("Total", W.model, "left")
1998
+ ] }),
1999
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellowBright", children: col(currency(total), W.cost) }),
2000
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: col(tokens(totalAmt), W.amount) }),
2001
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col("100%", W.share) })
2002
+ ] }),
2003
+ /* @__PURE__ */ jsx3(Box3, { height: 1 }),
2004
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2005
+ "local spend by model (composerData) \xB7 est. API-equivalent \xB7 ",
2006
+ clamped + 1,
2007
+ "/",
2008
+ rows.length
2009
+ ] })
2010
+ ] });
2011
+ }
2012
+ function fmtLabel(label) {
2013
+ if (label.length === 7 && label[4] === "-") {
2014
+ return `${MONTHS[Number(label.slice(5, 7))]} '${label.slice(2, 4)}`;
2015
+ }
2016
+ return shortDate(label);
2017
+ }
2018
+
2019
+ // src/ui/onboarding.tsx
2020
+ import { Box as Box4, Text as Text4 } from "ink";
2021
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2022
+ function Onboarding({ items, cursor, onToggle, onConfirm }) {
2023
+ const anyEnabled = items.some((it) => it.enabled);
2024
+ const startIdx = items.length;
2025
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
2026
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: "Welcome to tokmon" }) }),
2027
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Pick the tools you want to track. You can change this anytime in settings." }),
2028
+ /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2029
+ items.map((it, i) => {
2030
+ const selected = cursor === i;
2031
+ const box = it.enabled ? "[\u2713]" : "[ ]";
2032
+ return /* @__PURE__ */ jsxs4(ClickableBox, { onClick: () => onToggle(i), children: [
2033
+ /* @__PURE__ */ jsxs4(Text4, { color: selected ? "green" : void 0, children: [
2034
+ selected ? "\u25B8" : " ",
2035
+ " "
2036
+ ] }),
2037
+ /* @__PURE__ */ jsx4(Text4, { bold: it.enabled, color: it.enabled ? it.color : void 0, dimColor: !it.enabled, children: box }),
2038
+ /* @__PURE__ */ jsx4(Text4, { color: it.color, children: " \u25CF " }),
2039
+ /* @__PURE__ */ jsx4(Box4, { width: 10, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, children: it.name }) }),
2040
+ 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" })
2041
+ ] }, it.id);
2042
+ }),
2043
+ /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2044
+ /* @__PURE__ */ jsxs4(ClickableBox, { onClick: onConfirm, children: [
2045
+ /* @__PURE__ */ jsxs4(Text4, { color: cursor === startIdx ? "green" : void 0, children: [
2046
+ cursor === startIdx ? "\u25B8" : " ",
2047
+ " "
2048
+ ] }),
2049
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? "\u25B6 Start tokmon" : "\u25B6 Start (nothing selected)" })
2050
+ ] }),
2051
+ /* @__PURE__ */ jsx4(Box4, { height: 1 }),
2052
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 move \xB7 space toggle \xB7 enter start \xB7 q quit" })
2053
+ ] });
2054
+ }
2055
+
2056
+ // src/ui/settings.tsx
2057
+ import { Box as Box5, Text as Text5 } from "ink";
2058
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2059
+ var GENERAL_ROWS = 6;
2060
+ var PROVIDER_ROWS_START = GENERAL_ROWS;
2061
+ var ACCOUNT_ROWS_START = GENERAL_ROWS + PROVIDER_ORDER.length;
2062
+ var FORM_FIELDS = ["provider", "name", "homeDir", "color"];
2063
+ var COLOR_PALETTE = [
2064
+ "cyan",
2065
+ "magenta",
2066
+ "green",
2067
+ "yellow",
2068
+ "blue",
2069
+ "red",
2070
+ "cyanBright",
2071
+ "magentaBright",
2072
+ "greenBright"
2073
+ ];
2074
+ function SettingsView({
2075
+ config: config2,
2076
+ cursor,
2077
+ tzEdit,
2078
+ tzError,
2079
+ resolvedTz,
2080
+ accountForm,
2081
+ activeAccountId
2082
+ }) {
2083
+ if (accountForm) return /* @__PURE__ */ jsx5(AccountFormView, { form: accountForm, accounts: config2.accounts });
2084
+ const editingTz = tzEdit !== null;
2085
+ const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
2086
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
2087
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Settings" }),
2088
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: configLocation() }),
2089
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2090
+ /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "General" }),
2091
+ /* @__PURE__ */ jsxs5(Row, { cursor, idx: 0, label: "Refresh interval", children: [
2092
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2093
+ "\u25C2",
2094
+ " "
2095
+ ] }),
2096
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
2097
+ config2.interval,
2098
+ "s"
2099
+ ] }),
2100
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2101
+ " ",
2102
+ "\u25B8"
2103
+ ] })
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxs5(Row, { cursor, idx: 1, label: "Billing poll", children: [
2106
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2107
+ "\u25C2",
2108
+ " "
2109
+ ] }),
2110
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
2111
+ config2.billingInterval,
2112
+ "m"
2113
+ ] }),
2114
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2115
+ " ",
2116
+ "\u25B8"
2117
+ ] })
2118
+ ] }),
2119
+ /* @__PURE__ */ jsx5(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" }) }),
2120
+ /* @__PURE__ */ jsx5(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2121
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[" }),
2122
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: tzEdit }),
2123
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "_" }),
2124
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "]" })
2125
+ ] }) : /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: tzDisplay }) }),
2126
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
2127
+ " ",
2128
+ tzError
2129
+ ] }),
2130
+ /* @__PURE__ */ jsxs5(Row, { cursor, idx: 4, label: "Dashboard", children: [
2131
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2132
+ "\u25C2",
2133
+ " "
2134
+ ] }),
2135
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
2136
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2137
+ " ",
2138
+ "\u25B8"
2139
+ ] })
2140
+ ] }),
2141
+ /* @__PURE__ */ jsxs5(Row, { cursor, idx: 5, label: "Default focus", children: [
2142
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2143
+ "\u25C2",
2144
+ " "
2145
+ ] }),
2146
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
2147
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2148
+ " ",
2149
+ "\u25B8"
2150
+ ] })
2151
+ ] }),
2152
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2153
+ /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "Providers" }),
2154
+ PROVIDER_ORDER.map((pid, i) => {
2155
+ const idx = PROVIDER_ROWS_START + i;
2156
+ const selected = cursor === idx;
2157
+ const enabled = !config2.disabledProviders.includes(pid);
2158
+ const p = PROVIDERS[pid];
2159
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
2160
+ /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2161
+ selected ? "\u25B8" : " ",
2162
+ " "
2163
+ ] }),
2164
+ /* @__PURE__ */ jsx5(Text5, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? "[\u2713]" : "[ ]" }),
2165
+ /* @__PURE__ */ jsx5(Text5, { color: p.color, children: " \u25CF " }),
2166
+ /* @__PURE__ */ jsx5(Box5, { width: 9, children: /* @__PURE__ */ jsx5(Text5, { bold: selected, children: p.name }) }),
2167
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: enabled ? "tracking" : "off" })
2168
+ ] }, pid);
2169
+ }),
2170
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2171
+ /* @__PURE__ */ jsx5(Text5, { bold: true, dimColor: true, children: "Accounts" }),
2172
+ config2.accounts.length === 0 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " none configured \u2014 enabled providers track automatically" }),
2173
+ config2.accounts.map((acc, i) => {
2174
+ const idx = ACCOUNT_ROWS_START + i;
2175
+ const selected = cursor === idx;
2176
+ const isActive = acc.id === activeAccountId;
2177
+ const provider = PROVIDERS[acc.providerId];
2178
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
2179
+ /* @__PURE__ */ jsxs5(Text5, { color: selected ? "green" : void 0, children: [
2180
+ selected ? "\u25B8" : " ",
2181
+ " "
2182
+ ] }),
2183
+ /* @__PURE__ */ jsxs5(Text5, { color: acc.color || provider.color, children: [
2184
+ isActive ? "\u25CF" : "\u25CB",
2185
+ " "
2186
+ ] }),
2187
+ /* @__PURE__ */ jsx5(Box5, { width: 16, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: truncateName(acc.name, 15) }) }),
2188
+ /* @__PURE__ */ jsx5(Box5, { width: 9, children: /* @__PURE__ */ jsx5(Text5, { color: provider.color, children: provider.name }) }),
2189
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: truncateName(acc.homeDir, 24) })
2190
+ ] }, acc.id);
2191
+ }),
2192
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2193
+ /* @__PURE__ */ jsxs5(Text5, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
2194
+ cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "\u25B8" : " ",
2195
+ " "
2196
+ ] }),
2197
+ /* @__PURE__ */ jsx5(Text5, { color: "greenBright", children: "+ " }),
2198
+ /* @__PURE__ */ jsx5(Text5, { children: "Add account" })
2199
+ ] }),
2200
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2201
+ editingTz ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 space toggle provider \xB7 s/Esc close" }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 \u21E7\u2191\u2193 reorder \xB7 Enter edit \xB7 space activate \xB7 d delete \xB7 s/Esc close" }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter add account \xB7 s/Esc close" }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit s/Esc close" })
2202
+ ] });
2203
+ }
2204
+ function Row({ cursor, idx, label, children }) {
2205
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
2206
+ /* @__PURE__ */ jsxs5(Text5, { color: cursor === idx ? "green" : void 0, children: [
2207
+ cursor === idx ? "\u25B8" : " ",
2208
+ " "
2209
+ ] }),
2210
+ /* @__PURE__ */ jsx5(Box5, { width: 20, children: /* @__PURE__ */ jsx5(Text5, { children: label }) }),
2211
+ children
2212
+ ] });
2213
+ }
2214
+ function AccountFormView({ form, accounts }) {
2215
+ const previewId = form.mode === "add" ? generateAccountId(form.name || "account", accounts) : form.editingId ?? "";
2216
+ const accent = form.color;
2217
+ const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
2218
+ const step = stepIndex[form.field];
2219
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
2220
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2221
+ /* @__PURE__ */ jsx5(Text5, { color: accent, bold: true, children: "\u258D" }),
2222
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
2223
+ " ",
2224
+ form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
2225
+ ] }),
2226
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2227
+ " step ",
2228
+ step,
2229
+ " of 4"
2230
+ ] })
2231
+ ] }),
2232
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Stepper, { active: form.field, accent }) }),
2233
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: accent, paddingX: 2, paddingY: 1, children: [
2234
+ /* @__PURE__ */ jsx5(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
2235
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2236
+ /* @__PURE__ */ jsx5(
2237
+ FormField,
2238
+ {
2239
+ label: "Name",
2240
+ hint: "display name for this account",
2241
+ value: form.name,
2242
+ focused: form.field === "name",
2243
+ accent,
2244
+ placeholder: "e.g. Work, Personal"
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2248
+ /* @__PURE__ */ jsx5(
2249
+ FormField,
2250
+ {
2251
+ label: "Home directory",
2252
+ hint: "path containing the tool's data dir \xB7 ~ for default",
2253
+ value: form.homeDir,
2254
+ focused: form.field === "homeDir",
2255
+ accent,
2256
+ placeholder: "~/work",
2257
+ mono: true
2258
+ }
2259
+ ),
2260
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2261
+ /* @__PURE__ */ jsx5(ColorField, { value: form.color, focused: form.field === "color" }),
2262
+ /* @__PURE__ */ jsx5(Box5, { height: 1 }),
2263
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2264
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "id \u2524 " }),
2265
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: accent, children: previewId || "account" }),
2266
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u251C auto-generated from name" })
2267
+ ] })
2268
+ ] }),
2269
+ form.error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
2270
+ "\u26A0 ",
2271
+ form.error
2272
+ ] }) }),
2273
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
2274
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "tab/\u2191\u2193 " }),
2275
+ /* @__PURE__ */ jsx5(Text5, { children: "switch field" }),
2276
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
2277
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "enter " }),
2278
+ /* @__PURE__ */ jsx5(Text5, { children: form.field === "color" ? "save" : "next" }),
2279
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
2280
+ (form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs5(Fragment3, { children: [
2281
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2190\u2192 " }),
2282
+ /* @__PURE__ */ jsx5(Text5, { children: form.field === "provider" ? "pick provider" : "pick color" }),
2283
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " })
2284
+ ] }),
2285
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "esc " }),
2286
+ /* @__PURE__ */ jsx5(Text5, { children: "cancel" })
2287
+ ] })
2288
+ ] });
2289
+ }
2290
+ function Stepper({ active, accent }) {
2291
+ const steps = [
2292
+ { id: "provider", label: "Provider" },
2293
+ { id: "name", label: "Name" },
2294
+ { id: "homeDir", label: "Home" },
2295
+ { id: "color", label: "Color" }
2296
+ ];
2297
+ const activeIdx = steps.findIndex((s) => s.id === active);
2298
+ return /* @__PURE__ */ jsx5(Box5, { children: steps.map((s, i) => {
2299
+ const done = i < activeIdx;
2300
+ const cur = i === activeIdx;
2301
+ const dot = done ? "\u25CF" : cur ? "\u25C9" : "\u25CB";
2302
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
2303
+ /* @__PURE__ */ jsxs5(Text5, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
2304
+ dot,
2305
+ " "
2306
+ ] }),
2307
+ /* @__PURE__ */ jsx5(Text5, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
2308
+ i < steps.length - 1 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u2500 " })
2309
+ ] }, s.id);
2310
+ }) });
2311
+ }
2312
+ function ProviderField({ value, focused }) {
2313
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2314
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
2315
+ focused ? "\u25B8" : " ",
2316
+ " Provider"
2317
+ ] }) }),
2318
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2319
+ /* @__PURE__ */ jsxs5(Text5, { children: [
2320
+ " ",
2321
+ focused ? "\u258C" : " ",
2322
+ " "
2323
+ ] }),
2324
+ PROVIDER_ORDER.map((pid) => {
2325
+ const selected = pid === value;
2326
+ const p = PROVIDERS[pid];
2327
+ return /* @__PURE__ */ jsx5(Box5, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs5(Text5, { bold: true, color: p.color, children: [
2328
+ "[",
2329
+ p.name,
2330
+ "]"
2331
+ ] }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: p.name }) }, pid);
2332
+ })
2333
+ ] }),
2334
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " which tool this account tracks" }) })
2335
+ ] });
2336
+ }
2337
+ function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
2338
+ const isPlaceholder = value === "";
2339
+ const display = isPlaceholder ? placeholder : value;
2340
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2341
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
2342
+ focused ? "\u25B8" : " ",
2343
+ " ",
2344
+ label
2345
+ ] }) }),
2346
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2347
+ /* @__PURE__ */ jsxs5(Text5, { color: focused ? accent : void 0, children: [
2348
+ " ",
2349
+ focused ? "\u258C" : " ",
2350
+ " "
2351
+ ] }),
2352
+ /* @__PURE__ */ jsx5(
2353
+ Text5,
2354
+ {
2355
+ bold: focused && !isPlaceholder,
2356
+ color: focused && !isPlaceholder ? accent : void 0,
2357
+ dimColor: isPlaceholder,
2358
+ italic: mono && isPlaceholder,
2359
+ children: display
2360
+ }
2361
+ ),
2362
+ focused && /* @__PURE__ */ jsx5(Text5, { color: accent, children: "\u258F" })
2363
+ ] }),
2364
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2365
+ " ",
2366
+ hint
2367
+ ] }) })
2368
+ ] });
2369
+ }
2370
+ function ColorField({ value, focused }) {
2371
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2372
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsxs5(Text5, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
2373
+ focused ? "\u25B8" : " ",
2374
+ " Accent color"
2375
+ ] }) }),
2376
+ /* @__PURE__ */ jsxs5(Box5, { children: [
2377
+ /* @__PURE__ */ jsxs5(Text5, { children: [
2378
+ " ",
2379
+ focused ? "\u258C" : " ",
2380
+ " "
2381
+ ] }),
2382
+ COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx5(Box5, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsx5(Text5, { bold: true, color: c, children: "[\u25CF]" }) : /* @__PURE__ */ jsx5(Text5, { color: c, dimColor: !focused, children: " \u25CF" }) }, c))
2383
+ ] }),
2384
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
2385
+ ] });
2386
+ }
2387
+
2388
+ // src/app.tsx
2389
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2390
+ var TABS = ["Dashboard", "Table"];
2391
+ var VIEWS = ["Daily", "Weekly", "Monthly"];
2392
+ var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
2393
+ var CURSOR_SORTS = ["cost \u2193", "amount \u2193", "model"];
2394
+ var IS_TTY = process.stdin.isTTY === true;
573
2395
  var DEFAULT_CONFIG = {
574
2396
  interval: 2,
575
2397
  billingInterval: 5,
576
2398
  clearScreen: true,
577
2399
  timezone: null,
578
2400
  accounts: [],
579
- activeAccountId: null
2401
+ activeAccountId: null,
2402
+ disabledProviders: [],
2403
+ onboarded: false,
2404
+ dashboardLayout: "grid",
2405
+ defaultFocus: "all"
580
2406
  };
581
- var GENERAL_ROWS = 4;
582
- var IS_TTY = process.stdin.isTTY === true;
583
- function truncateName(s, n) {
584
- return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
585
- }
586
- function buildSlots(config2) {
587
- const slots = [];
588
- if (config2.accounts.length === 0) {
589
- slots.push({ id: null, name: "Default", homeDir: void 0, color: "green" });
590
- return slots;
591
- }
592
- slots.push({ id: null, name: "All", homeDir: void 0, color: "whiteBright" });
593
- for (const a of config2.accounts) {
594
- slots.push({
595
- id: a.id,
596
- name: a.name,
597
- homeDir: expandHome(a.homeDir),
598
- color: a.color || "cyan"
599
- });
600
- }
601
- return slots;
602
- }
603
2407
  function App({ interval: cliInterval }) {
604
- const [config2, setConfig] = useState(null);
605
- const [stats, setStats] = useState(/* @__PURE__ */ new Map());
606
- const [table, setTable] = useState(null);
607
- const [tableLoading, setTableLoading] = useState(false);
608
- const [error, setError] = useState(null);
609
- const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
610
- const [tab, setTab] = useState(0);
611
- const [view, setView] = useState(0);
612
- const [cursor, setCursor] = useState(0);
613
- const [expanded, setExpanded] = useState(-1);
614
- const [sort, setSort] = useState(1);
615
- const [showSettings, setShowSettings] = useState(false);
616
- const [settingsCursor, setSettingsCursor] = useState(0);
617
- const [tzEdit, setTzEdit] = useState(null);
618
- const [tzError, setTzError] = useState(null);
619
- const [accountForm, setAccountForm] = useState(null);
620
- const tableLoadedOnce = useRef(false);
2408
+ const [config2, setConfig] = useState2(null);
2409
+ const [detected, setDetected] = useState2([]);
2410
+ const [stats, setStats] = useState2(/* @__PURE__ */ new Map());
2411
+ const [peak, setPeak] = useState2(null);
2412
+ const [table, setTable] = useState2(null);
2413
+ const [tableLoading, setTableLoading] = useState2(false);
2414
+ const [error, setError] = useState2(null);
2415
+ const [updated, setUpdated] = useState2(/* @__PURE__ */ new Date());
2416
+ const [tab, setTab] = useState2(0);
2417
+ const [view, setView] = useState2(0);
2418
+ const [cursor, setCursor] = useState2(0);
2419
+ const [expanded, setExpanded] = useState2(-1);
2420
+ const [sort, setSort] = useState2(1);
2421
+ const [tableProvider, setTableProvider] = useState2(null);
2422
+ const [search, setSearch] = useState2("");
2423
+ const [searchMode, setSearchMode] = useState2(false);
2424
+ const [cursorRows, setCursorRows] = useState2(null);
2425
+ const [showSettings, setShowSettings] = useState2(false);
2426
+ const [settingsCursor, setSettingsCursor] = useState2(0);
2427
+ const [tzEdit, setTzEdit] = useState2(null);
2428
+ const [tzError, setTzError] = useState2(null);
2429
+ const [accountForm, setAccountForm] = useState2(null);
2430
+ const [onboardSel, setOnboardSel] = useState2(null);
2431
+ const [onboardCursor, setOnboardCursor] = useState2(0);
621
2432
  const { stdout } = useStdout();
622
2433
  const { exit } = useApp();
623
2434
  const rows = stdout?.rows ?? 24;
624
2435
  const cols = stdout?.columns ?? 80;
625
2436
  const cfg = config2 ?? DEFAULT_CONFIG;
626
2437
  const interval2 = cliInterval ?? cfg.interval * 1e3;
2438
+ const billingMs = cfg.billingInterval * 6e4;
627
2439
  const tz = resolveTimezone(cfg.timezone);
628
- const slots = buildSlots(cfg);
2440
+ const configReady = config2 !== null;
2441
+ const accounts = useMemo(() => buildAccounts(cfg, detected), [cfg, detected]);
2442
+ const accountsRef = useRef2([]);
2443
+ accountsRef.current = accounts;
2444
+ const rowCountRef = useRef2(0);
2445
+ const accountsKey = accounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join("|");
2446
+ 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 }));
629
2447
  const activeSlotIdx = (() => {
630
- if (cfg.accounts.length === 0) return 0;
631
2448
  if (cfg.activeAccountId === null) return 0;
632
2449
  const i = slots.findIndex((s) => s.id === cfg.activeAccountId);
633
2450
  return i < 0 ? 0 : i;
634
2451
  })();
635
- const activeSlot = slots[activeSlotIdx];
636
- const slotKey = (s) => s.id ?? "__default__";
637
- const visibleSlots = activeSlot.id === null && cfg.accounts.length > 0 ? slots.slice(1) : [activeSlot];
638
- useEffect(() => {
2452
+ const focusId = slots[activeSlotIdx]?.id ?? null;
2453
+ const visibleAccounts = focusId === null ? accounts : accounts.filter((a) => a.id === focusId);
2454
+ const groups = accountsByProvider(visibleAccounts);
2455
+ const tableProvs = accountsByProvider(accounts).map((g) => g.provider);
2456
+ const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
2457
+ const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
2458
+ const tableAccounts = effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [];
2459
+ const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
2460
+ const needsOnboarding = configReady && !cfg.onboarded;
2461
+ const onboardEnabled = onboardSel ?? detected;
2462
+ const onboardItems = PROVIDER_ORDER.map((pid) => ({
2463
+ id: pid,
2464
+ name: PROVIDERS[pid].name,
2465
+ color: PROVIDERS[pid].color,
2466
+ detected: detected.includes(pid),
2467
+ enabled: onboardEnabled.includes(pid)
2468
+ }));
2469
+ useEffect2(() => {
639
2470
  loadConfig().then((c) => {
640
2471
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
2472
+ if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
641
2473
  setConfig(c);
642
2474
  });
2475
+ detectProviders().then(setDetected);
643
2476
  }, []);
644
- const billingMs = cfg.billingInterval * 6e4;
645
- const configReady = config2 !== null;
646
- const accountsKey = cfg.accounts.map((a) => `${a.id}:${a.homeDir}`).join("|");
647
- const dataSlotsRef = useRef([]);
648
- dataSlotsRef.current = cfg.accounts.length > 0 ? slots.slice(1) : slots;
649
- useEffect(() => {
2477
+ useEffect2(() => {
650
2478
  if (!configReady) return;
651
2479
  let active = true;
2480
+ let timer;
652
2481
  const load = async () => {
653
- await Promise.all(dataSlotsRef.current.map(async (slot) => {
654
- try {
655
- const dashboard = await fetchDashboard(tz, slot.homeDir);
656
- if (!active) return;
657
- setStats((prev) => {
658
- const next = new Map(prev);
659
- const cur = next.get(slotKey(slot)) ?? { slot, dashboard: null, billing: null };
660
- next.set(slotKey(slot), { ...cur, slot, dashboard });
661
- return next;
662
- });
663
- } catch (e) {
664
- if (active) setError(e instanceof Error ? e.message : String(e));
2482
+ try {
2483
+ await Promise.all(accountsRef.current.map(async (acc) => {
2484
+ const provider = PROVIDERS[acc.providerId];
2485
+ if (!provider.hasUsage || !provider.fetchSummary) return;
2486
+ try {
2487
+ const dashboard = await provider.fetchSummary(acc, tz);
2488
+ if (active) setStats((prev) => upsert(prev, acc, { dashboard }));
2489
+ } catch {
2490
+ }
2491
+ }));
2492
+ if (active) {
2493
+ setError(null);
2494
+ setUpdated(/* @__PURE__ */ new Date());
665
2495
  }
666
- }));
667
- if (active) {
668
- setError(null);
669
- setUpdated(/* @__PURE__ */ new Date());
2496
+ } finally {
2497
+ if (active) timer = setTimeout(load, interval2);
670
2498
  }
671
2499
  };
672
2500
  load();
673
- const id = setInterval(load, interval2);
674
2501
  return () => {
675
2502
  active = false;
676
- clearInterval(id);
2503
+ clearTimeout(timer);
677
2504
  };
678
2505
  }, [interval2, tz, configReady, accountsKey]);
679
- useEffect(() => {
2506
+ useEffect2(() => {
680
2507
  if (!configReady) return;
681
2508
  let active = true;
2509
+ let timer;
682
2510
  const load = async () => {
683
- await Promise.all(dataSlotsRef.current.map(async (slot) => {
684
- try {
685
- const billing = await fetchBilling(slot.homeDir);
686
- if (!active) return;
687
- setStats((prev) => {
688
- const next = new Map(prev);
689
- const cur = next.get(slotKey(slot)) ?? { slot, dashboard: null, billing: null };
690
- next.set(slotKey(slot), { ...cur, slot, billing });
691
- return next;
692
- });
693
- } catch {
694
- }
695
- }));
2511
+ try {
2512
+ const peakP = accountsRef.current.some((a) => a.providerId === "claude") ? fetchPeak() : Promise.resolve(null);
2513
+ await Promise.all(accountsRef.current.map(async (acc) => {
2514
+ const provider = PROVIDERS[acc.providerId];
2515
+ if (!provider.hasBilling || !provider.fetchBilling) return;
2516
+ try {
2517
+ const billing = await provider.fetchBilling(acc);
2518
+ if (active) setStats((prev) => upsert(prev, acc, { billing }));
2519
+ } catch {
2520
+ }
2521
+ }));
2522
+ const p = await peakP;
2523
+ if (active && p) setPeak(p);
2524
+ } finally {
2525
+ if (active) timer = setTimeout(load, billingMs);
2526
+ }
696
2527
  };
697
2528
  load();
698
- const id = setInterval(load, billingMs);
699
2529
  return () => {
700
2530
  active = false;
701
- clearInterval(id);
2531
+ clearTimeout(timer);
702
2532
  };
703
2533
  }, [billingMs, configReady, accountsKey]);
704
- useEffect(() => {
705
- tableLoadedOnce.current = false;
2534
+ const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
2535
+ useEffect2(() => {
706
2536
  setTable(null);
707
- }, [tz, activeSlot.id]);
708
- useEffect(() => {
709
- if (tab !== 1) return;
710
- if (tableLoadedOnce.current && table) return;
711
- let active = true;
712
- setTableLoading(true);
713
- fetchTable(tz, activeSlot.id === null ? void 0 : activeSlot.homeDir).then((result) => {
714
- if (active) {
715
- setTable(result);
716
- setTableLoading(false);
717
- tableLoadedOnce.current = true;
718
- }
719
- }).catch(() => {
720
- if (active) setTableLoading(false);
721
- });
722
- return () => {
723
- active = false;
724
- };
725
- }, [tab, tz, activeSlot.id]);
726
- useEffect(() => {
727
- if (tab !== 1 || !tableLoadedOnce.current) return;
2537
+ setCursorRows(null);
2538
+ setCursor(0);
2539
+ setExpanded(-1);
2540
+ setSort(tableIsCursor ? 0 : 1);
2541
+ }, [tableKey]);
2542
+ useEffect2(() => {
2543
+ if (tab !== 1 || !effTableProvider) return;
728
2544
  let active = true;
729
- const id = setInterval(async () => {
2545
+ let timer;
2546
+ const fetchOnce = async () => {
730
2547
  try {
731
- const result = await fetchTable(tz, activeSlot.id === null ? void 0 : activeSlot.homeDir);
732
- if (active) setTable(result);
2548
+ if (tableIsCursor) {
2549
+ const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
2550
+ if (active) setCursorRows(s?.models ?? []);
2551
+ } else {
2552
+ const r = await fetchScopeTable(tableAccounts, tz);
2553
+ if (active) setTable(r);
2554
+ }
733
2555
  } catch {
734
2556
  }
735
- }, Math.max(interval2, 1e4));
2557
+ };
2558
+ const run = async () => {
2559
+ setTableLoading(true);
2560
+ await fetchOnce();
2561
+ if (!active) return;
2562
+ setTableLoading(false);
2563
+ const loop = async () => {
2564
+ await fetchOnce();
2565
+ if (active) timer = setTimeout(loop, Math.max(interval2, 1e4));
2566
+ };
2567
+ timer = setTimeout(loop, Math.max(interval2, 1e4));
2568
+ };
2569
+ run();
736
2570
  return () => {
737
2571
  active = false;
738
- clearInterval(id);
2572
+ clearTimeout(timer);
739
2573
  };
740
- }, [tab, interval2, tz, activeSlot.id]);
2574
+ }, [tab, tableKey, interval2]);
2575
+ useEffect2(() => {
2576
+ setCursor(0);
2577
+ setExpanded(-1);
2578
+ }, [search]);
741
2579
  const resetView = useCallback(() => {
742
2580
  setCursor(0);
743
2581
  setExpanded(-1);
744
2582
  }, []);
2583
+ const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
745
2584
  const mouse = useMouse();
746
- useEffect(() => {
2585
+ useEffect2(() => {
747
2586
  if (!IS_TTY) return;
748
2587
  mouse.enable();
749
2588
  const onScroll = (_pos, dir) => {
750
- if (tab === 1) {
751
- setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
752
- }
2589
+ if (tab === 1) setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
753
2590
  };
754
2591
  mouse.events.on("scroll", onScroll);
755
2592
  return () => {
@@ -763,17 +2600,49 @@ function App({ interval: cliInterval }) {
763
2600
  return next;
764
2601
  });
765
2602
  }
2603
+ function toggleOnboard(i) {
2604
+ if (i < 0 || i >= PROVIDER_ORDER.length) return;
2605
+ const pid = PROVIDER_ORDER[i];
2606
+ setOnboardSel((prev) => {
2607
+ const base = prev ?? detected;
2608
+ return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
2609
+ });
2610
+ }
2611
+ function toggleProvider(pid) {
2612
+ updateConfig((c) => ({
2613
+ ...c,
2614
+ disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
2615
+ }));
2616
+ }
2617
+ function confirmOnboarding() {
2618
+ const enabled = onboardEnabled;
2619
+ updateConfig((c) => ({
2620
+ ...c,
2621
+ disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
2622
+ onboarded: true
2623
+ }));
2624
+ }
766
2625
  function cycleAccount(dir) {
767
2626
  if (slots.length <= 1) return;
768
2627
  const next = (activeSlotIdx + dir + slots.length) % slots.length;
769
- const targetId = slots[next].id;
770
- updateConfig((c) => ({ ...c, activeAccountId: targetId }));
2628
+ updateConfig((c) => ({ ...c, activeAccountId: slots[next].id }));
771
2629
  resetView();
772
2630
  }
2631
+ function cycleTableProvider(dir) {
2632
+ if (tableProvs.length <= 1) return;
2633
+ const cur = effTableProvider ? tableProvs.indexOf(effTableProvider) : 0;
2634
+ setTableProvider(tableProvs[(cur + dir + tableProvs.length) % tableProvs.length]);
2635
+ setCursor(0);
2636
+ setExpanded(-1);
2637
+ setSearch("");
2638
+ setSearchMode(false);
2639
+ }
773
2640
  function openAddAccount() {
2641
+ const providerId = detected[0] ?? "claude";
774
2642
  setAccountForm({
775
2643
  mode: "add",
776
- field: "name",
2644
+ field: "provider",
2645
+ providerId,
777
2646
  name: "",
778
2647
  homeDir: "~",
779
2648
  color: pickAccentColor(cfg.accounts),
@@ -784,10 +2653,11 @@ function App({ interval: cliInterval }) {
784
2653
  function openEditAccount(acc) {
785
2654
  setAccountForm({
786
2655
  mode: "edit",
787
- field: "name",
2656
+ field: "provider",
2657
+ providerId: acc.providerId,
788
2658
  name: acc.name,
789
2659
  homeDir: acc.homeDir,
790
- color: acc.color || "cyan",
2660
+ color: acc.color || PROVIDERS[acc.providerId].color,
791
2661
  editingId: acc.id,
792
2662
  error: null
793
2663
  });
@@ -796,7 +2666,6 @@ function App({ interval: cliInterval }) {
796
2666
  if (!accountForm) return;
797
2667
  const name = accountForm.name.trim();
798
2668
  const homeDir = accountForm.homeDir.trim() || "~";
799
- const color = accountForm.color;
800
2669
  if (!name) {
801
2670
  setAccountForm({ ...accountForm, error: "Name required", field: "name" });
802
2671
  return;
@@ -804,30 +2673,28 @@ function App({ interval: cliInterval }) {
804
2673
  updateConfig((c) => {
805
2674
  if (accountForm.mode === "add") {
806
2675
  const id = generateAccountId(name, c.accounts);
807
- const account = { id, name, homeDir, color };
808
- return {
809
- ...c,
810
- accounts: [...c.accounts, account],
811
- activeAccountId: c.accounts.length === 0 ? id : c.activeAccountId
812
- };
813
- } else {
814
- return {
815
- ...c,
816
- accounts: c.accounts.map(
817
- (a) => a.id === accountForm.editingId ? { ...a, name, homeDir, color } : a
818
- )
819
- };
2676
+ const account = { id, providerId: accountForm.providerId, name, homeDir, color: accountForm.color };
2677
+ return { ...c, accounts: [...c.accounts, account] };
820
2678
  }
2679
+ return {
2680
+ ...c,
2681
+ accounts: c.accounts.map((a) => a.id === accountForm.editingId ? { ...a, providerId: accountForm.providerId, name, homeDir, color: accountForm.color } : a)
2682
+ };
821
2683
  });
822
2684
  setAccountForm(null);
823
2685
  }
824
2686
  function cycleFormField(dir) {
825
- const order = ["name", "homeDir", "color"];
826
2687
  setAccountForm((f) => {
827
2688
  if (!f) return f;
828
- const i = order.indexOf(f.field);
829
- const next = order[(i + dir + order.length) % order.length];
830
- return { ...f, field: next };
2689
+ const i = FORM_FIELDS.indexOf(f.field);
2690
+ return { ...f, field: FORM_FIELDS[(i + dir + FORM_FIELDS.length) % FORM_FIELDS.length] };
2691
+ });
2692
+ }
2693
+ function cycleProvider(dir) {
2694
+ setAccountForm((f) => {
2695
+ if (!f) return f;
2696
+ const i = PROVIDER_ORDER.indexOf(f.providerId);
2697
+ return { ...f, providerId: PROVIDER_ORDER[(i + dir + PROVIDER_ORDER.length) % PROVIDER_ORDER.length] };
831
2698
  });
832
2699
  }
833
2700
  function cycleColor(dir) {
@@ -835,8 +2702,7 @@ function App({ interval: cliInterval }) {
835
2702
  if (!f) return f;
836
2703
  const i = COLOR_PALETTE.indexOf(f.color);
837
2704
  const idx = i < 0 ? 0 : i;
838
- const next = COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length];
839
- return { ...f, color: next };
2705
+ return { ...f, color: COLOR_PALETTE[(idx + dir + COLOR_PALETTE.length) % COLOR_PALETTE.length] };
840
2706
  });
841
2707
  }
842
2708
  function deleteAccount(id) {
@@ -854,16 +2720,35 @@ function App({ interval: cliInterval }) {
854
2720
  [next[idx], next[target]] = [next[target], next[idx]];
855
2721
  return { ...c, accounts: next };
856
2722
  });
857
- setSettingsCursor((c) => {
858
- const target = c + dir;
859
- const min = accountRowsStart;
860
- const max = accountRowsStart + cfg.accounts.length - 1;
861
- return Math.max(min, Math.min(max, target));
862
- });
2723
+ setSettingsCursor((c) => Math.max(ACCOUNT_ROWS_START, Math.min(ACCOUNT_ROWS_START + cfg.accounts.length - 1, c + dir)));
863
2724
  }
864
- const accountRowsStart = GENERAL_ROWS;
865
- const totalSettingsRows = GENERAL_ROWS + cfg.accounts.length + 1;
2725
+ const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
866
2726
  useInput((input, key) => {
2727
+ if (needsOnboarding) {
2728
+ if (input === "q") {
2729
+ exit();
2730
+ return;
2731
+ }
2732
+ const startIdx = PROVIDER_ORDER.length;
2733
+ if (key.upArrow) {
2734
+ setOnboardCursor((c) => Math.max(0, c - 1));
2735
+ return;
2736
+ }
2737
+ if (key.downArrow) {
2738
+ setOnboardCursor((c) => Math.min(startIdx, c + 1));
2739
+ return;
2740
+ }
2741
+ if (input === " ") {
2742
+ toggleOnboard(onboardCursor);
2743
+ return;
2744
+ }
2745
+ if (key.return) {
2746
+ if (onboardCursor === startIdx) confirmOnboarding();
2747
+ else toggleOnboard(onboardCursor);
2748
+ return;
2749
+ }
2750
+ return;
2751
+ }
867
2752
  if (showSettings && accountForm) {
868
2753
  if (key.escape) {
869
2754
  setAccountForm(null);
@@ -881,6 +2766,21 @@ function App({ interval: cliInterval }) {
881
2766
  cycleFormField(1);
882
2767
  return;
883
2768
  }
2769
+ if (accountForm.field === "provider") {
2770
+ if (key.leftArrow) {
2771
+ cycleProvider(-1);
2772
+ return;
2773
+ }
2774
+ if (key.rightArrow) {
2775
+ cycleProvider(1);
2776
+ return;
2777
+ }
2778
+ if (key.return) {
2779
+ setAccountForm((f) => f && { ...f, field: "name" });
2780
+ return;
2781
+ }
2782
+ return;
2783
+ }
884
2784
  if (accountForm.field === "color") {
885
2785
  if (key.leftArrow) {
886
2786
  cycleColor(-1);
@@ -897,27 +2797,21 @@ function App({ interval: cliInterval }) {
897
2797
  return;
898
2798
  }
899
2799
  if (key.return) {
900
- if (accountForm.field === "name") {
901
- setAccountForm((f) => f && { ...f, field: "homeDir" });
902
- } else if (accountForm.field === "homeDir") {
903
- setAccountForm((f) => f && { ...f, field: "color" });
904
- }
2800
+ setAccountForm((f) => f && { ...f, field: f.field === "name" ? "homeDir" : "color" });
905
2801
  return;
906
2802
  }
907
2803
  if (key.backspace || key.delete) {
908
2804
  setAccountForm((f) => {
909
- if (!f || f.field === "color") return f;
910
- const cur = f[f.field];
911
- return { ...f, [f.field]: cur.slice(0, -1), error: null };
2805
+ if (!f || f.field !== "name" && f.field !== "homeDir") return f;
2806
+ return { ...f, [f.field]: f[f.field].slice(0, -1), error: null };
912
2807
  });
913
2808
  return;
914
2809
  }
915
2810
  if (input && !key.ctrl && !key.meta) {
916
2811
  setAccountForm((f) => {
917
- if (!f || f.field === "color") return f;
2812
+ if (!f || f.field !== "name" && f.field !== "homeDir") return f;
918
2813
  return { ...f, [f.field]: f[f.field] + input, error: null };
919
2814
  });
920
- return;
921
2815
  }
922
2816
  return;
923
2817
  }
@@ -950,8 +2844,22 @@ function App({ interval: cliInterval }) {
950
2844
  if (input && !key.ctrl && !key.meta) {
951
2845
  setTzEdit((s) => (s ?? "") + input);
952
2846
  setTzError(null);
2847
+ }
2848
+ return;
2849
+ }
2850
+ if (tab === 1 && searchMode) {
2851
+ if (key.return || key.escape) {
2852
+ setSearchMode(false);
2853
+ if (key.escape) setSearch("");
953
2854
  return;
954
2855
  }
2856
+ if (key.backspace || key.delete) {
2857
+ setSearch((s) => s.slice(0, -1));
2858
+ return;
2859
+ }
2860
+ if (input && !key.ctrl && !key.meta) {
2861
+ setSearch((s) => s + input);
2862
+ }
955
2863
  return;
956
2864
  }
957
2865
  if (input === "q") {
@@ -963,7 +2871,7 @@ function App({ interval: cliInterval }) {
963
2871
  setShowSettings(false);
964
2872
  return;
965
2873
  }
966
- const accIdxNav = settingsCursor - accountRowsStart;
2874
+ const accIdxNav = settingsCursor - ACCOUNT_ROWS_START;
967
2875
  const onAccountRow = accIdxNav >= 0 && accIdxNav < cfg.accounts.length;
968
2876
  if (onAccountRow && key.shift && (key.upArrow || key.downArrow)) {
969
2877
  moveAccount(accIdxNav, key.upArrow ? -1 : 1);
@@ -996,12 +2904,23 @@ function App({ interval: cliInterval }) {
996
2904
  setTzEdit(cfg.timezone ?? "");
997
2905
  setTzError(null);
998
2906
  }
999
- if (key.leftArrow || key.rightArrow) {
1000
- updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
1001
- }
2907
+ if (key.leftArrow || key.rightArrow) updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
2908
+ return;
2909
+ }
2910
+ if (settingsCursor === 4 && (key.leftArrow || key.rightArrow || key.return)) {
2911
+ updateConfig((c) => ({ ...c, dashboardLayout: c.dashboardLayout === "grid" ? "single" : "grid" }));
1002
2912
  return;
1003
2913
  }
1004
- const accIdx = settingsCursor - accountRowsStart;
2914
+ if (settingsCursor === 5 && (key.leftArrow || key.rightArrow || key.return)) {
2915
+ updateConfig((c) => ({ ...c, defaultFocus: c.defaultFocus === "all" ? "last" : "all" }));
2916
+ return;
2917
+ }
2918
+ const provIdx = settingsCursor - PROVIDER_ROWS_START;
2919
+ if (provIdx >= 0 && provIdx < PROVIDER_ORDER.length) {
2920
+ if (input === " " || key.return || key.leftArrow || key.rightArrow) toggleProvider(PROVIDER_ORDER[provIdx]);
2921
+ return;
2922
+ }
2923
+ const accIdx = settingsCursor - ACCOUNT_ROWS_START;
1005
2924
  if (accIdx >= 0 && accIdx < cfg.accounts.length) {
1006
2925
  const acc = cfg.accounts[accIdx];
1007
2926
  if (key.return) {
@@ -1020,7 +2939,6 @@ function App({ interval: cliInterval }) {
1020
2939
  }
1021
2940
  if (accIdx === cfg.accounts.length && key.return) {
1022
2941
  openAddAccount();
1023
- return;
1024
2942
  }
1025
2943
  return;
1026
2944
  }
@@ -1051,43 +2969,58 @@ function App({ interval: cliInterval }) {
1051
2969
  return;
1052
2970
  }
1053
2971
  if (tab === 1) {
1054
- if (input === "d") {
1055
- setView(0);
1056
- resetView();
1057
- return;
1058
- }
1059
- if (input === "w") {
1060
- setView(1);
1061
- resetView();
2972
+ if (input === "p") {
2973
+ cycleTableProvider(1);
1062
2974
  return;
1063
2975
  }
1064
- if (input === "m") {
1065
- setView(2);
1066
- resetView();
2976
+ if (input === "P") {
2977
+ cycleTableProvider(-1);
1067
2978
  return;
1068
2979
  }
1069
- if (key.leftArrow) {
1070
- setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
1071
- resetView();
2980
+ if (input === "/") {
2981
+ setSearchMode(true);
1072
2982
  return;
1073
2983
  }
1074
- if (key.rightArrow) {
1075
- setView((v) => (v + 1) % VIEWS.length);
1076
- resetView();
2984
+ if (key.escape) {
2985
+ if (search) setSearch("");
2986
+ else setExpanded(-1);
1077
2987
  return;
1078
2988
  }
1079
2989
  if (input === "o") {
1080
- setSort((s) => (s + 1) % SORTS.length);
2990
+ setSort((s) => (s + 1) % SORTS_FOR.length);
1081
2991
  resetView();
1082
2992
  return;
1083
2993
  }
1084
- if (key.return) {
1085
- setExpanded((e) => e === cursor ? -1 : cursor);
1086
- return;
1087
- }
1088
- if (key.escape) {
1089
- setExpanded(-1);
1090
- return;
2994
+ if (!tableIsCursor) {
2995
+ if (input === "d") {
2996
+ setView(0);
2997
+ resetView();
2998
+ return;
2999
+ }
3000
+ if (input === "w") {
3001
+ setView(1);
3002
+ resetView();
3003
+ return;
3004
+ }
3005
+ if (input === "m") {
3006
+ setView(2);
3007
+ resetView();
3008
+ return;
3009
+ }
3010
+ if (key.leftArrow) {
3011
+ setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
3012
+ resetView();
3013
+ return;
3014
+ }
3015
+ if (key.rightArrow) {
3016
+ setView((v) => (v + 1) % VIEWS.length);
3017
+ resetView();
3018
+ return;
3019
+ }
3020
+ if (key.return) {
3021
+ setExpanded((e) => e === cursor ? -1 : cursor);
3022
+ return;
3023
+ }
1091
3024
  }
1092
3025
  } else {
1093
3026
  if (key.leftArrow || key.rightArrow) {
@@ -1101,828 +3034,218 @@ function App({ interval: cliInterval }) {
1101
3034
  return;
1102
3035
  }
1103
3036
  if (key.downArrow) {
1104
- setCursor((c) => c + 1);
3037
+ setCursor((c) => clampRow(c + 1));
1105
3038
  return;
1106
3039
  }
1107
3040
  if (key.pageDown || input === "G") {
1108
- setCursor((c) => input === "G" ? 99999 : c + Math.max(1, rows - 12));
1109
- return;
1110
- }
1111
- if (key.pageUp || input === "g") {
1112
- setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
3041
+ setCursor((c) => clampRow(input === "G" ? rowCountRef.current - 1 : c + Math.max(1, rows - 12)));
1113
3042
  return;
1114
- }
1115
- }, { isActive: IS_TTY });
1116
- if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
1117
- if (!config2) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
1118
- const firstStats = stats.size > 0 ? [...stats.values()][0] : null;
1119
- if (!firstStats?.dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
1120
- const peakBilling = firstStats.billing;
1121
- const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
1122
- const tableData = sortRows(rawTableData, sort);
1123
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: [
1124
- /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
1125
- /* @__PURE__ */ jsxs(Box, { children: [
1126
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
1127
- "\u25C9",
1128
- " tokmon"
1129
- ] }),
1130
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1131
- " \xB7 every ",
1132
- cfg.interval,
1133
- "s"
1134
- ] })
1135
- ] }),
1136
- /* @__PURE__ */ jsxs(Box, { children: [
1137
- peakBilling?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
1138
- /* @__PURE__ */ jsx(PeakBadge, { peak: peakBilling.peak }),
1139
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
1140
- ] }),
1141
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
1142
- ] })
1143
- ] }),
1144
- showSettings ? /* @__PURE__ */ jsx(
1145
- SettingsView,
1146
- {
1147
- config: cfg,
1148
- cursor: settingsCursor,
1149
- tzEdit,
1150
- tzError,
1151
- resolvedTz: tz,
1152
- accountForm,
1153
- onSettingsClick: (i) => setSettingsCursor(i),
1154
- onAddAccount: openAddAccount,
1155
- onEditAccount: openEditAccount,
1156
- onActivateAccount: (id) => updateConfig((c) => ({ ...c, activeAccountId: id })),
1157
- activeAccountId: cfg.activeAccountId
1158
- }
1159
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1160
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, marginBottom: 1, children: [
1161
- /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
1162
- setTab(i);
1163
- resetView();
1164
- } }),
1165
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192" })
1166
- ] }),
1167
- tab === 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1168
- /* @__PURE__ */ jsx(
1169
- DashboardView,
1170
- {
1171
- slots: visibleSlots,
1172
- stats,
1173
- compact: visibleSlots.length > 1
1174
- }
1175
- ),
1176
- slots.length > 1 && /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1177
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "focus " }),
1178
- /* @__PURE__ */ jsx(
1179
- AccountStrip,
1180
- {
1181
- slots,
1182
- activeIdx: activeSlotIdx,
1183
- onSelect: (i) => {
1184
- const id = slots[i].id;
1185
- updateConfig((c) => ({ ...c, activeAccountId: id }));
1186
- resetView();
1187
- }
1188
- }
1189
- )
1190
- ] })
1191
- ] }),
1192
- tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
1193
- /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort], onSelect: (i) => {
1194
- setView(i);
1195
- resetView();
1196
- } }),
1197
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1198
- tableLoading && !table ? /* @__PURE__ */ jsx(Spinner, { label: "Loading 6 months of history" }) : /* @__PURE__ */ jsx(
1199
- TableView,
1200
- {
1201
- rows: tableData,
1202
- cursor,
1203
- expanded,
1204
- maxRows: rows - 14,
1205
- cols,
1206
- onRowClick: (idx) => {
1207
- if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
1208
- else setCursor(idx);
1209
- }
1210
- }
1211
- )
1212
- ] })
1213
- ] }),
1214
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, { hasAccounts: slots.length > 1 })
1215
- ] });
1216
- }
1217
- function Footer({ hasAccounts }) {
1218
- return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1219
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
1220
- /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
1221
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
1222
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
1223
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings " }),
1224
- hasAccounts && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "0-9=jump a/A=cycle " }),
1225
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "q=quit" })
1226
- ] });
1227
- }
1228
- function TabBar({ tabs, active, onSelect }) {
1229
- return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
1230
- " ",
1231
- t,
1232
- " "
1233
- ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1234
- " ",
1235
- t,
1236
- " "
1237
- ] }) }, t)) });
1238
- }
1239
- function AccountStrip({ slots, activeIdx, onSelect }) {
1240
- return /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: slots.map((s, i) => {
1241
- const active = i === activeIdx;
1242
- const dot = s.id === null ? "\u2726" : "\u25CF";
1243
- const label = truncateName(s.name, 16);
1244
- return /* @__PURE__ */ jsxs(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
1245
- /* @__PURE__ */ jsx(Text, { dimColor: !active, children: i }),
1246
- /* @__PURE__ */ jsx(Text, { children: " " }),
1247
- /* @__PURE__ */ jsx(Text, { color: s.color, bold: active, dimColor: !active, children: dot }),
1248
- /* @__PURE__ */ jsx(Text, { children: " " }),
1249
- active ? /* @__PURE__ */ jsx(Text, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
1250
- ] }, s.id ?? "__all__");
1251
- }) });
1252
- }
1253
- function ViewBar({ views, active, sort, onSelect }) {
1254
- return /* @__PURE__ */ jsxs(Box, { children: [
1255
- views.map((v, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
1256
- "[",
1257
- v,
1258
- "]"
1259
- ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
1260
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " sort: " }),
1261
- /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: sort }),
1262
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " o=cycle" })
1263
- ] });
1264
- }
1265
- function sortRows(rows, sortIdx) {
1266
- if (rows.length === 0) return rows;
1267
- const sorted = [...rows];
1268
- switch (sortIdx) {
1269
- case 0:
1270
- return sorted.sort((a, b) => a.label.localeCompare(b.label));
1271
- case 1:
1272
- return sorted.sort((a, b) => b.label.localeCompare(a.label));
1273
- case 2:
1274
- return sorted.sort((a, b) => a.cost - b.cost);
1275
- case 3:
1276
- return sorted.sort((a, b) => b.cost - a.cost);
1277
- default:
1278
- return sorted;
1279
- }
1280
- }
1281
- var COLOR_PALETTE = [
1282
- "cyan",
1283
- "magenta",
1284
- "green",
1285
- "yellow",
1286
- "blue",
1287
- "red",
1288
- "cyanBright",
1289
- "magentaBright",
1290
- "greenBright"
1291
- ];
1292
- function SettingsView({
1293
- config: config2,
1294
- cursor,
1295
- tzEdit,
1296
- tzError,
1297
- resolvedTz,
1298
- accountForm,
1299
- onAddAccount,
1300
- onEditAccount,
1301
- onActivateAccount,
1302
- activeAccountId
1303
- }) {
1304
- const editingTz = tzEdit !== null;
1305
- const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
1306
- const accountRowsStart = GENERAL_ROWS;
1307
- if (accountForm) {
1308
- return /* @__PURE__ */ jsx(AccountFormView, { form: accountForm, accounts: config2.accounts });
1309
- }
1310
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1311
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
1312
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
1313
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1314
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "General" }),
1315
- /* @__PURE__ */ jsxs(Box, { children: [
1316
- /* @__PURE__ */ jsxs(Text, { color: cursor === 0 ? "green" : void 0, children: [
1317
- cursor === 0 ? "\u25B8" : " ",
1318
- " "
1319
- ] }),
1320
- /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
1321
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1322
- "\u25C2",
1323
- " "
1324
- ] }),
1325
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
1326
- config2.interval,
1327
- "s"
1328
- ] }),
1329
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1330
- " ",
1331
- "\u25B8"
1332
- ] })
1333
- ] }),
1334
- /* @__PURE__ */ jsxs(Box, { children: [
1335
- /* @__PURE__ */ jsxs(Text, { color: cursor === 1 ? "green" : void 0, children: [
1336
- cursor === 1 ? "\u25B8" : " ",
1337
- " "
1338
- ] }),
1339
- /* @__PURE__ */ jsx(Text, { children: "Billing poll " }),
1340
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1341
- "\u25C2",
1342
- " "
1343
- ] }),
1344
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
1345
- config2.billingInterval,
1346
- "m"
1347
- ] }),
1348
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1349
- " ",
1350
- "\u25B8"
1351
- ] })
1352
- ] }),
1353
- /* @__PURE__ */ jsxs(Box, { children: [
1354
- /* @__PURE__ */ jsxs(Text, { color: cursor === 2 ? "green" : void 0, children: [
1355
- cursor === 2 ? "\u25B8" : " ",
1356
- " "
1357
- ] }),
1358
- /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
1359
- /* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
1360
- ] }),
1361
- /* @__PURE__ */ jsxs(Box, { children: [
1362
- /* @__PURE__ */ jsxs(Text, { color: cursor === 3 ? "green" : void 0, children: [
1363
- cursor === 3 ? "\u25B8" : " ",
1364
- " "
1365
- ] }),
1366
- /* @__PURE__ */ jsx(Text, { children: "Timezone " }),
1367
- editingTz ? /* @__PURE__ */ jsxs(Fragment, { children: [
1368
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
1369
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: tzEdit }),
1370
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" }),
1371
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "]" })
1372
- ] }) : /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: tzDisplay })
1373
- ] }),
1374
- cursor === 3 && tzError && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1375
- " ",
1376
- tzError
1377
- ] }),
1378
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1379
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Claude accounts" }),
1380
- config2.accounts.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " none \u2014 using default Claude HOME" }),
1381
- config2.accounts.map((acc, i) => {
1382
- const idx = accountRowsStart + i;
1383
- const selected = cursor === idx;
1384
- const isActive = acc.id === activeAccountId;
1385
- return /* @__PURE__ */ jsxs(Box, { children: [
1386
- /* @__PURE__ */ jsxs(Text, { color: selected ? "green" : void 0, children: [
1387
- selected ? "\u25B8" : " ",
1388
- " "
1389
- ] }),
1390
- /* @__PURE__ */ jsxs(Text, { color: acc.color || "cyan", children: [
1391
- isActive ? "\u25CF" : "\u25CB",
1392
- " "
3043
+ }
3044
+ if (key.pageUp || input === "g") {
3045
+ setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
3046
+ return;
3047
+ }
3048
+ }, { isActive: IS_TTY });
3049
+ if (error) return /* @__PURE__ */ jsx6(Box6, { padding: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "red", children: error }) });
3050
+ if (!config2) return /* @__PURE__ */ jsx6(Box6, { padding: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading..." }) });
3051
+ if (needsOnboarding) {
3052
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx6(Onboarding, { items: onboardItems, cursor: onboardCursor, onToggle: toggleOnboard, onConfirm: confirmOnboarding }) });
3053
+ }
3054
+ const tokenRows = sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort);
3055
+ const cursorTableRows = sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort);
3056
+ rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
3057
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: [
3058
+ /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
3059
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3060
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "greenBright", children: [
3061
+ "\u25C9",
3062
+ " tokmon"
1393
3063
  ] }),
1394
- /* @__PURE__ */ jsx(Text, { bold: true, children: truncateName(acc.name, 24) }),
1395
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1396
- " ",
1397
- truncateName(acc.homeDir, 28)
3064
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3065
+ " \xB7 every ",
3066
+ cfg.interval,
3067
+ "s"
1398
3068
  ] })
1399
- ] }, acc.id);
1400
- }),
1401
- /* @__PURE__ */ jsxs(Box, { children: [
1402
- /* @__PURE__ */ jsxs(Text, { color: cursor === accountRowsStart + config2.accounts.length ? "green" : void 0, children: [
1403
- cursor === accountRowsStart + config2.accounts.length ? "\u25B8" : " ",
1404
- " "
1405
- ] }),
1406
- /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "+ " }),
1407
- /* @__PURE__ */ jsx(Text, { children: "Add account" })
1408
- ] }),
1409
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1410
- editingTz ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : cursor >= accountRowsStart && cursor < accountRowsStart + config2.accounts.length ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \xB7 \u21E7\u2191\u2193 reorder \xB7 Enter edit \xB7 space activate \xB7 d delete \xB7 s/Esc close" }) : cursor === accountRowsStart + config2.accounts.length ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \xB7 Enter add account \xB7 s/Esc close" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit s/Esc close" })
1411
- ] });
1412
- }
1413
- function AccountFormView({ form, accounts }) {
1414
- const previewId = form.mode === "add" ? generateAccountId(form.name || "account", accounts) : form.editingId ?? "";
1415
- const accent = form.color;
1416
- const stepIndex = { name: 1, homeDir: 2, color: 3 };
1417
- const step = stepIndex[form.field];
1418
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1419
- /* @__PURE__ */ jsxs(Box, { children: [
1420
- /* @__PURE__ */ jsx(Text, { color: accent, bold: true, children: "\u258D" }),
1421
- /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1422
- " ",
1423
- form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
1424
3069
  ] }),
1425
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1426
- " step ",
1427
- step,
1428
- " of 3"
3070
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3071
+ peak && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3072
+ /* @__PURE__ */ jsx6(PeakBadge, { peak }),
3073
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \xB7 " })
3074
+ ] }),
3075
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: time(updated, tz) })
1429
3076
  ] })
1430
3077
  ] }),
1431
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Stepper, { active: form.field, accent }) }),
1432
- /* @__PURE__ */ jsxs(
1433
- Box,
3078
+ showSettings ? /* @__PURE__ */ jsx6(
3079
+ SettingsView,
1434
3080
  {
1435
- marginTop: 1,
1436
- flexDirection: "column",
1437
- borderStyle: "round",
1438
- borderColor: accent,
1439
- paddingX: 2,
1440
- paddingY: 1,
1441
- children: [
1442
- /* @__PURE__ */ jsx(
1443
- FormField,
1444
- {
1445
- label: "Name",
1446
- hint: "display name for this Claude account",
1447
- value: form.name,
1448
- focused: form.field === "name",
1449
- accent,
1450
- placeholder: "e.g. Work, Personal"
1451
- }
1452
- ),
1453
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1454
- /* @__PURE__ */ jsx(
1455
- FormField,
1456
- {
1457
- label: "Home directory",
1458
- hint: "path containing .claude/ \xB7 ~ for default",
1459
- value: form.homeDir,
1460
- focused: form.field === "homeDir",
1461
- accent,
1462
- placeholder: "~/claude-work",
1463
- mono: true
1464
- }
1465
- ),
1466
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1467
- /* @__PURE__ */ jsx(
1468
- ColorField,
1469
- {
1470
- value: form.color,
1471
- focused: form.field === "color"
1472
- }
1473
- ),
1474
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1475
- /* @__PURE__ */ jsxs(Box, { children: [
1476
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "id " }),
1477
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2524 " }),
1478
- /* @__PURE__ */ jsx(Text, { bold: true, color: accent, children: previewId || "account" }),
1479
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u251C" }),
1480
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " auto-generated from name" })
1481
- ] })
1482
- ]
3081
+ config: cfg,
3082
+ cursor: settingsCursor,
3083
+ tzEdit,
3084
+ tzError,
3085
+ resolvedTz: tz,
3086
+ accountForm,
3087
+ activeAccountId: cfg.activeAccountId
1483
3088
  }
1484
- ),
1485
- form.error && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1486
- "\u26A0 ",
1487
- form.error
1488
- ] }) }),
1489
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1490
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "tab/\u2191\u2193 " }),
1491
- /* @__PURE__ */ jsx(Text, { children: "switch field" }),
1492
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1493
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter " }),
1494
- /* @__PURE__ */ jsx(Text, { children: form.field === "color" ? "save" : "next" }),
1495
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1496
- form.field === "color" && /* @__PURE__ */ jsxs(Fragment, { children: [
1497
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190\u2192 " }),
1498
- /* @__PURE__ */ jsx(Text, { children: "pick color" }),
1499
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
1500
- ] }),
1501
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "esc " }),
1502
- /* @__PURE__ */ jsx(Text, { children: "cancel" })
1503
- ] })
1504
- ] });
1505
- }
1506
- function Stepper({ active, accent }) {
1507
- const steps = [
1508
- { id: "name", label: "Name" },
1509
- { id: "homeDir", label: "Home" },
1510
- { id: "color", label: "Color" }
1511
- ];
1512
- const order = steps.map((s) => s.id);
1513
- const activeIdx = order.indexOf(active);
1514
- return /* @__PURE__ */ jsx(Box, { children: steps.map((s, i) => {
1515
- const done = i < activeIdx;
1516
- const cur = i === activeIdx;
1517
- const dot = done ? "\u25CF" : cur ? "\u25C9" : "\u25CB";
1518
- return /* @__PURE__ */ jsxs(Box, { children: [
1519
- /* @__PURE__ */ jsxs(Text, { color: cur ? accent : done ? accent : void 0, dimColor: !cur && !done, children: [
1520
- dot,
1521
- " "
1522
- ] }),
1523
- /* @__PURE__ */ jsx(Text, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
1524
- i < steps.length - 1 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2500 " })
1525
- ] }, s.id);
1526
- }) });
1527
- }
1528
- function FormField({
1529
- label,
1530
- hint,
1531
- value,
1532
- focused,
1533
- accent,
1534
- placeholder,
1535
- mono
1536
- }) {
1537
- const display = value === "" ? placeholder : value;
1538
- const isPlaceholder = value === "";
1539
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1540
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
1541
- focused ? "\u25B8" : " ",
1542
- " ",
1543
- label
1544
- ] }) }),
1545
- /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
1546
- /* @__PURE__ */ jsxs(Text, { color: focused ? accent : void 0, children: [
1547
- " ",
1548
- focused ? "\u258C" : " ",
1549
- " "
3089
+ ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
3090
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, marginBottom: 1, children: [
3091
+ /* @__PURE__ */ jsx6(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
3092
+ setTab(i);
3093
+ resetView();
3094
+ } }),
3095
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " Tab/\u2190\u2192" })
1550
3096
  ] }),
1551
- /* @__PURE__ */ jsx(
1552
- Text,
1553
- {
1554
- bold: focused && !isPlaceholder,
1555
- color: focused && !isPlaceholder ? accent : void 0,
1556
- dimColor: isPlaceholder,
1557
- italic: mono && isPlaceholder,
1558
- children: display
1559
- }
1560
- ),
1561
- focused && /* @__PURE__ */ jsx(Text, { color: accent, children: "\u258F" })
1562
- ] }),
1563
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1564
- " ",
1565
- hint
1566
- ] }) })
1567
- ] });
1568
- }
1569
- function ColorField({ value, focused }) {
1570
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1571
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
1572
- focused ? "\u25B8" : " ",
1573
- " Accent color"
1574
- ] }) }),
1575
- /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
1576
- /* @__PURE__ */ jsxs(Text, { children: [
1577
- " ",
1578
- focused ? "\u258C" : " ",
1579
- " "
3097
+ tab === 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3098
+ /* @__PURE__ */ jsx6(DashboardView, { groups, stats, cols, focusId, layout: cfg.dashboardLayout }),
3099
+ slots.length > 1 && /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3100
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "focus " }),
3101
+ /* @__PURE__ */ jsx6(
3102
+ AccountStrip,
3103
+ {
3104
+ slots,
3105
+ activeIdx: activeSlotIdx,
3106
+ onSelect: (i) => {
3107
+ updateConfig((c) => ({ ...c, activeAccountId: slots[i].id }));
3108
+ resetView();
3109
+ }
3110
+ }
3111
+ )
3112
+ ] })
1580
3113
  ] }),
1581
- COLOR_PALETTE.map((c, i) => {
1582
- const selected = c === value;
1583
- return /* @__PURE__ */ jsx(Box, { marginRight: 1, children: selected ? /* @__PURE__ */ jsx(Text, { bold: true, color: c, children: "[\u25CF]" }) : /* @__PURE__ */ jsx(Text, { color: c, dimColor: !focused, children: i === COLOR_PALETTE.length - 1 ? " \u25CF" : " \u25CF" }) }, c);
1584
- })
3114
+ tab === 1 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
3115
+ tableProvs.length > 0 && /* @__PURE__ */ jsx6(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
3116
+ setTableProvider(p);
3117
+ setCursor(0);
3118
+ setExpanded(-1);
3119
+ setSearch("");
3120
+ setSearchMode(false);
3121
+ } }),
3122
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3123
+ /* @__PURE__ */ jsx6(
3124
+ ControlBar,
3125
+ {
3126
+ views: VIEWS,
3127
+ period: view,
3128
+ sort: SORTS_FOR[sort % SORTS_FOR.length],
3129
+ search,
3130
+ searching: searchMode,
3131
+ showPeriod: !tableIsCursor
3132
+ }
3133
+ ),
3134
+ /* @__PURE__ */ jsx6(Box6, { height: 1 }),
3135
+ !effTableProvider ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No providers enabled \u2014 press s to pick providers." }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx6(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx6(
3136
+ CursorSpendTable,
3137
+ {
3138
+ rows: cursorTableRows,
3139
+ cursor,
3140
+ maxRows: Math.max(1, rows - 16),
3141
+ onRowClick: (idx) => setCursor(idx)
3142
+ }
3143
+ ) : /* @__PURE__ */ jsx6(
3144
+ TokenTable,
3145
+ {
3146
+ rows: tokenRows,
3147
+ cursor,
3148
+ expanded,
3149
+ maxRows: Math.max(1, rows - 16),
3150
+ cols,
3151
+ onRowClick: (idx) => {
3152
+ if (idx === cursor) setExpanded((e) => e === idx ? -1 : idx);
3153
+ else setCursor(idx);
3154
+ }
3155
+ }
3156
+ )
3157
+ ] })
1585
3158
  ] }),
1586
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
1587
- ] });
1588
- }
1589
- function DashboardView({ slots, stats, compact: _compact }) {
1590
- const slotKey = (s) => s.id ?? "__default__";
1591
- const items = slots.map((slot) => ({ slot, s: stats.get(slotKey(slot)) })).filter((x) => !!x.s?.dashboard);
1592
- if (items.length === 0) return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." });
1593
- const agg = aggregateUsage(items.map((i) => i.s.dashboard));
1594
- const isMulti = items.length > 1;
1595
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1596
- /* @__PURE__ */ jsxs(
1597
- Box,
1598
- {
1599
- flexDirection: "column",
1600
- paddingLeft: 1,
1601
- borderStyle: "bold",
1602
- borderColor: isMulti ? "green" : items[0].slot.color,
1603
- borderRight: false,
1604
- borderTop: false,
1605
- borderBottom: false,
1606
- children: [
1607
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
1608
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1609
- /* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: agg.today }),
1610
- /* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: agg.week }),
1611
- /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: agg.month }),
1612
- agg.burnRate > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1613
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1614
- /* @__PURE__ */ jsxs(Box, { children: [
1615
- /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Burn rate" }) }),
1616
- /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { color: "red", children: currency(agg.burnRate) }) }),
1617
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
1618
- ] })
1619
- ] })
1620
- ]
1621
- }
1622
- ),
1623
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1624
- /* @__PURE__ */ jsx(RateLimitsCard, { items })
3159
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx6(Footer, { hasAccounts: slots.length > 1 })
1625
3160
  ] });
1626
3161
  }
1627
- function aggregateUsage(list) {
1628
- const z = {
1629
- today: { cost: 0, tokens: 0 },
1630
- week: { cost: 0, tokens: 0 },
1631
- month: { cost: 0, tokens: 0 },
1632
- burnRate: 0
1633
- };
1634
- for (const d of list) {
1635
- z.today.cost += d.today.cost;
1636
- z.today.tokens += d.today.tokens;
1637
- z.week.cost += d.week.cost;
1638
- z.week.tokens += d.week.tokens;
1639
- z.month.cost += d.month.cost;
1640
- z.month.tokens += d.month.tokens;
1641
- z.burnRate += d.burnRate;
1642
- }
1643
- return z;
1644
- }
1645
- function SummaryRow({ label, summary }) {
1646
- return /* @__PURE__ */ jsxs(Box, { children: [
1647
- /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
1648
- /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
1649
- /* @__PURE__ */ jsx(Box, { width: 18, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1650
- tokens(summary.tokens),
1651
- " tokens"
1652
- ] }) })
1653
- ] });
3162
+ function upsert(prev, account, patch) {
3163
+ const next = new Map(prev);
3164
+ const cur = next.get(account.id) ?? { account, dashboard: null, billing: null };
3165
+ next.set(account.id, { ...cur, account, ...patch });
3166
+ return next;
1654
3167
  }
1655
- function RateLimitsCard({ items }) {
1656
- const anyData = items.some((i) => i.s.billing?.session || i.s.billing?.weekly || i.s.billing?.sonnet);
1657
- const anyError = items.some((i) => i.s.billing?.error);
1658
- const borderColor = !anyData && anyError ? "red" : "yellow";
1659
- return /* @__PURE__ */ jsxs(
1660
- Box,
1661
- {
1662
- flexDirection: "column",
1663
- paddingLeft: 1,
1664
- borderStyle: "bold",
1665
- borderColor,
1666
- borderRight: false,
1667
- borderTop: false,
1668
- borderBottom: false,
1669
- children: [
1670
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
1671
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1672
- !anyData ? anyError ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map(({ slot, s }) => s.billing?.error && /* @__PURE__ */ jsxs(Box, { children: [
1673
- /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1674
- /* @__PURE__ */ jsx(Box, { width: 22, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncateName(slot.name, 20) }) }),
1675
- /* @__PURE__ */ jsx(Text, { color: "red", children: s.billing.error })
1676
- ] }, slot.id ?? "__default__")) }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fetching..." }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1677
- /* @__PURE__ */ jsx(MetricBlock, { label: "5h", pick: (b) => b?.session, items, showResets: true }),
1678
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1679
- /* @__PURE__ */ jsx(MetricBlock, { label: "Week", pick: (b) => b?.weekly, items, showResets: true }),
1680
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1681
- /* @__PURE__ */ jsx(MetricBlock, { label: "Sonnet", pick: (b) => b?.sonnet, items }),
1682
- items.some((i) => i.s.billing?.extraUsage) && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1683
- /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Extra" }),
1684
- items.map(({ slot, s }) => {
1685
- const e = s.billing?.extraUsage;
1686
- if (!e) return null;
1687
- const sym = e.currency === "EUR" ? "\u20AC" : "$";
1688
- return /* @__PURE__ */ jsxs(Box, { children: [
1689
- /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1690
- /* @__PURE__ */ jsx(Box, { width: 22, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncateName(slot.name, 20) }) }),
1691
- /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1692
- sym,
1693
- e.used.toFixed(2)
1694
- ] }),
1695
- e.limit != null ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1696
- " / ",
1697
- sym,
1698
- e.limit.toFixed(2)
1699
- ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: " used" })
1700
- ] }, slot.id ?? "__default__");
1701
- })
1702
- ] })
1703
- ] })
1704
- ]
3168
+ async function fetchScopeTable(scope, tz) {
3169
+ const tables = await Promise.all(scope.map(async (acc) => {
3170
+ const provider = PROVIDERS[acc.providerId];
3171
+ if (!provider.fetchTable) return null;
3172
+ try {
3173
+ return await provider.fetchTable(acc, tz);
3174
+ } catch {
3175
+ return null;
1705
3176
  }
1706
- );
1707
- }
1708
- function MetricBlock({
1709
- label,
1710
- pick,
1711
- items,
1712
- showResets
1713
- }) {
1714
- const rows = items.map((i) => ({
1715
- slot: i.slot,
1716
- lim: pick(i.s.billing) ?? null,
1717
- error: i.s.billing?.error ?? null
1718
3177
  }));
1719
- if (rows.every((r) => r.lim === null && !r.error)) return null;
1720
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1721
- /* @__PURE__ */ jsx(Text, { bold: true, children: label }),
1722
- rows.map(({ slot, lim, error }) => {
1723
- if (lim) {
1724
- return /* @__PURE__ */ jsx(
1725
- AccountLimitBar,
1726
- {
1727
- slot,
1728
- pct: lim.utilization,
1729
- resets: showResets ? lim.resetsAt : null,
1730
- showName: items.length > 1
1731
- },
1732
- slot.id ?? "__default__"
1733
- );
1734
- }
1735
- return /* @__PURE__ */ jsxs(Box, { children: [
1736
- /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1737
- /* @__PURE__ */ jsx(Box, { width: 22, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncateName(slot.name, 20) }) }),
1738
- /* @__PURE__ */ jsx(Text, { color: error ? "red" : void 0, dimColor: !error, children: error ?? "no data" })
1739
- ] }, slot.id ?? "__default__");
1740
- })
1741
- ] });
3178
+ const valid = tables.filter((t) => t !== null);
3179
+ if (valid.length === 0) return { daily: [], weekly: [], monthly: [] };
3180
+ if (valid.length === 1) return valid[0];
3181
+ return mergeTables(valid);
1742
3182
  }
1743
- function AccountLimitBar({
1744
- slot,
1745
- pct,
1746
- resets,
1747
- showName
1748
- }) {
1749
- const width = 28;
1750
- const filled = Math.max(0, Math.min(width, Math.round(pct / 100 * width)));
1751
- return /* @__PURE__ */ jsxs(Box, { children: [
1752
- /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u25CF " }),
1753
- showName ? /* @__PURE__ */ jsx(Box, { width: 22, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncateName(slot.name, 20) }) }) : /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
1754
- /* @__PURE__ */ jsx(Text, { color: slot.color, children: "\u2501".repeat(filled) }),
1755
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) }),
1756
- /* @__PURE__ */ jsx(Text, { children: " " }),
1757
- /* @__PURE__ */ jsx(Box, { width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1758
- Math.round(pct),
1759
- "%"
1760
- ] }) }),
1761
- resets && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1762
- " resets ",
1763
- resets
1764
- ] })
1765
- ] });
3183
+ function sortRows(rows, sortIdx) {
3184
+ const sorted = [...rows];
3185
+ switch (sortIdx % SORTS.length) {
3186
+ case 0:
3187
+ return sorted.sort((a, b) => a.label.localeCompare(b.label));
3188
+ case 1:
3189
+ return sorted.sort((a, b) => b.label.localeCompare(a.label));
3190
+ case 2:
3191
+ return sorted.sort((a, b) => a.cost - b.cost);
3192
+ case 3:
3193
+ return sorted.sort((a, b) => b.cost - a.cost);
3194
+ default:
3195
+ return sorted;
3196
+ }
1766
3197
  }
1767
- function PeakBadge({ peak }) {
1768
- const color = peak.state === "peak" ? "red" : "green";
1769
- return /* @__PURE__ */ jsxs(Box, { children: [
1770
- /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
1771
- /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
1772
- peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1773
- " (",
1774
- fmtMinutes(peak.minutesUntilChange),
1775
- ")"
1776
- ] })
1777
- ] });
3198
+ function filterTokenRows(rows, q) {
3199
+ if (!q) return rows;
3200
+ const s = q.toLowerCase();
3201
+ return rows.filter((r) => r.label.toLowerCase().includes(s) || r.models.some((m) => m.toLowerCase().includes(s)));
1778
3202
  }
1779
- function fmtMinutes(mins) {
1780
- if (mins < 60) return `${mins}m`;
1781
- const h = Math.floor(mins / 60);
1782
- const m = mins % 60;
1783
- return m === 0 ? `${h}h` : `${h}h ${m}m`;
3203
+ function filterCursorRows(rows, q) {
3204
+ if (!q) return rows;
3205
+ const s = q.toLowerCase();
3206
+ return rows.filter((r) => r.name.toLowerCase().includes(s));
1784
3207
  }
1785
- function TableView({ rows: allRows, cursor, expanded, maxRows, cols, onRowClick }) {
1786
- const wide = cols > 90;
1787
- 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 };
1788
- const fixed = base.label + base.input + base.output + base.cc + base.cr + base.total + base.cost;
1789
- const minModels = wide ? 22 : 14;
1790
- const available = cols - fixed - 6;
1791
- const W = { ...base, models: Math.max(minModels, available) };
1792
- const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
1793
- const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
1794
- for (const r of allRows) {
1795
- totals.input += r.input;
1796
- totals.output += r.output;
1797
- totals.cacheCreate += r.cacheCreate;
1798
- totals.cacheRead += r.cacheRead;
1799
- totals.cost += r.cost;
3208
+ function sortCursorRows(rows, sortIdx) {
3209
+ const out = [...rows];
3210
+ switch (sortIdx % CURSOR_SORTS.length) {
3211
+ case 1:
3212
+ return out.sort((a, b) => b.requests - a.requests);
3213
+ case 2:
3214
+ return out.sort((a, b) => a.name.localeCompare(b.name));
3215
+ default:
3216
+ return out.sort((a, b) => b.usd - a.usd);
1800
3217
  }
1801
- const clampedCursor = Math.min(cursor, allRows.length - 1);
1802
- const scrollStart = Math.max(0, Math.min(clampedCursor - Math.floor(maxRows / 2), allRows.length - maxRows));
1803
- const visible = allRows.slice(scrollStart, scrollStart + maxRows);
1804
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1805
- /* @__PURE__ */ jsxs(Text, { children: [
1806
- /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1807
- " ",
1808
- col("Date", W.label, "left")
1809
- ] }),
1810
- /* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
1811
- /* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
1812
- /* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
1813
- /* @__PURE__ */ jsx(Text, { bold: true, children: col(wide ? "Cache Create" : "CchCrt", W.cc) }),
1814
- /* @__PURE__ */ jsx(Text, { bold: true, children: col(wide ? "Cache Read" : "CchRd", W.cr) }),
1815
- W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, children: col("Total", W.total) }),
1816
- /* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
1817
- ] }),
1818
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
1819
- visible.map((r, vi) => {
1820
- const idx = scrollStart + vi;
1821
- const selected = idx === clampedCursor;
1822
- const isExpanded = idx === expanded;
1823
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1824
- /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs(Text, { inverse: selected, children: [
1825
- /* @__PURE__ */ jsxs(Text, { color: selected ? void 0 : "cyan", children: [
1826
- selected ? "\u25B8 " : " ",
1827
- col(fmtLabel(r.label), W.label, "left")
1828
- ] }),
1829
- /* @__PURE__ */ jsx(Text, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
1830
- /* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
1831
- /* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
1832
- /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
1833
- /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheRead), W.cr) }),
1834
- W.total > 0 && /* @__PURE__ */ jsx(Text, { children: col(tokens(r.total), W.total) }),
1835
- /* @__PURE__ */ jsx(Text, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.cost), W.cost) })
1836
- ] }) }),
1837
- isExpanded && /* @__PURE__ */ jsx(RowDetail, { row: r, indent: W.label + 2 })
1838
- ] }, r.label);
1839
- }),
1840
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW + 2) }),
1841
- /* @__PURE__ */ jsxs(Text, { children: [
1842
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
1843
- " ",
1844
- col("Total", W.label, "left")
1845
- ] }),
1846
- /* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
1847
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
1848
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
1849
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
1850
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
1851
- W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
1852
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
1853
- ] }),
1854
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1855
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1856
- "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
1857
- clampedCursor + 1,
1858
- "/",
1859
- allRows.length
1860
- ] }),
1861
- /* @__PURE__ */ jsx(Box, { height: 1 }),
1862
- /* @__PURE__ */ jsx(Footer, { hasAccounts: false })
1863
- ] });
1864
3218
  }
1865
- function RowDetail({ row, indent }) {
1866
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: indent, marginY: 0, children: row.breakdown.map((m, i) => {
1867
- const prefix = i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
1868
- return /* @__PURE__ */ jsxs(Text, { children: [
1869
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1870
- prefix,
1871
- " "
1872
- ] }),
1873
- /* @__PURE__ */ jsx(Text, { bold: true, children: col(m.name, 16, "left") }),
1874
- /* @__PURE__ */ jsxs(Text, { children: [
1875
- col(tokens(m.input), 8),
1876
- " in "
1877
- ] }),
1878
- /* @__PURE__ */ jsxs(Text, { children: [
1879
- col(tokens(m.output), 8),
1880
- " out "
1881
- ] }),
1882
- /* @__PURE__ */ jsxs(Text, { children: [
1883
- col(tokens(m.cacheCreate), 8),
1884
- " cc "
1885
- ] }),
1886
- /* @__PURE__ */ jsxs(Text, { children: [
1887
- col(tokens(m.cacheRead), 9),
1888
- " cr "
1889
- ] }),
1890
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(m.cost) })
1891
- ] }, m.name);
3219
+ function AccountStrip({ slots, activeIdx, onSelect }) {
3220
+ return /* @__PURE__ */ jsx6(Box6, { flexWrap: "wrap", children: slots.map((s, i) => {
3221
+ const active = i === activeIdx;
3222
+ const dot = s.id === null ? "\u2726" : "\u25CF";
3223
+ const label = truncateName(s.name, 16);
3224
+ return /* @__PURE__ */ jsxs6(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
3225
+ /* @__PURE__ */ jsx6(Text6, { dimColor: !active, children: i }),
3226
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
3227
+ /* @__PURE__ */ jsx6(Text6, { color: s.color, bold: active, dimColor: !active, children: dot }),
3228
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
3229
+ active ? /* @__PURE__ */ jsx6(Text6, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: label })
3230
+ ] }, s.id ?? "__all__");
1892
3231
  }) });
1893
3232
  }
1894
- function Spinner({ label }) {
1895
- const [i, setI] = useState(0);
1896
- useEffect(() => {
1897
- const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
1898
- return () => clearInterval(id);
1899
- }, []);
1900
- return /* @__PURE__ */ jsxs(Box, { children: [
1901
- /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1902
- SPINNER_FRAMES[i],
1903
- " "
1904
- ] }),
1905
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
3233
+ function Footer({ hasAccounts }) {
3234
+ return /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
3235
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "by " }),
3236
+ /* @__PURE__ */ jsx6(Text6, { children: "David Ilie" }),
3237
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " (" }),
3238
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "davidilie.com" }),
3239
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: ") \xB7 s=settings " }),
3240
+ hasAccounts && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "0-9=jump a/A=cycle " }),
3241
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "q=quit" })
1906
3242
  ] });
1907
3243
  }
1908
- function fmtLabel(label) {
1909
- if (label.length === 10 && label[4] === "-") return shortDate(label);
1910
- if (label.length === 7 && label[4] === "-") {
1911
- const m = label.slice(5, 7);
1912
- return `${MONTHS[Number(m)]} '${label.slice(2, 4)}`;
1913
- }
1914
- return shortDate(label);
1915
- }
1916
- function ClickableBox({ onClick, children, ...props }) {
1917
- const ref = useRef(null);
1918
- useOnMouseClick(ref, (clicked) => {
1919
- if (clicked) onClick();
1920
- });
1921
- return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
1922
- }
1923
3244
 
1924
3245
  // src/cli.tsx
1925
- import { jsx as jsx2 } from "react/jsx-runtime";
3246
+ import { jsx as jsx7 } from "react/jsx-runtime";
3247
+ process.on("unhandledRejection", () => {
3248
+ });
1926
3249
  var args = process.argv.slice(2);
1927
3250
  var interval;
1928
3251
  for (let i = 0; i < args.length; i++) {
@@ -1931,15 +3254,17 @@ for (let i = 0; i < args.length; i++) {
1931
3254
  i++;
1932
3255
  }
1933
3256
  if (args[i] === "--help" || args[i] === "-h") {
1934
- console.log("tokmon - Terminal dashboard for Claude Code usage\n");
3257
+ console.log("tokmon - Terminal dashboard for Claude, Codex, and Cursor usage\n");
1935
3258
  console.log("Usage: tokmon [options]\n");
1936
3259
  console.log("Options:");
1937
3260
  console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
1938
3261
  console.log(" -h, --help Show this help\n");
1939
3262
  console.log("Keybindings:");
1940
- console.log(" Tab / \u2190\u2192 Switch views");
3263
+ console.log(" Tab Switch Dashboard / Table");
3264
+ console.log(" p / P Cycle table provider");
3265
+ console.log(" a / A Cycle account focus");
3266
+ console.log(" 0-9 Jump to account focus");
1941
3267
  console.log(" \u2191\u2193 Scroll table");
1942
- console.log(" 1-2 Jump to view");
1943
3268
  console.log(" s Settings");
1944
3269
  console.log(" q Quit");
1945
3270
  process.exit(0);
@@ -1949,5 +3274,6 @@ var config = await loadConfig();
1949
3274
  if (config.clearScreen && process.stdout.isTTY) {
1950
3275
  process.stdout.write("\x1B[2J\x1B[H");
1951
3276
  }
1952
- var { waitUntilExit } = render(/* @__PURE__ */ jsx2(MouseProvider, { children: /* @__PURE__ */ jsx2(App, { interval }) }));
3277
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx7(MouseProvider, { children: /* @__PURE__ */ jsx7(App, { interval }) }));
1953
3278
  await waitUntilExit();
3279
+ await flushDisk();