tokentracker-cli 0.5.21 → 0.5.24

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.
@@ -0,0 +1,1092 @@
1
+ const cp = require("node:child_process");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const http = require("node:http");
5
+ const https = require("node:https");
6
+
7
+ const { readClaudeCodeAccessToken, readCodexAccessToken } = require("./subscriptions");
8
+ const {
9
+ isCursorInstalled,
10
+ extractCursorSessionToken,
11
+ fetchCursorUsageSummary,
12
+ } = require("./cursor-config");
13
+
14
+ // 2-minute in-memory cache
15
+ let cache = { data: null, fetchedAt: 0 };
16
+ const CACHE_TTL_MS = 2 * 60 * 1000;
17
+
18
+ function clampPercent(value) {
19
+ if (value === null || value === undefined || value === "") return null;
20
+ const n = Number(value);
21
+ if (!Number.isFinite(n)) return null;
22
+ if (n <= 0) return 0;
23
+ if (n >= 100) return 100;
24
+ return n;
25
+ }
26
+
27
+ function buildWindow({ usedPercent, resetAt }) {
28
+ const pct = clampPercent(usedPercent);
29
+ if (pct === null) return null;
30
+ return {
31
+ used_percent: pct,
32
+ reset_at: typeof resetAt === "string" && resetAt ? resetAt : null,
33
+ };
34
+ }
35
+
36
+ function decodeJwtPayload(token) {
37
+ if (typeof token !== "string" || token.length === 0) return null;
38
+ const parts = token.split(".");
39
+ if (parts.length < 2) return null;
40
+ const payload = parts[1];
41
+ const padLen = (4 - (payload.length % 4)) % 4;
42
+ const padded = payload + "=".repeat(padLen);
43
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
44
+ try {
45
+ return JSON.parse(Buffer.from(base64, "base64").toString("utf8"));
46
+ } catch (_error) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch } = {}) {
52
+ const res = await fetchImpl("https://api.anthropic.com/api/oauth/usage", {
53
+ method: "GET",
54
+ headers: {
55
+ Authorization: `Bearer ${accessToken}`,
56
+ "anthropic-beta": "oauth-2025-04-20",
57
+ Accept: "application/json",
58
+ },
59
+ });
60
+ if (res.status === 401) {
61
+ throw new Error("token_expired");
62
+ }
63
+ if (!res.ok) {
64
+ throw new Error(`Claude API returned ${res.status}`);
65
+ }
66
+ const body = await res.json();
67
+ return {
68
+ five_hour: body.five_hour ?? null,
69
+ seven_day: body.seven_day ?? null,
70
+ seven_day_opus: body.seven_day_opus ?? null,
71
+ extra_usage: body.extra_usage ?? null,
72
+ };
73
+ }
74
+
75
+ async function fetchCodexUsageLimits(accessToken, { fetchImpl = fetch } = {}) {
76
+ const res = await fetchImpl("https://chatgpt.com/backend-api/wham/usage", {
77
+ method: "GET",
78
+ headers: {
79
+ Authorization: `Bearer ${accessToken}`,
80
+ Accept: "application/json",
81
+ },
82
+ });
83
+ if (!res.ok) {
84
+ throw new Error(`Codex API returned ${res.status}`);
85
+ }
86
+ const body = await res.json();
87
+ const rateLimit = body.rate_limit || {};
88
+ return {
89
+ primary_window: rateLimit.primary_window ?? null,
90
+ secondary_window: rateLimit.secondary_window ?? null,
91
+ };
92
+ }
93
+
94
+ function normalizeCursorUsageSummary(body) {
95
+ const plan = body?.individualUsage?.plan || null;
96
+ const billingCycleEnd = typeof body?.billingCycleEnd === "string" ? body.billingCycleEnd : null;
97
+ const autoPercent = clampPercent(plan?.autoPercentUsed);
98
+ const apiPercent = clampPercent(plan?.apiPercentUsed);
99
+
100
+ let planPercent = clampPercent(plan?.totalPercentUsed);
101
+ if (planPercent === null) {
102
+ const used = Number(plan?.used);
103
+ const limit = Number(plan?.limit);
104
+ if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
105
+ planPercent = clampPercent((used / limit) * 100);
106
+ } else if (autoPercent !== null && apiPercent !== null) {
107
+ planPercent = clampPercent((autoPercent + apiPercent) / 2);
108
+ } else if (autoPercent !== null) {
109
+ planPercent = autoPercent;
110
+ } else if (apiPercent !== null) {
111
+ planPercent = apiPercent;
112
+ }
113
+ }
114
+
115
+ return {
116
+ membership_type: typeof body?.membershipType === "string" ? body.membershipType : null,
117
+ primary_window: buildWindow({ usedPercent: planPercent, resetAt: billingCycleEnd }),
118
+ secondary_window: buildWindow({ usedPercent: autoPercent, resetAt: billingCycleEnd }),
119
+ tertiary_window: buildWindow({ usedPercent: apiPercent, resetAt: billingCycleEnd }),
120
+ };
121
+ }
122
+
123
+ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
124
+ if (!isCursorInstalled({ home })) {
125
+ return { configured: false };
126
+ }
127
+ const auth = extractCursorSessionToken({ home });
128
+ if (!auth?.cookie) {
129
+ return { configured: false };
130
+ }
131
+ try {
132
+ const body = await fetchCursorUsageSummary({ cookie: auth.cookie, fetchImpl });
133
+ return {
134
+ configured: true,
135
+ error: null,
136
+ ...normalizeCursorUsageSummary(body),
137
+ };
138
+ } catch (error) {
139
+ return {
140
+ configured: true,
141
+ error: error?.message || "Unknown error",
142
+ };
143
+ }
144
+ }
145
+
146
+ function resolveGeminiHome({ home, env } = {}) {
147
+ const explicit = typeof env?.GEMINI_HOME === "string" ? env.GEMINI_HOME.trim() : "";
148
+ return explicit ? path.resolve(explicit) : path.join(home, ".gemini");
149
+ }
150
+
151
+ function loadGeminiSettings({ home, env } = {}) {
152
+ const geminiHome = resolveGeminiHome({ home, env });
153
+ const settingsPath = path.join(geminiHome, "settings.json");
154
+ if (!fs.existsSync(settingsPath)) return null;
155
+ try {
156
+ return JSON.parse(fs.readFileSync(settingsPath, "utf8"));
157
+ } catch (_error) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function loadGeminiCredentials({ home, env } = {}) {
163
+ const geminiHome = resolveGeminiHome({ home, env });
164
+ const credsPath = path.join(geminiHome, "oauth_creds.json");
165
+ if (!fs.existsSync(credsPath)) return null;
166
+ try {
167
+ return JSON.parse(fs.readFileSync(credsPath, "utf8"));
168
+ } catch (_error) {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function extractGeminiOauthClientCredentials({ commandRunner } = {}) {
174
+ const result = runCommand(commandRunner, "which", ["gemini"], { timeout: 2000 });
175
+ const geminiPath = typeof result?.stdout === "string" ? result.stdout.trim() : "";
176
+ if (!geminiPath) return null;
177
+
178
+ let realPath = geminiPath;
179
+ try {
180
+ const resolved = fs.readlinkSync(geminiPath);
181
+ realPath = path.isAbsolute(resolved)
182
+ ? resolved
183
+ : path.join(path.dirname(geminiPath), resolved);
184
+ } catch (_error) {}
185
+
186
+ const binDir = path.dirname(realPath);
187
+ const baseDir = path.dirname(binDir);
188
+ const candidates = [
189
+ path.join(baseDir, "libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
190
+ path.join(baseDir, "lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
191
+ path.join(baseDir, "share/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
192
+ path.join(baseDir, "../gemini-cli-core/dist/src/code_assist/oauth2.js"),
193
+ path.join(baseDir, "node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
194
+ ];
195
+
196
+ for (const candidate of candidates) {
197
+ if (!fs.existsSync(candidate)) continue;
198
+ try {
199
+ const content = fs.readFileSync(candidate, "utf8");
200
+ const clientId = content.match(/OAUTH_CLIENT_ID\s*=\s*['"]([\w\-\.]+)['"]\s*;/)?.[1] || null;
201
+ const clientSecret = content.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([\w\-]+)['"]\s*;/)?.[1] || null;
202
+ if (clientId && clientSecret) {
203
+ return { clientId, clientSecret };
204
+ }
205
+ } catch (_error) {}
206
+ }
207
+ return null;
208
+ }
209
+
210
+ async function refreshGeminiAccessToken({
211
+ refreshToken,
212
+ home,
213
+ env,
214
+ fetchImpl = fetch,
215
+ commandRunner,
216
+ }) {
217
+ const oauthClient = extractGeminiOauthClientCredentials({ commandRunner });
218
+ if (!oauthClient?.clientId || !oauthClient?.clientSecret) {
219
+ throw new Error("Gemini API error: Could not find Gemini CLI OAuth configuration");
220
+ }
221
+
222
+ const body = new URLSearchParams({
223
+ client_id: oauthClient.clientId,
224
+ client_secret: oauthClient.clientSecret,
225
+ refresh_token: refreshToken,
226
+ grant_type: "refresh_token",
227
+ });
228
+
229
+ const res = await fetchImpl("https://oauth2.googleapis.com/token", {
230
+ method: "POST",
231
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
232
+ body,
233
+ });
234
+ if (!res.ok) {
235
+ throw new Error("Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.");
236
+ }
237
+
238
+ const json = await res.json();
239
+ if (!json?.access_token) {
240
+ throw new Error("Could not parse Gemini usage: invalid token refresh response");
241
+ }
242
+
243
+ const geminiHome = resolveGeminiHome({ home, env });
244
+ const credsPath = path.join(geminiHome, "oauth_creds.json");
245
+ try {
246
+ const creds = loadGeminiCredentials({ home, env }) || {};
247
+ creds.access_token = json.access_token;
248
+ if (json.id_token) creds.id_token = json.id_token;
249
+ if (typeof json.expires_in === "number" && Number.isFinite(json.expires_in)) {
250
+ creds.expiry_date = Date.now() + json.expires_in * 1000;
251
+ }
252
+ fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2));
253
+ } catch (_error) {}
254
+
255
+ return json.access_token;
256
+ }
257
+
258
+ async function loadGeminiCodeAssistStatus(accessToken, { fetchImpl = fetch } = {}) {
259
+ const res = await fetchImpl("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
260
+ method: "POST",
261
+ headers: {
262
+ Authorization: `Bearer ${accessToken}`,
263
+ "Content-Type": "application/json",
264
+ },
265
+ body: JSON.stringify({ metadata: { ideType: "GEMINI_CLI", pluginType: "GEMINI" } }),
266
+ });
267
+ if (!res.ok) {
268
+ return { tier: null, projectId: null };
269
+ }
270
+ const json = await res.json();
271
+ const tier = typeof json?.currentTier?.id === "string" ? json.currentTier.id : null;
272
+ const rawProject = json?.cloudaicompanionProject;
273
+ const projectId =
274
+ typeof rawProject === "string"
275
+ ? rawProject.trim() || null
276
+ : typeof rawProject?.id === "string"
277
+ ? rawProject.id
278
+ : typeof rawProject?.projectId === "string"
279
+ ? rawProject.projectId
280
+ : null;
281
+ return { tier, projectId };
282
+ }
283
+
284
+ function normalizeGeminiModelBuckets(buckets) {
285
+ if (!Array.isArray(buckets)) return [];
286
+ const byModel = new Map();
287
+ for (const bucket of buckets) {
288
+ const modelId = typeof bucket?.modelId === "string" ? bucket.modelId : null;
289
+ const remainingFraction = Number(bucket?.remainingFraction);
290
+ if (!modelId || !Number.isFinite(remainingFraction)) continue;
291
+ const existing = byModel.get(modelId);
292
+ if (!existing || remainingFraction < existing.remainingFraction) {
293
+ byModel.set(modelId, {
294
+ model_id: modelId,
295
+ remainingFraction,
296
+ reset_at: parseAntigravityDate(bucket?.resetTime),
297
+ });
298
+ }
299
+ }
300
+ return Array.from(byModel.values()).sort((a, b) => a.model_id.localeCompare(b.model_id));
301
+ }
302
+
303
+ function isGeminiFlashLiteModel(id) {
304
+ return String(id || "").toLowerCase().includes("flash-lite");
305
+ }
306
+
307
+ function isGeminiFlashModel(id) {
308
+ const lower = String(id || "").toLowerCase();
309
+ return lower.includes("flash") && !isGeminiFlashLiteModel(lower);
310
+ }
311
+
312
+ function isGeminiProModel(id) {
313
+ return String(id || "").toLowerCase().includes("pro");
314
+ }
315
+
316
+ function normalizeGeminiQuotaResponse({ buckets, email, tier }) {
317
+ const models = normalizeGeminiModelBuckets(buckets);
318
+ if (!models.length) {
319
+ throw new Error("Could not parse Gemini usage: no quota buckets in response");
320
+ }
321
+
322
+ const pickLowest = (predicate) =>
323
+ models
324
+ .filter((model) => predicate(model.model_id))
325
+ .sort((a, b) => a.remainingFraction - b.remainingFraction)[0] || null;
326
+
327
+ const plan =
328
+ tier === "standard-tier"
329
+ ? "Paid"
330
+ : tier === "legacy-tier"
331
+ ? "Legacy"
332
+ : tier === "free-tier"
333
+ ? "Free"
334
+ : null;
335
+
336
+ const pro = pickLowest(isGeminiProModel);
337
+ const flash = pickLowest(isGeminiFlashModel);
338
+ const flashLite = pickLowest(isGeminiFlashLiteModel);
339
+ const fallback = !pro && !flash && !flashLite
340
+ ? [...models].sort((a, b) => a.remainingFraction - b.remainingFraction)[0]
341
+ : null;
342
+
343
+ const toWindow = (model) =>
344
+ model
345
+ ? buildWindow({
346
+ usedPercent: 100 - model.remainingFraction * 100,
347
+ resetAt: model.reset_at,
348
+ })
349
+ : null;
350
+
351
+ return {
352
+ account_email: email || null,
353
+ account_plan: plan,
354
+ primary_window: toWindow(pro || fallback),
355
+ secondary_window: toWindow(flash),
356
+ tertiary_window: toWindow(flashLite),
357
+ };
358
+ }
359
+
360
+ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner } = {}) {
361
+ const settings = loadGeminiSettings({ home, env });
362
+ const selectedType = settings?.security?.auth?.selectedType ?? null;
363
+ if (!settings && !loadGeminiCredentials({ home, env })) {
364
+ return { configured: false };
365
+ }
366
+ if (selectedType === "api-key") {
367
+ return { configured: true, error: "Gemini API key auth not supported. Use Google account (OAuth) instead." };
368
+ }
369
+ if (selectedType === "vertex-ai") {
370
+ return { configured: true, error: "Gemini Vertex AI auth not supported. Use Google account (OAuth) instead." };
371
+ }
372
+
373
+ const creds = loadGeminiCredentials({ home, env });
374
+ if (!creds?.access_token) {
375
+ return { configured: true, error: "Not logged in to Gemini. Run 'gemini' in Terminal to authenticate." };
376
+ }
377
+
378
+ try {
379
+ let accessToken = creds.access_token;
380
+ const expiry = Number(creds.expiry_date);
381
+ if (Number.isFinite(expiry) && expiry > 0 && expiry < Date.now() && creds.refresh_token) {
382
+ accessToken = await refreshGeminiAccessToken({
383
+ refreshToken: creds.refresh_token,
384
+ home,
385
+ env,
386
+ fetchImpl,
387
+ commandRunner,
388
+ });
389
+ }
390
+
391
+ const claims = decodeJwtPayload(creds.id_token);
392
+ const codeAssist = await loadGeminiCodeAssistStatus(accessToken, { fetchImpl });
393
+ const res = await fetchImpl("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
394
+ method: "POST",
395
+ headers: {
396
+ Authorization: `Bearer ${accessToken}`,
397
+ "Content-Type": "application/json",
398
+ },
399
+ body: JSON.stringify(codeAssist.projectId ? { project: codeAssist.projectId } : {}),
400
+ });
401
+ if (res.status === 401) {
402
+ throw new Error("Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.");
403
+ }
404
+ if (!res.ok) {
405
+ throw new Error(`Gemini API error: HTTP ${res.status}`);
406
+ }
407
+ const json = await res.json();
408
+ return {
409
+ configured: true,
410
+ error: null,
411
+ ...normalizeGeminiQuotaResponse({
412
+ buckets: json?.buckets,
413
+ email: claims?.email || null,
414
+ tier: codeAssist.tier,
415
+ }),
416
+ };
417
+ } catch (error) {
418
+ return {
419
+ configured: true,
420
+ error: error?.message || "Unknown error",
421
+ };
422
+ }
423
+ }
424
+
425
+ function runCommand(commandRunner, command, args, options = {}) {
426
+ const runner = typeof commandRunner === "function" ? commandRunner : cp.spawnSync;
427
+ return runner(command, args, {
428
+ encoding: "utf8",
429
+ maxBuffer: 10 * 1024 * 1024,
430
+ ...options,
431
+ });
432
+ }
433
+
434
+ function isBinaryAvailable(binary, { commandRunner } = {}) {
435
+ const result = runCommand(commandRunner, "which", [binary], { timeout: 2000 });
436
+ return !result?.error && result?.status === 0;
437
+ }
438
+
439
+ function stripAnsi(text) {
440
+ return String(text || "").replace(/\x1B\[[0-9;?]*[A-Za-z]|\x1B\].*?\x07/g, "");
441
+ }
442
+
443
+ function extractFirstNumber(text) {
444
+ const match = String(text || "").match(/-?\d+(?:\.\d+)?/);
445
+ return match ? Number(match[0]) : null;
446
+ }
447
+
448
+ function parseMonthDayResetDate(dateStr, now = new Date()) {
449
+ if (typeof dateStr !== "string") return null;
450
+ const match = dateStr.match(/(\d{2})\/(\d{2})/);
451
+ if (!match) return null;
452
+ const month = Number(match[1]);
453
+ const day = Number(match[2]);
454
+ if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
455
+ const currentYear = now.getUTCFullYear();
456
+
457
+ let candidate = new Date(Date.UTC(currentYear, month - 1, day, 0, 0, 0, 0));
458
+ if (candidate.getTime() <= now.getTime()) {
459
+ candidate = new Date(Date.UTC(currentYear + 1, month - 1, day, 0, 0, 0, 0));
460
+ }
461
+ return candidate.toISOString();
462
+ }
463
+
464
+ function isKiroUsageOutputComplete(output) {
465
+ const lowered = stripAnsi(output).toLowerCase();
466
+ return lowered.includes("covered in plan")
467
+ || lowered.includes("resets on")
468
+ || lowered.includes("bonus credits")
469
+ || lowered.includes("plan:")
470
+ || lowered.includes("managed by admin")
471
+ || lowered.includes("managed by organization");
472
+ }
473
+
474
+ function parseKiroUsageOutput(output, { now = new Date() } = {}) {
475
+ const stripped = stripAnsi(output).trim();
476
+ if (!stripped) {
477
+ throw new Error("Failed to parse Kiro usage: empty output");
478
+ }
479
+
480
+ const lowered = stripped.toLowerCase();
481
+ if (
482
+ lowered.includes("not logged in")
483
+ || lowered.includes("login required")
484
+ || lowered.includes("failed to initialize auth portal")
485
+ || lowered.includes("kiro-cli login")
486
+ || lowered.includes("oauth error")
487
+ ) {
488
+ throw new Error("Not logged in to Kiro. Run 'kiro-cli login' first.");
489
+ }
490
+ if (lowered.includes("could not retrieve usage information")) {
491
+ throw new Error("Failed to parse Kiro usage: Kiro CLI could not retrieve usage information.");
492
+ }
493
+
494
+ let planName = "Kiro";
495
+ const legacyPlan = stripped.match(/\|\s*(KIRO\s+\w+)/);
496
+ if (legacyPlan?.[1]) {
497
+ planName = legacyPlan[1].trim();
498
+ }
499
+ const modernPlan = stripped.match(/Plan:\s*(.+)/);
500
+ if (modernPlan?.[1]) {
501
+ planName = modernPlan[1].split("\n")[0].trim() || planName;
502
+ }
503
+
504
+ const resetMatch = stripped.match(/resets on (\d{2}\/\d{2})/i);
505
+ const primaryReset = resetMatch ? parseMonthDayResetDate(resetMatch[1], now) : null;
506
+
507
+ let creditsPercent = null;
508
+ const percentMatch = stripped.match(/█+\s*(\d+)%/);
509
+ if (percentMatch?.[1]) {
510
+ creditsPercent = clampPercent(Number(percentMatch[1]));
511
+ }
512
+
513
+ let creditsUsed = null;
514
+ let creditsTotal = null;
515
+ const coveredMatch = stripped.match(/\((\d+(?:\.\d+)?)\s+of\s+(\d+(?:\.\d+)?)\s+covered/i);
516
+ if (coveredMatch?.[1] && coveredMatch?.[2]) {
517
+ creditsUsed = Number(coveredMatch[1]);
518
+ creditsTotal = Number(coveredMatch[2]);
519
+ }
520
+ if (creditsPercent === null && creditsUsed !== null && creditsTotal && creditsTotal > 0) {
521
+ creditsPercent = clampPercent((creditsUsed / creditsTotal) * 100);
522
+ }
523
+
524
+ const managedPlan = lowered.includes("managed by admin") || lowered.includes("managed by organization");
525
+ if (creditsPercent === null && creditsUsed === null && managedPlan) {
526
+ return {
527
+ plan_name: planName,
528
+ primary_window: buildWindow({ usedPercent: 0, resetAt: null }),
529
+ secondary_window: null,
530
+ };
531
+ }
532
+ if (creditsPercent === null && creditsUsed === null) {
533
+ throw new Error("Failed to parse Kiro usage: usage output format may have changed.");
534
+ }
535
+
536
+ let bonusWindow = null;
537
+ const bonusMatch = stripped.match(/Bonus credits:[\s\S]*?(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)/i);
538
+ const expiryMatch = stripped.match(/expires in (\d+) days?/i);
539
+ if (bonusMatch?.[1] && bonusMatch?.[2]) {
540
+ const bonusUsed = Number(bonusMatch[1]);
541
+ const bonusTotal = Number(bonusMatch[2]);
542
+ const bonusPct = bonusTotal > 0 ? clampPercent((bonusUsed / bonusTotal) * 100) : 0;
543
+ let bonusReset = null;
544
+ if (expiryMatch?.[1]) {
545
+ const days = Number(expiryMatch[1]);
546
+ if (Number.isFinite(days) && days >= 0) {
547
+ bonusReset = new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString();
548
+ }
549
+ }
550
+ bonusWindow = buildWindow({ usedPercent: bonusPct, resetAt: bonusReset });
551
+ }
552
+
553
+ return {
554
+ plan_name: planName,
555
+ primary_window: buildWindow({ usedPercent: creditsPercent, resetAt: primaryReset }),
556
+ secondary_window: bonusWindow,
557
+ };
558
+ }
559
+
560
+ function fetchKiroLimits({ commandRunner, now = new Date() } = {}) {
561
+ if (!isBinaryAvailable("kiro-cli", { commandRunner })) {
562
+ return { configured: false };
563
+ }
564
+
565
+ const result = runCommand(
566
+ commandRunner,
567
+ "kiro-cli",
568
+ ["chat", "--no-interactive", "/usage"],
569
+ {
570
+ timeout: 20_000,
571
+ env: { ...process.env, TERM: "xterm-256color" },
572
+ },
573
+ );
574
+
575
+ const stdout = typeof result?.stdout === "string" ? result.stdout : "";
576
+ const stderr = typeof result?.stderr === "string" ? result.stderr : "";
577
+ const output = stderr.trim() || stdout.trim();
578
+
579
+ try {
580
+ if (result?.error?.code === "ETIMEDOUT" && !isKiroUsageOutputComplete(output)) {
581
+ throw new Error("Kiro CLI timed out.");
582
+ }
583
+ if (!output && result?.status !== 0) {
584
+ throw new Error(`Kiro CLI failed with status ${result.status}.`);
585
+ }
586
+
587
+ return {
588
+ configured: true,
589
+ error: null,
590
+ ...parseKiroUsageOutput(output, { now }),
591
+ };
592
+ } catch (error) {
593
+ return {
594
+ configured: true,
595
+ error: error?.message || "Unknown error",
596
+ };
597
+ }
598
+ }
599
+
600
+ function parseProcessLine(line) {
601
+ const match = String(line || "")
602
+ .trim()
603
+ .match(/^(\d+)\s+(.*)$/);
604
+ if (!match) return null;
605
+ return {
606
+ pid: Number(match[1]),
607
+ command: match[2],
608
+ };
609
+ }
610
+
611
+ function isAntigravityCommandLine(command) {
612
+ const lower = String(command || "").toLowerCase();
613
+ return lower.includes("language_server_macos")
614
+ && (
615
+ (lower.includes("--app_data_dir") && lower.includes("antigravity"))
616
+ || lower.includes("/antigravity/")
617
+ || lower.includes("\\antigravity\\")
618
+ );
619
+ }
620
+
621
+ function extractCommandFlag(command, flag) {
622
+ const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
623
+ const match = String(command || "").match(new RegExp(`${escaped}[=\\s]+([^\\s]+)`, "i"));
624
+ return match?.[1] || null;
625
+ }
626
+
627
+ function detectAntigravityProcess({ commandRunner } = {}) {
628
+ const result = runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
629
+ timeout: 4000,
630
+ });
631
+ const lines = String(result?.stdout || "").split("\n");
632
+
633
+ let sawProcess = false;
634
+ for (const line of lines) {
635
+ const parsed = parseProcessLine(line);
636
+ if (!parsed) continue;
637
+ if (!isAntigravityCommandLine(parsed.command)) continue;
638
+ sawProcess = true;
639
+ const csrfToken = extractCommandFlag(parsed.command, "--csrf_token");
640
+ if (!csrfToken) continue;
641
+ const extensionPort = extractFirstNumber(extractCommandFlag(parsed.command, "--extension_server_port"));
642
+ return {
643
+ configured: true,
644
+ pid: parsed.pid,
645
+ csrfToken,
646
+ extensionPort: Number.isFinite(extensionPort) ? extensionPort : null,
647
+ };
648
+ }
649
+
650
+ if (sawProcess) {
651
+ return { configured: true, error: "Antigravity CSRF token not found. Restart Antigravity and retry." };
652
+ }
653
+ return { configured: false };
654
+ }
655
+
656
+ function resolveLsofBinary() {
657
+ for (const candidate of ["/usr/sbin/lsof", "/usr/bin/lsof"]) {
658
+ if (fs.existsSync(candidate)) return candidate;
659
+ }
660
+ return null;
661
+ }
662
+
663
+ function parseListeningPorts(output) {
664
+ const matches = String(output || "").matchAll(/:(\d+)\s+\(LISTEN\)/g);
665
+ const ports = new Set();
666
+ for (const match of matches) {
667
+ const port = Number(match[1]);
668
+ if (Number.isFinite(port)) {
669
+ ports.add(port);
670
+ }
671
+ }
672
+ return Array.from(ports).sort((a, b) => a - b);
673
+ }
674
+
675
+ function listAntigravityPorts(pid, { commandRunner } = {}) {
676
+ const lsof = resolveLsofBinary();
677
+ if (!lsof) {
678
+ throw new Error("Antigravity port detection needs lsof. Install it, then retry.");
679
+ }
680
+ const result = runCommand(
681
+ commandRunner,
682
+ lsof,
683
+ ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
684
+ { timeout: 4000 },
685
+ );
686
+ const ports = parseListeningPorts(result?.stdout);
687
+ if (!ports.length) {
688
+ throw new Error("Antigravity is running but not exposing ports yet. Try again in a few seconds.");
689
+ }
690
+ return ports;
691
+ }
692
+
693
+ function antigravityDefaultBody() {
694
+ return {
695
+ metadata: {
696
+ ideName: "antigravity",
697
+ extensionName: "antigravity",
698
+ ideVersion: "unknown",
699
+ locale: "en",
700
+ },
701
+ };
702
+ }
703
+
704
+ function antigravityUnleashBody() {
705
+ return {
706
+ context: {
707
+ properties: {
708
+ devMode: "false",
709
+ extensionVersion: "unknown",
710
+ hasAnthropicModelAccess: "true",
711
+ ide: "antigravity",
712
+ ideVersion: "unknown",
713
+ installationId: "tokentracker",
714
+ language: "UNSPECIFIED",
715
+ os: "macos",
716
+ requestedModelId: "MODEL_UNSPECIFIED",
717
+ },
718
+ },
719
+ };
720
+ }
721
+
722
+ function requestLocalJson({
723
+ scheme,
724
+ port,
725
+ path,
726
+ body,
727
+ csrfToken,
728
+ timeoutMs = 8000,
729
+ requestFn,
730
+ }) {
731
+ if (typeof requestFn === "function") {
732
+ return requestFn({ scheme, port, path, body, csrfToken, timeoutMs });
733
+ }
734
+
735
+ const client = scheme === "https" ? https : http;
736
+ return new Promise((resolve, reject) => {
737
+ const rawBody = JSON.stringify(body);
738
+ const req = client.request(
739
+ {
740
+ hostname: "127.0.0.1",
741
+ port,
742
+ path,
743
+ method: "POST",
744
+ rejectUnauthorized: false,
745
+ timeout: timeoutMs,
746
+ headers: {
747
+ "Content-Type": "application/json",
748
+ "Content-Length": Buffer.byteLength(rawBody),
749
+ "Connect-Protocol-Version": "1",
750
+ "X-Codeium-Csrf-Token": csrfToken,
751
+ },
752
+ },
753
+ (res) => {
754
+ let data = "";
755
+ res.on("data", (chunk) => {
756
+ data += chunk;
757
+ });
758
+ res.on("end", () => {
759
+ if (res.statusCode !== 200) {
760
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
761
+ return;
762
+ }
763
+ try {
764
+ resolve(JSON.parse(data));
765
+ } catch (error) {
766
+ reject(new Error(`Invalid JSON response: ${error.message}`));
767
+ }
768
+ });
769
+ },
770
+ );
771
+ req.on("error", reject);
772
+ req.on("timeout", () => {
773
+ req.destroy(new Error("timeout"));
774
+ });
775
+ req.write(rawBody);
776
+ req.end();
777
+ });
778
+ }
779
+
780
+ function antigravityCodeIsOk(code) {
781
+ if (code === null || code === undefined) return true;
782
+ if (typeof code === "number") return code === 0;
783
+ if (typeof code === "string") {
784
+ const lower = code.toLowerCase();
785
+ return lower === "ok" || lower === "success" || lower === "0";
786
+ }
787
+ return false;
788
+ }
789
+
790
+ function parseAntigravityDate(value) {
791
+ if (typeof value === "string" && value) {
792
+ const iso = Date.parse(value);
793
+ if (Number.isFinite(iso)) return new Date(iso).toISOString();
794
+ const numeric = Number(value);
795
+ if (Number.isFinite(numeric)) {
796
+ return new Date(numeric * 1000).toISOString();
797
+ }
798
+ }
799
+ if (typeof value === "number" && Number.isFinite(value)) {
800
+ return new Date(value * 1000).toISOString();
801
+ }
802
+ return null;
803
+ }
804
+
805
+ function parseAntigravityModelConfigs(configs) {
806
+ if (!Array.isArray(configs)) return [];
807
+ return configs
808
+ .map((config) => {
809
+ const quota = config?.quotaInfo || null;
810
+ if (!quota) return null;
811
+ return {
812
+ label: typeof config?.label === "string" ? config.label : "",
813
+ model_id: typeof config?.modelOrAlias?.model === "string" ? config.modelOrAlias.model : "",
814
+ remaining_fraction:
815
+ typeof quota?.remainingFraction === "number" && Number.isFinite(quota.remainingFraction)
816
+ ? quota.remainingFraction
817
+ : null,
818
+ reset_at: parseAntigravityDate(quota?.resetTime),
819
+ };
820
+ })
821
+ .filter(Boolean);
822
+ }
823
+
824
+ function antigravityFamily(model) {
825
+ const text = `${model?.label || ""} ${model?.model_id || ""}`.toLowerCase();
826
+ if (text.includes("claude")) return "claude";
827
+ if (text.includes("gemini") && text.includes("pro")) return "gemini_pro";
828
+ if (text.includes("gemini") && text.includes("flash")) return "gemini_flash";
829
+ return "unknown";
830
+ }
831
+
832
+ function antigravityPriority(model) {
833
+ const text = `${model?.label || ""} ${model?.model_id || ""}`.toLowerCase();
834
+ if (text.includes("lite") || text.includes("autocomplete") || text.includes("tab_")) return null;
835
+ if (antigravityFamily(model) === "gemini_pro") {
836
+ return text.includes("pro-low") || (text.includes("pro") && text.includes("low")) ? 0 : 1;
837
+ }
838
+ return 0;
839
+ }
840
+
841
+ function chooseAntigravityModel(models, family) {
842
+ const candidates = models
843
+ .filter((model) => antigravityFamily(model) === family)
844
+ .map((model) => ({
845
+ model,
846
+ priority: antigravityPriority(model),
847
+ remaining:
848
+ typeof model.remaining_fraction === "number"
849
+ ? model.remaining_fraction
850
+ : Number.POSITIVE_INFINITY,
851
+ }))
852
+ .filter((entry) => entry.priority !== null);
853
+
854
+ if (!candidates.length) return null;
855
+ candidates.sort((a, b) => {
856
+ if (a.priority !== b.priority) return a.priority - b.priority;
857
+ return a.remaining - b.remaining;
858
+ });
859
+ return candidates[0].model;
860
+ }
861
+
862
+ function makeAntigravityWindow(model) {
863
+ if (!model) return null;
864
+ const remaining = typeof model.remaining_fraction === "number" ? model.remaining_fraction * 100 : 0;
865
+ return buildWindow({
866
+ usedPercent: 100 - remaining,
867
+ resetAt: model.reset_at,
868
+ });
869
+ }
870
+
871
+ function normalizeAntigravityResponse(body, { fallbackToConfigs = false } = {}) {
872
+ if (!antigravityCodeIsOk(body?.code)) {
873
+ throw new Error(`Antigravity API error: ${body?.code}`);
874
+ }
875
+
876
+ const userStatus = body?.userStatus || null;
877
+ const configs = fallbackToConfigs
878
+ ? body?.clientModelConfigs
879
+ : userStatus?.cascadeModelConfigData?.clientModelConfigs;
880
+ const models = parseAntigravityModelConfigs(configs);
881
+ if (!models.length) {
882
+ throw new Error("Could not parse Antigravity quota: no quota models available.");
883
+ }
884
+
885
+ const primary = chooseAntigravityModel(models, "claude");
886
+ const secondary = chooseAntigravityModel(models, "gemini_pro");
887
+ const tertiary = chooseAntigravityModel(models, "gemini_flash");
888
+ const fallback = !primary && !secondary && !tertiary
889
+ ? [...models].sort((a, b) => {
890
+ const aRemaining = typeof a.remaining_fraction === "number" ? a.remaining_fraction : Number.POSITIVE_INFINITY;
891
+ const bRemaining = typeof b.remaining_fraction === "number" ? b.remaining_fraction : Number.POSITIVE_INFINITY;
892
+ return aRemaining - bRemaining;
893
+ })[0]
894
+ : null;
895
+
896
+ return {
897
+ account_email: typeof userStatus?.email === "string" ? userStatus.email : null,
898
+ account_plan:
899
+ userStatus?.planStatus?.planInfo?.planDisplayName
900
+ || userStatus?.planStatus?.planInfo?.displayName
901
+ || userStatus?.planStatus?.planInfo?.productName
902
+ || userStatus?.planStatus?.planInfo?.planName
903
+ || userStatus?.planStatus?.planInfo?.planShortName
904
+ || null,
905
+ primary_window: makeAntigravityWindow(primary || fallback),
906
+ secondary_window: makeAntigravityWindow(secondary),
907
+ tertiary_window: makeAntigravityWindow(tertiary),
908
+ };
909
+ }
910
+
911
+ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn } = {}) {
912
+ try {
913
+ await requestLocalJson({
914
+ scheme: "https",
915
+ port,
916
+ path: "/exa.language_server_pb.LanguageServerService/GetUnleashData",
917
+ body: antigravityUnleashBody(),
918
+ csrfToken,
919
+ timeoutMs,
920
+ requestFn,
921
+ });
922
+ return true;
923
+ } catch (_error) {
924
+ return false;
925
+ }
926
+ }
927
+
928
+ async function fetchAntigravityLimits({ commandRunner, requestFn, timeoutMs = 8000 } = {}) {
929
+ const processInfo = detectAntigravityProcess({ commandRunner });
930
+ if (!processInfo.configured) {
931
+ return { configured: false };
932
+ }
933
+ if (processInfo.error) {
934
+ return { configured: true, error: processInfo.error };
935
+ }
936
+
937
+ try {
938
+ const ports = listAntigravityPorts(processInfo.pid, { commandRunner });
939
+ let workingPort = null;
940
+ for (const port of ports) {
941
+ if (await probeAntigravityPort(port, processInfo.csrfToken, { timeoutMs, requestFn })) {
942
+ workingPort = port;
943
+ break;
944
+ }
945
+ }
946
+ if (!workingPort) {
947
+ throw new Error("Antigravity port detection failed: no working API port found");
948
+ }
949
+
950
+ try {
951
+ const userStatus = await requestLocalJson({
952
+ scheme: "https",
953
+ port: workingPort,
954
+ path: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
955
+ body: antigravityDefaultBody(),
956
+ csrfToken: processInfo.csrfToken,
957
+ timeoutMs,
958
+ requestFn,
959
+ });
960
+ return {
961
+ configured: true,
962
+ error: null,
963
+ ...normalizeAntigravityResponse(userStatus),
964
+ };
965
+ } catch (primaryError) {
966
+ const fallbackPort =
967
+ Number.isFinite(processInfo.extensionPort) && processInfo.extensionPort > 0
968
+ ? processInfo.extensionPort
969
+ : workingPort;
970
+ const modelConfigs = await requestLocalJson({
971
+ scheme: fallbackPort === workingPort ? "https" : "http",
972
+ port: fallbackPort,
973
+ path: "/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs",
974
+ body: antigravityDefaultBody(),
975
+ csrfToken: processInfo.csrfToken,
976
+ timeoutMs,
977
+ requestFn,
978
+ });
979
+ return {
980
+ configured: true,
981
+ error: null,
982
+ ...normalizeAntigravityResponse(modelConfigs, { fallbackToConfigs: true }),
983
+ };
984
+ }
985
+ } catch (error) {
986
+ const message = error?.message === "timeout"
987
+ ? "Antigravity quota request timed out."
988
+ : error?.message || "Unknown error";
989
+ return {
990
+ configured: true,
991
+ error: message,
992
+ };
993
+ }
994
+ }
995
+
996
+ async function getUsageLimits({
997
+ home,
998
+ env,
999
+ platform,
1000
+ securityRunner,
1001
+ fetchImpl = fetch,
1002
+ commandRunner,
1003
+ requestFn,
1004
+ now = new Date(),
1005
+ } = {}) {
1006
+ const nowMs = Date.now();
1007
+ if (cache.data && nowMs - cache.fetchedAt < CACHE_TTL_MS) {
1008
+ return cache.data;
1009
+ }
1010
+
1011
+ const [claudeToken, codexToken] = await Promise.all([
1012
+ Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner })),
1013
+ readCodexAccessToken({ home, env }),
1014
+ ]);
1015
+
1016
+ const [claudeResult, codexResult, cursor, gemini, kiro, antigravity] = await Promise.all([
1017
+ claudeToken
1018
+ ? fetchClaudeUsageLimits(claudeToken, { fetchImpl }).then(
1019
+ (value) => ({ status: "fulfilled", value }),
1020
+ (reason) => ({ status: "rejected", reason }),
1021
+ )
1022
+ : Promise.resolve(null),
1023
+ codexToken
1024
+ ? fetchCodexUsageLimits(codexToken, { fetchImpl }).then(
1025
+ (value) => ({ status: "fulfilled", value }),
1026
+ (reason) => ({ status: "rejected", reason }),
1027
+ )
1028
+ : Promise.resolve(null),
1029
+ fetchCursorLimits({ home, fetchImpl }),
1030
+ fetchGeminiLimits({ home, env, fetchImpl, commandRunner }),
1031
+ Promise.resolve().then(() => fetchKiroLimits({ commandRunner, now })),
1032
+ fetchAntigravityLimits({ commandRunner, requestFn }),
1033
+ ]);
1034
+
1035
+ let claude;
1036
+ if (!claudeToken) {
1037
+ claude = { configured: false };
1038
+ } else if (!claudeResult || claudeResult.status === "rejected") {
1039
+ claude = { configured: true, error: claudeResult?.reason?.message || "Unknown error" };
1040
+ } else {
1041
+ claude = {
1042
+ configured: true,
1043
+ error: null,
1044
+ five_hour: claudeResult.value.five_hour,
1045
+ seven_day: claudeResult.value.seven_day,
1046
+ seven_day_opus: claudeResult.value.seven_day_opus,
1047
+ extra_usage: claudeResult.value.extra_usage,
1048
+ };
1049
+ }
1050
+
1051
+ let codex;
1052
+ if (!codexToken) {
1053
+ codex = { configured: false };
1054
+ } else if (!codexResult || codexResult.status === "rejected") {
1055
+ codex = { configured: true, error: codexResult?.reason?.message || "Unknown error" };
1056
+ } else {
1057
+ codex = {
1058
+ configured: true,
1059
+ error: null,
1060
+ primary_window: codexResult.value.primary_window,
1061
+ secondary_window: codexResult.value.secondary_window,
1062
+ };
1063
+ }
1064
+
1065
+ const data = {
1066
+ fetched_at: new Date(nowMs).toISOString(),
1067
+ claude,
1068
+ codex,
1069
+ cursor,
1070
+ gemini,
1071
+ kiro,
1072
+ antigravity,
1073
+ };
1074
+
1075
+ cache = { data, fetchedAt: nowMs };
1076
+ return data;
1077
+ }
1078
+
1079
+ function resetUsageLimitsCache() {
1080
+ cache = { data: null, fetchedAt: 0 };
1081
+ }
1082
+
1083
+ module.exports = {
1084
+ getUsageLimits,
1085
+ resetUsageLimitsCache,
1086
+ normalizeCursorUsageSummary,
1087
+ normalizeGeminiQuotaResponse,
1088
+ parseKiroUsageOutput,
1089
+ normalizeAntigravityResponse,
1090
+ parseListeningPorts,
1091
+ detectAntigravityProcess,
1092
+ };