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.
- package/dist/chunk-6QVIIZWX.js +533 -0
- package/dist/chunk-ZPH4754N.js +2542 -0
- package/dist/cli.js +242 -2624
- package/dist/server-JV3U3PIF.js +8 -0
- package/dist/web/assets/DepartureMono-Regular-2BZob_Zz.woff2 +0 -0
- package/dist/web/assets/index-Blfaml-c.js +113 -0
- package/dist/web/assets/index-DxSkJ-XD.css +1 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web-6MDSVWLV.js +110 -0
- 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
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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
|
-
|
|
2159
|
-
let
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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
|
|
119
|
+
return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
|
|
2179
120
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
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
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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(
|
|
2211
|
-
|
|
2212
|
-
|
|
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
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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
|
|
2641
|
-
import { join
|
|
162
|
+
import { readFile, writeFile, mkdir, rename } from "fs/promises";
|
|
163
|
+
import { join } from "path";
|
|
2642
164
|
function snapshotFile() {
|
|
2643
|
-
return
|
|
165
|
+
return join(cacheDir(), "dashboard-snapshot.json");
|
|
2644
166
|
}
|
|
2645
167
|
async function loadSnapshot() {
|
|
2646
168
|
try {
|
|
2647
|
-
const obj = JSON.parse(await
|
|
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
|
|
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
|
-
|
|
181
|
+
saveQueue = saveQueue.then(async () => {
|
|
2660
182
|
try {
|
|
2661
183
|
const dir = cacheDir();
|
|
2662
|
-
await
|
|
2663
|
-
const tmp =
|
|
2664
|
-
await
|
|
2665
|
-
await
|
|
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 {
|
|
195
|
+
import { useEffect, useRef, useState } from "react";
|
|
2674
196
|
import { Box, Text } from "ink";
|
|
2675
|
-
import { useOnMouseClick
|
|
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
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
const
|
|
2693
|
-
|
|
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, `
|
|
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
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
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
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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
|
-
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
2715
263
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
-
|
|
273
|
+
linkHits.delete(hit);
|
|
2720
274
|
};
|
|
2721
|
-
}, [
|
|
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
|
|
2786
|
-
const filled = Math.max(0, Math.min(width, Math.round(
|
|
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__ */
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4267
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 >=
|
|
4992
|
-
const showJump = hasAccounts && inner >=
|
|
4993
|
-
const showPage = paginated && inner >=
|
|
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
|
|
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]
|
|
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);
|