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.
- package/README.md +102 -51
- package/dist/cli.js +2588 -1262
- 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(
|
|
30
|
+
return join(envDir("APPDATA") ?? join(homedir(), "AppData", "Roaming"), "tokmon");
|
|
23
31
|
}
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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/
|
|
77
|
-
import {
|
|
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/
|
|
198
|
-
var
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
var
|
|
204
|
-
var
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
294
|
+
return out;
|
|
228
295
|
}
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
for (const dir of getClaudeDirs(homeDir)) {
|
|
272
|
-
let listing;
|
|
345
|
+
await mapLimit(files, 8, async (f) => {
|
|
273
346
|
try {
|
|
274
|
-
|
|
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
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
498
|
+
return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
446
499
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
520
|
+
return null;
|
|
495
521
|
}
|
|
496
522
|
}
|
|
497
|
-
|
|
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
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
543
|
-
if (
|
|
544
|
-
if (
|
|
545
|
-
return
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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] =
|
|
605
|
-
const [
|
|
606
|
-
const [
|
|
607
|
-
const [
|
|
608
|
-
const [
|
|
609
|
-
const [
|
|
610
|
-
const [
|
|
611
|
-
const [
|
|
612
|
-
const [
|
|
613
|
-
const [
|
|
614
|
-
const [
|
|
615
|
-
const [
|
|
616
|
-
const [
|
|
617
|
-
const [
|
|
618
|
-
const [
|
|
619
|
-
const [
|
|
620
|
-
const
|
|
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
|
|
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
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
if (!
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2503
|
+
clearTimeout(timer);
|
|
677
2504
|
};
|
|
678
2505
|
}, [interval2, tz, configReady, accountsKey]);
|
|
679
|
-
|
|
2506
|
+
useEffect2(() => {
|
|
680
2507
|
if (!configReady) return;
|
|
681
2508
|
let active = true;
|
|
2509
|
+
let timer;
|
|
682
2510
|
const load = async () => {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
}
|
|
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
|
-
|
|
2531
|
+
clearTimeout(timer);
|
|
702
2532
|
};
|
|
703
2533
|
}, [billingMs, configReady, accountsKey]);
|
|
704
|
-
|
|
705
|
-
|
|
2534
|
+
const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
|
|
2535
|
+
useEffect2(() => {
|
|
706
2536
|
setTable(null);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
2545
|
+
let timer;
|
|
2546
|
+
const fetchOnce = async () => {
|
|
730
2547
|
try {
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
2572
|
+
clearTimeout(timer);
|
|
739
2573
|
};
|
|
740
|
-
}, [tab,
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
2656
|
+
field: "provider",
|
|
2657
|
+
providerId: acc.providerId,
|
|
788
2658
|
name: acc.name,
|
|
789
2659
|
homeDir: acc.homeDir,
|
|
790
|
-
color: acc.color ||
|
|
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 =
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
910
|
-
|
|
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
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
1055
|
-
|
|
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 === "
|
|
1065
|
-
|
|
1066
|
-
resetView();
|
|
2976
|
+
if (input === "P") {
|
|
2977
|
+
cycleTableProvider(-1);
|
|
1067
2978
|
return;
|
|
1068
2979
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
resetView();
|
|
2980
|
+
if (input === "/") {
|
|
2981
|
+
setSearchMode(true);
|
|
1072
2982
|
return;
|
|
1073
2983
|
}
|
|
1074
|
-
if (key.
|
|
1075
|
-
|
|
1076
|
-
|
|
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) %
|
|
2990
|
+
setSort((s) => (s + 1) % SORTS_FOR.length);
|
|
1081
2991
|
resetView();
|
|
1082
2992
|
return;
|
|
1083
2993
|
}
|
|
1084
|
-
if (
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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" ?
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
"
|
|
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__ */
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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__ */
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
Box,
|
|
3078
|
+
showSettings ? /* @__PURE__ */ jsx6(
|
|
3079
|
+
SettingsView,
|
|
1434
3080
|
{
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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__ */
|
|
1552
|
-
|
|
1553
|
-
{
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
-
|
|
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
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
{
|
|
1662
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
|
1780
|
-
if (
|
|
1781
|
-
const
|
|
1782
|
-
|
|
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
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
|
1866
|
-
return /* @__PURE__ */
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
/* @__PURE__ */
|
|
1874
|
-
/* @__PURE__ */
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
/* @__PURE__ */
|
|
1902
|
-
|
|
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
|
|
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
|
|
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 /
|
|
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__ */
|
|
3277
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx7(MouseProvider, { children: /* @__PURE__ */ jsx7(App, { interval }) }));
|
|
1953
3278
|
await waitUntilExit();
|
|
3279
|
+
await flushDisk();
|