tokmon 0.14.4 → 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 +242 -2624
  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"],
@@ -566,2103 +85,106 @@ var GLYPHS_ASCII = {
566
85
  treeEnd: "`-",
567
86
  boxMark: "|",
568
87
  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 as useCallback2, useRef as useRef2, useMemo } from "react";
614
- import { Box as Box7, Text as Text7, Transform, useInput, useStdout, useApp } from "ink";
615
- import { useMouse as useMouse2 } 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)) return PRICING[key];
709
- }
710
- return FALLBACK;
711
- }
712
- function costOf(model, u) {
713
- const p = priceFor(model);
714
- 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;
715
- }
716
- function shortModel(model) {
717
- return model.replace("claude-", "").replace(/-\d{8}$/, "");
718
- }
719
- async function parseFile(path) {
720
- const entries = [];
721
- const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
722
- for await (const line of rl) {
723
- if (!line.includes('"usage"')) continue;
724
- try {
725
- const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
726
- if (obj.type !== "assistant" || !obj.message?.usage) continue;
727
- const ts = new Date(obj.timestamp ?? 0).getTime();
728
- if (!Number.isFinite(ts)) continue;
729
- const u = obj.message.usage;
730
- const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
731
- const input = safeNum(u.input_tokens);
732
- const output = safeNum(u.output_tokens);
733
- const cacheCreate = safeNum(u.cache_creation_input_tokens);
734
- const cacheRead = safeNum(u.cache_read_input_tokens);
735
- if (input + output + cacheCreate + cacheRead === 0) continue;
736
- const p = priceFor(model);
737
- const msgId = obj.message?.id;
738
- entries.push({
739
- // Claude logs a message's usage repeatedly and copies it into resumed
740
- // sessions — dedup by message id (+ requestId when present).
741
- id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
742
- ts,
743
- model: shortModel(model),
744
- cost: costOf(model, u),
745
- input,
746
- output,
747
- cacheCreate,
748
- cacheRead,
749
- cacheSavings: cacheRead * (p.i - p.cr)
750
- });
751
- } catch {
752
- }
753
- }
754
- return entries;
755
- }
756
- async function loadEntries(since, homeDir) {
757
- const files = [];
758
- const seen = /* @__PURE__ */ new Set();
759
- const seenIno = /* @__PURE__ */ new Set();
760
- for (const dir of getClaudeDirs(homeDir)) {
761
- let listing;
762
- try {
763
- listing = await readdir(dir, { recursive: true });
764
- } catch {
765
- continue;
766
- }
767
- for (const f of listing) {
768
- if (!f.endsWith(".jsonl")) continue;
769
- const path = join3(dir, f);
770
- if (seen.has(path)) continue;
771
- seen.add(path);
772
- try {
773
- const s = await fsStat(path);
774
- if (s.mtimeMs < since) continue;
775
- if (s.ino && process.platform !== "win32") {
776
- const idn = `${s.dev}:${s.ino}`;
777
- if (seenIno.has(idn)) continue;
778
- seenIno.add(idn);
779
- }
780
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
781
- } catch {
782
- }
783
- }
784
- }
785
- return loadCachedEntries(files, parseFile, since);
786
- }
787
- async function claudeDashboard(tz, homeDir) {
788
- const now = Date.now();
789
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
790
- const entries = await loadEntries(since, homeDir);
791
- return summarize(entries, tz);
792
- }
793
- async function claudeTable(tz, homeDir) {
794
- const entries = await loadEntries(monthsAgoStart(Date.now(), 6, tz), homeDir);
795
- return tabulate(entries, tz);
796
- }
797
-
798
- // src/providers/claude/billing.ts
799
- import { execFile as execFileCb } from "child_process";
800
- import { readFile as readFile3 } from "fs/promises";
801
- import { join as join4 } from "path";
802
- import { homedir as homedir3 } from "os";
803
- import { promisify } from "util";
804
-
805
- // src/format.ts
806
- function currency(value) {
807
- if (!Number.isFinite(value) || value <= 0) return "$0.00";
808
- if (value >= 1e4) {
809
- return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
810
- }
811
- return `$${value.toFixed(2)}`;
812
- }
813
- function tokens(value) {
814
- const v = Number.isFinite(value) && value > 0 ? value : 0;
815
- if (v >= 1e9) return `${(v / 1e9).toFixed(1)}B`;
816
- if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
817
- if (v >= 1e3) return `${(v / 1e3).toFixed(1)}K`;
818
- return String(Math.floor(v));
819
- }
820
- function time(date, tz) {
821
- return date.toLocaleTimeString(void 0, {
822
- hour: "2-digit",
823
- minute: "2-digit",
824
- second: "2-digit",
825
- timeZone: tz
826
- });
827
- }
828
- var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
829
- function shortDate(iso) {
830
- const [, m, d] = iso.split("-");
831
- return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
832
- }
833
- function col(s, w, align = "right") {
834
- if (s.length > w) return s.slice(0, w - 1) + "~";
835
- const spaces = " ".repeat(w - s.length);
836
- return align === "right" ? spaces + s : s + spaces;
837
- }
838
- function resetIn(iso) {
839
- const diff = new Date(iso).getTime() - Date.now();
840
- if (!Number.isFinite(diff) || diff <= 0) return "now";
841
- const mins = Math.round(diff / 6e4);
842
- if (mins < 60) return `${mins}m`;
843
- const hrs = Math.floor(mins / 60);
844
- const m = mins % 60;
845
- if (hrs < 24) return `${hrs}h ${m}m`;
846
- const days = Math.floor(hrs / 24);
847
- const h = hrs % 24;
848
- return `${days}d ${h}h`;
849
- }
850
-
851
- // src/providers/claude/billing.ts
852
- var execFile = promisify(execFileCb);
853
- function parseAuth(raw) {
854
- try {
855
- const creds = JSON.parse(raw);
856
- const o = creds?.claudeAiOauth ?? creds;
857
- const token = o?.accessToken;
858
- if (typeof token !== "string" || !token) return null;
859
- return {
860
- token,
861
- subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
862
- rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0
863
- };
864
- } catch {
865
- return null;
866
- }
867
- }
868
- async function readCredentialsFile(homeDir) {
869
- for (const dir of claudeConfigDirs(homeDir)) {
870
- try {
871
- const auth = parseAuth(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
872
- if (auth) return auth;
873
- } catch {
874
- }
875
- }
876
- return null;
877
- }
878
- async function readMacKeychain() {
879
- try {
880
- const { stdout } = await execFile("security", [
881
- "find-generic-password",
882
- "-s",
883
- "Claude Code-credentials",
884
- "-w"
885
- ], { timeout: 5e3 });
886
- return parseAuth(stdout.trim());
887
- } catch {
888
- return null;
889
- }
890
- }
891
- async function getAuth(homeDir) {
892
- const isDefault = !homeDir || homeDir === homedir3();
893
- if (isDefault && process.platform === "darwin") {
894
- const auth = await readMacKeychain();
895
- if (auth) return auth;
896
- }
897
- return readCredentialsFile(homeDir);
898
- }
899
- function planLabel(auth) {
900
- const sub = auth.subscriptionType;
901
- if (!sub) return null;
902
- const base = sub.charAt(0).toUpperCase() + sub.slice(1);
903
- const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
904
- return tier ? `${base} ${tier[1]}x` : base;
905
- }
906
- var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
907
- async function claudeBilling(account) {
908
- const auth = await getAuth(account.homeDir);
909
- if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
910
- const plan = planLabel(auth);
911
- try {
912
- const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
913
- headers: {
914
- "Authorization": `Bearer ${auth.token}`,
915
- "anthropic-beta": "oauth-2025-04-20",
916
- "User-Agent": "tokmon"
917
- },
918
- signal: AbortSignal.timeout(1e4)
919
- });
920
- if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
921
- if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
922
- if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
923
- const data = await readJson(res);
924
- if (!data) return { plan, metrics: [], error: "Unexpected API response" };
925
- const metrics = [];
926
- if (data.five_hour) {
927
- metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
928
- }
929
- if (data.seven_day) {
930
- metrics.push({ ...pct(data.seven_day.utilization, resetIn(data.seven_day.resets_at)), label: "Week" });
931
- }
932
- if (data.seven_day_sonnet) {
933
- metrics.push({ ...pct(data.seven_day_sonnet.utilization), label: "Sonnet" });
934
- }
935
- if (data.extra_usage?.is_enabled) {
936
- metrics.push({
937
- label: "Extra",
938
- used: (data.extra_usage.used_credits ?? 0) / 100,
939
- limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
940
- format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
941
- });
942
- }
943
- return { plan, metrics, error: null };
944
- } catch {
945
- return { plan, metrics: [], error: "Network error" };
946
- }
947
- }
948
-
949
- // src/providers/claude/index.ts
950
- var claudeProvider = {
951
- id: "claude",
952
- name: "Claude",
953
- color: "green",
954
- hasUsage: true,
955
- hasBilling: true,
956
- detect: (homeDir) => detectClaude(homeDir),
957
- fetchSummary: (account, tz) => claudeDashboard(tz, account.homeDir),
958
- fetchTable: (account, tz) => claudeTable(tz, account.homeDir),
959
- fetchBilling: (account) => claudeBilling(account)
960
- };
961
-
962
- // src/providers/codex/usage.ts
963
- import { readdir as readdir2, stat as fsStat2, access as access2 } from "fs/promises";
964
- import { createReadStream as createReadStream2 } from "fs";
965
- import { createInterface as createInterface2 } from "readline";
966
- import { join as join5 } from "path";
967
- import { homedir as homedir4 } from "os";
968
- var PRICING2 = {
969
- "gpt-5-codex": { in: 125e-8, cr: 125e-9, out: 1e-5 },
970
- "gpt-5-mini": { in: 25e-8, cr: 25e-9, out: 2e-6 },
971
- "gpt-5-nano": { in: 5e-8, cr: 5e-9, out: 4e-7 },
972
- "gpt-5": { in: 125e-8, cr: 125e-9, out: 1e-5 },
973
- "o4-mini": { in: 11e-7, cr: 275e-9, out: 44e-7 }
974
- };
975
- var FALLBACK2 = PRICING2["gpt-5-codex"];
976
- var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
977
- function codexHomes(homeDir) {
978
- if (homeDir) return [join5(homeDir, ".codex")];
979
- const homes = [];
980
- const codexHome = envDir("CODEX_HOME");
981
- if (codexHome) homes.push(codexHome);
982
- homes.push(join5(homedir4(), ".codex"));
983
- homes.push(join5(homedir4(), ".config", "codex"));
984
- return [...new Set(homes)];
985
- }
986
- async function detectCodex(homeDir) {
987
- for (const home of codexHomes(homeDir)) {
988
- try {
989
- await access2(join5(home, "sessions"));
990
- return true;
991
- } catch {
992
- }
993
- }
994
- return false;
995
- }
996
- function priceFor2(model) {
997
- const m = model.toLowerCase();
998
- for (const key of PRICE_KEYS2) {
999
- if (m.startsWith(key) || m.includes(key)) return PRICING2[key];
1000
- }
1001
- return FALLBACK2;
1002
- }
1003
- function extractModel(obj) {
1004
- const p = obj?.payload ?? obj;
1005
- return p?.model || p?.collaboration_mode?.settings?.model || p?.model_slug || p?.config?.model || p?.info?.model || null;
1006
- }
1007
- function subtractClamped(cur, prev) {
1008
- const sub = (a, b) => Math.max(0, (a ?? 0) - (b ?? 0));
1009
- return {
1010
- input_tokens: sub(cur.input_tokens, prev?.input_tokens),
1011
- cached_input_tokens: sub(cur.cached_input_tokens, prev?.cached_input_tokens),
1012
- output_tokens: sub(cur.output_tokens, prev?.output_tokens),
1013
- reasoning_output_tokens: sub(cur.reasoning_output_tokens, prev?.reasoning_output_tokens)
1014
- };
1015
- }
1016
- async function parseFile2(path) {
1017
- const entries = [];
1018
- let model = "gpt-5-codex";
1019
- let prevTotal = null;
1020
- const rl = createInterface2({ input: createReadStream2(path), crlfDelay: Infinity });
1021
- for await (const rawLine of rl) {
1022
- if (!rawLine.includes("token_count") && !rawLine.includes("turn_context")) continue;
1023
- try {
1024
- const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1025
- const obj = JSON.parse(line);
1026
- const payloadType = obj?.payload?.type ?? obj?.type;
1027
- if (payloadType === "turn_context") {
1028
- const m = extractModel(obj);
1029
- if (typeof m === "string" && m.trim()) model = m;
1030
- continue;
1031
- }
1032
- if (payloadType !== "token_count") continue;
1033
- const info = obj?.payload?.info;
1034
- const total = info?.total_token_usage;
1035
- let d = info?.last_token_usage;
1036
- if (!d && total) {
1037
- const reset = !!prevTotal && (total.input_tokens ?? 0) < (prevTotal.input_tokens ?? 0);
1038
- d = reset ? total : subtractClamped(total, prevTotal);
1039
- }
1040
- if (total) prevTotal = total;
1041
- if (!d) continue;
1042
- const ts = new Date(obj.timestamp ?? obj?.payload?.timestamp ?? 0).getTime();
1043
- if (!Number.isFinite(ts)) continue;
1044
- const inputTotal = safeNum(d.input_tokens);
1045
- const cached = Math.min(safeNum(d.cached_input_tokens), inputTotal);
1046
- const input = inputTotal - cached;
1047
- const output = safeNum(d.output_tokens);
1048
- if (input + output + cached === 0) continue;
1049
- const p = priceFor2(model);
1050
- entries.push({
1051
- ts,
1052
- model,
1053
- cost: input * p.in + cached * p.cr + output * p.out,
1054
- input,
1055
- output,
1056
- cacheCreate: 0,
1057
- cacheRead: cached,
1058
- cacheSavings: cached * (p.in - p.cr)
1059
- });
1060
- } catch {
1061
- }
1062
- }
1063
- return entries;
1064
- }
1065
- async function loadEntries2(since, homeDir) {
1066
- const files = [];
1067
- const seen = /* @__PURE__ */ new Set();
1068
- const seenIno = /* @__PURE__ */ new Set();
1069
- for (const home of codexHomes(homeDir)) {
1070
- const dir = join5(home, "sessions");
1071
- let listing;
1072
- try {
1073
- listing = await readdir2(dir, { recursive: true });
1074
- } catch {
1075
- continue;
1076
- }
1077
- for (const f of listing) {
1078
- if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1079
- const path = join5(dir, f);
1080
- if (seen.has(path)) continue;
1081
- seen.add(path);
1082
- try {
1083
- const s = await fsStat2(path);
1084
- if (s.mtimeMs < since) continue;
1085
- if (s.ino && process.platform !== "win32") {
1086
- const idn = `${s.dev}:${s.ino}`;
1087
- if (seenIno.has(idn)) continue;
1088
- seenIno.add(idn);
1089
- }
1090
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1091
- } catch {
1092
- }
1093
- }
1094
- }
1095
- return loadCachedEntries(files, parseFile2, since);
1096
- }
1097
- async function codexDashboard(tz, homeDir) {
1098
- const now = Date.now();
1099
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1100
- const entries = await loadEntries2(since, homeDir);
1101
- return summarize(entries, tz);
1102
- }
1103
- async function codexTable(tz, homeDir) {
1104
- const entries = await loadEntries2(monthsAgoStart(Date.now(), 6, tz), homeDir);
1105
- return tabulate(entries, tz);
1106
- }
1107
-
1108
- // src/providers/codex/billing.ts
1109
- import { execFile as execFileCb2 } from "child_process";
1110
- import { readFile as readFile4, readdir as readdir3, stat as fsStat3 } from "fs/promises";
1111
- import { createReadStream as createReadStream3 } from "fs";
1112
- import { createInterface as createInterface3 } from "readline";
1113
- import { join as join6 } from "path";
1114
- import { promisify as promisify2 } from "util";
1115
- var execFile2 = promisify2(execFileCb2);
1116
- var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1117
- var CREDIT_USD_RATE = 0.04;
1118
- async function readAuthFile(home) {
1119
- try {
1120
- const raw = await readFile4(join6(home, "auth.json"), "utf-8");
1121
- const auth = JSON.parse(raw);
1122
- const accessToken = auth?.tokens?.access_token;
1123
- if (!accessToken) return null;
1124
- return { accessToken, accountId: auth?.tokens?.account_id };
1125
- } catch {
1126
- return null;
1127
- }
1128
- }
1129
- async function readKeychainAuth() {
1130
- try {
1131
- const { stdout } = await execFile2("security", [
1132
- "find-generic-password",
1133
- "-s",
1134
- "Codex Auth",
1135
- "-w"
1136
- ], { timeout: 5e3 });
1137
- const auth = JSON.parse(stdout.trim());
1138
- const accessToken = auth?.tokens?.access_token;
1139
- if (!accessToken) return null;
1140
- return { accessToken, accountId: auth?.tokens?.account_id };
1141
- } catch {
1142
- return null;
1143
- }
1144
- }
1145
- async function getAuth2(homeDir) {
1146
- for (const home of codexHomes(homeDir)) {
1147
- const auth = await readAuthFile(home);
1148
- if (auth) return auth;
1149
- }
1150
- if (process.platform === "darwin") return readKeychainAuth();
1151
- return null;
1152
- }
1153
- function planLabel2(planType) {
1154
- if (typeof planType !== "string" || !planType.trim()) return null;
1155
- const p = planType.trim().toLowerCase();
1156
- if (p === "prolite") return "Pro 5x";
1157
- if (p === "pro") return "Pro 20x";
1158
- return planType.charAt(0).toUpperCase() + planType.slice(1);
1159
- }
1160
- function isoOrNull(ms) {
1161
- return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
1162
- }
1163
- function resetFrom(window) {
1164
- if (!window) return null;
1165
- let iso = null;
1166
- if (typeof window.reset_at === "number") iso = isoOrNull(window.reset_at * 1e3);
1167
- else if (typeof window.resets_at === "number") iso = isoOrNull(window.resets_at * 1e3);
1168
- else if (typeof window.reset_after_seconds === "number") iso = isoOrNull(Date.now() + window.reset_after_seconds * 1e3);
1169
- return iso ? resetIn(iso) : null;
1170
- }
1171
- function percentMetric(label, used, resets, primary) {
1172
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: resets, primary };
1173
- }
1174
- async function liveBilling(auth) {
1175
- try {
1176
- const headers = {
1177
- "Authorization": `Bearer ${auth.accessToken}`,
1178
- "Accept": "application/json",
1179
- "User-Agent": "tokmon"
1180
- };
1181
- if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
1182
- const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(1e4) });
1183
- if (!res.ok) return null;
1184
- const data = await readJson(res);
1185
- if (!data) return null;
1186
- const metrics = [];
1187
- const rl = data.rate_limit ?? null;
1188
- const primary = rl?.primary_window ?? null;
1189
- const secondary = rl?.secondary_window ?? null;
1190
- const headerPct = (name) => {
1191
- const h = res.headers.get(name);
1192
- if (h === null || h.trim() === "") return void 0;
1193
- const n = Number(h);
1194
- return Number.isFinite(n) ? n : void 0;
1195
- };
1196
- const primaryPct = headerPct("x-codex-primary-used-percent") ?? primary?.used_percent;
1197
- const secondaryPct = headerPct("x-codex-secondary-used-percent") ?? secondary?.used_percent;
1198
- if (typeof primaryPct === "number") metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1199
- if (typeof secondaryPct === "number") metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1200
- const balance = data?.credits?.balance;
1201
- if (typeof balance === "number" && balance >= 0) {
1202
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1203
- }
1204
- if (metrics.length === 0) return null;
1205
- return { plan: planLabel2(data.plan_type), metrics, error: null };
1206
- } catch {
1207
- return null;
1208
- }
1209
- }
1210
- async function newestRolloutFile(homeDir) {
1211
- let best = null;
1212
- for (const home of codexHomes(homeDir)) {
1213
- const dir = join6(home, "sessions");
1214
- let listing;
1215
- try {
1216
- listing = await readdir3(dir, { recursive: true });
1217
- } catch {
1218
- continue;
1219
- }
1220
- for (const f of listing) {
1221
- if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1222
- const path = join6(dir, f);
1223
- try {
1224
- const s = await fsStat3(path);
1225
- if (!best || s.mtimeMs > best.mtime) best = { path, mtime: s.mtimeMs };
1226
- } catch {
1227
- }
1228
- }
1229
- }
1230
- return best?.path ?? null;
1231
- }
1232
- async function snapshotBilling(homeDir) {
1233
- const path = await newestRolloutFile(homeDir);
1234
- if (!path) return null;
1235
- let last = null;
1236
- try {
1237
- const rl = createInterface3({ input: createReadStream3(path), crlfDelay: Infinity });
1238
- for await (const line of rl) {
1239
- if (!line.includes("rate_limits")) continue;
1240
- try {
1241
- const obj = JSON.parse(line);
1242
- if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
1243
- } catch {
1244
- }
1245
- }
1246
- } catch {
1247
- return null;
1248
- }
1249
- if (!last) return null;
1250
- const metrics = [];
1251
- if (typeof last.primary?.used_percent === "number") {
1252
- metrics.push(percentMetric("5h", last.primary.used_percent, resetFrom(last.primary), true));
1253
- }
1254
- if (typeof last.secondary?.used_percent === "number") {
1255
- metrics.push(percentMetric("Week", last.secondary.used_percent, resetFrom(last.secondary)));
1256
- }
1257
- const balance = last?.credits?.balance;
1258
- if (typeof balance === "number" && balance >= 0) {
1259
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1260
- }
1261
- if (metrics.length === 0) return null;
1262
- return { plan: planLabel2(last.plan_type), metrics, error: null };
1263
- }
1264
- async function codexBilling(account) {
1265
- const auth = await getAuth2(account.homeDir);
1266
- if (auth) {
1267
- const live = await liveBilling(auth);
1268
- if (live) return live;
1269
- }
1270
- const snap = await snapshotBilling(account.homeDir);
1271
- if (snap) return snap;
1272
- return {
1273
- plan: null,
1274
- metrics: [],
1275
- error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex"
1276
- };
1277
- }
1278
-
1279
- // src/providers/codex/index.ts
1280
- var codexProvider = {
1281
- id: "codex",
1282
- name: "Codex",
1283
- color: "cyan",
1284
- hasUsage: true,
1285
- hasBilling: true,
1286
- detect: (homeDir) => detectCodex(homeDir),
1287
- fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
1288
- fetchTable: (account, tz) => codexTable(tz, account.homeDir),
1289
- fetchBilling: (account) => codexBilling(account)
1290
- };
1291
-
1292
- // src/providers/cursor/billing.ts
1293
- import { access as access3 } from "fs/promises";
1294
- import { join as join8 } from "path";
1295
- import { homedir as homedir6 } from "os";
1296
-
1297
- // src/providers/cursor/activity.ts
1298
- import { join as join7 } from "path";
1299
- import { homedir as homedir5 } from "os";
1300
-
1301
- // src/providers/cursor/sqlite.ts
1302
- import { execFile as execFileCb3 } from "child_process";
1303
- import { promisify as promisify3 } from "util";
1304
- var execFile3 = promisify3(execFileCb3);
1305
- var nativeDb;
1306
- async function getNativeDb() {
1307
- if (nativeDb !== void 0) return nativeDb;
1308
- try {
1309
- nativeDb = (await import("sqlite")).DatabaseSync;
1310
- } catch {
1311
- nativeDb = null;
1312
- }
1313
- return nativeDb;
1314
- }
1315
- function classify(msg) {
1316
- if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
1317
- if (/database is (locked|busy)|readonly/i.test(msg)) return "locked";
1318
- if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
1319
- return "error";
1320
- }
1321
- async function runSqlite(db, sql, params = []) {
1322
- const DB = await getNativeDb();
1323
- if (DB) {
1324
- let handle;
1325
- try {
1326
- handle = new DB(db, { readOnly: true, timeout: 1500 });
1327
- const rows = handle.prepare(sql).all(...params);
1328
- return { status: "ok", rows };
1329
- } catch {
1330
- } finally {
1331
- try {
1332
- handle?.close();
1333
- } catch {
1334
- }
1335
- }
1336
- }
1337
- return runSqliteCli(db, sql, params);
1338
- }
1339
- function inlineParams(sql, params) {
1340
- let i = 0;
1341
- return sql.replace(/\?/g, () => {
1342
- const p = params[i++];
1343
- return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
1344
- });
1345
- }
1346
- async function runSqliteCli(db, sql, params) {
1347
- try {
1348
- const { stdout } = await execFile3(
1349
- "sqlite3",
1350
- // -json yields row objects; `.timeout` sets the busy handler silently
1351
- // (a `-cmd 'PRAGMA busy_timeout=…'` would print its result row first).
1352
- ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
1353
- { timeout: 1e4, maxBuffer: 8 << 20 }
1354
- );
1355
- const text = stdout.trim();
1356
- if (!text) return { status: "ok", rows: [] };
1357
- try {
1358
- return { status: "ok", rows: JSON.parse(text) };
1359
- } catch {
1360
- return { status: "error", rows: [] };
1361
- }
1362
- } catch (e) {
1363
- const err = e;
1364
- if (err?.code === "ENOENT") return { status: "missing", rows: [] };
1365
- return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
1366
- }
1367
- }
1368
- function sqliteStatusMessage(status) {
1369
- switch (status) {
1370
- case "missing":
1371
- return "Cursor data not found \u2014 open Cursor";
1372
- case "old":
1373
- return "Cursor DB unreadable";
1374
- case "locked":
1375
- return "Cursor DB busy \u2014 retrying next poll";
1376
- default:
1377
- return "Cursor data unavailable";
1378
- }
1379
- }
1380
-
1381
- // src/providers/cursor/activity.ts
1382
- var DAY_MS2 = 864e5;
1383
- function trackingDb(homeDir) {
1384
- return join7(homeDir ?? homedir5(), ".cursor", "ai-tracking", "ai-code-tracking.db");
1385
- }
1386
- function localDayKey(ms) {
1387
- const d = new Date(ms);
1388
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1389
- }
1390
- async function cursorActivity(homeDir) {
1391
- const db = trackingDb(homeDir);
1392
- try {
1393
- const now = Date.now();
1394
- const res = await runSqlite(
1395
- db,
1396
- `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;`
1397
- );
1398
- if (res.status !== "ok") return null;
1399
- const byDay = /* @__PURE__ */ new Map();
1400
- let month = 0;
1401
- for (const row of res.rows) {
1402
- const n = Number(row.c) || 0;
1403
- byDay.set(String(row.d), n);
1404
- month += n;
1405
- }
1406
- const series = [];
1407
- for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(localDayKey(now - i * DAY_MS2)) ?? 0);
1408
- if (month === 0 && series.every((v) => v === 0)) return null;
1409
- return { series, summary: `${tokens(month)} lines` };
1410
- } catch {
1411
- return null;
1412
- }
1413
- }
1414
-
1415
- // src/providers/cursor/composer.ts
1416
- async function cursorModelSpend(homeDir) {
1417
- const db = cursorStateDb(homeDir);
1418
- 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;";
1419
- const res = await runSqlite(db, sql);
1420
- if (res.status !== "ok") return null;
1421
- const models = [];
1422
- let total = 0;
1423
- for (const row of res.rows) {
1424
- const usd = (Number(row.cents) || 0) / 100;
1425
- if (usd <= 0) continue;
1426
- models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
1427
- total += usd;
1428
- }
1429
- if (total <= 0) return null;
1430
- return { total, models };
1431
- }
1432
-
1433
- // src/providers/cursor/billing.ts
1434
- var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
1435
- var USAGE_URL2 = `${BASE}/GetCurrentPeriodUsage`;
1436
- var PLAN_URL = `${BASE}/GetPlanInfo`;
1437
- function cursorStateDb(homeDir) {
1438
- const base = homeDir ?? homedir6();
1439
- const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
1440
- if (process.platform === "darwin") {
1441
- return join8(base, "Library", "Application Support", ...tail);
1442
- }
1443
- if (process.platform === "win32") {
1444
- const roaming = homeDir ? join8(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join8(base, "AppData", "Roaming");
1445
- return join8(roaming, ...tail);
1446
- }
1447
- const cfg = homeDir ? join8(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join8(base, ".config");
1448
- return join8(cfg, ...tail);
1449
- }
1450
- async function detectCursor(homeDir) {
1451
- try {
1452
- await access3(cursorStateDb(homeDir));
1453
- return true;
1454
- } catch {
1455
- return false;
1456
- }
1457
- }
1458
- async function readState(db, key) {
1459
- const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
1460
- const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
1461
- return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
1462
- }
1463
- async function connectPost(url, token) {
1464
- try {
1465
- const res = await fetch(url, {
1466
- method: "POST",
1467
- headers: {
1468
- "Authorization": `Bearer ${token}`,
1469
- "Content-Type": "application/json",
1470
- "Connect-Protocol-Version": "1",
1471
- "User-Agent": "tokmon"
1472
- },
1473
- body: "{}",
1474
- signal: AbortSignal.timeout(1e4)
1475
- });
1476
- if (!res.ok) return { __status: res.status };
1477
- return await readJson(res);
1478
- } catch {
1479
- return null;
1480
- }
1481
- }
1482
- var dollars = (cents) => cents / 100;
1483
- async function cursorBilling(account) {
1484
- const [core, activity, spend] = await Promise.all([
1485
- cursorBillingCore(account),
1486
- cursorActivity(account.homeDir),
1487
- cursorModelSpend(account.homeDir)
1488
- ]);
1489
- let merged = activity;
1490
- if (spend) {
1491
- const lines = activity?.summary ?? "";
1492
- const spendLabel = `$${Math.round(spend.total)} all-time`;
1493
- merged = {
1494
- series: activity?.series ?? [],
1495
- summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
1496
- };
1497
- }
1498
- return { ...core, activity: merged };
1499
- }
1500
- async function cursorBillingCore(account) {
1501
- const db = cursorStateDb(account.homeDir);
1502
- const [tokenRes, membershipRes] = await Promise.all([
1503
- readState(db, "cursorAuth/accessToken"),
1504
- readState(db, "cursorAuth/stripeMembershipType")
1505
- ]);
1506
- const token = tokenRes.value;
1507
- const membership = membershipRes.value;
1508
- const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
1509
- if (!token) {
1510
- const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
1511
- return { plan: planFallback, metrics: [], error };
1512
- }
1513
- const [usage, planInfo] = await Promise.all([
1514
- connectPost(USAGE_URL2, token),
1515
- connectPost(PLAN_URL, token)
1516
- ]);
1517
- if (!usage || usage.__status) {
1518
- const expired = usage?.__status === 401 || usage?.__status === 403;
1519
- return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error" };
1520
- }
1521
- const planName = planInfo?.planInfo?.planName ?? planFallback;
1522
- const price = planInfo?.planInfo?.price;
1523
- const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
1524
- const pu = usage.planUsage ?? {};
1525
- const metrics = [];
1526
- const rawEnd = usage.billingCycleEnd;
1527
- const endMs = typeof rawEnd === "string" && rawEnd.trim() ? Number(rawEnd) : NaN;
1528
- const resets = Number.isFinite(endMs) && endMs > 0 && endMs <= 864e13 ? resetIn(new Date(endMs).toISOString()) : null;
1529
- if (typeof pu.totalPercentUsed === "number" && typeof pu.limit === "number") {
1530
- metrics.push({
1531
- label: "Usage",
1532
- used: pu.totalPercentUsed,
1533
- limit: 100,
1534
- format: { kind: "percent" },
1535
- resetsAt: resets,
1536
- primary: true
1537
- });
1538
- const spentCents = typeof pu.totalSpend === "number" ? pu.totalSpend : pu.limit - (pu.remaining ?? 0);
1539
- metrics.push({
1540
- label: "Spend",
1541
- used: dollars(spentCents),
1542
- limit: dollars(pu.limit),
1543
- format: { kind: "dollars" }
1544
- });
1545
- }
1546
- if (pu.autoPercentUsed) {
1547
- metrics.push({ label: "Auto", used: pu.autoPercentUsed, limit: 100, format: { kind: "percent" } });
1548
- }
1549
- if (pu.apiPercentUsed) {
1550
- metrics.push({ label: "API", used: pu.apiPercentUsed, limit: 100, format: { kind: "percent" } });
1551
- }
1552
- const su = usage.spendLimitUsage;
1553
- if (su) {
1554
- const limitCents = su.individualLimit ?? su.pooledLimit ?? 0;
1555
- const remainingCents = su.individualRemaining ?? su.pooledRemaining ?? 0;
1556
- if (limitCents > 0) {
1557
- metrics.push({
1558
- label: "On-demand",
1559
- used: dollars(limitCents - remainingCents),
1560
- limit: dollars(limitCents),
1561
- format: { kind: "dollars" }
1562
- });
1563
- }
1564
- }
1565
- if (metrics.length === 0) {
1566
- return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data" };
1567
- }
1568
- return { plan, metrics, error: null };
1569
- }
1570
-
1571
- // src/providers/cursor/index.ts
1572
- var cursorProvider = {
1573
- id: "cursor",
1574
- name: "Cursor",
1575
- color: "magenta",
1576
- hasUsage: false,
1577
- // Cursor exposes spend/limits, not a token history
1578
- hasBilling: true,
1579
- detect: (homeDir) => detectCursor(homeDir),
1580
- fetchBilling: (account) => cursorBilling(account)
1581
- };
1582
-
1583
- // src/providers/pi/usage.ts
1584
- import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
1585
- import { createReadStream as createReadStream4 } from "fs";
1586
- import { createInterface as createInterface4 } from "readline";
1587
- import { join as join9 } from "path";
1588
- import { homedir as homedir7 } from "os";
1589
- function piSessionsDir(homeDir) {
1590
- return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
1591
- }
1592
- async function detectPi(homeDir) {
1593
- try {
1594
- await access4(piSessionsDir(homeDir));
1595
- return true;
1596
- } catch {
1597
- return false;
1598
- }
1599
- }
1600
- function pos(v) {
1601
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1602
- }
1603
- async function parseFile3(path) {
1604
- const entries = [];
1605
- const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
1606
- for await (const rawLine of rl) {
1607
- if (!rawLine.includes('"usage"')) continue;
1608
- try {
1609
- const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1610
- const obj = JSON.parse(line);
1611
- if (obj?.type !== "message") continue;
1612
- const msg = obj.message;
1613
- if (msg?.role !== "assistant" || !msg?.usage) continue;
1614
- const u = msg.usage;
1615
- const ts = new Date(obj.timestamp ?? msg.timestamp ?? 0).getTime();
1616
- if (!Number.isFinite(ts)) continue;
1617
- const input = safeNum(u.input);
1618
- const output = safeNum(u.output);
1619
- const cacheRead = safeNum(u.cacheRead);
1620
- const cacheCreate = safeNum(u.cacheWrite);
1621
- if (input + output + cacheRead + cacheCreate === 0) continue;
1622
- const c = u.cost ?? {};
1623
- const costInput = pos(c.input);
1624
- const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
1625
- entries.push({
1626
- ts,
1627
- model: typeof msg.model === "string" && msg.model ? msg.model : "unknown",
1628
- cost: pos(c.total),
1629
- input,
1630
- output,
1631
- cacheCreate,
1632
- cacheRead,
1633
- cacheSavings
1634
- });
1635
- } catch {
1636
- }
1637
- }
1638
- return entries;
1639
- }
1640
- async function loadEntries3(since, homeDir) {
1641
- const dir = piSessionsDir(homeDir);
1642
- const files = [];
1643
- const seenIno = /* @__PURE__ */ new Set();
1644
- let listing;
1645
- try {
1646
- listing = await readdir4(dir, { recursive: true });
1647
- } catch {
1648
- return [];
1649
- }
1650
- for (const f of listing) {
1651
- if (!f.endsWith(".jsonl")) continue;
1652
- const path = join9(dir, f);
1653
- try {
1654
- const s = await fsStat4(path);
1655
- if (s.mtimeMs < since) continue;
1656
- if (s.ino && process.platform !== "win32") {
1657
- const idn = `${s.dev}:${s.ino}`;
1658
- if (seenIno.has(idn)) continue;
1659
- seenIno.add(idn);
1660
- }
1661
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1662
- } catch {
1663
- }
1664
- }
1665
- return loadCachedEntries(files, parseFile3, since);
1666
- }
1667
- async function piDashboard(tz, homeDir) {
1668
- const now = Date.now();
1669
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1670
- return summarize(await loadEntries3(since, homeDir), tz);
1671
- }
1672
- async function piTable(tz, homeDir) {
1673
- return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1674
- }
1675
-
1676
- // src/providers/pi/index.ts
1677
- var piProvider = {
1678
- id: "pi",
1679
- name: "Pi",
1680
- color: "blue",
1681
- hasUsage: true,
1682
- hasBilling: false,
1683
- detect: (homeDir) => detectPi(homeDir),
1684
- fetchSummary: (account, tz) => piDashboard(tz, account.homeDir),
1685
- fetchTable: (account, tz) => piTable(tz, account.homeDir)
1686
- };
1687
-
1688
- // src/providers/opencode/usage.ts
1689
- import { access as access5 } from "fs/promises";
1690
- import { join as join10 } from "path";
1691
- import { homedir as homedir8 } from "os";
1692
- function opencodeDbPaths(homeDir) {
1693
- const base = homeDir ?? homedir8();
1694
- const paths = [];
1695
- if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1696
- paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
1697
- if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
1698
- if (process.platform === "win32") {
1699
- const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1700
- if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
1701
- }
1702
- return [...new Set(paths)];
1703
- }
1704
- async function findDb(homeDir) {
1705
- for (const p of opencodeDbPaths(homeDir)) {
1706
- try {
1707
- await access5(p);
1708
- return p;
1709
- } catch {
1710
- }
1711
- }
1712
- return null;
1713
- }
1714
- async function detectOpencode(homeDir) {
1715
- return await findDb(homeDir) !== null;
1716
- }
1717
- var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1718
- async function loadEntries4(since, homeDir) {
1719
- const db = await findDb(homeDir);
1720
- if (!db) return [];
1721
- 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 >= ?;";
1722
- const res = await runSqlite(db, sql, [Math.floor(since)]);
1723
- if (res.status !== "ok") return [];
1724
- const entries = [];
1725
- for (const row of res.rows) {
1726
- const ts = pos2(row.ts);
1727
- if (!ts) continue;
1728
- const input = pos2(row.input);
1729
- const output = pos2(row.output);
1730
- const cacheRead = pos2(row.cacheRead);
1731
- const cacheCreate = pos2(row.cacheWrite);
1732
- if (input + output + cacheRead + cacheCreate === 0) continue;
1733
- entries.push({
1734
- ts,
1735
- model: typeof row.model === "string" && row.model ? row.model : "unknown",
1736
- cost: pos2(row.cost),
1737
- input,
1738
- output,
1739
- cacheCreate,
1740
- cacheRead,
1741
- cacheSavings: 0
1742
- // opencode stores only a total cost, no per-component split to derive savings
1743
- });
1744
- }
1745
- return entries;
1746
- }
1747
- async function opencodeDashboard(tz, homeDir) {
1748
- const now = Date.now();
1749
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1750
- return summarize(await loadEntries4(since, homeDir), tz);
1751
- }
1752
- async function opencodeTable(tz, homeDir) {
1753
- return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1754
- }
1755
-
1756
- // src/providers/opencode/index.ts
1757
- var opencodeProvider = {
1758
- id: "opencode",
1759
- name: "opencode",
1760
- color: "yellow",
1761
- hasUsage: true,
1762
- hasBilling: false,
1763
- detect: (homeDir) => detectOpencode(homeDir),
1764
- fetchSummary: (account, tz) => opencodeDashboard(tz, account.homeDir),
1765
- fetchTable: (account, tz) => opencodeTable(tz, account.homeDir)
1766
- };
1767
-
1768
- // src/providers/copilot/billing.ts
1769
- import { execFile as execFileCb4 } from "child_process";
1770
- import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
1771
- import { join as join11 } from "path";
1772
- import { homedir as homedir9 } from "os";
1773
- import { promisify as promisify4 } from "util";
1774
- var execFile4 = promisify4(execFileCb4);
1775
- var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
1776
- var GH_KEYCHAIN_SERVICE = "gh:github.com";
1777
- var GO_KEYRING_PREFIX = "go-keyring-base64:";
1778
- function ghConfigDir(homeDir) {
1779
- if (!homeDir) {
1780
- const explicit = process.env.GH_CONFIG_DIR;
1781
- if (explicit && explicit.trim()) return explicit.trim();
1782
- if (process.platform === "win32") {
1783
- return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
1784
- }
1785
- const xdg = envDir("XDG_CONFIG_HOME");
1786
- return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
1787
- }
1788
- return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
1789
- }
1790
- function ghHostsPath(homeDir) {
1791
- return join11(ghConfigDir(homeDir), "hosts.yml");
1792
- }
1793
- async function detectCopilot(homeDir) {
1794
- try {
1795
- await access6(ghHostsPath(homeDir));
1796
- return true;
1797
- } catch {
1798
- }
1799
- try {
1800
- await execFile4("gh", ["--version"], { timeout: 3e3 });
1801
- return true;
1802
- } catch {
1803
- return false;
1804
- }
1805
- }
1806
- function unquoteYamlValue(value) {
1807
- const trimmed = value.trim();
1808
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1809
- return trimmed.slice(1, -1);
1810
- }
1811
- return trimmed;
1812
- }
1813
- function tokenFromHostsYaml(raw) {
1814
- const lines = raw.split(/\r?\n/);
1815
- let inGithub = false;
1816
- let githubIndent = -1;
1817
- for (const line of lines) {
1818
- const match = line.match(/^(\s*)([^:#][^:]*):\s*(.*)$/);
1819
- if (!match) continue;
1820
- const indent = match[1].length;
1821
- const key = match[2].trim();
1822
- const value = match[3].trim();
1823
- if (indent === 0) {
1824
- inGithub = key === "github.com";
1825
- githubIndent = inGithub ? indent : -1;
1826
- continue;
1827
- }
1828
- if (inGithub && indent > githubIndent && key === "oauth_token" && value) {
1829
- return unquoteYamlValue(value);
1830
- }
1831
- }
1832
- return null;
1833
- }
1834
- async function loadTokenFromHosts(homeDir) {
1835
- try {
1836
- const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
1837
- return token ? { token, source: "gh-hosts" } : null;
1838
- } catch {
1839
- return null;
1840
- }
1841
- }
1842
- async function readMacKeychainService(service) {
1843
- if (process.platform !== "darwin") return null;
1844
- try {
1845
- const { stdout } = await execFile4("security", [
1846
- "find-generic-password",
1847
- "-s",
1848
- service,
1849
- "-w"
1850
- ], { timeout: 5e3 });
1851
- const raw = stdout.trim();
1852
- if (!raw) return null;
1853
- if (raw.startsWith(GO_KEYRING_PREFIX)) {
1854
- return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1855
- }
1856
- return raw;
1857
- } catch {
1858
- return null;
1859
- }
1860
- }
1861
- async function loadTokenFromGhKeychain() {
1862
- const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
1863
- return token ? { token, source: "gh-keychain" } : null;
1864
- }
1865
- function vscodeUserDir(homeDir) {
1866
- const home = homeDir ?? homedir9();
1867
- if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
1868
- if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
1869
- return join11(home, ".config", "Code", "User");
1870
- }
1871
- function tokenFromText(raw) {
1872
- const patterns = [
1873
- /github\.com[^A-Za-z0-9_]+oauth_token[^A-Za-z0-9_]+([A-Za-z0-9_]{20,})/i,
1874
- /github\.com[^A-Za-z0-9_]+(gh[opusr]_[A-Za-z0-9_]{20,})/i,
1875
- /\b(gh[opusr]_[A-Za-z0-9_]{20,})\b/
1876
- ];
1877
- for (const pattern of patterns) {
1878
- const token = raw.match(pattern)?.[1];
1879
- if (token) return token;
1880
- }
1881
- return null;
1882
- }
1883
- async function loadTokenFromVsCode(homeDir) {
1884
- const userDir = vscodeUserDir(homeDir);
1885
- const candidates = [
1886
- join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1887
- join11(userDir, "globalStorage", "github.copilot", "auth.json"),
1888
- join11(userDir, "globalStorage", "state.vscdb")
1889
- ];
1890
- try {
1891
- for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
1892
- if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
1893
- candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
1894
- }
1895
- }
1896
- } catch {
1897
- }
1898
- for (const path of candidates) {
1899
- try {
1900
- const token = tokenFromText(await readFile5(path, "utf-8"));
1901
- if (token) return { token, source: "vscode" };
1902
- } catch {
1903
- }
1904
- }
1905
- return null;
1906
- }
1907
- async function loadToken(homeDir) {
1908
- return await loadTokenFromHosts(homeDir) || await loadTokenFromGhKeychain() || await loadTokenFromVsCode(homeDir);
1909
- }
1910
- function redactToken(token) {
1911
- return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
1912
- }
1913
- function resetDate(value) {
1914
- return typeof value === "string" && value.trim() ? resetIn(value) : null;
1915
- }
1916
- function percentMetric2(label, snapshot, reset, primary) {
1917
- if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
1918
- const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
1919
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
1920
- }
1921
- function countMetric(label, remaining, total, reset) {
1922
- if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
1923
- return {
1924
- label,
1925
- used: Math.max(0, total - remaining),
1926
- limit: total,
1927
- format: { kind: "count" },
1928
- resetsAt: reset
1929
- };
1930
- }
1931
- async function fetchUsage(token) {
1932
- try {
1933
- const res = await fetch(USAGE_URL3, {
1934
- headers: {
1935
- "Authorization": `token ${token}`,
1936
- "Accept": "application/json",
1937
- "Editor-Version": "vscode/1.96.2",
1938
- "Editor-Plugin-Version": "copilot-chat/0.26.7",
1939
- "User-Agent": "GitHubCopilotChat/0.26.7",
1940
- "X-Github-Api-Version": "2025-04-01"
1941
- },
1942
- signal: AbortSignal.timeout(1e4)
1943
- });
1944
- if (!res.ok) return { data: null, status: res.status };
1945
- return { data: await readJson(res), status: res.status };
1946
- } catch {
1947
- return { data: null, status: null };
1948
- }
1949
- }
1950
- async function copilotBilling(account) {
1951
- const cred = await loadToken(account.homeDir);
1952
- if (!cred) return { plan: null, metrics: [], error: "Not logged in \u2014 run gh auth login" };
1953
- const { data, status } = await fetchUsage(cred.token);
1954
- if (!data) {
1955
- if (status === 401 || status === 403) {
1956
- return { plan: null, metrics: [], error: `Token invalid (${redactToken(cred.token)}) \u2014 run gh auth login` };
1957
- }
1958
- if (status) return { plan: null, metrics: [], error: `Copilot API ${status}` };
1959
- return { plan: null, metrics: [], error: "Network error" };
1960
- }
1961
- const plan = typeof data.copilot_plan === "string" && data.copilot_plan.trim() ? data.copilot_plan : null;
1962
- const metrics = [];
1963
- const quotaReset = resetDate(data.quota_reset_date);
1964
- const snapshots = data.quota_snapshots;
1965
- const premium = percentMetric2("Premium", snapshots?.premium_interactions, quotaReset, true);
1966
- if (premium) metrics.push(premium);
1967
- const chat = percentMetric2("Chat", snapshots?.chat, quotaReset);
1968
- if (chat) metrics.push(chat);
1969
- if (data.limited_user_quotas && data.monthly_quotas) {
1970
- const reset = resetDate(data.limited_user_reset_date);
1971
- const limitedChat = countMetric("Chat", data.limited_user_quotas.chat, data.monthly_quotas.chat, reset);
1972
- if (limitedChat) metrics.push(limitedChat);
1973
- const completions = countMetric("Completions", data.limited_user_quotas.completions, data.monthly_quotas.completions, reset);
1974
- if (completions) metrics.push(completions);
1975
- }
1976
- if (metrics.length === 0) return { plan, metrics: [], error: "No usage data" };
1977
- return { plan, metrics, error: null };
1978
- }
1979
-
1980
- // src/providers/copilot/index.ts
1981
- var copilotProvider = {
1982
- id: "copilot",
1983
- name: "Copilot",
1984
- color: "white",
1985
- hasUsage: false,
1986
- hasBilling: true,
1987
- detect: (homeDir) => detectCopilot(homeDir),
1988
- fetchBilling: (account) => copilotBilling(account)
1989
- };
1990
-
1991
- // src/providers/antigravity/billing.ts
1992
- import { access as access7, readdir as readdir7 } from "fs/promises";
1993
- import { join as join13 } from "path";
1994
- import { homedir as homedir11 } from "os";
1995
-
1996
- // src/providers/cloud-code.ts
1997
- import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
1998
- import { spawnSync } from "child_process";
1999
- import { homedir as homedir10 } from "os";
2000
- import { dirname, join as join12 } from "path";
2001
- var CLOUD_CODE_URLS = [
2002
- "https://daily-cloudcode-pa.googleapis.com",
2003
- "https://cloudcode-pa.googleapis.com"
2004
- ];
2005
- var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
2006
- var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
2007
- var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
2008
- var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
2009
- 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;
2010
- var MAX_BUNDLE_READ = 32 * 1024 * 1024;
2011
- var cachedClient;
2012
- var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
2013
- var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
2014
- var CC_MODEL_BLACKLIST = {
2015
- MODEL_CHAT_20706: true,
2016
- MODEL_CHAT_23310: true,
2017
- MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
2018
- MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
2019
- MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
2020
- MODEL_GOOGLE_GEMINI_2_5_PRO: true,
2021
- MODEL_PLACEHOLDER_M19: true,
2022
- MODEL_PLACEHOLDER_M9: true,
2023
- MODEL_PLACEHOLDER_M12: true
88
+ warn: "!",
89
+ ellipsis: "...",
90
+ middot: "-",
91
+ emDash: "-",
92
+ eur: "EUR",
93
+ gbp: "GBP",
94
+ border: "classic"
2024
95
  };
2025
- function readVarint(bytes, start) {
2026
- let value = 0;
2027
- let shift = 0;
2028
- let pos3 = start;
2029
- while (pos3 < bytes.length) {
2030
- const b = bytes[pos3++];
2031
- value += (b & 127) * Math.pow(2, shift);
2032
- if ((b & 128) === 0) return { value, pos: pos3 };
2033
- shift += 7;
2034
- }
2035
- return null;
2036
- }
2037
- function readFields(bytes) {
2038
- const fields = {};
2039
- let pos3 = 0;
2040
- while (pos3 < bytes.length) {
2041
- const tag = readVarint(bytes, pos3);
2042
- if (!tag) break;
2043
- pos3 = tag.pos;
2044
- const fieldNum = Math.floor(tag.value / 8);
2045
- const wireType = tag.value % 8;
2046
- if (wireType === 0) {
2047
- const val = readVarint(bytes, pos3);
2048
- if (!val) break;
2049
- fields[fieldNum] = { type: 0, value: val.value };
2050
- pos3 = val.pos;
2051
- } else if (wireType === 1) {
2052
- if (pos3 + 8 > bytes.length) break;
2053
- pos3 += 8;
2054
- } else if (wireType === 2) {
2055
- const len = readVarint(bytes, pos3);
2056
- if (!len) break;
2057
- pos3 = len.pos;
2058
- if (pos3 + len.value > bytes.length) break;
2059
- fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
2060
- pos3 += len.value;
2061
- } else if (wireType === 5) {
2062
- if (pos3 + 4 > bytes.length) break;
2063
- pos3 += 4;
2064
- } else {
2065
- break;
2066
- }
2067
- }
2068
- return fields;
2069
- }
2070
- function utf8(data) {
2071
- return Buffer.from(data).toString("utf8");
2072
- }
2073
- function decodeBase64(text) {
2074
- try {
2075
- return Buffer.from(text, "base64");
2076
- } catch {
2077
- return null;
2078
- }
2079
- }
2080
- function unwrapKeyringBase64(raw) {
2081
- const text = raw.trim();
2082
- if (!text.startsWith("go-keyring-base64:")) return text;
2083
- const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
2084
- return decoded ? utf8(decoded).trim() : text;
2085
- }
2086
- function unwrapOAuthSentinel(base64Text) {
2087
- const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
2088
- if (!outerBytes) return null;
2089
- const outer = readFields(outerBytes);
2090
- if (outer[1]?.type !== 2 || !outer[1].data) return null;
2091
- const wrapper = readFields(outer[1].data);
2092
- const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
2093
- const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
2094
- if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
2095
- const payloadFields = readFields(payload);
2096
- if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
2097
- const innerText = utf8(payloadFields[1].data).trim();
2098
- return innerText ? decodeBase64(innerText) : null;
2099
- }
2100
- async function readAntigravityOAuthToken(db) {
2101
- const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
2102
- if (r.status !== "ok") return { token: null, status: r.status };
2103
- const raw = r.rows[0]?.value;
2104
- if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
2105
- const inner = unwrapOAuthSentinel(raw);
2106
- if (!inner) return { token: null, status: "ok" };
2107
- const fields = readFields(inner);
2108
- const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
2109
- const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
2110
- let expirySeconds = null;
2111
- if (fields[4]?.type === 2 && fields[4].data) {
2112
- const ts = readFields(fields[4].data);
2113
- expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
2114
- }
2115
- if (!accessToken && !refreshToken) return { token: null, status: "ok" };
2116
- return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
2117
- }
2118
- function redact(token) {
2119
- if (!token) return "none";
2120
- return `...${token.slice(-4)}`;
2121
- }
2122
- function geminiBundleCandidates() {
2123
- const candidates = [];
2124
- const addBundle = (nodeModulesRoot) => {
2125
- if (!nodeModulesRoot) return;
2126
- candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2127
- };
2128
- try {
2129
- const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
2130
- const resolved = typeof which.stdout === "string" ? which.stdout.trim().split("\n")[0]?.trim() : "";
2131
- if (resolved) candidates.push(resolved);
2132
- } catch {
2133
- }
2134
- const home = homedir10();
2135
- addBundle("/opt/homebrew/lib/node_modules");
2136
- addBundle("/usr/local/lib/node_modules");
2137
- addBundle(join12(home, ".local", "share", "node_modules"));
2138
- addBundle(join12(home, ".bun", "install", "global", "node_modules"));
2139
- try {
2140
- const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
2141
- const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
2142
- if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
2143
- } catch {
2144
- }
2145
- return [...new Set(candidates.filter(Boolean))];
2146
- }
2147
- async function resolveBundleDir(candidate) {
2148
- try {
2149
- if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
2150
- return candidate;
2151
- }
2152
- const real = await realpath(candidate);
2153
- return dirname(real);
2154
- } catch {
2155
- 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 ?? ""));
2156
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;
2157
106
  }
2158
- async function scanBundleDir(dir) {
2159
- let entries;
2160
- try {
2161
- entries = await readdir6(dir);
2162
- } catch {
2163
- return null;
2164
- }
2165
- const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
2166
- for (const name of targets) {
2167
- const filePath = join12(dir, name);
2168
- try {
2169
- const info = await stat(filePath);
2170
- if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
2171
- const contents = await readFile6(filePath, "utf8");
2172
- if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
2173
- const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
2174
- if (match) return { clientId: match[1], clientSecret: match[2] };
2175
- } catch {
2176
- }
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);
2177
118
  }
2178
- return null;
119
+ return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
2179
120
  }
2180
- async function discoverGoogleOAuthClient() {
2181
- try {
2182
- for (const candidate of geminiBundleCandidates()) {
2183
- const dir = await resolveBundleDir(candidate);
2184
- if (!dir) continue;
2185
- const found = await scanBundleDir(dir);
2186
- if (found) return found;
2187
- }
2188
- } catch {
2189
- }
2190
- return null;
121
+ var active = GLYPHS_UNICODE;
122
+ function setGlyphs(set) {
123
+ active = set;
2191
124
  }
2192
- async function resolveGoogleClient() {
2193
- const envId = process.env.TOKMON_GOOGLE_CLIENT_ID?.trim();
2194
- const envSecret = process.env.TOKMON_GOOGLE_CLIENT_SECRET?.trim();
2195
- if (envId && envSecret) return { clientId: envId, clientSecret: envSecret };
2196
- if (cachedClient === void 0) cachedClient = await discoverGoogleOAuthClient();
2197
- return cachedClient;
125
+ function glyphs() {
126
+ return active;
2198
127
  }
2199
- async function refreshAccessToken(refreshToken) {
2200
- if (!refreshToken) return null;
2201
- const client = await resolveGoogleClient();
2202
- if (!client) return null;
2203
- const body = new URLSearchParams({
2204
- client_id: client.clientId,
2205
- client_secret: client.clientSecret,
2206
- refresh_token: refreshToken,
2207
- grant_type: "refresh_token"
2208
- });
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() {
2209
138
  try {
2210
- const res = await fetch(GOOGLE_OAUTH_URL, {
2211
- method: "POST",
2212
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2213
- body,
2214
- 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)
2215
142
  });
2216
143
  if (!res.ok) return null;
2217
- const json = await readJson(res);
2218
- return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
2219
- } catch {
2220
- return null;
2221
- }
2222
- }
2223
- async function requestCloudCodeJson(path, token, body) {
2224
- for (const base of CLOUD_CODE_URLS) {
2225
- try {
2226
- const res = await fetch(`${base}${path}`, {
2227
- method: "POST",
2228
- headers: {
2229
- Accept: "application/json",
2230
- "Content-Type": "application/json",
2231
- Authorization: `Bearer ${token}`,
2232
- "User-Agent": "agy"
2233
- },
2234
- body: JSON.stringify(body ?? {}),
2235
- signal: AbortSignal.timeout(15e3)
2236
- });
2237
- if (res.status === 401 || res.status === 403) return { _authFailed: true };
2238
- if (!res.ok) continue;
2239
- const json = await readJson(res);
2240
- if (json && typeof json === "object") return json;
2241
- } catch {
2242
- }
2243
- }
2244
- return null;
2245
- }
2246
- function readPlan(loadData) {
2247
- const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
2248
- const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
2249
- const raw = paid || current;
2250
- if (!raw) return null;
2251
- return raw.replace(/^Gemini Code Assist (?:in|for)\s+/i, "").replace(/^Gemini Code Assist$/i, "Code Assist");
2252
- }
2253
- function parseBuckets(data) {
2254
- if (!Array.isArray(data?.buckets)) return [];
2255
- return data.buckets.flatMap((bucket) => {
2256
- const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2257
- if (!modelId) return [];
2258
- return [{
2259
- modelId,
2260
- remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2261
- resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2262
- }];
2263
- });
2264
- }
2265
- function parseModelBuckets(data) {
2266
- const models = data?.models;
2267
- if (!models || typeof models !== "object") return [];
2268
- return Object.keys(models).flatMap((key) => {
2269
- const model = models[key];
2270
- if (!model || typeof model !== "object" || model.isInternal) return [];
2271
- const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
2272
- if (CC_MODEL_BLACKLIST[modelId]) return [];
2273
- const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2274
- if (!displayName) return [];
2275
- const quotaInfo = model.quotaInfo;
2276
- return [{
2277
- modelId: displayName,
2278
- remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2279
- resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2280
- }];
2281
- });
2282
- }
2283
- async function fetchWithAccessToken(accessToken) {
2284
- const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
2285
- if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
2286
- if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
2287
- const plan = readPlan(loadData);
2288
- const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
2289
- let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
2290
- if (!quotaData || "_authFailed" in quotaData) {
2291
- quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
2292
- }
2293
- if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
2294
- if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
2295
- let buckets = parseBuckets(quotaData);
2296
- if (buckets.length === 0) {
2297
- const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
2298
- if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
2299
- if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
2300
- }
2301
- if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
2302
- return { ok: true, plan, buckets };
2303
- }
2304
- async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
2305
- const nowSec = Math.floor(Date.now() / 1e3);
2306
- let accessToken = token.accessToken?.trim() || null;
2307
- if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
2308
- accessToken = await refreshAccessToken(token.refreshToken);
2309
- if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
2310
- }
2311
- if (!accessToken) {
2312
- accessToken = await refreshAccessToken(token.refreshToken);
2313
- if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
2314
- }
2315
- const result = await fetchWithAccessToken(accessToken);
2316
- if (result.ok || result.error !== "Token expired") return result;
2317
- const refreshed = await refreshAccessToken(token.refreshToken);
2318
- if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
2319
- return fetchWithAccessToken(refreshed);
2320
- }
2321
- function normalizeLabel(label) {
2322
- return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
2323
- }
2324
- function poolLabel(label) {
2325
- const lower = normalizeLabel(label).toLowerCase();
2326
- if (lower.includes("gemini") && lower.includes("pro")) return "Pro";
2327
- if (lower.includes("gemini") && lower.includes("flash")) return "Flash";
2328
- return "Claude";
2329
- }
2330
- function sortKey(label) {
2331
- const lower = label.toLowerCase();
2332
- if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2333
- if (lower.includes("gemini")) return `0b_${label}`;
2334
- if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
2335
- if (lower.includes("claude")) return `1b_${label}`;
2336
- return `2_${label}`;
2337
- }
2338
- function cloudCodeBucketsToMetrics(buckets) {
2339
- const pooled = /* @__PURE__ */ new Map();
2340
- for (const bucket of buckets) {
2341
- const label = poolLabel(bucket.modelId);
2342
- const existing = pooled.get(label);
2343
- if (!existing || bucket.remainingFraction < existing.remainingFraction) {
2344
- pooled.set(label, { ...bucket, modelId: label });
2345
- }
2346
- }
2347
- return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
2348
- 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;
2349
151
  return {
2350
- label: bucket.modelId,
2351
- used: Math.round((1 - clamped) * 100),
2352
- limit: 100,
2353
- format: { kind: "percent" },
2354
- resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
2355
- primary: i === 0
152
+ state,
153
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
154
+ minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
2356
155
  };
2357
- });
2358
- }
2359
- function cloudCodeSqliteError(status) {
2360
- return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
2361
- }
2362
-
2363
- // src/providers/antigravity/billing.ts
2364
- async function exists(path) {
2365
- try {
2366
- await access7(path);
2367
- return true;
2368
- } catch {
2369
- return false;
2370
- }
2371
- }
2372
- async function firstExisting(paths) {
2373
- for (const path of paths) {
2374
- if (await exists(path)) return path;
2375
- }
2376
- return paths[0];
2377
- }
2378
- async function antigravityStateDb(homeDir) {
2379
- const base = homeDir ?? homedir11();
2380
- const tail = ["User", "globalStorage", "state.vscdb"];
2381
- if (process.platform === "darwin") {
2382
- const support = join13(base, "Library", "Application Support");
2383
- const exact = [
2384
- join13(support, "Antigravity IDE", ...tail),
2385
- join13(support, "Antigravity", ...tail)
2386
- ];
2387
- try {
2388
- const entries = await readdir7(support, { withFileTypes: true });
2389
- const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
2390
- return firstExisting([...exact, ...matches]);
2391
- } catch {
2392
- return firstExisting(exact);
2393
- }
2394
- }
2395
- if (process.platform === "win32") {
2396
- const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
2397
- return firstExisting([
2398
- join13(roaming, "Antigravity IDE", ...tail),
2399
- join13(roaming, "Antigravity", ...tail)
2400
- ]);
2401
- }
2402
- const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
2403
- return firstExisting([
2404
- join13(cfg, "Antigravity IDE", ...tail),
2405
- join13(cfg, "Antigravity", ...tail)
2406
- ]);
2407
- }
2408
- async function detectAntigravity(homeDir) {
2409
- return exists(await antigravityStateDb(homeDir));
2410
- }
2411
- async function antigravityBilling(account) {
2412
- try {
2413
- const db = await antigravityStateDb(account.homeDir);
2414
- const { token, status } = await readAntigravityOAuthToken(db);
2415
- if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
2416
- const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
2417
- if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2418
- return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2419
- } catch {
2420
- return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
2421
- }
2422
- }
2423
-
2424
- // src/providers/antigravity/index.ts
2425
- var antigravityProvider = {
2426
- id: "antigravity",
2427
- name: "Antigravity",
2428
- color: "red",
2429
- hasUsage: false,
2430
- hasBilling: true,
2431
- detect: (homeDir) => detectAntigravity(homeDir),
2432
- fetchBilling: (account) => antigravityBilling(account)
2433
- };
2434
-
2435
- // src/providers/gemini/billing.ts
2436
- import { access as access8, readFile as readFile7 } from "fs/promises";
2437
- import { join as join14 } from "path";
2438
- import { homedir as homedir12 } from "os";
2439
- function geminiCredsPath(homeDir) {
2440
- return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
2441
- }
2442
- async function detectGemini(homeDir) {
2443
- try {
2444
- await access8(geminiCredsPath(homeDir));
2445
- return true;
2446
- } catch {
2447
- return false;
2448
- }
2449
- }
2450
- async function readGeminiCreds(path) {
2451
- try {
2452
- return JSON.parse(await readFile7(path, "utf8"));
2453
156
  } catch {
2454
157
  return null;
2455
158
  }
2456
159
  }
2457
- async function geminiBilling(account) {
2458
- try {
2459
- const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2460
- const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2461
- const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2462
- if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2463
- const quota = await fetchCloudCodeQuota({
2464
- accessToken,
2465
- refreshToken,
2466
- expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2467
- }, "Token expired \u2014 run gemini");
2468
- if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2469
- return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2470
- } catch {
2471
- return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2472
- }
2473
- }
2474
-
2475
- // src/providers/gemini/index.ts
2476
- var geminiProvider = {
2477
- id: "gemini",
2478
- name: "Gemini",
2479
- color: "greenBright",
2480
- hasUsage: false,
2481
- hasBilling: true,
2482
- detect: (homeDir) => detectGemini(homeDir),
2483
- fetchBilling: (account) => geminiBilling(account)
2484
- };
2485
-
2486
- // src/providers/detect.ts
2487
- import { accessSync, constants } from "fs";
2488
- import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
2489
- import { homedir as homedir13 } from "os";
2490
- function searchDirs() {
2491
- const home = homedir13();
2492
- const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
2493
- const extra = process.platform === "win32" ? [
2494
- process.env.APPDATA && join15(process.env.APPDATA, "npm"),
2495
- process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
2496
- join15(home, "scoop", "shims")
2497
- ] : [
2498
- "/opt/homebrew/bin",
2499
- "/usr/local/bin",
2500
- "/usr/bin",
2501
- "/bin",
2502
- "/opt/local/bin",
2503
- join15(home, ".local", "bin"),
2504
- join15(home, "bin"),
2505
- join15(home, ".npm-global", "bin"),
2506
- join15(home, ".bun", "bin"),
2507
- join15(home, ".local", "share", "pnpm")
2508
- ];
2509
- return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
2510
- }
2511
- function isExec(p) {
2512
- try {
2513
- accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
2514
- return true;
2515
- } catch {
2516
- return false;
2517
- }
2518
- }
2519
- function onPath(names) {
2520
- const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()).concat("") : [""];
2521
- for (const dir of searchDirs()) {
2522
- for (const n of names) {
2523
- for (const e of exts) {
2524
- if (isExec(join15(dir, n + e))) return true;
2525
- }
2526
- }
2527
- }
2528
- return false;
2529
- }
2530
- function anyExists(paths) {
2531
- return paths.some((p) => !!p && isExec(p));
2532
- }
2533
- function installSignals(id) {
2534
- const home = homedir13();
2535
- const pf = process.env.ProgramFiles;
2536
- const pf86 = process.env["ProgramFiles(x86)"];
2537
- const lad = process.env.LOCALAPPDATA;
2538
- switch (id) {
2539
- case "claude":
2540
- return onPath(["claude"]) || anyExists([
2541
- "/Applications/Claude.app",
2542
- join15(home, "Applications", "Claude.app"),
2543
- lad && join15(lad, "Programs", "claude", "Claude.exe")
2544
- ]);
2545
- case "codex": {
2546
- const bin = process.env.CODEX_BIN;
2547
- if (bin && isAbsolute3(bin) && isExec(bin)) return true;
2548
- return onPath(["codex"]);
2549
- }
2550
- case "cursor":
2551
- return onPath(["cursor", "cursor-agent"]) || anyExists([
2552
- "/Applications/Cursor.app",
2553
- join15(home, "Applications", "Cursor.app"),
2554
- lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
2555
- pf && join15(pf, "Cursor", "Cursor.exe"),
2556
- pf86 && join15(pf86, "Cursor", "Cursor.exe"),
2557
- "/opt/Cursor/cursor",
2558
- "/usr/share/cursor/cursor",
2559
- "/usr/bin/cursor"
2560
- ]);
2561
- case "pi":
2562
- return onPath(["pi"]);
2563
- case "opencode":
2564
- return onPath(["opencode"]);
2565
- case "copilot":
2566
- return onPath(["gh"]);
2567
- case "antigravity":
2568
- return onPath(["antigravity"]) || anyExists([
2569
- "/Applications/Antigravity.app",
2570
- join15(home, "Applications", "Antigravity.app"),
2571
- lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
2572
- ]);
2573
- case "gemini":
2574
- return onPath(["gemini"]);
2575
- default:
2576
- return false;
2577
- }
2578
- }
2579
-
2580
- // src/providers/index.ts
2581
- var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
2582
- var PROVIDERS = {
2583
- claude: claudeProvider,
2584
- codex: codexProvider,
2585
- cursor: cursorProvider,
2586
- pi: piProvider,
2587
- opencode: opencodeProvider,
2588
- copilot: copilotProvider,
2589
- antigravity: antigravityProvider,
2590
- gemini: geminiProvider
2591
- };
2592
- var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
2593
- async function detectProviders() {
2594
- const found = await Promise.all(
2595
- PROVIDER_ORDER.map(async (id) => {
2596
- try {
2597
- if (installSignals(id)) return id;
2598
- return await PROVIDERS[id].detect() ? id : null;
2599
- } catch {
2600
- return null;
2601
- }
2602
- })
2603
- );
2604
- return found.filter((id) => id !== null);
2605
- }
2606
-
2607
- // src/accounts.ts
2608
- function buildAccounts(config2, detected) {
2609
- const out = [];
2610
- for (const pid of PROVIDER_ORDER) {
2611
- if (config2.disabledProviders.includes(pid)) continue;
2612
- const provider = PROVIDERS[pid];
2613
- const configured = config2.accounts.filter((a) => a.providerId === pid);
2614
- if (configured.length > 0) {
2615
- for (const a of configured) {
2616
- out.push({
2617
- id: a.id,
2618
- providerId: pid,
2619
- name: a.name,
2620
- color: a.color || provider.color,
2621
- homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
2622
- });
2623
- }
2624
- } else if (detected.includes(pid)) {
2625
- out.push({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
2626
- }
2627
- }
2628
- return out;
2629
- }
2630
- function accountsByProvider(accounts) {
2631
- const groups = [];
2632
- for (const pid of PROVIDER_ORDER) {
2633
- const list = accounts.filter((a) => a.providerId === pid);
2634
- if (list.length > 0) groups.push({ provider: pid, accounts: list });
2635
- }
2636
- return groups;
2637
- }
2638
160
 
2639
161
  // src/snapshot.ts
2640
- import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
2641
- import { join as join16 } from "path";
162
+ import { readFile, writeFile, mkdir, rename } from "fs/promises";
163
+ import { join } from "path";
2642
164
  function snapshotFile() {
2643
- return join16(cacheDir(), "dashboard-snapshot.json");
165
+ return join(cacheDir(), "dashboard-snapshot.json");
2644
166
  }
2645
167
  async function loadSnapshot() {
2646
168
  try {
2647
- const obj = JSON.parse(await readFile8(snapshotFile(), "utf-8"));
169
+ const obj = JSON.parse(await readFile(snapshotFile(), "utf-8"));
2648
170
  return obj && typeof obj === "object" ? obj : {};
2649
171
  } catch {
2650
172
  return {};
2651
173
  }
2652
174
  }
2653
- var saveQueue2 = Promise.resolve();
175
+ var saveQueue = Promise.resolve();
2654
176
  function saveSnapshot(stats) {
2655
177
  const obj = {};
2656
178
  for (const [id, s] of stats) {
2657
179
  if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
2658
180
  }
2659
- saveQueue2 = saveQueue2.then(async () => {
181
+ saveQueue = saveQueue.then(async () => {
2660
182
  try {
2661
183
  const dir = cacheDir();
2662
- await mkdir3(dir, { recursive: true });
2663
- const tmp = join16(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
2664
- await writeFile3(tmp, JSON.stringify(obj));
2665
- 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());
2666
188
  } catch {
2667
189
  }
2668
190
  });
@@ -2670,9 +192,9 @@ function saveSnapshot(stats) {
2670
192
 
2671
193
  // src/ui/shared.tsx
2672
194
  import { appendFileSync } from "fs";
2673
- import { useState, useEffect, useRef, useCallback } from "react";
195
+ import { useEffect, useRef, useState } from "react";
2674
196
  import { Box, Text } from "ink";
2675
- import { useOnMouseClick, useMouse, useElementPosition, useElementDimensions } from "@zenobius/ink-mouse";
197
+ import { useOnMouseClick } from "@zenobius/ink-mouse";
2676
198
  import { jsx, jsxs } from "react/jsx-runtime";
2677
199
  function truncateName(s, n) {
2678
200
  const ell = glyphs().ellipsis;
@@ -2685,40 +207,72 @@ function ClickableBox({ onClick, children, ...props }) {
2685
207
  });
2686
208
  return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
2687
209
  }
2688
- function LinkBox({ onClick, children, ...props }) {
2689
- const ref = useRef(null);
2690
- const mouse = useMouse();
2691
- const pos3 = useElementPosition(ref);
2692
- const dim = useElementDimensions(ref);
2693
- const handler = useCallback((m, action) => {
210
+ var SGR_PRESS = /\x1b\[<(\d+);(\d+);(\d+)M/g;
211
+ var linkHits = /* @__PURE__ */ new Set();
212
+ function dispatchLinkClicks(chunk) {
213
+ if (linkHits.size === 0) return;
214
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
215
+ SGR_PRESS.lastIndex = 0;
216
+ let m;
217
+ while ((m = SGR_PRESS.exec(s)) !== null) {
218
+ const code = Number(m[1]);
219
+ if (code & 64 || code & 32) continue;
220
+ const mx = Number(m[2]) - 1;
221
+ const my = Number(m[3]) - 1;
2694
222
  if (process.env.TOKMON_LINKDEBUG) {
2695
223
  try {
2696
- appendFileSync(process.env.TOKMON_LINKDEBUG, `evt action=${action} m=${m.x},${m.y} pos=${pos3.left},${pos3.top} dim=${dim.width},${dim.height}
224
+ appendFileSync(process.env.TOKMON_LINKDEBUG, `DISPATCH code=${code} mx=${mx} my=${my} hits=${linkHits.size}
2697
225
  `);
2698
226
  } catch {
2699
227
  }
2700
228
  }
2701
- if (action !== "press") return;
2702
- const mx = m.x - 1;
2703
- const my = m.y - 1;
2704
- const { left, top } = pos3;
2705
- const { width, height } = dim;
2706
- if (mx >= left && mx <= left + width && my >= top && my <= top + height) onClick();
2707
- }, [pos3, dim, onClick]);
229
+ for (const hit of [...linkHits]) {
230
+ if (hit(mx, my)) break;
231
+ }
232
+ }
233
+ }
234
+ function nodeBox(node) {
235
+ const yn = node?.yogaNode;
236
+ if (!yn) return null;
237
+ const l = yn.getComputedLayout();
238
+ let left = l.left, top = l.top;
239
+ let p = node?.parentNode;
240
+ while (p) {
241
+ const pn = p.yogaNode;
242
+ if (!pn) break;
243
+ const pl = pn.getComputedLayout();
244
+ left += pl.left;
245
+ top += pl.top;
246
+ p = p.parentNode;
247
+ }
248
+ return { left, top, width: l.width, height: l.height };
249
+ }
250
+ function LinkBox({ onClick, children, ...props }) {
251
+ const ref = useRef(null);
252
+ const onClickRef = useRef(onClick);
253
+ onClickRef.current = onClick;
2708
254
  useEffect(() => {
2709
- const events = mouse.events;
2710
- if (process.env.TOKMON_LINKDEBUG) {
2711
- try {
2712
- appendFileSync(process.env.TOKMON_LINKDEBUG, `LINKBOX subscribe events=${events ? "present" : "MISSING"} on=${typeof events?.on}
255
+ const hit = (mx, my) => {
256
+ const box = nodeBox(ref.current);
257
+ if (process.env.TOKMON_LINKDEBUG) {
258
+ try {
259
+ appendFileSync(process.env.TOKMON_LINKDEBUG, `HIT? mx=${mx} my=${my} box=${box ? `${box.left},${box.top} ${box.width}x${box.height}` : "null"}
2713
260
  `);
2714
- } catch {
261
+ } catch {
262
+ }
2715
263
  }
2716
- }
2717
- events.on("click", handler);
264
+ if (!box || box.width <= 0 || box.height <= 0) return false;
265
+ if (mx >= box.left && mx < box.left + box.width && my >= box.top && my < box.top + box.height) {
266
+ onClickRef.current();
267
+ return true;
268
+ }
269
+ return false;
270
+ };
271
+ linkHits.add(hit);
2718
272
  return () => {
2719
- events.off("click", handler);
273
+ linkHits.delete(hit);
2720
274
  };
2721
- }, [mouse.events, handler]);
275
+ }, []);
2722
276
  return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
2723
277
  }
2724
278
  function Spinner({ label }) {
@@ -2782,8 +336,8 @@ function sparkline(values) {
2782
336
  return spark[idx];
2783
337
  }).join("");
2784
338
  }
2785
- function Bar({ pct: pct2, color, width = 24 }) {
2786
- 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)));
2787
341
  return /* @__PURE__ */ jsxs(Text, { children: [
2788
342
  /* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
2789
343
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
@@ -3460,12 +1014,19 @@ function LoadingView({ groups, stats, cols, rows }) {
3460
1014
  " more"
3461
1015
  ] })
3462
1016
  ] }),
3463
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3464
- "loading ",
3465
- readyCount,
3466
- " / ",
3467
- groups.length
3468
- ] }) })
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
+ ] })
3469
1030
  ] });
3470
1031
  }
3471
1032
 
@@ -4011,6 +1572,13 @@ function App({ interval: cliInterval, initialConfig }) {
4011
1572
  const { exit } = useApp();
4012
1573
  const rows = stdout?.rows ?? 24;
4013
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
+ }, []);
4014
1582
  const cfg = config2 ?? DEFAULT_CONFIG;
4015
1583
  const interval2 = cliInterval ?? cfg.interval * 1e3;
4016
1584
  const billingMs = cfg.billingInterval * 6e4;
@@ -4245,12 +1813,12 @@ function App({ interval: cliInterval, initialConfig }) {
4245
1813
  useEffect3(() => {
4246
1814
  setDashPage((p) => Math.min(p, dashPageCount - 1));
4247
1815
  }, [dashPageCount]);
4248
- const resetView = useCallback2(() => {
1816
+ const resetView = useCallback(() => {
4249
1817
  setCursor(0);
4250
1818
  setExpanded(-1);
4251
1819
  }, []);
4252
1820
  const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
4253
- const mouse = useMouse2();
1821
+ const mouse = useMouse();
4254
1822
  useEffect3(() => {
4255
1823
  if (!IS_TTY) return;
4256
1824
  mouse.enable();
@@ -4263,20 +1831,11 @@ function App({ interval: cliInterval, initialConfig }) {
4263
1831
  }
4264
1832
  };
4265
1833
  mouse.events.on("scroll", onScroll);
4266
- let onClickDbg = null;
4267
- if (process.env.TOKMON_LINKDEBUG) {
4268
- onClickDbg = (p, a) => {
4269
- try {
4270
- appendFileSync2(process.env.TOKMON_LINKDEBUG, `APP click p=${p.x},${p.y} a=${a}
4271
- `);
4272
- } catch {
4273
- }
4274
- };
4275
- mouse.events.on("click", onClickDbg);
4276
- }
1834
+ const onData = (d) => dispatchLinkClicks(d);
1835
+ process.stdin.on("data", onData);
4277
1836
  return () => {
4278
1837
  mouse.events.off("scroll", onScroll);
4279
- if (onClickDbg) mouse.events.off("click", onClickDbg);
1838
+ process.stdin.off("data", onData);
4280
1839
  };
4281
1840
  }, [tab]);
4282
1841
  function updateConfig(fn) {
@@ -4424,6 +1983,32 @@ function App({ interval: cliInterval, initialConfig }) {
4424
1983
  setSettingsCursor((c) => Math.max(ACCOUNT_ROWS_START, Math.min(ACCOUNT_ROWS_START + cfg.accounts.length - 1, c + dir)));
4425
1984
  }
4426
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
+ }
4427
2012
  useInput((input, key) => {
4428
2013
  if (showPicker) {
4429
2014
  if (input === "q") {
@@ -4571,6 +2156,11 @@ function App({ interval: cliInterval, initialConfig }) {
4571
2156
  openUrl(REPO_URL);
4572
2157
  return;
4573
2158
  }
2159
+ if (input === "W" || input === "w" && tab !== 1 && !showSettings) {
2160
+ if (showLoader || !configReady) return;
2161
+ void toggleWeb();
2162
+ return;
2163
+ }
4574
2164
  if (showSettings) {
4575
2165
  if (key.escape || input === "s") {
4576
2166
  setShowSettings(false);
@@ -4801,6 +2391,14 @@ function App({ interval: cliInterval, initialConfig }) {
4801
2391
  ] })
4802
2392
  ] }),
4803
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
+ ] }),
4804
2402
  peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
4805
2403
  /* @__PURE__ */ jsx7(PeakBadge, { peak }),
4806
2404
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -4902,7 +2500,7 @@ function App({ interval: cliInterval, initialConfig }) {
4902
2500
  )
4903
2501
  ] })
4904
2502
  ] }),
4905
- (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" })
4906
2504
  ] });
4907
2505
  }
4908
2506
  function upsert(prev, account, patch) {
@@ -4981,16 +2579,26 @@ function AccountStrip({ slots, activeIdx, onSelect }) {
4981
2579
  ] }, s.id ?? "__all__");
4982
2580
  }) });
4983
2581
  }
4984
- 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 }) {
4985
2593
  const inner = cols - 4;
4986
- 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;
4987
2595
  const optHint = (glyphs().shift === "\u21E7" ? "\u2325" : "opt") + "-click links ";
4988
2596
  const OPT = IS_APPLE_TERMINAL ? optHint.length : 0;
4989
2597
  const JUMP = "0-9=jump a/A=cycle ".length;
4990
2598
  const PAGE = "scroll=page ".length;
4991
- const showOpt = IS_APPLE_TERMINAL && inner >= BASE2 + OPT;
4992
- const showJump = hasAccounts && inner >= BASE2 + (showOpt ? OPT : 0) + JUMP + (paginated ? PAGE : 0);
4993
- 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;
4994
2602
  return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
4995
2603
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
4996
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" }) }) }),
@@ -4999,7 +2607,9 @@ function Footer({ hasAccounts, paginated, cols }) {
4999
2607
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
5000
2608
  ") ",
5001
2609
  glyphs().middot,
5002
- " O=repo s=settings "
2610
+ " O=repo ",
2611
+ webOn ? "W=stop" : "W=web",
2612
+ " s=settings "
5003
2613
  ] }),
5004
2614
  showOpt && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: optHint }),
5005
2615
  showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
@@ -5062,6 +2672,12 @@ process.emitWarning = ((warning, ...rest) => {
5062
2672
  return emitWarning(warning, ...rest);
5063
2673
  });
5064
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
+ }
5065
2681
  var interval;
5066
2682
  var asciiFlag = null;
5067
2683
  for (let i = 0; i < args.length; i++) {
@@ -5074,7 +2690,8 @@ for (let i = 0; i < args.length; i++) {
5074
2690
  if (args[i] === "--help" || args[i] === "-h") {
5075
2691
  console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
5076
2692
  console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini\n");
5077
- 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");
5078
2695
  console.log("Options:");
5079
2696
  console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
5080
2697
  console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
@@ -5086,6 +2703,7 @@ for (let i = 0; i < args.length; i++) {
5086
2703
  console.log(" a / A Cycle account focus");
5087
2704
  console.log(" 0-9 Jump to account focus");
5088
2705
  console.log(" \u2191\u2193 Scroll table");
2706
+ console.log(" w / W Toggle web dashboard");
5089
2707
  console.log(" s Settings");
5090
2708
  console.log(" q Quit");
5091
2709
  process.exit(0);