tokentracker-cli 0.5.97 → 0.5.99

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.
@@ -1,5 +1,6 @@
1
1
  const cp = require("node:child_process");
2
2
  const fs = require("node:fs");
3
+ const os = require("node:os");
3
4
  const path = require("node:path");
4
5
  const http = require("node:http");
5
6
  const https = require("node:https");
@@ -14,6 +15,7 @@ const {
14
15
  // 2-minute in-memory cache
15
16
  let cache = { data: null, fetchedAt: 0 };
16
17
  const CACHE_TTL_MS = 2 * 60 * 1000;
18
+ const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
17
19
 
18
20
  function clampPercent(value) {
19
21
  if (value === null || value === undefined || value === "") return null;
@@ -52,14 +54,44 @@ function sleepMs(ms) {
52
54
  return new Promise((resolve) => setTimeout(resolve, ms));
53
55
  }
54
56
 
55
- async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch } = {}) {
57
+ function mergeAbortSignals(signalA, signalB) {
58
+ if (!signalA) return signalB;
59
+ if (!signalB) return signalA;
60
+ if (typeof AbortSignal.any === "function") {
61
+ return AbortSignal.any([signalA, signalB]);
62
+ }
63
+ return signalA;
64
+ }
65
+
66
+ function withFetchTimeout(fetchImpl, timeoutMs) {
67
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return fetchImpl;
68
+ return (url, options = {}) => {
69
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
70
+ return fetchImpl(url, {
71
+ ...options,
72
+ signal: mergeAbortSignals(options.signal, timeoutSignal),
73
+ });
74
+ };
75
+ }
76
+
77
+ function withProviderTimeout(promise, label, timeoutMs) {
78
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
79
+ let timer;
80
+ const timeout = new Promise((_, reject) => {
81
+ timer = setTimeout(() => {
82
+ reject(new Error(`${label} usage request timed out.`));
83
+ }, timeoutMs);
84
+ });
85
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
86
+ }
87
+
88
+ async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch, maxAttempts = 3 } = {}) {
56
89
  const url = "https://api.anthropic.com/api/oauth/usage";
57
90
  const headers = {
58
91
  Authorization: `Bearer ${accessToken}`,
59
92
  "anthropic-beta": "oauth-2025-04-20",
60
93
  Accept: "application/json",
61
94
  };
62
- const maxAttempts = 3;
63
95
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
64
96
  const res = await fetchImpl(url, { method: "GET", headers });
65
97
  if (res.status === 401) {
@@ -202,6 +234,190 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
202
234
  }
203
235
  }
204
236
 
237
+ function resolveKimiHome({ home, env } = {}) {
238
+ const explicit = typeof env?.KIMI_HOME === "string" ? env.KIMI_HOME.trim() : "";
239
+ return explicit ? path.resolve(explicit) : path.join(home || os.homedir(), ".kimi");
240
+ }
241
+
242
+ function loadKimiCredentials({ home, env } = {}) {
243
+ const kimiHome = resolveKimiHome({ home, env });
244
+ const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
245
+ if (!fs.existsSync(credsPath)) return null;
246
+ try {
247
+ return JSON.parse(fs.readFileSync(credsPath, "utf8"));
248
+ } catch (_error) {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ function saveKimiCredentials(creds, { home, env } = {}) {
254
+ const kimiHome = resolveKimiHome({ home, env });
255
+ const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
256
+ fs.mkdirSync(path.dirname(credsPath), { recursive: true });
257
+ fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2));
258
+ }
259
+
260
+ function hasKimiConfig({ home, env } = {}) {
261
+ return fs.existsSync(path.join(resolveKimiHome({ home, env }), "config.toml"));
262
+ }
263
+
264
+ function kimiNumber(value) {
265
+ if (value === null || value === undefined || value === "") return null;
266
+ const n = Number(value);
267
+ return Number.isFinite(n) ? n : null;
268
+ }
269
+
270
+ function kimiResetTime(value) {
271
+ if (typeof value !== "string" || !value) return null;
272
+ const ts = Date.parse(value);
273
+ return Number.isFinite(ts) && ts > 0 ? new Date(ts).toISOString() : null;
274
+ }
275
+
276
+ function kimiWindowFromUsage(data) {
277
+ if (!data || typeof data !== "object") return null;
278
+ const limit = kimiNumber(data.limit);
279
+ if (!Number.isFinite(limit) || limit <= 0) return null;
280
+ let used = kimiNumber(data.used);
281
+ if (used === null) {
282
+ const remaining = kimiNumber(data.remaining);
283
+ if (remaining !== null) used = limit - remaining;
284
+ }
285
+ if (!Number.isFinite(used)) return null;
286
+ return buildWindow({
287
+ usedPercent: (used / limit) * 100,
288
+ resetAt: kimiResetTime(data.resetTime || data.reset_at || data.resetAt),
289
+ });
290
+ }
291
+
292
+ function normalizeKimiUsageResponse(body) {
293
+ const firstLimit = Array.isArray(body?.limits) ? body.limits[0] : null;
294
+ const detail = firstLimit?.detail && typeof firstLimit.detail === "object" ? firstLimit.detail : firstLimit;
295
+ const parallelLimit = kimiNumber(body?.parallel?.limit);
296
+
297
+ return {
298
+ membership_level: typeof body?.user?.membership?.level === "string" ? body.user.membership.level : null,
299
+ subscription_type: typeof body?.subType === "string" ? body.subType : null,
300
+ parallel_limit: parallelLimit !== null ? parallelLimit : null,
301
+ primary_window: kimiWindowFromUsage(body?.usage),
302
+ secondary_window: kimiWindowFromUsage(detail),
303
+ tertiary_window: kimiWindowFromUsage(body?.totalQuota),
304
+ };
305
+ }
306
+
307
+ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
308
+ const expiresAt = Number(creds?.expires_at);
309
+ if (!Number.isFinite(expiresAt) || expiresAt <= 0) return false;
310
+ return expiresAt * 1000 <= nowMs + 30_000;
311
+ }
312
+
313
+ async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fetch } = {}) {
314
+ if (typeof refreshToken !== "string" || !refreshToken.trim()) {
315
+ throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
316
+ }
317
+
318
+ const body = new URLSearchParams({
319
+ client_id: "17e5f671-d194-4dfb-9706-5516cb48c098",
320
+ grant_type: "refresh_token",
321
+ refresh_token: refreshToken,
322
+ });
323
+
324
+ const res = await fetchImpl("https://auth.kimi.com/api/oauth/token", {
325
+ method: "POST",
326
+ headers: {
327
+ "Content-Type": "application/x-www-form-urlencoded",
328
+ "X-Msh-Platform": "kimi_cli",
329
+ },
330
+ body,
331
+ });
332
+ if (res.status === 401 || res.status === 403) {
333
+ throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
334
+ }
335
+ if (!res.ok) {
336
+ throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
337
+ }
338
+
339
+ const json = await res.json();
340
+ if (!json?.access_token) {
341
+ throw new Error("Could not parse Kimi token refresh response");
342
+ }
343
+
344
+ const expiresIn = Number(json.expires_in);
345
+ const next = {
346
+ access_token: String(json.access_token),
347
+ refresh_token: String(json.refresh_token || refreshToken),
348
+ expires_at: Date.now() / 1000 + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 900),
349
+ scope: String(json.scope || "kimi-code"),
350
+ token_type: String(json.token_type || "Bearer"),
351
+ expires_in: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 900,
352
+ };
353
+ saveKimiCredentials(next, { home, env });
354
+ return next.access_token;
355
+ }
356
+
357
+ async function fetchKimiUsage(accessToken, { fetchImpl = fetch } = {}) {
358
+ const res = await fetchImpl("https://api.kimi.com/coding/v1/usages", {
359
+ method: "GET",
360
+ headers: {
361
+ Authorization: `Bearer ${accessToken}`,
362
+ Accept: "application/json",
363
+ },
364
+ });
365
+ if (res.status === 401) {
366
+ throw new Error("token_expired");
367
+ }
368
+ if (!res.ok) {
369
+ throw new Error(`Kimi API returned ${res.status}`);
370
+ }
371
+ return res.json();
372
+ }
373
+
374
+ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
375
+ if (!hasKimiConfig({ home, env })) {
376
+ return { configured: false };
377
+ }
378
+ const creds = loadKimiCredentials({ home, env });
379
+ let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
380
+ if (!accessToken) {
381
+ return { configured: false };
382
+ }
383
+ try {
384
+ if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
385
+ accessToken = await refreshKimiAccessToken({
386
+ refreshToken: creds.refresh_token,
387
+ home,
388
+ env,
389
+ fetchImpl,
390
+ });
391
+ }
392
+ let body;
393
+ try {
394
+ body = await fetchKimiUsage(accessToken, { fetchImpl });
395
+ } catch (error) {
396
+ if (error?.message === "token_expired" && creds?.refresh_token) {
397
+ accessToken = await refreshKimiAccessToken({
398
+ refreshToken: creds.refresh_token,
399
+ home,
400
+ env,
401
+ fetchImpl,
402
+ });
403
+ body = await fetchKimiUsage(accessToken, { fetchImpl });
404
+ } else {
405
+ throw error;
406
+ }
407
+ }
408
+ return {
409
+ configured: true,
410
+ error: null,
411
+ ...normalizeKimiUsageResponse(body),
412
+ };
413
+ } catch (error) {
414
+ return {
415
+ configured: true,
416
+ error: error?.message || "Unknown error",
417
+ };
418
+ }
419
+ }
420
+
205
421
  function resolveGeminiHome({ home, env } = {}) {
206
422
  const explicit = typeof env?.GEMINI_HOME === "string" ? env.GEMINI_HOME.trim() : "";
207
423
  return explicit ? path.resolve(explicit) : path.join(home, ".gemini");
@@ -229,39 +445,83 @@ function loadGeminiCredentials({ home, env } = {}) {
229
445
  }
230
446
  }
231
447
 
232
- function extractGeminiOauthClientCredentials({ commandRunner } = {}) {
233
- const result = runCommand(commandRunner, "which", ["gemini"], { timeout: 2000 });
234
- const geminiPath = typeof result?.stdout === "string" ? result.stdout.trim() : "";
235
- if (!geminiPath) return null;
236
-
237
- let realPath = geminiPath;
448
+ function resolveSymlinkOnce(filePath) {
238
449
  try {
239
- const resolved = fs.readlinkSync(geminiPath);
240
- realPath = path.isAbsolute(resolved)
450
+ const resolved = fs.readlinkSync(filePath);
451
+ return path.isAbsolute(resolved)
241
452
  ? resolved
242
- : path.join(path.dirname(geminiPath), resolved);
453
+ : path.join(path.dirname(filePath), resolved);
454
+ } catch (_error) {
455
+ return filePath;
456
+ }
457
+ }
458
+
459
+ function expandGeminiExecutableCandidates({ home } = {}) {
460
+ const candidates = [];
461
+ const add = (filePath) => {
462
+ if (typeof filePath === "string" && filePath && !candidates.includes(filePath)) {
463
+ candidates.push(filePath);
464
+ }
465
+ };
466
+
467
+ const nvmDir = path.join(home || os.homedir(), ".nvm", "versions", "node");
468
+ try {
469
+ for (const version of fs.readdirSync(nvmDir)) {
470
+ add(path.join(nvmDir, version, "bin", "gemini"));
471
+ }
243
472
  } catch (_error) {}
244
473
 
245
- const binDir = path.dirname(realPath);
246
- const baseDir = path.dirname(binDir);
247
- const candidates = [
248
- path.join(baseDir, "libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
249
- path.join(baseDir, "lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
250
- path.join(baseDir, "share/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
251
- path.join(baseDir, "../gemini-cli-core/dist/src/code_assist/oauth2.js"),
252
- path.join(baseDir, "node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
474
+ add(path.join(path.dirname(resolveSymlinkOnce(process.execPath)), "gemini"));
475
+ add("/opt/homebrew/bin/gemini");
476
+ add("/usr/local/bin/gemini");
477
+
478
+ return candidates;
479
+ }
480
+
481
+ function extractGeminiOauthClientCredentials({ commandRunner, home } = {}) {
482
+ const result = runCommand(commandRunner, "which", ["gemini"], { timeout: 2000 });
483
+ const geminiPath = typeof result?.stdout === "string" ? result.stdout.trim() : "";
484
+
485
+ const geminiPaths = [
486
+ ...(geminiPath ? [geminiPath] : []),
487
+ ...expandGeminiExecutableCandidates({ home }),
253
488
  ];
254
489
 
255
- for (const candidate of candidates) {
256
- if (!fs.existsSync(candidate)) continue;
257
- try {
258
- const content = fs.readFileSync(candidate, "utf8");
259
- const clientId = content.match(/OAUTH_CLIENT_ID\s*=\s*['"]([\w\-\.]+)['"]\s*;/)?.[1] || null;
260
- const clientSecret = content.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([\w\-]+)['"]\s*;/)?.[1] || null;
261
- if (clientId && clientSecret) {
262
- return { clientId, clientSecret };
263
- }
264
- } catch (_error) {}
490
+ for (const candidateGeminiPath of geminiPaths) {
491
+ if (!fs.existsSync(candidateGeminiPath)) continue;
492
+ const realPath = resolveSymlinkOnce(candidateGeminiPath);
493
+ const binDir = path.dirname(realPath);
494
+ const baseDir = path.dirname(binDir);
495
+ const bundleDir = path.dirname(realPath);
496
+ const candidates = [
497
+ path.join(baseDir, "libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
498
+ path.join(baseDir, "lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
499
+ path.join(baseDir, "share/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
500
+ path.join(baseDir, "../gemini-cli-core/dist/src/code_assist/oauth2.js"),
501
+ path.join(baseDir, "node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"),
502
+ ];
503
+ if (path.basename(bundleDir) === "bundle") {
504
+ candidates.push(realPath);
505
+ try {
506
+ for (const file of fs.readdirSync(bundleDir)) {
507
+ if (/^chunk-.*\.js$/.test(file)) {
508
+ candidates.push(path.join(bundleDir, file));
509
+ }
510
+ }
511
+ } catch (_error) {}
512
+ }
513
+
514
+ for (const candidate of candidates) {
515
+ if (!fs.existsSync(candidate)) continue;
516
+ try {
517
+ const content = fs.readFileSync(candidate, "utf8");
518
+ const clientId = content.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/)?.[1] || null;
519
+ const clientSecret = content.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/)?.[1] || null;
520
+ if (clientId && clientSecret) {
521
+ return { clientId, clientSecret };
522
+ }
523
+ } catch (_error) {}
524
+ }
265
525
  }
266
526
  return null;
267
527
  }
@@ -273,7 +533,7 @@ async function refreshGeminiAccessToken({
273
533
  fetchImpl = fetch,
274
534
  commandRunner,
275
535
  }) {
276
- const oauthClient = extractGeminiOauthClientCredentials({ commandRunner });
536
+ const oauthClient = extractGeminiOauthClientCredentials({ commandRunner, home });
277
537
  if (!oauthClient?.clientId || !oauthClient?.clientSecret) {
278
538
  throw new Error("Gemini API error: Could not find Gemini CLI OAuth configuration");
279
539
  }
@@ -990,14 +1250,15 @@ function antigravityCodeIsOk(code) {
990
1250
 
991
1251
  function parseAntigravityDate(value) {
992
1252
  if (typeof value === "string" && value) {
993
- const iso = Date.parse(value);
994
- if (Number.isFinite(iso)) return new Date(iso).toISOString();
995
1253
  const numeric = Number(value);
996
- if (Number.isFinite(numeric)) {
1254
+ if (Number.isFinite(numeric) && numeric > 0) {
997
1255
  return new Date(numeric * 1000).toISOString();
998
1256
  }
1257
+ if (Number.isFinite(numeric)) return null;
1258
+ const iso = Date.parse(value);
1259
+ if (Number.isFinite(iso) && iso > 0) return new Date(iso).toISOString();
999
1260
  }
1000
- if (typeof value === "number" && Number.isFinite(value)) {
1261
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1001
1262
  return new Date(value * 1000).toISOString();
1002
1263
  }
1003
1264
  return null;
@@ -1203,6 +1464,7 @@ async function getUsageLimits({
1203
1464
  commandRunner,
1204
1465
  requestFn,
1205
1466
  now = new Date(),
1467
+ providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
1206
1468
  } = {}) {
1207
1469
  const nowMs = Date.now();
1208
1470
  if (cache.data && nowMs - cache.fetchedAt < CACHE_TTL_MS) {
@@ -1214,24 +1476,30 @@ async function getUsageLimits({
1214
1476
  readCodexAccessToken({ home, env }),
1215
1477
  ]);
1216
1478
 
1217
- const [claudeResult, codexResult, cursor, gemini, kiro, antigravity, copilot] = await Promise.all([
1479
+ const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
1480
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot] = await Promise.all([
1218
1481
  claudeToken
1219
- ? fetchClaudeUsageLimits(claudeToken, { fetchImpl }).then(
1482
+ ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
1220
1483
  (value) => ({ status: "fulfilled", value }),
1221
1484
  (reason) => ({ status: "rejected", reason }),
1222
1485
  )
1223
1486
  : Promise.resolve(null),
1224
1487
  codexToken
1225
- ? fetchCodexUsageLimits(codexToken, { fetchImpl }).then(
1488
+ ? withProviderTimeout(fetchCodexUsageLimits(codexToken, { fetchImpl: providerFetch }), "Codex", providerTimeoutMs).then(
1226
1489
  (value) => ({ status: "fulfilled", value }),
1227
1490
  (reason) => ({ status: "rejected", reason }),
1228
1491
  )
1229
1492
  : Promise.resolve(null),
1230
- fetchCursorLimits({ home, fetchImpl }),
1231
- fetchGeminiLimits({ home, env, fetchImpl, commandRunner }),
1493
+ withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
1494
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1495
+ withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
1496
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1497
+ withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
1498
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1232
1499
  Promise.resolve().then(() => fetchKiroLimits({ commandRunner, now })),
1233
1500
  fetchAntigravityLimits({ commandRunner, requestFn }),
1234
- fetchCopilotLimits({ home, env, fetchImpl }),
1501
+ withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch }), "GitHub Copilot", providerTimeoutMs)
1502
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1235
1503
  ]);
1236
1504
 
1237
1505
  let claude;
@@ -1269,6 +1537,7 @@ async function getUsageLimits({
1269
1537
  claude,
1270
1538
  codex,
1271
1539
  cursor,
1540
+ kimi,
1272
1541
  gemini,
1273
1542
  kiro,
1274
1543
  antigravity,
@@ -1286,8 +1555,11 @@ function resetUsageLimitsCache() {
1286
1555
  module.exports = {
1287
1556
  getUsageLimits,
1288
1557
  resetUsageLimitsCache,
1558
+ extractGeminiOauthClientCredentials,
1559
+ loadKimiCredentials,
1289
1560
  normalizeCursorUsageSummary,
1290
1561
  normalizeGeminiQuotaResponse,
1562
+ normalizeKimiUsageResponse,
1291
1563
  parseKiroUsageOutput,
1292
1564
  normalizeAntigravityResponse,
1293
1565
  parseListeningPorts,