tokentracker-cli 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dashboard/dist/assets/{Card-ANC9ZmIT.js → Card-jA08WeEw.js} +1 -1
  4. package/dashboard/dist/assets/{DashboardPage-DpZaCksZ.js → DashboardPage-chDVOYmG.js} +1 -1
  5. package/dashboard/dist/assets/{FadeIn-WXGyOn0H.js → FadeIn-DqSYXuUL.js} +1 -1
  6. package/dashboard/dist/assets/{HeaderGithubStar-DUkE0Dwd.js → HeaderGithubStar-C11rWv0B.js} +1 -1
  7. package/dashboard/dist/assets/{IpCheckPage-BPF8eGpg.js → IpCheckPage-CkEZ9yLK.js} +1 -1
  8. package/dashboard/dist/assets/{LandingPage-0uTpqpAU.js → LandingPage-BgckTHRQ.js} +1 -1
  9. package/dashboard/dist/assets/{LeaderboardPage-DzxRJEzb.js → LeaderboardPage-BCNW7UWp.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardProfilePage-C3_oUxhG.js → LeaderboardProfilePage-BLATxMt-.js} +1 -1
  11. package/dashboard/dist/assets/{LimitsPage-BVuvoeY9.js → LimitsPage-arF--WgR.js} +1 -1
  12. package/dashboard/dist/assets/{LoginPage-CfkNRmT6.js → LoginPage-DpoFP0va.js} +1 -1
  13. package/dashboard/dist/assets/{PopoverPopup-CfxiYbJm.js → PopoverPopup-kdgc2H6C.js} +1 -1
  14. package/dashboard/dist/assets/{ProviderIcon-DiPzAed2.js → ProviderIcon-DV5r9qqP.js} +1 -1
  15. package/dashboard/dist/assets/{SettingsPage-Devu7beE.js → SettingsPage-Bb22ORmU.js} +1 -1
  16. package/dashboard/dist/assets/{SkillsPage-CSe8fW4V.js → SkillsPage-xhtBqVKC.js} +1 -1
  17. package/dashboard/dist/assets/{WidgetsPage-BrLp5YLk.js → WidgetsPage-CUoSVDET.js} +1 -1
  18. package/dashboard/dist/assets/{chevron-down-nFF6Yj_r.js → chevron-down-DYb2EChD.js} +1 -1
  19. package/dashboard/dist/assets/{download-DhSZ--68.js → download-C-_8o6dh.js} +1 -1
  20. package/dashboard/dist/assets/{leaderboard-columns-CvFdXrw5.js → leaderboard-columns-BgzBlYo7.js} +1 -1
  21. package/dashboard/dist/assets/{main-DtrPNYb7.js → main-11hApDak.js} +3 -3
  22. package/dashboard/dist/assets/{use-limits-display-prefs-Yy8t7tbB.js → use-limits-display-prefs-BeGKWUuk.js} +1 -1
  23. package/dashboard/dist/assets/{use-native-settings-uemf9RSH.js → use-native-settings-nTTHktn0.js} +1 -1
  24. package/dashboard/dist/assets/{use-reduced-motion-DH8DxE18.js → use-reduced-motion-DU8Gm6j1.js} +1 -1
  25. package/dashboard/dist/assets/{use-usage-limits-C3vUT6PH.js → use-usage-limits-DTPmEB8Y.js} +1 -1
  26. package/dashboard/dist/index.html +1 -1
  27. package/dashboard/dist/share.html +1 -1
  28. package/package.json +1 -1
  29. package/src/commands/init.js +9 -5
  30. package/src/commands/serve.js +0 -12
  31. package/src/commands/sync.js +370 -7
  32. package/src/lib/grok-hook.js +86 -7
  33. package/src/lib/pricing/curated-overrides.json +1 -1
  34. package/src/lib/pricing/seed-snapshot.json +1 -1
  35. package/src/lib/rollout.js +403 -140
  36. package/src/lib/subscriptions.js +92 -40
  37. package/src/lib/usage-limits.js +104 -19
@@ -10,6 +10,10 @@ const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin")
10
10
  const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
11
11
  const MACOS_SECURITY_BIN = "/usr/bin/security";
12
12
  const CLAUDE_CODE_KEYCHAIN_SERVICES = ["Claude Code-credentials"];
13
+ // On Linux, Claude Code persists the same OAuth payload as a plain JSON file
14
+ // (~/.claude/.credentials.json) instead of the macOS Keychain. The payload
15
+ // shape is identical: { claudeAiOauth: { accessToken, subscriptionType, ... } }
16
+ const CLAUDE_CODE_CREDENTIALS_FILE = ".credentials.json";
13
17
 
14
18
  function normalizeString(value) {
15
19
  if (typeof value !== "string") return null;
@@ -193,25 +197,56 @@ function readMacosKeychainPassword({ service, securityRunner, timeoutMs } = {})
193
197
  return trimmed.length > 0 ? trimmed : null;
194
198
  }
195
199
 
196
- function detectClaudeCodeCredentialsPresence({ platform, securityRunner } = {}) {
197
- if (platform !== "darwin") return null;
198
-
199
- for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
200
- const present = probeMacosKeychainGenericPassword({
201
- service,
202
- securityRunner,
203
- });
204
- if (!present) continue;
200
+ function readClaudeCodeCredentialsLinuxFile({ home, fsReader } = {}) {
201
+ const homeDir = typeof home === "string" && home ? home : os.homedir();
202
+ const credPath = path.join(homeDir, ".claude", CLAUDE_CODE_CREDENTIALS_FILE);
203
+ const reader = typeof fsReader === "function" ? fsReader : fs.readFileSync;
204
+ try {
205
+ return reader(credPath, "utf8");
206
+ } catch (_e) {
207
+ return null;
208
+ }
209
+ }
205
210
 
206
- // Existence-only probe: do not read secrets or infer paid tier.
207
- return {
208
- tool: "claude",
209
- provider: "anthropic",
210
- product: "credentials",
211
- planType: "present",
212
- };
211
+ function detectClaudeCodeCredentialsPresence({ platform, securityRunner, home, fsReader } = {}) {
212
+ if (platform === "darwin") {
213
+ for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
214
+ const present = probeMacosKeychainGenericPassword({
215
+ service,
216
+ securityRunner,
217
+ });
218
+ if (!present) continue;
219
+
220
+ // Existence-only probe: do not read secrets or infer paid tier.
221
+ return {
222
+ tool: "claude",
223
+ provider: "anthropic",
224
+ product: "credentials",
225
+ planType: "present",
226
+ };
227
+ }
228
+ return null;
213
229
  }
214
230
 
231
+ if (platform !== "linux") return null;
232
+
233
+ // Linux: credentials live in ~/.claude/.credentials.json (mode 0600).
234
+ // Existence-only: just check that the file is readable and contains the OAuth key.
235
+ const raw = readClaudeCodeCredentialsLinuxFile({ home, fsReader });
236
+ if (!raw) return null;
237
+ try {
238
+ const payload = JSON.parse(raw);
239
+ if (payload?.claudeAiOauth && typeof payload.claudeAiOauth === "object") {
240
+ return {
241
+ tool: "claude",
242
+ provider: "anthropic",
243
+ product: "credentials",
244
+ planType: "present",
245
+ };
246
+ }
247
+ } catch (_e) {
248
+ // fall through
249
+ }
215
250
  return null;
216
251
  }
217
252
 
@@ -228,16 +263,21 @@ function extractClaudeKeychainSubscription(payload) {
228
263
  return { subscriptionType, rateLimitTier };
229
264
  }
230
265
 
231
- function detectClaudeCodeSubscriptionDetails({ platform, securityRunner } = {}) {
232
- if (platform !== "darwin") return null;
233
-
234
- for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
235
- const raw = readMacosKeychainPassword({
236
- service,
237
- securityRunner,
238
- });
239
- if (!raw) continue;
266
+ function detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home, fsReader } = {}) {
267
+ const rawPayloads = [];
268
+ if (platform === "darwin") {
269
+ for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
270
+ const raw = readMacosKeychainPassword({ service, securityRunner });
271
+ if (raw) rawPayloads.push(raw);
272
+ }
273
+ } else if (platform === "linux") {
274
+ const raw = readClaudeCodeCredentialsLinuxFile({ home, fsReader });
275
+ if (raw) rawPayloads.push(raw);
276
+ } else {
277
+ return null;
278
+ }
240
279
 
280
+ for (const raw of rawPayloads) {
241
281
  let payload;
242
282
  try {
243
283
  payload = JSON.parse(raw);
@@ -277,14 +317,14 @@ async function collectLocalSubscriptions({
277
317
  if (opencode) out.push(opencode);
278
318
 
279
319
  if (probeKeychainDetails) {
280
- const claude = detectClaudeCodeSubscriptionDetails({ platform, securityRunner });
320
+ const claude = detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home });
281
321
  if (claude) out.push(claude);
282
322
  else if (probeKeychain) {
283
- const present = detectClaudeCodeCredentialsPresence({ platform, securityRunner });
323
+ const present = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
284
324
  if (present) out.push(present);
285
325
  }
286
326
  } else if (probeKeychain) {
287
- const claude = detectClaudeCodeCredentialsPresence({ platform, securityRunner });
327
+ const claude = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
288
328
  if (claude) out.push(claude);
289
329
  }
290
330
 
@@ -314,20 +354,32 @@ async function detectOpenclawSessionIntegration({ home, env }) {
314
354
  };
315
355
  }
316
356
 
317
- function readClaudeCodeAccessToken({ platform, securityRunner } = {}) {
318
- if (platform !== "darwin") return null;
319
-
320
- for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
321
- try {
322
- const raw = readMacosKeychainPassword({ service, securityRunner });
323
- if (!raw) continue;
324
- const payload = JSON.parse(raw);
325
- return normalizeString(payload?.claudeAiOauth?.accessToken);
326
- } catch (_e) {
327
- continue;
357
+ function readClaudeCodeAccessToken({ platform, securityRunner, home, fsReader } = {}) {
358
+ if (platform === "darwin") {
359
+ for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
360
+ try {
361
+ const raw = readMacosKeychainPassword({ service, securityRunner });
362
+ if (!raw) continue;
363
+ const payload = JSON.parse(raw);
364
+ return normalizeString(payload?.claudeAiOauth?.accessToken);
365
+ } catch (_e) {
366
+ continue;
367
+ }
328
368
  }
369
+ return null;
370
+ }
371
+
372
+ if (platform !== "linux") return null;
373
+
374
+ // Linux: Claude Code stores the OAuth payload as a JSON file with mode 0600.
375
+ const raw = readClaudeCodeCredentialsLinuxFile({ home, fsReader });
376
+ if (!raw) return null;
377
+ try {
378
+ const payload = JSON.parse(raw);
379
+ return normalizeString(payload?.claudeAiOauth?.accessToken);
380
+ } catch (_e) {
381
+ return null;
329
382
  }
330
- return null;
331
383
  }
332
384
 
333
385
  async function readCodexAccessToken({ home, env } = {}) {
@@ -25,6 +25,9 @@ const {
25
25
  let cache = { data: null, fetchedAt: 0 };
26
26
  const CACHE_TTL_MS = 2 * 60 * 1000;
27
27
  const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
28
+ const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
29
+ const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
30
+ const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
28
31
 
29
32
  function clampPercent(value) {
30
33
  if (value === null || value === undefined || value === "") return null;
@@ -808,9 +811,15 @@ function runCommand(commandRunner, command, args, options = {}) {
808
811
  });
809
812
  }
810
813
 
811
- function isBinaryAvailable(binary, { commandRunner } = {}) {
814
+ function whichBinary(binary, { commandRunner } = {}) {
812
815
  const result = runCommand(commandRunner, "which", [binary], { timeout: 2000 });
813
- return !result?.error && result?.status === 0;
816
+ if (result?.error || result?.status !== 0) return null;
817
+ const stdout = typeof result?.stdout === "string" ? result.stdout.trim() : "";
818
+ return stdout ? stdout.split("\n")[0] : null;
819
+ }
820
+
821
+ function isBinaryAvailable(binary, { commandRunner } = {}) {
822
+ return whichBinary(binary, { commandRunner }) !== null;
814
823
  }
815
824
 
816
825
  function stripAnsi(text) {
@@ -1173,11 +1182,84 @@ function detectAntigravityProcess({ commandRunner } = {}) {
1173
1182
  return { configured: false };
1174
1183
  }
1175
1184
 
1176
- function resolveLsofBinary() {
1185
+ function resolveAntigravityLimitsCachePath({ home } = {}) {
1186
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", ANTIGRAVITY_LIMITS_CACHE_FILE);
1187
+ }
1188
+
1189
+ function parseTimeMs(value) {
1190
+ if (typeof value !== "string" || !value) return null;
1191
+ const ts = Date.parse(value);
1192
+ return Number.isFinite(ts) && ts > 0 ? ts : null;
1193
+ }
1194
+
1195
+ function isCacheWindowUsable(window, { cachedAtMs, nowMs } = {}) {
1196
+ if (!window || typeof window !== "object") return false;
1197
+ const resetAtMs = parseTimeMs(window.reset_at);
1198
+ if (resetAtMs !== null) return resetAtMs > nowMs;
1199
+ return Number.isFinite(cachedAtMs)
1200
+ && nowMs - cachedAtMs <= ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS;
1201
+ }
1202
+
1203
+ function hasAntigravityWindow(limits) {
1204
+ return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window);
1205
+ }
1206
+
1207
+ function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
1208
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
1209
+ if (!Number.isFinite(cachedAtMs)) return null;
1210
+ if (cachedAtMs > nowMs + 60_000) return null;
1211
+ if (nowMs - cachedAtMs > ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS) return null;
1212
+
1213
+ const cached = {
1214
+ configured: true,
1215
+ error: null,
1216
+ account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
1217
+ account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
1218
+ primary_window: isCacheWindowUsable(raw?.primary_window, { cachedAtMs, nowMs }) ? raw.primary_window : null,
1219
+ secondary_window: isCacheWindowUsable(raw?.secondary_window, { cachedAtMs, nowMs }) ? raw.secondary_window : null,
1220
+ tertiary_window: isCacheWindowUsable(raw?.tertiary_window, { cachedAtMs, nowMs }) ? raw.tertiary_window : null,
1221
+ cached: true,
1222
+ cached_at: raw.cached_at,
1223
+ };
1224
+ return hasAntigravityWindow(cached) ? cached : null;
1225
+ }
1226
+
1227
+ function readAntigravityLimitsCache({ home, nowMs = Date.now() } = {}) {
1228
+ const cachePath = resolveAntigravityLimitsCachePath({ home });
1229
+ try {
1230
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
1231
+ return normalizeAntigravityCachedLimits(parsed?.antigravity, { nowMs });
1232
+ } catch (_error) {
1233
+ return null;
1234
+ }
1235
+ }
1236
+
1237
+ function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
1238
+ if (!limits?.configured || limits.error || !hasAntigravityWindow(limits)) return;
1239
+ const cachePath = resolveAntigravityLimitsCachePath({ home });
1240
+ const payload = {
1241
+ antigravity: {
1242
+ account_email: limits.account_email || null,
1243
+ account_plan: limits.account_plan || null,
1244
+ primary_window: limits.primary_window || null,
1245
+ secondary_window: limits.secondary_window || null,
1246
+ tertiary_window: limits.tertiary_window || null,
1247
+ cached_at: new Date(nowMs).toISOString(),
1248
+ },
1249
+ };
1250
+ try {
1251
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
1252
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
1253
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1254
+ fs.renameSync(tmpPath, cachePath);
1255
+ } catch (_error) {}
1256
+ }
1257
+
1258
+ function resolveLsofBinary({ commandRunner } = {}) {
1177
1259
  for (const candidate of ["/usr/sbin/lsof", "/usr/bin/lsof"]) {
1178
1260
  if (fs.existsSync(candidate)) return candidate;
1179
1261
  }
1180
- return null;
1262
+ return whichBinary("lsof", { commandRunner });
1181
1263
  }
1182
1264
 
1183
1265
  function parseListeningPorts(output) {
@@ -1193,7 +1275,7 @@ function parseListeningPorts(output) {
1193
1275
  }
1194
1276
 
1195
1277
  function listAntigravityPorts(pid, { commandRunner } = {}) {
1196
- const lsof = resolveLsofBinary();
1278
+ const lsof = resolveLsofBinary({ commandRunner });
1197
1279
  if (!lsof) {
1198
1280
  throw new Error("Antigravity port detection needs lsof. Install it, then retry.");
1199
1281
  }
@@ -1446,15 +1528,25 @@ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn } =
1446
1528
  }
1447
1529
  }
1448
1530
 
1449
- async function fetchAntigravityLimits({ commandRunner, requestFn, timeoutMs = 8000 } = {}) {
1531
+ async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutMs = 8000, nowMs = Date.now() } = {}) {
1450
1532
  const processInfo = detectAntigravityProcess({ commandRunner });
1451
1533
  if (!processInfo.configured) {
1452
- return { configured: false };
1534
+ return readAntigravityLimitsCache({ home, nowMs }) || { configured: false };
1453
1535
  }
1454
1536
  if (processInfo.error) {
1455
1537
  return { configured: true, error: processInfo.error };
1456
1538
  }
1457
1539
 
1540
+ const finalize = (payload, normalizeOptions) => {
1541
+ const result = {
1542
+ configured: true,
1543
+ error: null,
1544
+ ...normalizeAntigravityResponse(payload, normalizeOptions),
1545
+ };
1546
+ writeAntigravityLimitsCache(result, { home, nowMs });
1547
+ return result;
1548
+ };
1549
+
1458
1550
  try {
1459
1551
  const ports = listAntigravityPorts(processInfo.pid, { commandRunner });
1460
1552
  let workingPort = null;
@@ -1478,11 +1570,7 @@ async function fetchAntigravityLimits({ commandRunner, requestFn, timeoutMs = 80
1478
1570
  timeoutMs,
1479
1571
  requestFn,
1480
1572
  });
1481
- return {
1482
- configured: true,
1483
- error: null,
1484
- ...normalizeAntigravityResponse(userStatus),
1485
- };
1573
+ return finalize(userStatus);
1486
1574
  } catch (primaryError) {
1487
1575
  const fallbackPort =
1488
1576
  Number.isFinite(processInfo.extensionPort) && processInfo.extensionPort > 0
@@ -1497,11 +1585,7 @@ async function fetchAntigravityLimits({ commandRunner, requestFn, timeoutMs = 80
1497
1585
  timeoutMs,
1498
1586
  requestFn,
1499
1587
  });
1500
- return {
1501
- configured: true,
1502
- error: null,
1503
- ...normalizeAntigravityResponse(modelConfigs, { fallbackToConfigs: true }),
1504
- };
1588
+ return finalize(modelConfigs, { fallbackToConfigs: true });
1505
1589
  }
1506
1590
  } catch (error) {
1507
1591
  const message = error?.message === "timeout"
@@ -1531,7 +1615,7 @@ async function getUsageLimits({
1531
1615
  }
1532
1616
 
1533
1617
  const [claudeToken, codexAuth] = await Promise.all([
1534
- Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner })),
1618
+ Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner, home })),
1535
1619
  readCodexAuthBundle({ home, env }),
1536
1620
  ]);
1537
1621
 
@@ -1594,7 +1678,7 @@ async function getUsageLimits({
1594
1678
  withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
1595
1679
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1596
1680
  Promise.resolve().then(() => fetchKiroLimits({ commandRunner, now })),
1597
- fetchAntigravityLimits({ commandRunner, requestFn }),
1681
+ fetchAntigravityLimits({ home, commandRunner, requestFn, nowMs }),
1598
1682
  withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch }), "GitHub Copilot", providerTimeoutMs)
1599
1683
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
1600
1684
  ]);
@@ -1670,6 +1754,7 @@ module.exports = {
1670
1754
  normalizeAntigravityResponse,
1671
1755
  parseListeningPorts,
1672
1756
  detectAntigravityProcess,
1757
+ fetchAntigravityLimits,
1673
1758
  fetchCopilotLimits,
1674
1759
  readCopilotOauthToken,
1675
1760
  describeCopilotOtelStatus,