tokmon 0.14.5 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/chunk-6QVIIZWX.js +533 -0
  2. package/dist/chunk-ZPH4754N.js +2542 -0
  3. package/dist/cli.js +204 -2622
  4. package/dist/server-JV3U3PIF.js +8 -0
  5. package/dist/web/assets/DepartureMono-Regular-2BZob_Zz.woff2 +0 -0
  6. package/dist/web/assets/index-Blfaml-c.js +113 -0
  7. package/dist/web/assets/index-DxSkJ-XD.css +1 -0
  8. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  9. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  10. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  11. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  12. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  13. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  14. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  15. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  16. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  17. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  18. package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  19. package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  20. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  21. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  22. package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  23. package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  24. package/dist/web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  25. package/dist/web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  26. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  27. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  28. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  29. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  30. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  31. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  32. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  33. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  34. package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  35. package/dist/web/index.html +20 -0
  36. package/dist/web-6MDSVWLV.js +110 -0
  37. package/package.json +7 -3
package/dist/cli.js CHANGED
@@ -1,516 +1,35 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ PROVIDERS,
4
+ PROVIDER_ORDER,
5
+ accountsByProvider,
6
+ buildAccounts,
7
+ cacheDir,
8
+ col,
9
+ configLocation,
10
+ currency,
11
+ cursorModelSpend,
12
+ detectProviders,
13
+ flushDisk,
14
+ generateAccountId,
15
+ isValidTimezone,
16
+ loadConfig,
17
+ mergeTables,
18
+ pickAccentColor,
19
+ readJson,
20
+ resolveTimezone,
21
+ saveConfig,
22
+ shortDate,
23
+ systemTimezone,
24
+ time,
25
+ tokens
26
+ } from "./chunk-ZPH4754N.js";
2
27
 
3
28
  // src/cli.tsx
4
29
  import { EventEmitter } from "events";
5
30
  import { render } from "ink";
6
31
  import { MouseProvider } from "@zenobius/ink-mouse";
7
32
 
8
- // src/config.ts
9
- import { readFile, writeFile, mkdir, rename } from "fs/promises";
10
- import { join, isAbsolute } from "path";
11
- import { homedir } from "os";
12
- function envDir(name) {
13
- const v = process.env[name];
14
- return v && v.trim() && isAbsolute(v.trim()) ? v.trim() : void 0;
15
- }
16
- var DEFAULTS = {
17
- interval: 2,
18
- billingInterval: 5,
19
- clearScreen: true,
20
- timezone: null,
21
- accounts: [],
22
- activeAccountId: null,
23
- disabledProviders: [],
24
- onboarded: false,
25
- dashboardLayout: "grid",
26
- defaultFocus: "all",
27
- ascii: "auto",
28
- knownProviders: []
29
- };
30
- var LEGACY_KNOWN = ["claude", "codex", "cursor"];
31
- var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
32
- function configDir() {
33
- if (process.platform === "win32") {
34
- return join(envDir("APPDATA") ?? join(homedir(), "AppData", "Roaming"), "tokmon");
35
- }
36
- return join(envDir("XDG_CONFIG_HOME") ?? join(homedir(), ".config"), "tokmon");
37
- }
38
- function configLocation() {
39
- return join(configDir(), "config.json");
40
- }
41
- function cacheDir() {
42
- if (process.platform === "win32") {
43
- return join(envDir("LOCALAPPDATA") ?? envDir("APPDATA") ?? join(homedir(), "AppData", "Local"), "tokmon", "cache");
44
- }
45
- if (process.platform === "darwin") {
46
- return join(homedir(), "Library", "Caches", "tokmon");
47
- }
48
- return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
49
- }
50
- var PROVIDER_IDS = ["claude", "codex", "cursor", "pi", "opencode", "copilot", "antigravity", "gemini"];
51
- function clampNum(v, fallback, min) {
52
- return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
53
- }
54
- async function loadConfig() {
55
- let raw;
56
- try {
57
- raw = await readFile(configLocation(), "utf-8");
58
- } catch {
59
- return { ...DEFAULTS };
60
- }
61
- let parsed;
62
- try {
63
- parsed = JSON.parse(raw);
64
- } catch {
65
- try {
66
- await writeFile(configLocation() + ".bak", raw);
67
- } catch {
68
- }
69
- return { ...DEFAULTS };
70
- }
71
- try {
72
- 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));
73
- return {
74
- ...DEFAULTS,
75
- ...parsed,
76
- // Coerce the numeric/boolean knobs: a hand-edited `"interval": "fast"` or
77
- // a negative value would otherwise reach setTimeout as NaN → a 0ms tight
78
- // poll loop. Clamp to sane minimums.
79
- interval: clampNum(parsed.interval, DEFAULTS.interval, 1),
80
- billingInterval: clampNum(parsed.billingInterval, DEFAULTS.billingInterval, 1),
81
- clearScreen: typeof parsed.clearScreen === "boolean" ? parsed.clearScreen : DEFAULTS.clearScreen,
82
- timezone: typeof parsed.timezone === "string" && parsed.timezone.trim() ? parsed.timezone : null,
83
- accounts,
84
- activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null,
85
- disabledProviders: (Array.isArray(parsed.disabledProviders) ? parsed.disabledProviders : []).filter((p) => PROVIDER_IDS.includes(p)),
86
- // Only skip onboarding when it was explicitly completed. Configs that
87
- // predate the flag (even ones with a legacy account) still get the
88
- // provider picker once, so existing users can opt into Codex/Cursor.
89
- onboarded: parsed.onboarded === true,
90
- dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
91
- defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
92
- ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
93
- knownProviders: Array.isArray(parsed.knownProviders) ? parsed.knownProviders.filter((p) => PROVIDER_IDS.includes(p)) : parsed.onboarded === true ? [...LEGACY_KNOWN] : []
94
- };
95
- } catch {
96
- return { ...DEFAULTS };
97
- }
98
- }
99
- var saveQueue = Promise.resolve();
100
- function saveConfig(config2) {
101
- saveQueue = saveQueue.then(async () => {
102
- try {
103
- const dir = configDir();
104
- await mkdir(dir, { recursive: true });
105
- const tmp = join(dir, `config.json.${process.pid}.tmp`);
106
- await writeFile(tmp, JSON.stringify(config2, null, 2) + "\n");
107
- await rename(tmp, configLocation());
108
- } catch {
109
- }
110
- });
111
- return saveQueue;
112
- }
113
- function slugify(value) {
114
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
115
- }
116
- function generateAccountId(name, existing) {
117
- const base = slugify(name) || "account";
118
- const taken = new Set(existing.map((a) => a.id));
119
- if (!taken.has(base)) return base;
120
- for (let i = 2; i < 1e3; i++) {
121
- const candidate = `${base}_${i}`;
122
- if (!taken.has(candidate)) return candidate;
123
- }
124
- return `${base}_${Date.now()}`;
125
- }
126
- function pickAccentColor(existing) {
127
- const used = new Set(existing.map((a) => a.color).filter(Boolean));
128
- for (const c of ACCENT_COLORS) {
129
- if (!used.has(c)) return c;
130
- }
131
- return ACCENT_COLORS[existing.length % ACCENT_COLORS.length];
132
- }
133
- function expandHome(p) {
134
- if (!p) return homedir();
135
- if (p === "~" || p === "~/" || p === "~\\") return homedir();
136
- if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
137
- return p;
138
- }
139
-
140
- // src/providers/usage-core.ts
141
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
142
- import { join as join2 } from "path";
143
-
144
- // src/tz.ts
145
- function systemTimezone() {
146
- try {
147
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
148
- } catch {
149
- return "UTC";
150
- }
151
- }
152
- function isValidTimezone(tz) {
153
- try {
154
- new Intl.DateTimeFormat("en-CA", { timeZone: tz });
155
- return true;
156
- } catch {
157
- return false;
158
- }
159
- }
160
- function resolveTimezone(cfg) {
161
- if (!cfg) return systemTimezone();
162
- return isValidTimezone(cfg) ? cfg : systemTimezone();
163
- }
164
- var dayFmtCache = /* @__PURE__ */ new Map();
165
- function dayFmt(tz) {
166
- let f = dayFmtCache.get(tz);
167
- if (!f) {
168
- f = new Intl.DateTimeFormat("en-CA", {
169
- timeZone: tz,
170
- year: "numeric",
171
- month: "2-digit",
172
- day: "2-digit"
173
- });
174
- dayFmtCache.set(tz, f);
175
- }
176
- return f;
177
- }
178
- function dayKey(ts, tz) {
179
- return dayFmt(tz).format(new Date(ts));
180
- }
181
- function monthKey(ts, tz) {
182
- return dayKey(ts, tz).slice(0, 7);
183
- }
184
- var partsFmtCache = /* @__PURE__ */ new Map();
185
- function partsFmt(tz) {
186
- let f = partsFmtCache.get(tz);
187
- if (!f) {
188
- f = new Intl.DateTimeFormat("en-US", {
189
- timeZone: tz,
190
- hourCycle: "h23",
191
- year: "numeric",
192
- month: "2-digit",
193
- day: "2-digit",
194
- hour: "2-digit",
195
- minute: "2-digit",
196
- second: "2-digit",
197
- weekday: "short"
198
- });
199
- partsFmtCache.set(tz, f);
200
- }
201
- return f;
202
- }
203
- var WEEKDAY_MAP = {
204
- Sun: 0,
205
- Mon: 1,
206
- Tue: 2,
207
- Wed: 3,
208
- Thu: 4,
209
- Fri: 5,
210
- Sat: 6
211
- };
212
- function tzParts(ts, tz) {
213
- const parts = partsFmt(tz).formatToParts(new Date(ts));
214
- const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
215
- return {
216
- y: Number(get("year")),
217
- m: Number(get("month")),
218
- d: Number(get("day")),
219
- hh: Number(get("hour")),
220
- mm: Number(get("minute")),
221
- ss: Number(get("second")),
222
- weekday: WEEKDAY_MAP[get("weekday")] ?? 0
223
- };
224
- }
225
- function instantFromTz(y, m, d, hh, mm, ss, tz) {
226
- const guess = Date.UTC(y, m - 1, d, hh, mm, ss);
227
- const r = tzParts(guess, tz);
228
- const rendered = Date.UTC(r.y, r.m - 1, r.d, r.hh, r.mm, r.ss);
229
- const offset = rendered - guess;
230
- return guess - offset;
231
- }
232
- function startOfDay(ts, tz) {
233
- const p = tzParts(ts, tz);
234
- return instantFromTz(p.y, p.m, p.d, 0, 0, 0, tz);
235
- }
236
- function startOfMonth(ts, tz) {
237
- const p = tzParts(ts, tz);
238
- return instantFromTz(p.y, p.m, 1, 0, 0, 0, tz);
239
- }
240
- function startOfWeek(ts, tz) {
241
- const p = tzParts(ts, tz);
242
- const offset = p.weekday === 0 ? 6 : p.weekday - 1;
243
- return instantFromTz(p.y, p.m, p.d - offset, 0, 0, 0, tz);
244
- }
245
- function monthsAgoStart(ts, months, tz) {
246
- const p = tzParts(ts, tz);
247
- return instantFromTz(p.y, p.m - months, 1, 0, 0, 0, tz);
248
- }
249
- function weekKey(ts, tz) {
250
- return dayKey(startOfWeek(ts, tz), tz);
251
- }
252
-
253
- // src/providers/usage-core.ts
254
- var SPARK_DAYS = 14;
255
- var DAY_MS = 864e5;
256
- var CACHE_VERSION = 4;
257
- var STABLE_AGE_MS = 5 * 6e4;
258
- var PRUNE_AGE_MS = 200 * DAY_MS;
259
- var memCache = /* @__PURE__ */ new Map();
260
- var diskLoaded = false;
261
- var dirty = false;
262
- var flushTimer = null;
263
- function cacheFile() {
264
- return join2(cacheDir(), `usage-v${CACHE_VERSION}.json`);
265
- }
266
- function encode(mtimeMs, size, entries) {
267
- const mods = [];
268
- const idx = /* @__PURE__ */ new Map();
269
- const rows = entries.map((e) => {
270
- let mi = idx.get(e.model);
271
- if (mi === void 0) {
272
- mi = mods.length;
273
- mods.push(e.model);
274
- idx.set(e.model, mi);
275
- }
276
- return [e.ts, mi, e.input, e.output, e.cacheCreate, e.cacheRead, e.cost, e.cacheSavings, e.id ?? 0];
277
- });
278
- return { m: mtimeMs, s: size, mods, rows };
279
- }
280
- function decode(s) {
281
- const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
282
- const out = [];
283
- for (const r of s.rows) {
284
- if (!Array.isArray(r) || r.length < 8) continue;
285
- const ts = r[0];
286
- if (typeof ts !== "number" || !Number.isFinite(ts)) continue;
287
- const mi = r[1];
288
- out.push({
289
- ts,
290
- model: typeof mi === "number" && typeof s.mods[mi] === "string" ? s.mods[mi] : "unknown",
291
- input: num(r[2]),
292
- output: num(r[3]),
293
- cacheCreate: num(r[4]),
294
- cacheRead: num(r[5]),
295
- cost: num(r[6]),
296
- cacheSavings: num(r[7]),
297
- id: typeof r[8] === "string" ? r[8] : void 0
298
- });
299
- }
300
- return out;
301
- }
302
- async function ensureDiskLoaded() {
303
- if (diskLoaded) return;
304
- diskLoaded = true;
305
- try {
306
- const obj = JSON.parse(await readFile2(cacheFile(), "utf-8"));
307
- for (const [path, s] of Object.entries(obj)) {
308
- if (s && typeof s.m === "number" && Array.isArray(s.rows) && Array.isArray(s.mods)) {
309
- memCache.set(path, { mtimeMs: s.m, size: typeof s.s === "number" ? s.s : -1, entries: decode(s) });
310
- }
311
- }
312
- } catch {
313
- }
314
- }
315
- async function flushDisk() {
316
- if (!dirty) return;
317
- const now = Date.now();
318
- const obj = {};
319
- for (const [path, v] of memCache) {
320
- if (now - v.mtimeMs > STABLE_AGE_MS && now - v.mtimeMs < PRUNE_AGE_MS) {
321
- obj[path] = encode(v.mtimeMs, v.size, v.entries);
322
- }
323
- }
324
- try {
325
- await mkdir2(cacheDir(), { recursive: true });
326
- const tmp = `${cacheFile()}.${process.pid}.tmp`;
327
- await writeFile2(tmp, JSON.stringify(obj));
328
- await rename2(tmp, cacheFile());
329
- dirty = false;
330
- } catch {
331
- }
332
- }
333
- function scheduleFlush() {
334
- if (flushTimer) return;
335
- flushTimer = setTimeout(() => {
336
- flushTimer = null;
337
- void flushDisk();
338
- }, 4e3);
339
- flushTimer.unref?.();
340
- }
341
- async function mapLimit(items, limit, fn) {
342
- let i = 0;
343
- const worker = async () => {
344
- while (i < items.length) await fn(items[i++]);
345
- };
346
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
347
- }
348
- async function loadCachedEntries(files, parse, since) {
349
- await ensureDiskLoaded();
350
- const chunks = [];
351
- await mapLimit(files, 8, async (f) => {
352
- try {
353
- let c = memCache.get(f.path);
354
- if (!c || c.mtimeMs !== f.mtimeMs || c.size !== f.size) {
355
- const entries = await parse(f.path);
356
- c = { mtimeMs: f.mtimeMs, size: f.size, entries };
357
- memCache.set(f.path, c);
358
- if (Date.now() - f.mtimeMs > STABLE_AGE_MS) dirty = true;
359
- }
360
- chunks.push(c.entries);
361
- } catch {
362
- }
363
- });
364
- if (dirty) scheduleFlush();
365
- return dedupe(chunks.flat().filter((e) => e.ts >= since));
366
- }
367
- function safeNum(v) {
368
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
369
- }
370
- function dedupe(entries) {
371
- const seen = /* @__PURE__ */ new Set();
372
- const out = [];
373
- for (const e of entries) {
374
- const k = e.id ?? `${e.ts} ${e.model} ${e.input} ${e.output} ${e.cacheCreate} ${e.cacheRead}`;
375
- if (seen.has(k)) continue;
376
- seen.add(k);
377
- out.push(e);
378
- }
379
- return out;
380
- }
381
- function summarize(entries, tz) {
382
- const now = Date.now();
383
- const todayStart = startOfDay(now, tz);
384
- const weekStart = startOfWeek(now, tz);
385
- const monthStart = startOfMonth(now, tz);
386
- const today = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
387
- const week = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
388
- const month = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
389
- const byDay = /* @__PURE__ */ new Map();
390
- let oldestToday = now;
391
- let hadToday = false;
392
- const add = (s, e) => {
393
- s.cost += e.cost;
394
- s.tokens += e.input + e.output + e.cacheCreate + e.cacheRead;
395
- s.cacheRead += e.cacheRead;
396
- s.cacheSavings += e.cacheSavings;
397
- };
398
- for (const e of entries) {
399
- if (e.ts >= monthStart) add(month, e);
400
- if (e.ts >= weekStart) add(week, e);
401
- if (e.ts >= todayStart) {
402
- add(today, e);
403
- hadToday = true;
404
- if (e.ts < oldestToday) oldestToday = e.ts;
405
- }
406
- const dk = dayKey(e.ts, tz);
407
- byDay.set(dk, (byDay.get(dk) ?? 0) + e.cost);
408
- }
409
- const hrs = Math.max((now - oldestToday) / 36e5, 1 / 60);
410
- const burnRate = hadToday ? today.cost / hrs : 0;
411
- const series = [];
412
- for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(dayKey(now - i * DAY_MS, tz)) ?? 0);
413
- return { today, week, month, burnRate, series };
414
- }
415
- function groupBy(entries, keyFn) {
416
- const groups = /* @__PURE__ */ new Map();
417
- for (const e of entries) {
418
- const key = keyFn(e);
419
- const arr = groups.get(key);
420
- if (arr) arr.push(e);
421
- else groups.set(key, [e]);
422
- }
423
- const rows = [];
424
- for (const [label, group] of groups) {
425
- let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
426
- const byModel = /* @__PURE__ */ new Map();
427
- for (const e of group) {
428
- input += e.input;
429
- output += e.output;
430
- cacheCreate += e.cacheCreate;
431
- cacheRead += e.cacheRead;
432
- cost += e.cost;
433
- const m = byModel.get(e.model);
434
- if (m) {
435
- m.input += e.input;
436
- m.output += e.output;
437
- m.cacheCreate += e.cacheCreate;
438
- m.cacheRead += e.cacheRead;
439
- m.cost += e.cost;
440
- } else {
441
- byModel.set(e.model, {
442
- name: e.model,
443
- input: e.input,
444
- output: e.output,
445
- cacheCreate: e.cacheCreate,
446
- cacheRead: e.cacheRead,
447
- cost: e.cost
448
- });
449
- }
450
- }
451
- rows.push({
452
- label,
453
- models: [...byModel.keys()].sort(),
454
- input,
455
- output,
456
- cacheCreate,
457
- cacheRead,
458
- total: input + output + cacheCreate + cacheRead,
459
- cost,
460
- breakdown: [...byModel.values()].sort((a, b) => b.cost - a.cost)
461
- });
462
- }
463
- return rows.sort((a, b) => a.label.localeCompare(b.label));
464
- }
465
- function tabulate(entries, tz) {
466
- return {
467
- daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
468
- weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
469
- monthly: groupBy(entries, (e) => monthKey(e.ts, tz))
470
- };
471
- }
472
- function mergeRows(groups) {
473
- const byLabel = /* @__PURE__ */ new Map();
474
- for (const rows of groups) {
475
- for (const r of rows) {
476
- const ex = byLabel.get(r.label);
477
- if (!ex) {
478
- byLabel.set(r.label, { ...r, models: [...r.models], breakdown: r.breakdown.map((m) => ({ ...m })) });
479
- continue;
480
- }
481
- ex.input += r.input;
482
- ex.output += r.output;
483
- ex.cacheCreate += r.cacheCreate;
484
- ex.cacheRead += r.cacheRead;
485
- ex.total += r.total;
486
- ex.cost += r.cost;
487
- const bd = new Map(ex.breakdown.map((m) => [m.name, m]));
488
- for (const m of r.breakdown) {
489
- const e = bd.get(m.name);
490
- if (e) {
491
- e.input += m.input;
492
- e.output += m.output;
493
- e.cacheCreate += m.cacheCreate;
494
- e.cacheRead += m.cacheRead;
495
- e.cost += m.cost;
496
- } else {
497
- bd.set(m.name, { ...m });
498
- }
499
- }
500
- ex.breakdown = [...bd.values()].sort((a, b) => b.cost - a.cost);
501
- ex.models = [...bd.keys()].sort();
502
- }
503
- }
504
- return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
505
- }
506
- function mergeTables(list) {
507
- return {
508
- daily: mergeRows(list.map((t) => t.daily)),
509
- weekly: mergeRows(list.map((t) => t.weekly)),
510
- monthly: mergeRows(list.map((t) => t.monthly))
511
- };
512
- }
513
-
514
33
  // src/glyphs.ts
515
34
  var GLYPHS_UNICODE = {
516
35
  spark: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"],
@@ -541,2141 +60,131 @@ var GLYPHS_UNICODE = {
541
60
  emDash: "\u2014",
542
61
  eur: "\u20AC",
543
62
  gbp: "\xA3",
544
- border: "round"
545
- };
546
- var GLYPHS_ASCII = {
547
- spark: [".", ":", "-", "=", "+", "*", "#", "@"],
548
- barFull: "#",
549
- barEmpty: "-",
550
- rule: "-",
551
- spinner: ["|", "/", "-", "\\"],
552
- dot: "*",
553
- dotSel: "*",
554
- radioOff: "o",
555
- dotAll: "+",
556
- caretR: ">",
557
- caretL: "<",
558
- play: ">",
559
- arrowU: "^",
560
- arrowD: "v",
561
- arrowL: "<",
562
- arrowR: ">",
563
- shift: "^",
564
- vbar: "|",
565
- treeMid: "+-",
566
- treeEnd: "`-",
567
- boxMark: "|",
568
- check: "x",
569
- warn: "!",
570
- ellipsis: "...",
571
- middot: "-",
572
- emDash: "-",
573
- eur: "EUR",
574
- gbp: "GBP",
575
- border: "classic"
576
- };
577
- function detectUnicode(env, isTTY, platform) {
578
- if (!isTTY) return false;
579
- if (env.TERM === "dumb") return false;
580
- if (platform === "win32") {
581
- return Boolean(env.WT_SESSION || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || /xterm/i.test(env.TERM ?? ""));
582
- }
583
- const loc = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
584
- if (loc && /\.(iso|latin|ascii|cp\d|koi|gbk|big5)/i.test(loc)) return false;
585
- if (/^(C|POSIX)$/i.test(loc)) return false;
586
- return true;
587
- }
588
- function resolveGlyphs(opts) {
589
- let ascii;
590
- if (opts.flag === "on") ascii = true;
591
- else if (opts.flag === "off") ascii = false;
592
- else {
593
- const e = (opts.env.TOKMON_ASCII ?? "").toLowerCase();
594
- if (/^(1|true|on|yes)$/.test(e)) ascii = true;
595
- else if (/^(0|false|off|no)$/.test(e)) ascii = false;
596
- else if (opts.config === "on") ascii = true;
597
- else if (opts.config === "off") ascii = false;
598
- else ascii = !detectUnicode(opts.env, opts.isTTY, opts.platform);
599
- }
600
- return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
601
- }
602
- var active = GLYPHS_UNICODE;
603
- function setGlyphs(set) {
604
- active = set;
605
- }
606
- function glyphs() {
607
- return active;
608
- }
609
-
610
- // src/app.tsx
611
- import { spawn } from "child_process";
612
- import { appendFileSync as appendFileSync2 } from "fs";
613
- import { useState as useState3, useEffect as useEffect3, useCallback, useRef as useRef2, useMemo } from "react";
614
- import { Box as Box7, Text as Text7, Transform, useInput, useStdout, useApp } from "ink";
615
- import { useMouse } from "@zenobius/ink-mouse";
616
-
617
- // src/http.ts
618
- async function readJson(res) {
619
- const type = (res.headers.get("content-type") ?? "").toLowerCase();
620
- if (type && !type.includes("json")) return null;
621
- try {
622
- return await res.json();
623
- } catch {
624
- return null;
625
- }
626
- }
627
-
628
- // src/peak.ts
629
- async function fetchPeak() {
630
- try {
631
- const res = await fetch("https://promoclock.co/api/status", {
632
- headers: { "Accept": "application/json", "User-Agent": "tokmon" },
633
- signal: AbortSignal.timeout(3e3)
634
- });
635
- if (!res.ok) return null;
636
- const data = await readJson(res);
637
- if (!data) return null;
638
- let state;
639
- if (data.isPeak === true || data.status === "peak") state = "peak";
640
- else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
641
- else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
642
- else return null;
643
- return {
644
- state,
645
- label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
646
- minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
647
- };
648
- } catch {
649
- return null;
650
- }
651
- }
652
-
653
- // src/providers/claude/usage.ts
654
- import { readdir, stat as fsStat, access } from "fs/promises";
655
- import { createReadStream } from "fs";
656
- import { createInterface } from "readline";
657
- import { join as join3, isAbsolute as isAbsolute2 } from "path";
658
- import { homedir as homedir2 } from "os";
659
- var PRICING = {
660
- "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
661
- "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
662
- "claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
663
- "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
664
- "claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
665
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
666
- "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
667
- "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
668
- };
669
- var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
670
- var FALLBACK = PRICING["claude-opus-4"];
671
- function claudeConfigDirs(homeDir) {
672
- if (homeDir) {
673
- return [join3(homeDir, ".claude"), join3(homeDir, ".config", "claude")];
674
- }
675
- const home = homedir2();
676
- const dirs = [join3(home, ".claude")];
677
- const xdg = envDir("XDG_CONFIG_HOME");
678
- if (xdg) {
679
- dirs.push(join3(xdg, "claude"));
680
- } else if (process.platform !== "win32") {
681
- dirs.push(join3(home, ".config", "claude"));
682
- }
683
- const appData = envDir("APPDATA");
684
- if (appData) dirs.push(join3(appData, "claude"));
685
- if (process.env.CLAUDE_CONFIG_DIR) {
686
- for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
687
- const t = p.trim();
688
- if (t && isAbsolute2(t)) dirs.push(t);
689
- }
690
- }
691
- return [...new Set(dirs)];
692
- }
693
- function getClaudeDirs(homeDir) {
694
- return claudeConfigDirs(homeDir).map((d) => join3(d, "projects"));
695
- }
696
- async function detectClaude(homeDir) {
697
- for (const dir of getClaudeDirs(homeDir)) {
698
- try {
699
- await access(dir);
700
- return true;
701
- } catch {
702
- }
703
- }
704
- return false;
705
- }
706
- function priceFor(model) {
707
- for (const key of PRICE_KEYS) {
708
- if (!model.startsWith(key)) continue;
709
- const rest = model.slice(key.length);
710
- if (rest === "" || rest[0] === "-") return PRICING[key];
711
- }
712
- return FALLBACK;
713
- }
714
- function costOf(model, u) {
715
- const p = priceFor(model);
716
- 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;
717
- }
718
- function shortModel(model) {
719
- return model.replace("claude-", "").replace(/-\d{8}$/, "");
720
- }
721
- async function parseFile(path) {
722
- const entries = [];
723
- const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
724
- for await (const line of rl) {
725
- if (!line.includes('"usage"')) continue;
726
- try {
727
- const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
728
- if (obj.type !== "assistant" || !obj.message?.usage) continue;
729
- const ts = new Date(obj.timestamp ?? 0).getTime();
730
- if (!Number.isFinite(ts)) continue;
731
- const u = obj.message.usage;
732
- const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
733
- const input = safeNum(u.input_tokens);
734
- const output = safeNum(u.output_tokens);
735
- const cacheCreate = safeNum(u.cache_creation_input_tokens);
736
- const cacheRead = safeNum(u.cache_read_input_tokens);
737
- if (input + output + cacheCreate + cacheRead === 0) continue;
738
- const p = priceFor(model);
739
- const msgId = obj.message?.id;
740
- entries.push({
741
- // Claude logs a message's usage repeatedly and copies it into resumed
742
- // sessions — dedup by message id (+ requestId when present).
743
- id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
744
- ts,
745
- model: shortModel(model),
746
- cost: costOf(model, u),
747
- input,
748
- output,
749
- cacheCreate,
750
- cacheRead,
751
- cacheSavings: cacheRead * (p.i - p.cr)
752
- });
753
- } catch {
754
- }
755
- }
756
- return entries;
757
- }
758
- async function loadEntries(since, homeDir) {
759
- const files = [];
760
- const seen = /* @__PURE__ */ new Set();
761
- const seenIno = /* @__PURE__ */ new Set();
762
- for (const dir of getClaudeDirs(homeDir)) {
763
- let listing;
764
- try {
765
- listing = await readdir(dir, { recursive: true });
766
- } catch {
767
- continue;
768
- }
769
- for (const f of listing) {
770
- if (!f.endsWith(".jsonl")) continue;
771
- const path = join3(dir, f);
772
- if (seen.has(path)) continue;
773
- seen.add(path);
774
- try {
775
- const s = await fsStat(path);
776
- if (s.mtimeMs < since) continue;
777
- if (s.ino && process.platform !== "win32") {
778
- const idn = `${s.dev}:${s.ino}`;
779
- if (seenIno.has(idn)) continue;
780
- seenIno.add(idn);
781
- }
782
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
783
- } catch {
784
- }
785
- }
786
- }
787
- return loadCachedEntries(files, parseFile, since);
788
- }
789
- async function claudeDashboard(tz, homeDir) {
790
- const now = Date.now();
791
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
792
- const entries = await loadEntries(since, homeDir);
793
- return summarize(entries, tz);
794
- }
795
- async function claudeTable(tz, homeDir) {
796
- const entries = await loadEntries(monthsAgoStart(Date.now(), 6, tz), homeDir);
797
- return tabulate(entries, tz);
798
- }
799
-
800
- // src/providers/claude/billing.ts
801
- import { execFile as execFileCb } from "child_process";
802
- import { readFile as readFile3 } from "fs/promises";
803
- import { join as join4 } from "path";
804
- import { homedir as homedir3 } from "os";
805
- import { promisify } from "util";
806
-
807
- // src/format.ts
808
- function currency(value) {
809
- if (!Number.isFinite(value) || value <= 0) return "$0.00";
810
- if (value >= 1e4) {
811
- return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
812
- }
813
- return `$${value.toFixed(2)}`;
814
- }
815
- function tokens(value) {
816
- const v = Number.isFinite(value) && value > 0 ? value : 0;
817
- if (v >= 1e9) return `${(v / 1e9).toFixed(1)}B`;
818
- if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
819
- if (v >= 1e3) return `${(v / 1e3).toFixed(1)}K`;
820
- return String(Math.floor(v));
821
- }
822
- function time(date, tz) {
823
- return date.toLocaleTimeString(void 0, {
824
- hour: "2-digit",
825
- minute: "2-digit",
826
- second: "2-digit",
827
- timeZone: tz
828
- });
829
- }
830
- var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
831
- function shortDate(iso) {
832
- const [, m, d] = iso.split("-");
833
- return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
834
- }
835
- function col(s, w, align = "right") {
836
- if (s.length > w) return s.slice(0, w - 1) + "~";
837
- const spaces = " ".repeat(w - s.length);
838
- return align === "right" ? spaces + s : s + spaces;
839
- }
840
- function resetIn(iso) {
841
- const diff = new Date(iso).getTime() - Date.now();
842
- if (!Number.isFinite(diff) || diff <= 0) return "now";
843
- const mins = Math.round(diff / 6e4);
844
- if (mins < 60) return `${mins}m`;
845
- const hrs = Math.floor(mins / 60);
846
- const m = mins % 60;
847
- if (hrs < 24) return `${hrs}h ${m}m`;
848
- const days = Math.floor(hrs / 24);
849
- const h = hrs % 24;
850
- return `${days}d ${h}h`;
851
- }
852
-
853
- // src/providers/claude/billing.ts
854
- var execFile = promisify(execFileCb);
855
- function parseAuth(raw) {
856
- try {
857
- const creds = JSON.parse(raw);
858
- const o = creds?.claudeAiOauth ?? creds;
859
- const token = o?.accessToken;
860
- if (typeof token !== "string" || !token) return null;
861
- return {
862
- token,
863
- subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
864
- rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0
865
- };
866
- } catch {
867
- return null;
868
- }
869
- }
870
- async function readCredentialsFile(homeDir) {
871
- for (const dir of claudeConfigDirs(homeDir)) {
872
- try {
873
- const auth = parseAuth(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
874
- if (auth) return auth;
875
- } catch {
876
- }
877
- }
878
- return null;
879
- }
880
- async function readMacKeychain() {
881
- try {
882
- const { stdout } = await execFile("security", [
883
- "find-generic-password",
884
- "-s",
885
- "Claude Code-credentials",
886
- "-w"
887
- ], { timeout: 5e3 });
888
- return parseAuth(stdout.trim());
889
- } catch {
890
- return null;
891
- }
892
- }
893
- async function getAuth(homeDir) {
894
- const isDefault = !homeDir || homeDir === homedir3();
895
- if (isDefault && process.platform === "darwin") {
896
- const auth = await readMacKeychain();
897
- if (auth) return auth;
898
- }
899
- return readCredentialsFile(homeDir);
900
- }
901
- function planLabel(auth) {
902
- const sub = auth.subscriptionType;
903
- if (!sub) return null;
904
- const base = sub.charAt(0).toUpperCase() + sub.slice(1);
905
- const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
906
- return tier ? `${base} ${tier[1]}x` : base;
907
- }
908
- var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
909
- async function claudeBilling(account) {
910
- const auth = await getAuth(account.homeDir);
911
- if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
912
- const plan = planLabel(auth);
913
- try {
914
- const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
915
- headers: {
916
- "Authorization": `Bearer ${auth.token}`,
917
- "anthropic-beta": "oauth-2025-04-20",
918
- "User-Agent": "tokmon"
919
- },
920
- signal: AbortSignal.timeout(1e4)
921
- });
922
- if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
923
- if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
924
- if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
925
- const data = await readJson(res);
926
- if (!data) return { plan, metrics: [], error: "Unexpected API response" };
927
- const metrics = [];
928
- if (data.five_hour) {
929
- metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
930
- }
931
- if (data.seven_day) {
932
- metrics.push({ ...pct(data.seven_day.utilization, resetIn(data.seven_day.resets_at)), label: "Week" });
933
- }
934
- if (data.seven_day_sonnet) {
935
- metrics.push({ ...pct(data.seven_day_sonnet.utilization), label: "Sonnet" });
936
- }
937
- if (data.extra_usage?.is_enabled) {
938
- metrics.push({
939
- label: "Extra",
940
- used: (data.extra_usage.used_credits ?? 0) / 100,
941
- limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
942
- format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
943
- });
944
- }
945
- return { plan, metrics, error: null };
946
- } catch {
947
- return { plan, metrics: [], error: "Network error" };
948
- }
949
- }
950
-
951
- // src/providers/claude/index.ts
952
- var claudeProvider = {
953
- id: "claude",
954
- name: "Claude",
955
- color: "green",
956
- hasUsage: true,
957
- hasBilling: true,
958
- detect: (homeDir) => detectClaude(homeDir),
959
- fetchSummary: (account, tz) => claudeDashboard(tz, account.homeDir),
960
- fetchTable: (account, tz) => claudeTable(tz, account.homeDir),
961
- fetchBilling: (account) => claudeBilling(account)
962
- };
963
-
964
- // src/providers/codex/usage.ts
965
- import { readdir as readdir2, stat as fsStat2, access as access2 } from "fs/promises";
966
- import { createReadStream as createReadStream2 } from "fs";
967
- import { createInterface as createInterface2 } from "readline";
968
- import { join as join5 } from "path";
969
- import { homedir as homedir4 } from "os";
970
- var PRICING2 = {
971
- "gpt-5-codex": { in: 125e-8, cr: 125e-9, out: 1e-5 },
972
- "gpt-5-mini": { in: 25e-8, cr: 25e-9, out: 2e-6 },
973
- "gpt-5-nano": { in: 5e-8, cr: 5e-9, out: 4e-7 },
974
- "gpt-5": { in: 125e-8, cr: 125e-9, out: 1e-5 },
975
- "o4-mini": { in: 11e-7, cr: 275e-9, out: 44e-7 }
976
- };
977
- var FALLBACK2 = PRICING2["gpt-5-codex"];
978
- var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
979
- function codexHomes(homeDir) {
980
- if (homeDir) return [join5(homeDir, ".codex")];
981
- const homes = [];
982
- const codexHome = envDir("CODEX_HOME");
983
- if (codexHome) homes.push(codexHome);
984
- homes.push(join5(homedir4(), ".codex"));
985
- homes.push(join5(homedir4(), ".config", "codex"));
986
- return [...new Set(homes)];
987
- }
988
- async function detectCodex(homeDir) {
989
- for (const home of codexHomes(homeDir)) {
990
- try {
991
- await access2(join5(home, "sessions"));
992
- return true;
993
- } catch {
994
- }
995
- }
996
- return false;
997
- }
998
- function priceFor2(model) {
999
- const m = model.toLowerCase();
1000
- for (const key of PRICE_KEYS2) {
1001
- if (m.startsWith(key) || m.includes(key)) return PRICING2[key];
1002
- }
1003
- return FALLBACK2;
1004
- }
1005
- function extractModel(obj) {
1006
- const p = obj?.payload ?? obj;
1007
- return p?.model || p?.collaboration_mode?.settings?.model || p?.model_slug || p?.config?.model || p?.info?.model || null;
1008
- }
1009
- function subtractClamped(cur, prev) {
1010
- const sub = (a, b) => Math.max(0, (a ?? 0) - (b ?? 0));
1011
- return {
1012
- input_tokens: sub(cur.input_tokens, prev?.input_tokens),
1013
- cached_input_tokens: sub(cur.cached_input_tokens, prev?.cached_input_tokens),
1014
- output_tokens: sub(cur.output_tokens, prev?.output_tokens),
1015
- reasoning_output_tokens: sub(cur.reasoning_output_tokens, prev?.reasoning_output_tokens)
1016
- };
1017
- }
1018
- function eventSig(last, total) {
1019
- const f = (x) => x ? `${x.input_tokens ?? 0},${x.cached_input_tokens ?? 0},${x.output_tokens ?? 0},${x.reasoning_output_tokens ?? 0}` : "-";
1020
- return `${f(last)}|${f(total)}`;
1021
- }
1022
- async function parseFile2(path) {
1023
- const entries = [];
1024
- let model = "gpt-5-codex";
1025
- let prevTotal = null;
1026
- let prevSig = null;
1027
- const rl = createInterface2({ input: createReadStream2(path), crlfDelay: Infinity });
1028
- for await (const rawLine of rl) {
1029
- if (!rawLine.includes("token_count") && !rawLine.includes("turn_context")) continue;
1030
- try {
1031
- const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1032
- const obj = JSON.parse(line);
1033
- const payloadType = obj?.payload?.type ?? obj?.type;
1034
- if (payloadType === "turn_context") {
1035
- const m = extractModel(obj);
1036
- if (typeof m === "string" && m.trim()) model = m;
1037
- continue;
1038
- }
1039
- if (payloadType !== "token_count") continue;
1040
- const info = obj?.payload?.info;
1041
- const total = info?.total_token_usage;
1042
- const last = info?.last_token_usage;
1043
- const sig = eventSig(last, total);
1044
- if (sig === prevSig) continue;
1045
- prevSig = sig;
1046
- let d = last;
1047
- if (!d && total) {
1048
- const reset = !!prevTotal && (total.input_tokens ?? 0) < (prevTotal.input_tokens ?? 0);
1049
- d = reset ? total : subtractClamped(total, prevTotal);
1050
- }
1051
- if (total) prevTotal = total;
1052
- if (!d) continue;
1053
- const ts = new Date(obj.timestamp ?? obj?.payload?.timestamp ?? 0).getTime();
1054
- if (!Number.isFinite(ts)) continue;
1055
- const inputTotal = safeNum(d.input_tokens);
1056
- const cached = Math.min(safeNum(d.cached_input_tokens), inputTotal);
1057
- const input = inputTotal - cached;
1058
- const output = safeNum(d.output_tokens);
1059
- if (input + output + cached === 0) continue;
1060
- const p = priceFor2(model);
1061
- entries.push({
1062
- ts,
1063
- model,
1064
- cost: input * p.in + cached * p.cr + output * p.out,
1065
- input,
1066
- output,
1067
- cacheCreate: 0,
1068
- cacheRead: cached,
1069
- cacheSavings: cached * (p.in - p.cr)
1070
- });
1071
- } catch {
1072
- }
1073
- }
1074
- return entries;
1075
- }
1076
- async function loadEntries2(since, homeDir) {
1077
- const files = [];
1078
- const seen = /* @__PURE__ */ new Set();
1079
- const seenIno = /* @__PURE__ */ new Set();
1080
- for (const home of codexHomes(homeDir)) {
1081
- const dir = join5(home, "sessions");
1082
- let listing;
1083
- try {
1084
- listing = await readdir2(dir, { recursive: true });
1085
- } catch {
1086
- continue;
1087
- }
1088
- for (const f of listing) {
1089
- if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1090
- const path = join5(dir, f);
1091
- if (seen.has(path)) continue;
1092
- seen.add(path);
1093
- try {
1094
- const s = await fsStat2(path);
1095
- if (s.mtimeMs < since) continue;
1096
- if (s.ino && process.platform !== "win32") {
1097
- const idn = `${s.dev}:${s.ino}`;
1098
- if (seenIno.has(idn)) continue;
1099
- seenIno.add(idn);
1100
- }
1101
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1102
- } catch {
1103
- }
1104
- }
1105
- }
1106
- return loadCachedEntries(files, parseFile2, since);
1107
- }
1108
- async function codexDashboard(tz, homeDir) {
1109
- const now = Date.now();
1110
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1111
- const entries = await loadEntries2(since, homeDir);
1112
- return summarize(entries, tz);
1113
- }
1114
- async function codexTable(tz, homeDir) {
1115
- const entries = await loadEntries2(monthsAgoStart(Date.now(), 6, tz), homeDir);
1116
- return tabulate(entries, tz);
1117
- }
1118
-
1119
- // src/providers/codex/billing.ts
1120
- import { execFile as execFileCb2 } from "child_process";
1121
- import { readFile as readFile4, readdir as readdir3, stat as fsStat3 } from "fs/promises";
1122
- import { createReadStream as createReadStream3 } from "fs";
1123
- import { createInterface as createInterface3 } from "readline";
1124
- import { join as join6 } from "path";
1125
- import { promisify as promisify2 } from "util";
1126
- var execFile2 = promisify2(execFileCb2);
1127
- var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1128
- var CREDIT_USD_RATE = 0.04;
1129
- async function readAuthFile(home) {
1130
- try {
1131
- const raw = await readFile4(join6(home, "auth.json"), "utf-8");
1132
- const auth = JSON.parse(raw);
1133
- const accessToken = auth?.tokens?.access_token;
1134
- if (!accessToken) return null;
1135
- return { accessToken, accountId: auth?.tokens?.account_id };
1136
- } catch {
1137
- return null;
1138
- }
1139
- }
1140
- async function readKeychainAuth() {
1141
- try {
1142
- const { stdout } = await execFile2("security", [
1143
- "find-generic-password",
1144
- "-s",
1145
- "Codex Auth",
1146
- "-w"
1147
- ], { timeout: 5e3 });
1148
- const auth = JSON.parse(stdout.trim());
1149
- const accessToken = auth?.tokens?.access_token;
1150
- if (!accessToken) return null;
1151
- return { accessToken, accountId: auth?.tokens?.account_id };
1152
- } catch {
1153
- return null;
1154
- }
1155
- }
1156
- async function getAuth2(homeDir) {
1157
- for (const home of codexHomes(homeDir)) {
1158
- const auth = await readAuthFile(home);
1159
- if (auth) return auth;
1160
- }
1161
- if (process.platform === "darwin") return readKeychainAuth();
1162
- return null;
1163
- }
1164
- function planLabel2(planType) {
1165
- if (typeof planType !== "string" || !planType.trim()) return null;
1166
- const p = planType.trim().toLowerCase();
1167
- if (p === "prolite") return "Pro 5x";
1168
- if (p === "pro") return "Pro 20x";
1169
- return planType.charAt(0).toUpperCase() + planType.slice(1);
1170
- }
1171
- function isoOrNull(ms) {
1172
- return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
1173
- }
1174
- function resetFrom(window) {
1175
- if (!window) return null;
1176
- let iso = null;
1177
- if (typeof window.reset_at === "number") iso = isoOrNull(window.reset_at * 1e3);
1178
- else if (typeof window.resets_at === "number") iso = isoOrNull(window.resets_at * 1e3);
1179
- else if (typeof window.reset_after_seconds === "number") iso = isoOrNull(Date.now() + window.reset_after_seconds * 1e3);
1180
- return iso ? resetIn(iso) : null;
1181
- }
1182
- function percentMetric(label, used, resets, primary) {
1183
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: resets, primary };
1184
- }
1185
- async function liveBilling(auth) {
1186
- try {
1187
- const headers = {
1188
- "Authorization": `Bearer ${auth.accessToken}`,
1189
- "Accept": "application/json",
1190
- "User-Agent": "tokmon"
1191
- };
1192
- if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
1193
- const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(1e4) });
1194
- if (!res.ok) return null;
1195
- const data = await readJson(res);
1196
- if (!data) return null;
1197
- const metrics = [];
1198
- const rl = data.rate_limit ?? null;
1199
- const primary = rl?.primary_window ?? null;
1200
- const secondary = rl?.secondary_window ?? null;
1201
- const headerPct = (name) => {
1202
- const h = res.headers.get(name);
1203
- if (h === null || h.trim() === "") return void 0;
1204
- const n = Number(h);
1205
- return Number.isFinite(n) ? n : void 0;
1206
- };
1207
- const primaryPct = headerPct("x-codex-primary-used-percent") ?? primary?.used_percent;
1208
- const secondaryPct = headerPct("x-codex-secondary-used-percent") ?? secondary?.used_percent;
1209
- if (typeof primaryPct === "number") metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1210
- if (typeof secondaryPct === "number") metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1211
- const balance = data?.credits?.balance;
1212
- if (typeof balance === "number" && balance >= 0) {
1213
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1214
- }
1215
- if (metrics.length === 0) return null;
1216
- return { plan: planLabel2(data.plan_type), metrics, error: null };
1217
- } catch {
1218
- return null;
1219
- }
1220
- }
1221
- async function newestRolloutFile(homeDir) {
1222
- let best = null;
1223
- for (const home of codexHomes(homeDir)) {
1224
- const dir = join6(home, "sessions");
1225
- let listing;
1226
- try {
1227
- listing = await readdir3(dir, { recursive: true });
1228
- } catch {
1229
- continue;
1230
- }
1231
- for (const f of listing) {
1232
- if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1233
- const path = join6(dir, f);
1234
- try {
1235
- const s = await fsStat3(path);
1236
- if (!best || s.mtimeMs > best.mtime) best = { path, mtime: s.mtimeMs };
1237
- } catch {
1238
- }
1239
- }
1240
- }
1241
- return best?.path ?? null;
1242
- }
1243
- async function snapshotBilling(homeDir) {
1244
- const path = await newestRolloutFile(homeDir);
1245
- if (!path) return null;
1246
- let last = null;
1247
- try {
1248
- const rl = createInterface3({ input: createReadStream3(path), crlfDelay: Infinity });
1249
- for await (const line of rl) {
1250
- if (!line.includes("rate_limits")) continue;
1251
- try {
1252
- const obj = JSON.parse(line);
1253
- if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
1254
- } catch {
1255
- }
1256
- }
1257
- } catch {
1258
- return null;
1259
- }
1260
- if (!last) return null;
1261
- const metrics = [];
1262
- if (typeof last.primary?.used_percent === "number") {
1263
- metrics.push(percentMetric("5h", last.primary.used_percent, resetFrom(last.primary), true));
1264
- }
1265
- if (typeof last.secondary?.used_percent === "number") {
1266
- metrics.push(percentMetric("Week", last.secondary.used_percent, resetFrom(last.secondary)));
1267
- }
1268
- const balance = last?.credits?.balance;
1269
- if (typeof balance === "number" && balance >= 0) {
1270
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1271
- }
1272
- if (metrics.length === 0) return null;
1273
- return { plan: planLabel2(last.plan_type), metrics, error: null };
1274
- }
1275
- async function codexBilling(account) {
1276
- const auth = await getAuth2(account.homeDir);
1277
- if (auth) {
1278
- const live = await liveBilling(auth);
1279
- if (live) return live;
1280
- }
1281
- const snap = await snapshotBilling(account.homeDir);
1282
- if (snap) return snap;
1283
- return {
1284
- plan: null,
1285
- metrics: [],
1286
- error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex"
1287
- };
1288
- }
1289
-
1290
- // src/providers/codex/index.ts
1291
- var codexProvider = {
1292
- id: "codex",
1293
- name: "Codex",
1294
- color: "cyan",
1295
- hasUsage: true,
1296
- hasBilling: true,
1297
- detect: (homeDir) => detectCodex(homeDir),
1298
- fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
1299
- fetchTable: (account, tz) => codexTable(tz, account.homeDir),
1300
- fetchBilling: (account) => codexBilling(account)
1301
- };
1302
-
1303
- // src/providers/cursor/billing.ts
1304
- import { access as access3 } from "fs/promises";
1305
- import { join as join8 } from "path";
1306
- import { homedir as homedir6 } from "os";
1307
-
1308
- // src/providers/cursor/activity.ts
1309
- import { join as join7 } from "path";
1310
- import { homedir as homedir5 } from "os";
1311
-
1312
- // src/providers/cursor/sqlite.ts
1313
- import { execFile as execFileCb3 } from "child_process";
1314
- import { promisify as promisify3 } from "util";
1315
- var execFile3 = promisify3(execFileCb3);
1316
- var nativeDb;
1317
- async function getNativeDb() {
1318
- if (nativeDb !== void 0) return nativeDb;
1319
- try {
1320
- nativeDb = (await import("sqlite")).DatabaseSync;
1321
- } catch {
1322
- nativeDb = null;
1323
- }
1324
- return nativeDb;
1325
- }
1326
- function classify(msg) {
1327
- if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
1328
- if (/database is (locked|busy)|readonly/i.test(msg)) return "locked";
1329
- if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
1330
- return "error";
1331
- }
1332
- async function runSqlite(db, sql, params = []) {
1333
- const DB = await getNativeDb();
1334
- if (DB) {
1335
- let handle;
1336
- try {
1337
- handle = new DB(db, { readOnly: true, timeout: 1500 });
1338
- const rows = handle.prepare(sql).all(...params);
1339
- return { status: "ok", rows };
1340
- } catch {
1341
- } finally {
1342
- try {
1343
- handle?.close();
1344
- } catch {
1345
- }
1346
- }
1347
- }
1348
- return runSqliteCli(db, sql, params);
1349
- }
1350
- function inlineParams(sql, params) {
1351
- let i = 0;
1352
- return sql.replace(/\?/g, () => {
1353
- const p = params[i++];
1354
- return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
1355
- });
1356
- }
1357
- async function runSqliteCli(db, sql, params) {
1358
- try {
1359
- const { stdout } = await execFile3(
1360
- "sqlite3",
1361
- // -json yields row objects; `.timeout` sets the busy handler silently
1362
- // (a `-cmd 'PRAGMA busy_timeout=…'` would print its result row first).
1363
- ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
1364
- { timeout: 1e4, maxBuffer: 8 << 20 }
1365
- );
1366
- const text = stdout.trim();
1367
- if (!text) return { status: "ok", rows: [] };
1368
- try {
1369
- return { status: "ok", rows: JSON.parse(text) };
1370
- } catch {
1371
- return { status: "error", rows: [] };
1372
- }
1373
- } catch (e) {
1374
- const err = e;
1375
- if (err?.code === "ENOENT") return { status: "missing", rows: [] };
1376
- return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
1377
- }
1378
- }
1379
- function sqliteStatusMessage(status) {
1380
- switch (status) {
1381
- case "missing":
1382
- return "Cursor data not found \u2014 open Cursor";
1383
- case "old":
1384
- return "Cursor DB unreadable";
1385
- case "locked":
1386
- return "Cursor DB busy \u2014 retrying next poll";
1387
- default:
1388
- return "Cursor data unavailable";
1389
- }
1390
- }
1391
-
1392
- // src/providers/cursor/activity.ts
1393
- var DAY_MS2 = 864e5;
1394
- function trackingDb(homeDir) {
1395
- return join7(homeDir ?? homedir5(), ".cursor", "ai-tracking", "ai-code-tracking.db");
1396
- }
1397
- function localDayKey(ms) {
1398
- const d = new Date(ms);
1399
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1400
- }
1401
- async function cursorActivity(homeDir) {
1402
- const db = trackingDb(homeDir);
1403
- try {
1404
- const now = Date.now();
1405
- const res = await runSqlite(
1406
- db,
1407
- `SELECT date(createdAt/1000,'unixepoch','localtime') AS d, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
1408
- );
1409
- if (res.status !== "ok") return null;
1410
- const byDay = /* @__PURE__ */ new Map();
1411
- let month = 0;
1412
- for (const row of res.rows) {
1413
- const n = Number(row.c) || 0;
1414
- byDay.set(String(row.d), n);
1415
- month += n;
1416
- }
1417
- const series = [];
1418
- for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(localDayKey(now - i * DAY_MS2)) ?? 0);
1419
- if (month === 0 && series.every((v) => v === 0)) return null;
1420
- return { series, summary: `${tokens(month)} lines` };
1421
- } catch {
1422
- return null;
1423
- }
1424
- }
1425
-
1426
- // src/providers/cursor/composer.ts
1427
- async function cursorModelSpend(homeDir) {
1428
- const db = cursorStateDb(homeDir);
1429
- const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt 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 cents DESC;";
1430
- const res = await runSqlite(db, sql);
1431
- if (res.status !== "ok") return null;
1432
- const models = [];
1433
- let total = 0;
1434
- for (const row of res.rows) {
1435
- const usd = (Number(row.cents) || 0) / 100;
1436
- if (usd <= 0) continue;
1437
- models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
1438
- total += usd;
1439
- }
1440
- if (total <= 0) return null;
1441
- return { total, models };
1442
- }
1443
-
1444
- // src/providers/cursor/billing.ts
1445
- var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
1446
- var USAGE_URL2 = `${BASE}/GetCurrentPeriodUsage`;
1447
- var PLAN_URL = `${BASE}/GetPlanInfo`;
1448
- function cursorStateDb(homeDir) {
1449
- const base = homeDir ?? homedir6();
1450
- const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
1451
- if (process.platform === "darwin") {
1452
- return join8(base, "Library", "Application Support", ...tail);
1453
- }
1454
- if (process.platform === "win32") {
1455
- const roaming = homeDir ? join8(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join8(base, "AppData", "Roaming");
1456
- return join8(roaming, ...tail);
1457
- }
1458
- const cfg = homeDir ? join8(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join8(base, ".config");
1459
- return join8(cfg, ...tail);
1460
- }
1461
- async function detectCursor(homeDir) {
1462
- try {
1463
- await access3(cursorStateDb(homeDir));
1464
- return true;
1465
- } catch {
1466
- return false;
1467
- }
1468
- }
1469
- async function readState(db, key) {
1470
- const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
1471
- const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
1472
- return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
1473
- }
1474
- async function connectPost(url, token) {
1475
- try {
1476
- const res = await fetch(url, {
1477
- method: "POST",
1478
- headers: {
1479
- "Authorization": `Bearer ${token}`,
1480
- "Content-Type": "application/json",
1481
- "Connect-Protocol-Version": "1",
1482
- "User-Agent": "tokmon"
1483
- },
1484
- body: "{}",
1485
- signal: AbortSignal.timeout(1e4)
1486
- });
1487
- if (!res.ok) return { __status: res.status };
1488
- return await readJson(res);
1489
- } catch {
1490
- return null;
1491
- }
1492
- }
1493
- var dollars = (cents) => cents / 100;
1494
- async function cursorBilling(account) {
1495
- const [core, activity, spend] = await Promise.all([
1496
- cursorBillingCore(account),
1497
- cursorActivity(account.homeDir),
1498
- cursorModelSpend(account.homeDir)
1499
- ]);
1500
- let merged = activity;
1501
- if (spend) {
1502
- const lines = activity?.summary ?? "";
1503
- const spendLabel = `$${Math.round(spend.total)} all-time`;
1504
- merged = {
1505
- series: activity?.series ?? [],
1506
- summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
1507
- };
1508
- }
1509
- return { ...core, activity: merged };
1510
- }
1511
- async function cursorBillingCore(account) {
1512
- const db = cursorStateDb(account.homeDir);
1513
- const [tokenRes, membershipRes] = await Promise.all([
1514
- readState(db, "cursorAuth/accessToken"),
1515
- readState(db, "cursorAuth/stripeMembershipType")
1516
- ]);
1517
- const token = tokenRes.value;
1518
- const membership = membershipRes.value;
1519
- const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
1520
- if (!token) {
1521
- const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
1522
- return { plan: planFallback, metrics: [], error };
1523
- }
1524
- const [usage, planInfo] = await Promise.all([
1525
- connectPost(USAGE_URL2, token),
1526
- connectPost(PLAN_URL, token)
1527
- ]);
1528
- if (!usage || usage.__status) {
1529
- const expired = usage?.__status === 401 || usage?.__status === 403;
1530
- return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error" };
1531
- }
1532
- const planName = planInfo?.planInfo?.planName ?? planFallback;
1533
- const price = planInfo?.planInfo?.price;
1534
- const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
1535
- const pu = usage.planUsage ?? {};
1536
- const metrics = [];
1537
- const rawEnd = usage.billingCycleEnd;
1538
- const endMs = typeof rawEnd === "string" && rawEnd.trim() ? Number(rawEnd) : NaN;
1539
- const resets = Number.isFinite(endMs) && endMs > 0 && endMs <= 864e13 ? resetIn(new Date(endMs).toISOString()) : null;
1540
- if (typeof pu.totalPercentUsed === "number" && typeof pu.limit === "number") {
1541
- metrics.push({
1542
- label: "Usage",
1543
- used: pu.totalPercentUsed,
1544
- limit: 100,
1545
- format: { kind: "percent" },
1546
- resetsAt: resets,
1547
- primary: true
1548
- });
1549
- const spentCents = typeof pu.totalSpend === "number" ? pu.totalSpend : pu.limit - (pu.remaining ?? 0);
1550
- metrics.push({
1551
- label: "Spend",
1552
- used: dollars(spentCents),
1553
- limit: dollars(pu.limit),
1554
- format: { kind: "dollars" }
1555
- });
1556
- }
1557
- if (pu.autoPercentUsed) {
1558
- metrics.push({ label: "Auto", used: pu.autoPercentUsed, limit: 100, format: { kind: "percent" } });
1559
- }
1560
- if (pu.apiPercentUsed) {
1561
- metrics.push({ label: "API", used: pu.apiPercentUsed, limit: 100, format: { kind: "percent" } });
1562
- }
1563
- const su = usage.spendLimitUsage;
1564
- if (su) {
1565
- const limitCents = su.individualLimit ?? su.pooledLimit ?? 0;
1566
- const remainingCents = su.individualRemaining ?? su.pooledRemaining ?? 0;
1567
- if (limitCents > 0) {
1568
- metrics.push({
1569
- label: "On-demand",
1570
- used: dollars(limitCents - remainingCents),
1571
- limit: dollars(limitCents),
1572
- format: { kind: "dollars" }
1573
- });
1574
- }
1575
- }
1576
- if (metrics.length === 0) {
1577
- return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data" };
1578
- }
1579
- return { plan, metrics, error: null };
1580
- }
1581
-
1582
- // src/providers/cursor/index.ts
1583
- var cursorProvider = {
1584
- id: "cursor",
1585
- name: "Cursor",
1586
- color: "magenta",
1587
- hasUsage: false,
1588
- // Cursor exposes spend/limits, not a token history
1589
- hasBilling: true,
1590
- detect: (homeDir) => detectCursor(homeDir),
1591
- fetchBilling: (account) => cursorBilling(account)
1592
- };
1593
-
1594
- // src/providers/pi/usage.ts
1595
- import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
1596
- import { createReadStream as createReadStream4 } from "fs";
1597
- import { createInterface as createInterface4 } from "readline";
1598
- import { join as join9 } from "path";
1599
- import { homedir as homedir7 } from "os";
1600
- function piSessionsDir(homeDir) {
1601
- return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
1602
- }
1603
- async function detectPi(homeDir) {
1604
- try {
1605
- await access4(piSessionsDir(homeDir));
1606
- return true;
1607
- } catch {
1608
- return false;
1609
- }
1610
- }
1611
- function pos(v) {
1612
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1613
- }
1614
- async function parseFile3(path) {
1615
- const entries = [];
1616
- const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
1617
- for await (const rawLine of rl) {
1618
- if (!rawLine.includes('"usage"')) continue;
1619
- try {
1620
- const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1621
- const obj = JSON.parse(line);
1622
- if (obj?.type !== "message") continue;
1623
- const msg = obj.message;
1624
- if (msg?.role !== "assistant" || !msg?.usage) continue;
1625
- const u = msg.usage;
1626
- const ts = new Date(obj.timestamp ?? msg.timestamp ?? 0).getTime();
1627
- if (!Number.isFinite(ts)) continue;
1628
- const input = safeNum(u.input);
1629
- const output = safeNum(u.output);
1630
- const cacheRead = safeNum(u.cacheRead);
1631
- const cacheCreate = safeNum(u.cacheWrite);
1632
- if (input + output + cacheRead + cacheCreate === 0) continue;
1633
- const c = u.cost ?? {};
1634
- const costInput = pos(c.input);
1635
- const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
1636
- const model = typeof msg.responseModel === "string" && msg.responseModel || typeof msg.model === "string" && msg.model || "unknown";
1637
- entries.push({
1638
- ts,
1639
- model,
1640
- cost: pos(c.total),
1641
- input,
1642
- output,
1643
- cacheCreate,
1644
- cacheRead,
1645
- cacheSavings
1646
- });
1647
- } catch {
1648
- }
1649
- }
1650
- return entries;
1651
- }
1652
- async function loadEntries3(since, homeDir) {
1653
- const dir = piSessionsDir(homeDir);
1654
- const files = [];
1655
- const seenIno = /* @__PURE__ */ new Set();
1656
- let listing;
1657
- try {
1658
- listing = await readdir4(dir, { recursive: true });
1659
- } catch {
1660
- return [];
1661
- }
1662
- for (const f of listing) {
1663
- if (!f.endsWith(".jsonl")) continue;
1664
- const path = join9(dir, f);
1665
- try {
1666
- const s = await fsStat4(path);
1667
- if (s.mtimeMs < since) continue;
1668
- if (s.ino && process.platform !== "win32") {
1669
- const idn = `${s.dev}:${s.ino}`;
1670
- if (seenIno.has(idn)) continue;
1671
- seenIno.add(idn);
1672
- }
1673
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1674
- } catch {
1675
- }
1676
- }
1677
- return loadCachedEntries(files, parseFile3, since);
1678
- }
1679
- async function piDashboard(tz, homeDir) {
1680
- const now = Date.now();
1681
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1682
- return summarize(await loadEntries3(since, homeDir), tz);
1683
- }
1684
- async function piTable(tz, homeDir) {
1685
- return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1686
- }
1687
-
1688
- // src/providers/pi/index.ts
1689
- var piProvider = {
1690
- id: "pi",
1691
- name: "Pi",
1692
- color: "blue",
1693
- hasUsage: true,
1694
- hasBilling: false,
1695
- detect: (homeDir) => detectPi(homeDir),
1696
- fetchSummary: (account, tz) => piDashboard(tz, account.homeDir),
1697
- fetchTable: (account, tz) => piTable(tz, account.homeDir)
1698
- };
1699
-
1700
- // src/providers/opencode/usage.ts
1701
- import { access as access5 } from "fs/promises";
1702
- import { join as join10 } from "path";
1703
- import { homedir as homedir8 } from "os";
1704
- function opencodeDbPaths(homeDir) {
1705
- const base = homeDir ?? homedir8();
1706
- const paths = [];
1707
- if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1708
- paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
1709
- if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
1710
- if (process.platform === "win32") {
1711
- const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1712
- if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
1713
- }
1714
- return [...new Set(paths)];
1715
- }
1716
- async function findDb(homeDir) {
1717
- for (const p of opencodeDbPaths(homeDir)) {
1718
- try {
1719
- await access5(p);
1720
- return p;
1721
- } catch {
1722
- }
1723
- }
1724
- return null;
1725
- }
1726
- async function detectOpencode(homeDir) {
1727
- return await findDb(homeDir) !== null;
1728
- }
1729
- var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1730
- async function loadEntries4(since, homeDir) {
1731
- const db = await findDb(homeDir);
1732
- if (!db) return [];
1733
- const sql = "SELECT time_created AS ts, json_extract(data,'$.modelID') AS model, json_extract(data,'$.cost') AS cost, json_extract(data,'$.tokens.input') AS input, json_extract(data,'$.tokens.output') AS output, json_extract(data,'$.tokens.reasoning') AS reasoning, json_extract(data,'$.tokens.cache.read') AS cacheRead, json_extract(data,'$.tokens.cache.write') AS cacheWrite FROM message WHERE json_valid(data) AND json_extract(data,'$.role')='assistant' AND json_type(data,'$.tokens')='object' AND time_created >= ?;";
1734
- const res = await runSqlite(db, sql, [Math.floor(since)]);
1735
- if (res.status !== "ok") return [];
1736
- const entries = [];
1737
- for (const row of res.rows) {
1738
- const ts = pos2(row.ts);
1739
- if (!ts) continue;
1740
- const input = pos2(row.input);
1741
- const output = pos2(row.output);
1742
- const cacheRead = pos2(row.cacheRead);
1743
- const cacheCreate = pos2(row.cacheWrite);
1744
- if (input + output + cacheRead + cacheCreate === 0) continue;
1745
- entries.push({
1746
- ts,
1747
- model: typeof row.model === "string" && row.model ? row.model : "unknown",
1748
- cost: pos2(row.cost),
1749
- input,
1750
- output,
1751
- cacheCreate,
1752
- cacheRead,
1753
- cacheSavings: 0
1754
- // opencode stores only a total cost, no per-component split to derive savings
1755
- });
1756
- }
1757
- return entries;
1758
- }
1759
- async function opencodeDashboard(tz, homeDir) {
1760
- const now = Date.now();
1761
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1762
- return summarize(await loadEntries4(since, homeDir), tz);
1763
- }
1764
- async function opencodeTable(tz, homeDir) {
1765
- return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1766
- }
1767
-
1768
- // src/providers/opencode/index.ts
1769
- var opencodeProvider = {
1770
- id: "opencode",
1771
- name: "opencode",
1772
- color: "yellow",
1773
- hasUsage: true,
1774
- hasBilling: false,
1775
- detect: (homeDir) => detectOpencode(homeDir),
1776
- fetchSummary: (account, tz) => opencodeDashboard(tz, account.homeDir),
1777
- fetchTable: (account, tz) => opencodeTable(tz, account.homeDir)
1778
- };
1779
-
1780
- // src/providers/copilot/billing.ts
1781
- import { execFile as execFileCb4 } from "child_process";
1782
- import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
1783
- import { join as join11 } from "path";
1784
- import { homedir as homedir9 } from "os";
1785
- import { promisify as promisify4 } from "util";
1786
- var execFile4 = promisify4(execFileCb4);
1787
- var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
1788
- var GH_KEYCHAIN_SERVICE = "gh:github.com";
1789
- var GO_KEYRING_PREFIX = "go-keyring-base64:";
1790
- function ghConfigDir(homeDir) {
1791
- if (!homeDir) {
1792
- const explicit = process.env.GH_CONFIG_DIR;
1793
- if (explicit && explicit.trim()) return explicit.trim();
1794
- if (process.platform === "win32") {
1795
- return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
1796
- }
1797
- const xdg = envDir("XDG_CONFIG_HOME");
1798
- return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
1799
- }
1800
- return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
1801
- }
1802
- function ghHostsPath(homeDir) {
1803
- return join11(ghConfigDir(homeDir), "hosts.yml");
1804
- }
1805
- async function detectCopilot(homeDir) {
1806
- try {
1807
- await access6(ghHostsPath(homeDir));
1808
- return true;
1809
- } catch {
1810
- }
1811
- try {
1812
- await execFile4("gh", ["--version"], { timeout: 3e3 });
1813
- return true;
1814
- } catch {
1815
- return false;
1816
- }
1817
- }
1818
- function unquoteYamlValue(value) {
1819
- const trimmed = value.trim();
1820
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1821
- return trimmed.slice(1, -1);
1822
- }
1823
- return trimmed;
1824
- }
1825
- function tokenFromHostsYaml(raw) {
1826
- const lines = raw.split(/\r?\n/);
1827
- let inGithub = false;
1828
- let githubIndent = -1;
1829
- for (const line of lines) {
1830
- const match = line.match(/^(\s*)([^:#][^:]*):\s*(.*)$/);
1831
- if (!match) continue;
1832
- const indent = match[1].length;
1833
- const key = match[2].trim();
1834
- const value = match[3].trim();
1835
- if (indent === 0) {
1836
- inGithub = key === "github.com";
1837
- githubIndent = inGithub ? indent : -1;
1838
- continue;
1839
- }
1840
- if (inGithub && indent > githubIndent && key === "oauth_token" && value) {
1841
- return unquoteYamlValue(value);
1842
- }
1843
- }
1844
- return null;
1845
- }
1846
- async function loadTokenFromHosts(homeDir) {
1847
- try {
1848
- const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
1849
- return token ? { token, source: "gh-hosts" } : null;
1850
- } catch {
1851
- return null;
1852
- }
1853
- }
1854
- async function readMacKeychainService(service) {
1855
- if (process.platform !== "darwin") return null;
1856
- try {
1857
- const { stdout } = await execFile4("security", [
1858
- "find-generic-password",
1859
- "-s",
1860
- service,
1861
- "-w"
1862
- ], { timeout: 5e3 });
1863
- const raw = stdout.trim();
1864
- if (!raw) return null;
1865
- if (raw.startsWith(GO_KEYRING_PREFIX)) {
1866
- return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1867
- }
1868
- return raw;
1869
- } catch {
1870
- return null;
1871
- }
1872
- }
1873
- async function loadTokenFromGhKeychain() {
1874
- const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
1875
- return token ? { token, source: "gh-keychain" } : null;
1876
- }
1877
- function vscodeUserDir(homeDir) {
1878
- const home = homeDir ?? homedir9();
1879
- if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
1880
- if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
1881
- return join11(home, ".config", "Code", "User");
1882
- }
1883
- function tokenFromText(raw) {
1884
- const patterns = [
1885
- /github\.com[^A-Za-z0-9_]+oauth_token[^A-Za-z0-9_]+([A-Za-z0-9_]{20,})/i,
1886
- /github\.com[^A-Za-z0-9_]+(gh[opusr]_[A-Za-z0-9_]{20,})/i,
1887
- /\b(gh[opusr]_[A-Za-z0-9_]{20,})\b/
1888
- ];
1889
- for (const pattern of patterns) {
1890
- const token = raw.match(pattern)?.[1];
1891
- if (token) return token;
1892
- }
1893
- return null;
1894
- }
1895
- async function loadTokenFromVsCode(homeDir) {
1896
- const userDir = vscodeUserDir(homeDir);
1897
- const candidates = [
1898
- join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1899
- join11(userDir, "globalStorage", "github.copilot", "auth.json"),
1900
- join11(userDir, "globalStorage", "state.vscdb")
1901
- ];
1902
- try {
1903
- for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
1904
- if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
1905
- candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
1906
- }
1907
- }
1908
- } catch {
1909
- }
1910
- for (const path of candidates) {
1911
- try {
1912
- const token = tokenFromText(await readFile5(path, "utf-8"));
1913
- if (token) return { token, source: "vscode" };
1914
- } catch {
1915
- }
1916
- }
1917
- return null;
1918
- }
1919
- async function loadToken(homeDir) {
1920
- return await loadTokenFromHosts(homeDir) || await loadTokenFromGhKeychain() || await loadTokenFromVsCode(homeDir);
1921
- }
1922
- function redactToken(token) {
1923
- return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
1924
- }
1925
- function resetDate(value) {
1926
- return typeof value === "string" && value.trim() ? resetIn(value) : null;
1927
- }
1928
- function percentMetric2(label, snapshot, reset, primary) {
1929
- if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
1930
- if (snapshot.unlimited === true || snapshot.entitlement === 0) return null;
1931
- const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
1932
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
1933
- }
1934
- function countMetric(label, remaining, total, reset) {
1935
- if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
1936
- return {
1937
- label,
1938
- used: Math.max(0, total - remaining),
1939
- limit: total,
1940
- format: { kind: "count" },
1941
- resetsAt: reset
1942
- };
1943
- }
1944
- async function fetchUsage(token) {
1945
- try {
1946
- const res = await fetch(USAGE_URL3, {
1947
- headers: {
1948
- "Authorization": `token ${token}`,
1949
- "Accept": "application/json",
1950
- "Editor-Version": "vscode/1.96.2",
1951
- "Editor-Plugin-Version": "copilot-chat/0.26.7",
1952
- "User-Agent": "GitHubCopilotChat/0.26.7",
1953
- "X-Github-Api-Version": "2025-04-01"
1954
- },
1955
- signal: AbortSignal.timeout(1e4)
1956
- });
1957
- if (!res.ok) return { data: null, status: res.status };
1958
- return { data: await readJson(res), status: res.status };
1959
- } catch {
1960
- return { data: null, status: null };
1961
- }
1962
- }
1963
- async function copilotBilling(account) {
1964
- const cred = await loadToken(account.homeDir);
1965
- if (!cred) return { plan: null, metrics: [], error: "Not logged in \u2014 run gh auth login" };
1966
- const { data, status } = await fetchUsage(cred.token);
1967
- if (!data) {
1968
- if (status === 401 || status === 403) {
1969
- return { plan: null, metrics: [], error: `Token invalid (${redactToken(cred.token)}) \u2014 run gh auth login` };
1970
- }
1971
- if (status) return { plan: null, metrics: [], error: `Copilot API ${status}` };
1972
- return { plan: null, metrics: [], error: "Network error" };
1973
- }
1974
- const plan = typeof data.copilot_plan === "string" && data.copilot_plan.trim() ? data.copilot_plan : null;
1975
- const metrics = [];
1976
- const quotaReset = resetDate(data.quota_reset_date);
1977
- const snapshots = data.quota_snapshots;
1978
- const premium = percentMetric2("Premium", snapshots?.premium_interactions, quotaReset, true);
1979
- if (premium) metrics.push(premium);
1980
- const chat = percentMetric2("Chat", snapshots?.chat, quotaReset);
1981
- if (chat) metrics.push(chat);
1982
- if (data.limited_user_quotas && data.monthly_quotas) {
1983
- const reset = resetDate(data.limited_user_reset_date);
1984
- const limitedChat = countMetric("Chat", data.limited_user_quotas.chat, data.monthly_quotas.chat, reset);
1985
- if (limitedChat) metrics.push(limitedChat);
1986
- const completions = countMetric("Completions", data.limited_user_quotas.completions, data.monthly_quotas.completions, reset);
1987
- if (completions) metrics.push(completions);
1988
- }
1989
- if (metrics.length === 0) return { plan, metrics: [], error: "No usage data" };
1990
- return { plan, metrics, error: null };
1991
- }
1992
-
1993
- // src/providers/copilot/index.ts
1994
- var copilotProvider = {
1995
- id: "copilot",
1996
- name: "Copilot",
1997
- color: "white",
1998
- hasUsage: false,
1999
- hasBilling: true,
2000
- detect: (homeDir) => detectCopilot(homeDir),
2001
- fetchBilling: (account) => copilotBilling(account)
63
+ border: "round"
2002
64
  };
2003
-
2004
- // src/providers/antigravity/billing.ts
2005
- import { access as access7, readdir as readdir7 } from "fs/promises";
2006
- import { join as join13 } from "path";
2007
- import { homedir as homedir11 } from "os";
2008
-
2009
- // src/providers/cloud-code.ts
2010
- import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
2011
- import { spawnSync } from "child_process";
2012
- import { homedir as homedir10 } from "os";
2013
- import { dirname, join as join12 } from "path";
2014
- var CLOUD_CODE_URLS = [
2015
- "https://daily-cloudcode-pa.googleapis.com",
2016
- "https://cloudcode-pa.googleapis.com"
2017
- ];
2018
- var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
2019
- var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
2020
- var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
2021
- var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
2022
- var GOOGLE_OAUTH_CLIENT_REGEX = /OAUTH_CLIENT_ID\s*=\s*["']([0-9]{6,}-[a-z0-9]+\.apps\.googleusercontent\.com)["']\s*;?\s*(?:var|const|let)?\s*OAUTH_CLIENT_SECRET\s*=\s*["'](GOCSPX-[A-Za-z0-9_-]+)["']/s;
2023
- var MAX_BUNDLE_READ = 32 * 1024 * 1024;
2024
- var cachedClient;
2025
- var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
2026
- var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
2027
- var CC_MODEL_BLACKLIST = {
2028
- MODEL_CHAT_20706: true,
2029
- MODEL_CHAT_23310: true,
2030
- MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
2031
- MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
2032
- MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
2033
- MODEL_GOOGLE_GEMINI_2_5_PRO: true,
2034
- MODEL_PLACEHOLDER_M19: true,
2035
- MODEL_PLACEHOLDER_M9: true,
2036
- MODEL_PLACEHOLDER_M12: true
65
+ var GLYPHS_ASCII = {
66
+ spark: [".", ":", "-", "=", "+", "*", "#", "@"],
67
+ barFull: "#",
68
+ barEmpty: "-",
69
+ rule: "-",
70
+ spinner: ["|", "/", "-", "\\"],
71
+ dot: "*",
72
+ dotSel: "*",
73
+ radioOff: "o",
74
+ dotAll: "+",
75
+ caretR: ">",
76
+ caretL: "<",
77
+ play: ">",
78
+ arrowU: "^",
79
+ arrowD: "v",
80
+ arrowL: "<",
81
+ arrowR: ">",
82
+ shift: "^",
83
+ vbar: "|",
84
+ treeMid: "+-",
85
+ treeEnd: "`-",
86
+ boxMark: "|",
87
+ check: "x",
88
+ warn: "!",
89
+ ellipsis: "...",
90
+ middot: "-",
91
+ emDash: "-",
92
+ eur: "EUR",
93
+ gbp: "GBP",
94
+ border: "classic"
2037
95
  };
2038
- function readVarint(bytes, start) {
2039
- let value = 0;
2040
- let shift = 0;
2041
- let pos3 = start;
2042
- while (pos3 < bytes.length) {
2043
- const b = bytes[pos3++];
2044
- value += (b & 127) * Math.pow(2, shift);
2045
- if ((b & 128) === 0) return { value, pos: pos3 };
2046
- shift += 7;
2047
- }
2048
- return null;
2049
- }
2050
- function readFields(bytes) {
2051
- const fields = {};
2052
- let pos3 = 0;
2053
- while (pos3 < bytes.length) {
2054
- const tag = readVarint(bytes, pos3);
2055
- if (!tag) break;
2056
- pos3 = tag.pos;
2057
- const fieldNum = Math.floor(tag.value / 8);
2058
- const wireType = tag.value % 8;
2059
- if (wireType === 0) {
2060
- const val = readVarint(bytes, pos3);
2061
- if (!val) break;
2062
- fields[fieldNum] = { type: 0, value: val.value };
2063
- pos3 = val.pos;
2064
- } else if (wireType === 1) {
2065
- if (pos3 + 8 > bytes.length) break;
2066
- pos3 += 8;
2067
- } else if (wireType === 2) {
2068
- const len = readVarint(bytes, pos3);
2069
- if (!len) break;
2070
- pos3 = len.pos;
2071
- if (pos3 + len.value > bytes.length) break;
2072
- fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
2073
- pos3 += len.value;
2074
- } else if (wireType === 5) {
2075
- if (pos3 + 4 > bytes.length) break;
2076
- pos3 += 4;
2077
- } else {
2078
- break;
2079
- }
2080
- }
2081
- return fields;
2082
- }
2083
- function utf8(data) {
2084
- return Buffer.from(data).toString("utf8");
2085
- }
2086
- function decodeBase64(text) {
2087
- try {
2088
- return Buffer.from(text, "base64");
2089
- } catch {
2090
- return null;
2091
- }
2092
- }
2093
- function unwrapKeyringBase64(raw) {
2094
- const text = raw.trim();
2095
- if (!text.startsWith("go-keyring-base64:")) return text;
2096
- const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
2097
- return decoded ? utf8(decoded).trim() : text;
2098
- }
2099
- function unwrapOAuthSentinel(base64Text) {
2100
- const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
2101
- if (!outerBytes) return null;
2102
- const outer = readFields(outerBytes);
2103
- if (outer[1]?.type !== 2 || !outer[1].data) return null;
2104
- const wrapper = readFields(outer[1].data);
2105
- const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
2106
- const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
2107
- if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
2108
- const payloadFields = readFields(payload);
2109
- if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
2110
- const innerText = utf8(payloadFields[1].data).trim();
2111
- return innerText ? decodeBase64(innerText) : null;
2112
- }
2113
- async function readAntigravityOAuthToken(db) {
2114
- const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
2115
- if (r.status !== "ok") return { token: null, status: r.status };
2116
- const raw = r.rows[0]?.value;
2117
- if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
2118
- const inner = unwrapOAuthSentinel(raw);
2119
- if (!inner) return { token: null, status: "ok" };
2120
- const fields = readFields(inner);
2121
- const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
2122
- const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
2123
- let expirySeconds = null;
2124
- if (fields[4]?.type === 2 && fields[4].data) {
2125
- const ts = readFields(fields[4].data);
2126
- expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
2127
- }
2128
- if (!accessToken && !refreshToken) return { token: null, status: "ok" };
2129
- return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
2130
- }
2131
- function redact(token) {
2132
- if (!token) return "none";
2133
- return `...${token.slice(-4)}`;
2134
- }
2135
- function geminiBundleCandidates() {
2136
- const candidates = [];
2137
- const addBundle = (nodeModulesRoot) => {
2138
- if (!nodeModulesRoot) return;
2139
- candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2140
- };
2141
- try {
2142
- const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
2143
- const resolved = typeof which.stdout === "string" ? which.stdout.trim().split("\n")[0]?.trim() : "";
2144
- if (resolved) candidates.push(resolved);
2145
- } catch {
2146
- }
2147
- const home = homedir10();
2148
- addBundle("/opt/homebrew/lib/node_modules");
2149
- addBundle("/usr/local/lib/node_modules");
2150
- addBundle(join12(home, ".local", "share", "node_modules"));
2151
- addBundle(join12(home, ".bun", "install", "global", "node_modules"));
2152
- try {
2153
- const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
2154
- const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
2155
- if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
2156
- } catch {
2157
- }
2158
- return [...new Set(candidates.filter(Boolean))];
2159
- }
2160
- async function resolveBundleDir(candidate) {
2161
- try {
2162
- if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
2163
- return candidate;
2164
- }
2165
- const real = await realpath(candidate);
2166
- return dirname(real);
2167
- } catch {
2168
- return null;
96
+ function detectUnicode(env, isTTY, platform) {
97
+ if (!isTTY) return false;
98
+ if (env.TERM === "dumb") return false;
99
+ if (platform === "win32") {
100
+ return Boolean(env.WT_SESSION || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || /xterm/i.test(env.TERM ?? ""));
2169
101
  }
102
+ const loc = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
103
+ if (loc && /\.(iso|latin|ascii|cp\d|koi|gbk|big5)/i.test(loc)) return false;
104
+ if (/^(C|POSIX)$/i.test(loc)) return false;
105
+ return true;
2170
106
  }
2171
- async function scanBundleDir(dir) {
2172
- let entries;
2173
- try {
2174
- entries = await readdir6(dir);
2175
- } catch {
2176
- return null;
2177
- }
2178
- const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
2179
- for (const name of targets) {
2180
- const filePath = join12(dir, name);
2181
- try {
2182
- const info = await stat(filePath);
2183
- if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
2184
- const contents = await readFile6(filePath, "utf8");
2185
- if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
2186
- const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
2187
- if (match) return { clientId: match[1], clientSecret: match[2] };
2188
- } catch {
2189
- }
107
+ function resolveGlyphs(opts) {
108
+ let ascii;
109
+ if (opts.flag === "on") ascii = true;
110
+ else if (opts.flag === "off") ascii = false;
111
+ else {
112
+ const e = (opts.env.TOKMON_ASCII ?? "").toLowerCase();
113
+ if (/^(1|true|on|yes)$/.test(e)) ascii = true;
114
+ else if (/^(0|false|off|no)$/.test(e)) ascii = false;
115
+ else if (opts.config === "on") ascii = true;
116
+ else if (opts.config === "off") ascii = false;
117
+ else ascii = !detectUnicode(opts.env, opts.isTTY, opts.platform);
2190
118
  }
2191
- return null;
119
+ return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
2192
120
  }
2193
- async function discoverGoogleOAuthClient() {
2194
- try {
2195
- for (const candidate of geminiBundleCandidates()) {
2196
- const dir = await resolveBundleDir(candidate);
2197
- if (!dir) continue;
2198
- const found = await scanBundleDir(dir);
2199
- if (found) return found;
2200
- }
2201
- } catch {
2202
- }
2203
- return null;
121
+ var active = GLYPHS_UNICODE;
122
+ function setGlyphs(set) {
123
+ active = set;
2204
124
  }
2205
- async function resolveGoogleClient() {
2206
- const envId = process.env.TOKMON_GOOGLE_CLIENT_ID?.trim();
2207
- const envSecret = process.env.TOKMON_GOOGLE_CLIENT_SECRET?.trim();
2208
- if (envId && envSecret) return { clientId: envId, clientSecret: envSecret };
2209
- if (cachedClient === void 0) cachedClient = await discoverGoogleOAuthClient();
2210
- return cachedClient;
125
+ function glyphs() {
126
+ return active;
2211
127
  }
2212
- async function refreshAccessToken(refreshToken) {
2213
- if (!refreshToken) return null;
2214
- const client = await resolveGoogleClient();
2215
- if (!client) return null;
2216
- const body = new URLSearchParams({
2217
- client_id: client.clientId,
2218
- client_secret: client.clientSecret,
2219
- refresh_token: refreshToken,
2220
- grant_type: "refresh_token"
2221
- });
128
+
129
+ // src/app.tsx
130
+ import { spawn } from "child_process";
131
+ import { appendFileSync as appendFileSync2 } from "fs";
132
+ import { useState as useState3, useEffect as useEffect3, useCallback, useRef as useRef2, useMemo } from "react";
133
+ import { Box as Box7, Text as Text7, Transform, useInput, useStdout, useApp } from "ink";
134
+ import { useMouse } from "@zenobius/ink-mouse";
135
+
136
+ // src/peak.ts
137
+ async function fetchPeak() {
2222
138
  try {
2223
- const res = await fetch(GOOGLE_OAUTH_URL, {
2224
- method: "POST",
2225
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2226
- body,
2227
- signal: AbortSignal.timeout(15e3)
139
+ const res = await fetch("https://promoclock.co/api/status", {
140
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
141
+ signal: AbortSignal.timeout(3e3)
2228
142
  });
2229
143
  if (!res.ok) return null;
2230
- const json = await readJson(res);
2231
- return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
2232
- } catch {
2233
- return null;
2234
- }
2235
- }
2236
- async function requestCloudCodeJson(path, token, body) {
2237
- for (const base of CLOUD_CODE_URLS) {
2238
- try {
2239
- const res = await fetch(`${base}${path}`, {
2240
- method: "POST",
2241
- headers: {
2242
- Accept: "application/json",
2243
- "Content-Type": "application/json",
2244
- Authorization: `Bearer ${token}`,
2245
- "User-Agent": "agy"
2246
- },
2247
- body: JSON.stringify(body ?? {}),
2248
- signal: AbortSignal.timeout(15e3)
2249
- });
2250
- if (res.status === 401 || res.status === 403) return { _authFailed: true };
2251
- if (!res.ok) continue;
2252
- const json = await readJson(res);
2253
- if (json && typeof json === "object") return json;
2254
- } catch {
2255
- }
2256
- }
2257
- return null;
2258
- }
2259
- function readPlan(loadData) {
2260
- const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
2261
- const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
2262
- const raw = paid || current;
2263
- if (!raw) return null;
2264
- return raw.replace(/^Gemini Code Assist (?:in|for)\s+/i, "").replace(/^Gemini Code Assist$/i, "Code Assist");
2265
- }
2266
- function parseBuckets(data) {
2267
- if (!Array.isArray(data?.buckets)) return [];
2268
- return data.buckets.flatMap((bucket) => {
2269
- const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2270
- if (!modelId) return [];
2271
- return [{
2272
- modelId,
2273
- remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2274
- resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2275
- }];
2276
- });
2277
- }
2278
- function parseModelBuckets(data) {
2279
- const models = data?.models;
2280
- if (!models || typeof models !== "object") return [];
2281
- return Object.keys(models).flatMap((key) => {
2282
- const model = models[key];
2283
- if (!model || typeof model !== "object" || model.isInternal) return [];
2284
- const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
2285
- if (CC_MODEL_BLACKLIST[modelId]) return [];
2286
- const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2287
- if (!displayName) return [];
2288
- const quotaInfo = model.quotaInfo;
2289
- return [{
2290
- modelId: displayName,
2291
- remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2292
- resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2293
- }];
2294
- });
2295
- }
2296
- async function fetchWithAccessToken(accessToken) {
2297
- const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
2298
- if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
2299
- if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
2300
- const plan = readPlan(loadData);
2301
- const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
2302
- let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
2303
- if (!quotaData || "_authFailed" in quotaData) {
2304
- quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
2305
- }
2306
- if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
2307
- if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
2308
- let buckets = parseBuckets(quotaData);
2309
- if (buckets.length === 0) {
2310
- const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
2311
- if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
2312
- if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
2313
- }
2314
- if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
2315
- return { ok: true, plan, buckets };
2316
- }
2317
- async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
2318
- const nowSec = Math.floor(Date.now() / 1e3);
2319
- let accessToken = token.accessToken?.trim() || null;
2320
- if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
2321
- accessToken = await refreshAccessToken(token.refreshToken);
2322
- if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
2323
- }
2324
- if (!accessToken) {
2325
- accessToken = await refreshAccessToken(token.refreshToken);
2326
- if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
2327
- }
2328
- const result = await fetchWithAccessToken(accessToken);
2329
- if (result.ok || result.error !== "Token expired") return result;
2330
- const refreshed = await refreshAccessToken(token.refreshToken);
2331
- if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
2332
- return fetchWithAccessToken(refreshed);
2333
- }
2334
- function normalizeLabel(label) {
2335
- return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
2336
- }
2337
- function poolLabel(label) {
2338
- const lower = normalizeLabel(label).toLowerCase();
2339
- if (lower.includes("gemini") && lower.includes("pro")) return "Pro";
2340
- if (lower.includes("gemini") && lower.includes("flash")) return "Flash";
2341
- return "Claude";
2342
- }
2343
- function sortKey(label) {
2344
- const lower = label.toLowerCase();
2345
- if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2346
- if (lower.includes("gemini")) return `0b_${label}`;
2347
- if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
2348
- if (lower.includes("claude")) return `1b_${label}`;
2349
- return `2_${label}`;
2350
- }
2351
- function cloudCodeBucketsToMetrics(buckets) {
2352
- const pooled = /* @__PURE__ */ new Map();
2353
- for (const bucket of buckets) {
2354
- const label = poolLabel(bucket.modelId);
2355
- const existing = pooled.get(label);
2356
- if (!existing || bucket.remainingFraction < existing.remainingFraction) {
2357
- pooled.set(label, { ...bucket, modelId: label });
2358
- }
2359
- }
2360
- return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
2361
- const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
144
+ const data = await readJson(res);
145
+ if (!data) return null;
146
+ let state;
147
+ if (data.isPeak === true || data.status === "peak") state = "peak";
148
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
149
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
150
+ else return null;
2362
151
  return {
2363
- label: bucket.modelId,
2364
- used: Math.round((1 - clamped) * 100),
2365
- limit: 100,
2366
- format: { kind: "percent" },
2367
- resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
2368
- primary: i === 0
152
+ state,
153
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
154
+ minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
2369
155
  };
2370
- });
2371
- }
2372
- function cloudCodeSqliteError(status) {
2373
- return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
2374
- }
2375
-
2376
- // src/providers/antigravity/billing.ts
2377
- async function exists(path) {
2378
- try {
2379
- await access7(path);
2380
- return true;
2381
- } catch {
2382
- return false;
2383
- }
2384
- }
2385
- async function firstExisting(paths) {
2386
- for (const path of paths) {
2387
- if (await exists(path)) return path;
2388
- }
2389
- return paths[0];
2390
- }
2391
- async function antigravityStateDb(homeDir) {
2392
- const base = homeDir ?? homedir11();
2393
- const tail = ["User", "globalStorage", "state.vscdb"];
2394
- if (process.platform === "darwin") {
2395
- const support = join13(base, "Library", "Application Support");
2396
- const exact = [
2397
- join13(support, "Antigravity IDE", ...tail),
2398
- join13(support, "Antigravity", ...tail)
2399
- ];
2400
- try {
2401
- const entries = await readdir7(support, { withFileTypes: true });
2402
- const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
2403
- return firstExisting([...exact, ...matches]);
2404
- } catch {
2405
- return firstExisting(exact);
2406
- }
2407
- }
2408
- if (process.platform === "win32") {
2409
- const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
2410
- return firstExisting([
2411
- join13(roaming, "Antigravity IDE", ...tail),
2412
- join13(roaming, "Antigravity", ...tail)
2413
- ]);
2414
- }
2415
- const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
2416
- return firstExisting([
2417
- join13(cfg, "Antigravity IDE", ...tail),
2418
- join13(cfg, "Antigravity", ...tail)
2419
- ]);
2420
- }
2421
- async function detectAntigravity(homeDir) {
2422
- return exists(await antigravityStateDb(homeDir));
2423
- }
2424
- async function antigravityBilling(account) {
2425
- try {
2426
- const db = await antigravityStateDb(account.homeDir);
2427
- const { token, status } = await readAntigravityOAuthToken(db);
2428
- if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
2429
- const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
2430
- if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2431
- return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2432
- } catch {
2433
- return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
2434
- }
2435
- }
2436
-
2437
- // src/providers/antigravity/index.ts
2438
- var antigravityProvider = {
2439
- id: "antigravity",
2440
- name: "Antigravity",
2441
- color: "red",
2442
- hasUsage: false,
2443
- hasBilling: true,
2444
- detect: (homeDir) => detectAntigravity(homeDir),
2445
- fetchBilling: (account) => antigravityBilling(account)
2446
- };
2447
-
2448
- // src/providers/gemini/billing.ts
2449
- import { access as access8, readFile as readFile7 } from "fs/promises";
2450
- import { join as join14 } from "path";
2451
- import { homedir as homedir12 } from "os";
2452
- function geminiCredsPath(homeDir) {
2453
- return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
2454
- }
2455
- async function detectGemini(homeDir) {
2456
- try {
2457
- await access8(geminiCredsPath(homeDir));
2458
- return true;
2459
- } catch {
2460
- return false;
2461
- }
2462
- }
2463
- async function readGeminiCreds(path) {
2464
- try {
2465
- return JSON.parse(await readFile7(path, "utf8"));
2466
156
  } catch {
2467
157
  return null;
2468
158
  }
2469
159
  }
2470
- async function geminiBilling(account) {
2471
- try {
2472
- const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2473
- const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2474
- const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2475
- if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2476
- const quota = await fetchCloudCodeQuota({
2477
- accessToken,
2478
- refreshToken,
2479
- expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2480
- }, "Token expired \u2014 run gemini");
2481
- if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2482
- return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2483
- } catch {
2484
- return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2485
- }
2486
- }
2487
-
2488
- // src/providers/gemini/index.ts
2489
- var geminiProvider = {
2490
- id: "gemini",
2491
- name: "Gemini",
2492
- color: "greenBright",
2493
- hasUsage: false,
2494
- hasBilling: true,
2495
- detect: (homeDir) => detectGemini(homeDir),
2496
- fetchBilling: (account) => geminiBilling(account)
2497
- };
2498
-
2499
- // src/providers/detect.ts
2500
- import { accessSync, constants } from "fs";
2501
- import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
2502
- import { homedir as homedir13 } from "os";
2503
- function searchDirs() {
2504
- const home = homedir13();
2505
- const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
2506
- const extra = process.platform === "win32" ? [
2507
- process.env.APPDATA && join15(process.env.APPDATA, "npm"),
2508
- process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
2509
- join15(home, "scoop", "shims")
2510
- ] : [
2511
- "/opt/homebrew/bin",
2512
- "/usr/local/bin",
2513
- "/usr/bin",
2514
- "/bin",
2515
- "/opt/local/bin",
2516
- join15(home, ".local", "bin"),
2517
- join15(home, "bin"),
2518
- join15(home, ".npm-global", "bin"),
2519
- join15(home, ".bun", "bin"),
2520
- join15(home, ".local", "share", "pnpm")
2521
- ];
2522
- return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
2523
- }
2524
- function isExec(p) {
2525
- try {
2526
- accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
2527
- return true;
2528
- } catch {
2529
- return false;
2530
- }
2531
- }
2532
- function onPath(names) {
2533
- const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()).concat("") : [""];
2534
- for (const dir of searchDirs()) {
2535
- for (const n of names) {
2536
- for (const e of exts) {
2537
- if (isExec(join15(dir, n + e))) return true;
2538
- }
2539
- }
2540
- }
2541
- return false;
2542
- }
2543
- function anyExists(paths) {
2544
- return paths.some((p) => !!p && isExec(p));
2545
- }
2546
- function installSignals(id) {
2547
- const home = homedir13();
2548
- const pf = process.env.ProgramFiles;
2549
- const pf86 = process.env["ProgramFiles(x86)"];
2550
- const lad = process.env.LOCALAPPDATA;
2551
- switch (id) {
2552
- case "claude":
2553
- return onPath(["claude"]) || anyExists([
2554
- "/Applications/Claude.app",
2555
- join15(home, "Applications", "Claude.app"),
2556
- lad && join15(lad, "Programs", "claude", "Claude.exe")
2557
- ]);
2558
- case "codex": {
2559
- const bin = process.env.CODEX_BIN;
2560
- if (bin && isAbsolute3(bin) && isExec(bin)) return true;
2561
- return onPath(["codex"]);
2562
- }
2563
- case "cursor":
2564
- return onPath(["cursor", "cursor-agent"]) || anyExists([
2565
- "/Applications/Cursor.app",
2566
- join15(home, "Applications", "Cursor.app"),
2567
- lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
2568
- pf && join15(pf, "Cursor", "Cursor.exe"),
2569
- pf86 && join15(pf86, "Cursor", "Cursor.exe"),
2570
- "/opt/Cursor/cursor",
2571
- "/usr/share/cursor/cursor",
2572
- "/usr/bin/cursor"
2573
- ]);
2574
- case "pi":
2575
- return onPath(["pi"]);
2576
- case "opencode":
2577
- return onPath(["opencode"]);
2578
- case "copilot":
2579
- return onPath(["gh"]);
2580
- case "antigravity":
2581
- return onPath(["antigravity"]) || anyExists([
2582
- "/Applications/Antigravity.app",
2583
- join15(home, "Applications", "Antigravity.app"),
2584
- lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
2585
- ]);
2586
- case "gemini":
2587
- return onPath(["gemini"]);
2588
- default:
2589
- return false;
2590
- }
2591
- }
2592
-
2593
- // src/providers/index.ts
2594
- var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
2595
- var PROVIDERS = {
2596
- claude: claudeProvider,
2597
- codex: codexProvider,
2598
- cursor: cursorProvider,
2599
- pi: piProvider,
2600
- opencode: opencodeProvider,
2601
- copilot: copilotProvider,
2602
- antigravity: antigravityProvider,
2603
- gemini: geminiProvider
2604
- };
2605
- var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
2606
- async function detectProviders() {
2607
- const found = await Promise.all(
2608
- PROVIDER_ORDER.map(async (id) => {
2609
- try {
2610
- if (installSignals(id)) return id;
2611
- return await PROVIDERS[id].detect() ? id : null;
2612
- } catch {
2613
- return null;
2614
- }
2615
- })
2616
- );
2617
- return found.filter((id) => id !== null);
2618
- }
2619
-
2620
- // src/accounts.ts
2621
- function buildAccounts(config2, detected) {
2622
- const out = [];
2623
- for (const pid of PROVIDER_ORDER) {
2624
- if (config2.disabledProviders.includes(pid)) continue;
2625
- const provider = PROVIDERS[pid];
2626
- const configured = config2.accounts.filter((a) => a.providerId === pid);
2627
- if (configured.length > 0) {
2628
- for (const a of configured) {
2629
- out.push({
2630
- id: a.id,
2631
- providerId: pid,
2632
- name: a.name,
2633
- color: a.color || provider.color,
2634
- homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
2635
- });
2636
- }
2637
- } else if (detected.includes(pid)) {
2638
- out.push({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
2639
- }
2640
- }
2641
- return out;
2642
- }
2643
- function accountsByProvider(accounts) {
2644
- const groups = [];
2645
- for (const pid of PROVIDER_ORDER) {
2646
- const list = accounts.filter((a) => a.providerId === pid);
2647
- if (list.length > 0) groups.push({ provider: pid, accounts: list });
2648
- }
2649
- return groups;
2650
- }
2651
160
 
2652
161
  // src/snapshot.ts
2653
- import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
2654
- import { join as join16 } from "path";
162
+ import { readFile, writeFile, mkdir, rename } from "fs/promises";
163
+ import { join } from "path";
2655
164
  function snapshotFile() {
2656
- return join16(cacheDir(), "dashboard-snapshot.json");
165
+ return join(cacheDir(), "dashboard-snapshot.json");
2657
166
  }
2658
167
  async function loadSnapshot() {
2659
168
  try {
2660
- const obj = JSON.parse(await readFile8(snapshotFile(), "utf-8"));
169
+ const obj = JSON.parse(await readFile(snapshotFile(), "utf-8"));
2661
170
  return obj && typeof obj === "object" ? obj : {};
2662
171
  } catch {
2663
172
  return {};
2664
173
  }
2665
174
  }
2666
- var saveQueue2 = Promise.resolve();
175
+ var saveQueue = Promise.resolve();
2667
176
  function saveSnapshot(stats) {
2668
177
  const obj = {};
2669
178
  for (const [id, s] of stats) {
2670
179
  if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
2671
180
  }
2672
- saveQueue2 = saveQueue2.then(async () => {
181
+ saveQueue = saveQueue.then(async () => {
2673
182
  try {
2674
183
  const dir = cacheDir();
2675
- await mkdir3(dir, { recursive: true });
2676
- const tmp = join16(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
2677
- await writeFile3(tmp, JSON.stringify(obj));
2678
- await rename3(tmp, snapshotFile());
184
+ await mkdir(dir, { recursive: true });
185
+ const tmp = join(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
186
+ await writeFile(tmp, JSON.stringify(obj));
187
+ await rename(tmp, snapshotFile());
2679
188
  } catch {
2680
189
  }
2681
190
  });
@@ -2827,8 +336,8 @@ function sparkline(values) {
2827
336
  return spark[idx];
2828
337
  }).join("");
2829
338
  }
2830
- function Bar({ pct: pct2, color, width = 24 }) {
2831
- const filled = Math.max(0, Math.min(width, Math.round(pct2 / 100 * width)));
339
+ function Bar({ pct, color, width = 24 }) {
340
+ const filled = Math.max(0, Math.min(width, Math.round(pct / 100 * width)));
2832
341
  return /* @__PURE__ */ jsxs(Text, { children: [
2833
342
  /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
2834
343
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
@@ -3505,12 +1014,19 @@ function LoadingView({ groups, stats, cols, rows }) {
3505
1014
  " more"
3506
1015
  ] })
3507
1016
  ] }),
3508
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3509
- "loading ",
3510
- readyCount,
3511
- " / ",
3512
- groups.length
3513
- ] }) })
1017
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1018
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1019
+ "loading ",
1020
+ readyCount,
1021
+ " / ",
1022
+ groups.length
1023
+ ] }),
1024
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1025
+ " ",
1026
+ glyphs().middot,
1027
+ " W opens the web dashboard once loaded"
1028
+ ] })
1029
+ ] })
3514
1030
  ] });
3515
1031
  }
3516
1032
 
@@ -4056,6 +1572,13 @@ function App({ interval: cliInterval, initialConfig }) {
4056
1572
  const { exit } = useApp();
4057
1573
  const rows = stdout?.rows ?? 24;
4058
1574
  const cols = stdout?.columns ?? 80;
1575
+ const webRef = useRef2(null);
1576
+ const webBusyRef = useRef2(false);
1577
+ const [webUrl, setWebUrl] = useState3(null);
1578
+ const [webStatus, setWebStatus] = useState3("off");
1579
+ useEffect3(() => () => {
1580
+ void webRef.current?.stop();
1581
+ }, []);
4059
1582
  const cfg = config2 ?? DEFAULT_CONFIG;
4060
1583
  const interval2 = cliInterval ?? cfg.interval * 1e3;
4061
1584
  const billingMs = cfg.billingInterval * 6e4;
@@ -4460,6 +1983,32 @@ function App({ interval: cliInterval, initialConfig }) {
4460
1983
  setSettingsCursor((c) => Math.max(ACCOUNT_ROWS_START, Math.min(ACCOUNT_ROWS_START + cfg.accounts.length - 1, c + dir)));
4461
1984
  }
4462
1985
  const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
1986
+ async function toggleWeb() {
1987
+ if (webBusyRef.current) return;
1988
+ webBusyRef.current = true;
1989
+ try {
1990
+ if (webRef.current) {
1991
+ setWebStatus("stopping");
1992
+ const ctrl = webRef.current;
1993
+ webRef.current = null;
1994
+ await ctrl.stop();
1995
+ setWebUrl(null);
1996
+ setWebStatus("off");
1997
+ } else {
1998
+ setWebStatus("starting");
1999
+ const { startWebServer } = await import("./server-JV3U3PIF.js");
2000
+ const ctrl = await startWebServer({ config: cfg, log: false });
2001
+ webRef.current = ctrl;
2002
+ setWebUrl(ctrl.url);
2003
+ setWebStatus("on");
2004
+ openUrl(ctrl.url);
2005
+ }
2006
+ } catch {
2007
+ setWebStatus(webRef.current ? "on" : "off");
2008
+ } finally {
2009
+ webBusyRef.current = false;
2010
+ }
2011
+ }
4463
2012
  useInput((input, key) => {
4464
2013
  if (showPicker) {
4465
2014
  if (input === "q") {
@@ -4607,6 +2156,11 @@ function App({ interval: cliInterval, initialConfig }) {
4607
2156
  openUrl(REPO_URL);
4608
2157
  return;
4609
2158
  }
2159
+ if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2160
+ if (showLoader || !configReady) return;
2161
+ void toggleWeb();
2162
+ return;
2163
+ }
4610
2164
  if (showSettings) {
4611
2165
  if (key.escape || input === "s") {
4612
2166
  setShowSettings(false);
@@ -4837,6 +2391,14 @@ function App({ interval: cliInterval, initialConfig }) {
4837
2391
  ] })
4838
2392
  ] }),
4839
2393
  /* @__PURE__ */ jsxs7(Box7, { children: [
2394
+ webStatus !== "off" && /* @__PURE__ */ jsxs7(Fragment4, { children: [
2395
+ /* @__PURE__ */ jsx7(WebStatus, { status: webStatus, url: webUrl }),
2396
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2397
+ " ",
2398
+ glyphs().middot,
2399
+ " "
2400
+ ] })
2401
+ ] }),
4840
2402
  peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4841
2403
  /* @__PURE__ */ jsx7(PeakBadge, { peak }),
4842
2404
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -4938,7 +2500,7 @@ function App({ interval: cliInterval, initialConfig }) {
4938
2500
  )
4939
2501
  ] })
4940
2502
  ] }),
4941
- (tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
2503
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols, webOn: webStatus === "on" || webStatus === "starting" })
4942
2504
  ] });
4943
2505
  }
4944
2506
  function upsert(prev, account, patch) {
@@ -5017,16 +2579,26 @@ function AccountStrip({ slots, activeIdx, onSelect }) {
5017
2579
  ] }, s.id ?? "__all__");
5018
2580
  }) });
5019
2581
  }
5020
- function Footer({ hasAccounts, paginated, cols }) {
2582
+ function WebStatus({ status, url }) {
2583
+ if (status === "starting") return /* @__PURE__ */ jsx7(Spinner, { label: "web starting" });
2584
+ if (status === "stopping") return /* @__PURE__ */ jsx7(Spinner, { label: "web stopping" });
2585
+ if (status === "on") return /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
2586
+ glyphs().dot,
2587
+ " web :",
2588
+ url ? url.split(":").pop() : ""
2589
+ ] });
2590
+ return null;
2591
+ }
2592
+ function Footer({ hasAccounts, paginated, cols, webOn }) {
5021
2593
  const inner = cols - 4;
5022
- const BASE2 = "by David Ilie (davidilie.com) \xB7 O=repo s=settings q=quit".length;
2594
+ const BASE = "by David Ilie (davidilie.com) \xB7 O=repo W=web s=settings q=quit".length;
5023
2595
  const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
5024
2596
  const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
5025
2597
  const JUMP = "0-9=jump a/A=cycle ".length;
5026
2598
  const PAGE = "scroll=page ".length;
5027
- const showOpt = IS_APPLE_TERMINAL && inner >= BASE2 + OPT;
5028
- const showJump = hasAccounts && inner >= BASE2 + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
5029
- const showPage = paginated && inner >= BASE2 + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
2599
+ const showOpt = IS_APPLE_TERMINAL && inner >= BASE + OPT;
2600
+ const showJump = hasAccounts && inner >= BASE + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
2601
+ const showPage = paginated && inner >= BASE + (showOpt ? OPT : 0) + (showJump ? JUMP : 0) + PAGE;
5030
2602
  return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
5031
2603
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
5032
2604
  /* @__PURE__ */ jsx7(LinkBox, { onClick: () => openUrl(REPO_URL), children: /* @__PURE__ */ jsx7(Transform, { transform: (s) => osc8(s, REPO_URL), children: /* @__PURE__ */ jsx7(Text7, { underline: true, children: "David Ilie" }) }) }),
@@ -5035,7 +2607,9 @@ function Footer({ hasAccounts, paginated, cols }) {
5035
2607
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
5036
2608
  ") ",
5037
2609
  glyphs().middot,
5038
- " O=repo s=settings "
2610
+ " O=repo ",
2611
+ webOn ? "W=stop" : "W=web",
2612
+ " s=settings "
5039
2613
  ] }),
5040
2614
  showOpt && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: optHint }),
5041
2615
  showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
@@ -5098,6 +2672,12 @@ process.emitWarning = ((warning, ...rest) => {
5098
2672
  return emitWarning(warning, ...rest);
5099
2673
  });
5100
2674
  var args = process.argv.slice(2);
2675
+ var subcommand = args[0]?.toLowerCase();
2676
+ if (subcommand === "serve" || subcommand === "web") {
2677
+ const { startWeb } = await import("./web-6MDSVWLV.js");
2678
+ await startWeb(args.slice(1));
2679
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
2680
+ }
5101
2681
  var interval;
5102
2682
  var asciiFlag = null;
5103
2683
  for (let i = 0; i < args.length; i++) {
@@ -5110,7 +2690,8 @@ for (let i = 0; i < args.length; i++) {
5110
2690
  if (args[i] === "--help" || args[i] === "-h") {
5111
2691
  console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
5112
2692
  console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini\n");
5113
- console.log("Usage: tokmon [options]\n");
2693
+ console.log("Usage: tokmon [options]");
2694
+ console.log(" tokmon serve [--port <n>] [--no-open] Launch the web dashboard\n");
5114
2695
  console.log("Options:");
5115
2696
  console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
5116
2697
  console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
@@ -5122,6 +2703,7 @@ for (let i = 0; i < args.length; i++) {
5122
2703
  console.log(" a / A Cycle account focus");
5123
2704
  console.log(" 0-9 Jump to account focus");
5124
2705
  console.log(" \u2191\u2193 Scroll table");
2706
+ console.log(" w / W Toggle web dashboard");
5125
2707
  console.log(" s Settings");
5126
2708
  console.log(" q Quit");
5127
2709
  process.exit(0);