tokentracker-cli 1.4.7 → 1.4.10

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 (154) hide show
  1. package/README.de.md +2 -2
  2. package/README.ja.md +2 -2
  3. package/README.ko.md +2 -2
  4. package/README.md +11 -3
  5. package/README.zh-CN.md +3 -3
  6. package/bin/tracker.js +6 -0
  7. package/dashboard/dist/assets/{AchievementBadge-DF8q1x6R.js → AchievementBadge-BTqlXUP1.js} +1 -1
  8. package/dashboard/dist/assets/{AchievementsPage-B8Dg5myo.js → AchievementsPage-DbKUhaY-.js} +2 -2
  9. package/dashboard/dist/assets/{AchievementsSection-oPPiyP3y.js → AchievementsSection-El1F-OC7.js} +1 -1
  10. package/dashboard/dist/assets/{BadgeDetailModal-PneMW4B4.js → BadgeDetailModal--8PySLzP.js} +1 -1
  11. package/dashboard/dist/assets/{BotAnimated-GOlSvEiz.js → BotAnimated-DBcOS4vh.js} +1 -1
  12. package/dashboard/dist/assets/{Card-BT5G0GE0.js → Card-CeGs8TaY.js} +1 -1
  13. package/dashboard/dist/assets/{CloudCheckoutPage-BNeL5Muo.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
  14. package/dashboard/dist/assets/CloudPage-CmeFDrmk.js +2 -0
  15. package/dashboard/dist/assets/{CommandPalette-CiMSYHNK.js → CommandPalette-CrSNoZW9.js} +1 -1
  16. package/dashboard/dist/assets/{CommunityStatsModal-fK3BE9CW.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
  17. package/dashboard/dist/assets/{ConfirmModal-CrUlEHyZ.js → ConfirmModal-DDTUf-My.js} +1 -1
  18. package/dashboard/dist/assets/{Controls-CbjSwq6k.js → Controls-DXsJjEU1.js} +1 -1
  19. package/dashboard/dist/assets/DashboardPage-fr4sgbLB.js +1 -0
  20. package/dashboard/dist/assets/{DateRangePopover-E-2Z6imh.js → DateRangePopover-gTiPISL_.js} +1 -1
  21. package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
  22. package/dashboard/dist/assets/{DialogClose-xJKE22Y6.js → DialogClose-CXwouQe4.js} +1 -1
  23. package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
  24. package/dashboard/dist/assets/{DialogTitle-f8vnnH7z.js → DialogTitle-Cd_k-Nyw.js} +1 -1
  25. package/dashboard/dist/assets/{ElectricLogo-Ci9WZfHK.js → ElectricLogo-C4vUNTCI.js} +1 -1
  26. package/dashboard/dist/assets/{FadeIn-BXqtyAmn.js → FadeIn-CGM5RBFy.js} +1 -1
  27. package/dashboard/dist/assets/{HeaderGithubStar-BV5SYJMi.js → HeaderGithubStar-el5-DhVb.js} +1 -1
  28. package/dashboard/dist/assets/{HoverTooltip-D3gpW3Jh.js → HoverTooltip-C7YaWwJx.js} +1 -1
  29. package/dashboard/dist/assets/{IpCheckPage-G7fIWXpL.js → IpCheckPage-EKhbH9hJ.js} +1 -1
  30. package/dashboard/dist/assets/LandingPage-DJYOv1AM.js +2 -0
  31. package/dashboard/dist/assets/{LeaderboardPage-D2_wXazL.js → LeaderboardPage-CwzJxA8p.js} +2 -2
  32. package/dashboard/dist/assets/{LeaderboardProBadge-3xJ-mH2v.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
  33. package/dashboard/dist/assets/{LeaderboardProfileModal-23pH6Ud7.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
  34. package/dashboard/dist/assets/{LeaderboardProfilePage-DSezSR58.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
  35. package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
  36. package/dashboard/dist/assets/{LocalOnlyNotice-Dsk-d0Hr.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
  37. package/dashboard/dist/assets/{LoginCard-C7dugIIn.js → LoginCard-Czq-UgAU.js} +1 -1
  38. package/dashboard/dist/assets/{LoginModal-B7RuLsTF.js → LoginModal-D4uxfzZb.js} +1 -1
  39. package/dashboard/dist/assets/{LoginPage-u32vJQN9.js → LoginPage-Bg3BG0Wv.js} +1 -1
  40. package/dashboard/dist/assets/{PetPage-Dbc975oU.js → PetPage-BgwBxRyx.js} +2 -2
  41. package/dashboard/dist/assets/{PopoverPopup-D-yECvH9.js → PopoverPopup-k3o_Jp2b.js} +1 -1
  42. package/dashboard/dist/assets/{ProviderIcon-CHtZiJUN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
  43. package/dashboard/dist/assets/{ResetPasswordPage-ctuIsNk0.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
  44. package/dashboard/dist/assets/{Select-DN4Kq_R1.js → Select-kA0dV5Qw.js} +1 -1
  45. package/dashboard/dist/assets/{SelectItemText-C-HmeD8L.js → SelectItemText-D4YCIn1x.js} +1 -1
  46. package/dashboard/dist/assets/{SelfHostPage-BZOL3JMt.js → SelfHostPage-C3Y9qF3P.js} +1 -1
  47. package/dashboard/dist/assets/{ServiceStatusPage-C0sDYa8c.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
  48. package/dashboard/dist/assets/{SessionsPage-l0-FkcJB.js → SessionsPage-CA-efxhU.js} +3 -3
  49. package/dashboard/dist/assets/SettingsPage-Mr84vxQL.js +5 -0
  50. package/dashboard/dist/assets/{SkillsPage-Cz6G9d3o.js → SkillsPage-BtRhLSpz.js} +1 -1
  51. package/dashboard/dist/assets/TokenGalaxy-CcFjCCuC.js +4426 -0
  52. package/dashboard/dist/assets/TooltipPopup-6aTx3jDf.js +1 -0
  53. package/dashboard/dist/assets/TrendMonitor-DoywXr8h.js +19 -0
  54. package/dashboard/dist/assets/{WidgetsPage-BKo3DBxT.js → WidgetsPage-BuGSzvR0.js} +1 -1
  55. package/dashboard/dist/assets/{WrappedPage-Blm3aPO5.js → WrappedPage-Bm6X-SUZ.js} +1 -1
  56. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
  57. package/dashboard/dist/assets/__vite-browser-external-ClVJNQu3.js +45 -0
  58. package/dashboard/dist/assets/{agent-logos-DMtDEx7_.js → agent-logos-Sc8Sejgx.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-left-Bo9y7VzF.js → arrow-left-Cm1LG2i_.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-right-BLMU8Y5U.js → arrow-right-WP3kIwiy.js} +1 -1
  61. package/dashboard/dist/assets/{arrow-up-right-B4hXO4sc.js → arrow-up-right-CjyprVMj.js} +1 -1
  62. package/dashboard/dist/assets/{calendar-days-D9HaEtgo.js → calendar-days-Bm88-Gcb.js} +1 -1
  63. package/dashboard/dist/assets/{check-BJNGKpX8.js → check-CzIxFg0I.js} +1 -1
  64. package/dashboard/dist/assets/{chevron-down-CoGC_RLK.js → chevron-down-bfGK8629.js} +1 -1
  65. package/dashboard/dist/assets/{cloud-checkout-Du5FoHqV.js → cloud-checkout-C9DFQ7yM.js} +1 -1
  66. package/dashboard/dist/assets/{composite-BfIUPpl8.js → composite-CdExmgTx.js} +1 -1
  67. package/dashboard/dist/assets/copy-CPPdujn2.js +1 -0
  68. package/dashboard/dist/assets/{download-C1K_v0C0.js → download-Cl6_dKeK.js} +1 -1
  69. package/dashboard/dist/assets/{external-link-D-xHhfj5.js → external-link-B0r6Wo4e.js} +1 -1
  70. package/dashboard/dist/assets/{flame-DXD6uvhM.js → flame-wEezvzXx.js} +1 -1
  71. package/dashboard/dist/assets/{format-tokens-B9riGvNj.js → format-tokens-vWpBpKpM.js} +1 -1
  72. package/dashboard/dist/assets/{gift-LPR-nD0J.js → gift-BFjblRqU.js} +1 -1
  73. package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
  74. package/dashboard/dist/assets/{icons-DLdtLqEt.js → icons-DX3dCNJP.js} +1 -1
  75. package/dashboard/dist/assets/{index-CXdJ7zMN.js → index-D-TnqdBv.js} +1 -1
  76. package/dashboard/dist/assets/{index-CC3cXeBa.js → index-D1Zdrpom.js} +1 -1
  77. package/dashboard/dist/assets/index-DBF1bJlm.js +502 -0
  78. package/dashboard/dist/assets/{index-BL11MboX.js → index-p8Gt3NFg.js} +1 -1
  79. package/dashboard/dist/assets/{info-tnXKCyuK.js → info-Cqi971MA.js} +1 -1
  80. package/dashboard/dist/assets/{link-2-DOk9Tcg1.js → link-2-79sm__cT.js} +1 -1
  81. package/dashboard/dist/assets/{loader-circle-DIl96Mw-.js → loader-circle-CDoVfOUb.js} +1 -1
  82. package/dashboard/dist/assets/local-usage-transfer-EcXnuwCj.js +1 -0
  83. package/dashboard/dist/assets/{modifiers.esm-2H_1taJ6.js → modifiers.esm-v5_BKSmY.js} +1 -1
  84. package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
  85. package/dashboard/dist/assets/{monitor-smartphone-tnp-03hs.js → monitor-smartphone-CO67DffO.js} +1 -1
  86. package/dashboard/dist/assets/{play-BGMuGTdN.js → play-CbvuGtc3.js} +1 -1
  87. package/dashboard/dist/assets/{plus-XETAaErq.js → plus-jc2gXlRM.js} +1 -1
  88. package/dashboard/dist/assets/{provider-display-BYNT4zPc.js → provider-display-BK3EbRLM.js} +1 -1
  89. package/dashboard/dist/assets/{react-OW7HALVO.js → react-DMs4C4qS.js} +1 -1
  90. package/dashboard/dist/assets/{safePolygon-CAfS8-ct.js → safePolygon-cY9H0GBN.js} +1 -1
  91. package/dashboard/dist/assets/{search-B7ZJhoau.js → search-fqTJoYYA.js} +1 -1
  92. package/dashboard/dist/assets/{server-CWrP7mtL.js → server-jpBtvTvr.js} +1 -1
  93. package/dashboard/dist/assets/{shield-check-ip5IQEK3.js → shield-check-DQiBlJbN.js} +1 -1
  94. package/dashboard/dist/assets/{skills-api-CpyoNTeB.js → skills-api-CN2r_rmt.js} +1 -1
  95. package/dashboard/dist/assets/{sortable.esm-YvzWzPDu.js → sortable.esm-DrS52yOK.js} +1 -1
  96. package/dashboard/dist/assets/{store-DRK6-JBg.js → store-K2Mk6aMn.js} +1 -1
  97. package/dashboard/dist/assets/{terminal-DQRoJSvh.js → terminal-Q4SruAlh.js} +1 -1
  98. package/dashboard/dist/assets/{trash-8h_26atF.js → trash-C1Ob-g8E.js} +1 -1
  99. package/dashboard/dist/assets/{use-community-stats-DYlhIXsp.js → use-community-stats-C-Gxpp9D.js} +1 -1
  100. package/dashboard/dist/assets/{use-latest-request-guard-AxJF9SEJ.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
  101. package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
  102. package/dashboard/dist/assets/{use-native-settings-BQgB6_PB.js → use-native-settings-BZLEIUK8.js} +1 -1
  103. package/dashboard/dist/assets/{use-ordered-list-ogi7Okzf.js → use-ordered-list-De_Mcm_H.js} +1 -1
  104. package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
  105. package/dashboard/dist/assets/{use-transform-BQqoo1et.js → use-transform-XCDD5IrX.js} +1 -1
  106. package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
  107. package/dashboard/dist/assets/{useCurrency-Bv9g8mpq.js → useCurrency-DXPclxBT.js} +1 -1
  108. package/dashboard/dist/assets/{usePositioner-CQiZGknc.js → usePositioner-C-E1npCy.js} +1 -1
  109. package/dashboard/dist/assets/{useTokenFormat-BUVirY0k.js → useTokenFormat-B70xhy9u.js} +1 -1
  110. package/dashboard/dist/assets/{zap-DR3VD0yY.js → zap-D7gRgGCj.js} +1 -1
  111. package/dashboard/dist/index.html +5 -5
  112. package/dashboard/dist/ip-check.html +2 -2
  113. package/dashboard/dist/leaderboard.html +2 -2
  114. package/dashboard/dist/privacy.html +8 -2
  115. package/dashboard/dist/share.html +2 -2
  116. package/package.json +4 -3
  117. package/src/cli.js +10 -0
  118. package/src/commands/sync.js +18 -3
  119. package/src/lib/codex-rollout-parser.js +29 -2
  120. package/src/lib/codex-service-tier.js +13 -0
  121. package/src/lib/commandcode-limits.js +27 -11
  122. package/src/lib/grok-limits.js +1 -0
  123. package/src/lib/local-api.js +122 -11
  124. package/src/lib/local-usage-transfer.js +271 -0
  125. package/src/lib/opencode-go-limits.js +17 -6
  126. package/src/lib/pricing/curated-overrides.json +24 -0
  127. package/src/lib/pricing/index.js +50 -39
  128. package/src/lib/pricing/seed-snapshot.json +1 -1
  129. package/src/lib/provider-api-key-store.js +110 -0
  130. package/src/lib/rollout.js +27 -3
  131. package/src/lib/sentry.js +231 -0
  132. package/src/lib/session-analytics.js +15 -2
  133. package/src/lib/static-server.js +15 -2
  134. package/src/lib/usage-limits.js +74 -24
  135. package/src/lib/workbuddy-ipc.js +191 -0
  136. package/src/lib/workbuddy-limits.js +564 -0
  137. package/dashboard/dist/assets/CloudPage-26bhEYqI.js +0 -2
  138. package/dashboard/dist/assets/DashboardPage-CP_PMMga.js +0 -1
  139. package/dashboard/dist/assets/DevicePage-B0fLd80M.js +0 -1
  140. package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +0 -1
  141. package/dashboard/dist/assets/LandingPage-BQNOdAkR.js +0 -2
  142. package/dashboard/dist/assets/LimitsPage-BLJsmVVy.js +0 -9
  143. package/dashboard/dist/assets/SettingsPage-CrczfZiZ.js +0 -5
  144. package/dashboard/dist/assets/TokenGalaxy-B5IGaLQK.js +0 -4426
  145. package/dashboard/dist/assets/TooltipPopup-B6FqLWO7.js +0 -1
  146. package/dashboard/dist/assets/TrendMonitor-B4kJUwmF.js +0 -19
  147. package/dashboard/dist/assets/__vite-browser-external--HDUIUZr.js +0 -45
  148. package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
  149. package/dashboard/dist/assets/host-mode-C2K0MMOs.js +0 -1
  150. package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
  151. package/dashboard/dist/assets/use-limits-display-prefs-DOZzuTwo.js +0 -1
  152. package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +0 -1
  153. package/dashboard/dist/assets/use-session-efficiency-pref-DP3hu3Rp.js +0 -1
  154. package/dashboard/dist/assets/use-usage-limits-DAf6O4aZ.js +0 -1
@@ -35,6 +35,7 @@ const { fetchDevinLimits } = require("./devin-limits");
35
35
  const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
36
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
37
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
38
+ const { fetchWorkbuddyLimits, fetchWorkbuddyAiLimits, selectWorkbuddyLimits } = require("./workbuddy-limits");
38
39
  const { fetchProviderServiceStatus } = require("./provider-status");
39
40
  const { resolveKimiProfile } = require("./kimi-profile");
40
41
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
@@ -56,6 +57,10 @@ const execFileAsync = promisify(cp.execFile);
56
57
  const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
57
58
  const cacheBySelection = {};
58
59
  for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
60
+ // Credential replacements (provider-key save) bump this generation so a fetch
61
+ // that started under the old credential cannot repopulate the memory or disk
62
+ // caches after invalidation.
63
+ let usageLimitsCacheGeneration = 0;
59
64
  function usageLimitsSelectionKey(options) {
60
65
  const devin = options?.devinEnabled === true ? "on" : "off";
61
66
  const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
@@ -745,13 +750,12 @@ function normalizeCursorUsageSummary(body) {
745
750
  }
746
751
 
747
752
  /**
748
- * Cursor's Sand "Grok Bot" RPC reports the shared Super weekly pool (same
749
- * reset and about the same percent as Grok billing's creditUsagePercent).
750
- * Once the Grok card lists a Grok Bot product share, that Sand bar would
751
- * show the whole pool a second time under Cursor. Hide it only in that
752
- * case. A real separate Sand quota (different reset, or a percent more than
753
- * a point away from the pool) stays. Accounts with no Grok Bot product row
754
- * keep the Sand bar.
753
+ * Cursor's Sand RPC reports Grok Bot usage. grok.com shows that as a weekly
754
+ * Grok Bot limit of its own, separate from the SuperGrok pool and with its own
755
+ * reset, so the Sand bar normally stays. It is hidden only when it is
756
+ * indistinguishable from the Grok pool, so the same numbers are not drawn
757
+ * twice: Grok billing lists a GrokBot product share, and the two windows reset
758
+ * at the same instant and sit within a point of each other.
755
759
  */
756
760
  function cursorSandDuplicatesGrokPool(cursor, grok) {
757
761
  const sand = cursor?.quaternary_window;
@@ -2603,8 +2607,12 @@ function writeCodexLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2603
2607
  } catch (_error) {}
2604
2608
  }
2605
2609
 
2606
- function resolveOpencodeGoLimitsCachePath({ home } = {}) {
2607
- return path.join(home || os.homedir(), ".tokentracker", "tracker", OPENCODE_GO_LIMITS_CACHE_FILE);
2610
+ function resolveOpencodeGoLimitsCachePath({ trackerDir, home } = {}) {
2611
+ const dir =
2612
+ typeof trackerDir === "string" && trackerDir.trim()
2613
+ ? trackerDir.trim()
2614
+ : path.join(home || os.homedir(), ".tokentracker", "tracker");
2615
+ return path.join(dir, OPENCODE_GO_LIMITS_CACHE_FILE);
2608
2616
  }
2609
2617
 
2610
2618
  function hasOpencodeGoWindow(limits) {
@@ -2632,10 +2640,15 @@ function normalizeOpencodeGoCachedLimits(
2632
2640
  if (cachedAtMs > nowMs + 60_000) return null;
2633
2641
  if (nowMs - cachedAtMs > effectiveMaxAge) return null;
2634
2642
 
2643
+ const credentialSource =
2644
+ raw?.credential_source === "env" || raw?.credential_source === "stored"
2645
+ ? raw.credential_source
2646
+ : null;
2635
2647
  const cached = {
2636
2648
  configured: true,
2637
2649
  error: null,
2638
2650
  source: typeof raw?.source === "string" ? raw.source : "api",
2651
+ credential_source: credentialSource,
2639
2652
  subscription_status: typeof raw?.subscription_status === "string" ? raw.subscription_status : null,
2640
2653
  plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
2641
2654
  primary_window: isOpencodeGoCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
@@ -2647,8 +2660,8 @@ function normalizeOpencodeGoCachedLimits(
2647
2660
  return hasOpencodeGoWindow(cached) ? cached : null;
2648
2661
  }
2649
2662
 
2650
- function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
2651
- const cachePath = resolveOpencodeGoLimitsCachePath({ home });
2663
+ function readOpencodeGoLimitsCache({ trackerDir, home, nowMs = Date.now() } = {}) {
2664
+ const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
2652
2665
  try {
2653
2666
  const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2654
2667
  return normalizeOpencodeGoCachedLimits(parsed?.opencodeGo, { nowMs });
@@ -2657,12 +2670,17 @@ function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
2657
2670
  }
2658
2671
  }
2659
2672
 
2660
- function writeOpencodeGoLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2673
+ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now() } = {}) {
2661
2674
  if (!limits?.configured || limits.error || !hasOpencodeGoWindow(limits)) return;
2662
- const cachePath = resolveOpencodeGoLimitsCachePath({ home });
2675
+ const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
2676
+ const credentialSource =
2677
+ limits.credential_source === "env" || limits.credential_source === "stored"
2678
+ ? limits.credential_source
2679
+ : null;
2663
2680
  const payload = {
2664
2681
  opencodeGo: {
2665
2682
  source: limits.source || "api",
2683
+ credential_source: credentialSource,
2666
2684
  subscription_status: limits.subscription_status || null,
2667
2685
  plan_label: limits.plan_label || null,
2668
2686
  primary_window: limits.primary_window || null,
@@ -3989,7 +4007,12 @@ async function getUsageLimits(options = {}) {
3989
4007
  if (inFlightBySelection[selection]) {
3990
4008
  return inFlightBySelection[selection];
3991
4009
  }
3992
- const promise = fetchUsageLimitsUncached({ ...options, selectionKey: selection }).finally(() => {
4010
+ const cacheGeneration = usageLimitsCacheGeneration;
4011
+ const promise = fetchUsageLimitsUncached({
4012
+ ...options,
4013
+ selectionKey: selection,
4014
+ cacheGeneration,
4015
+ }).finally(() => {
3993
4016
  if (inFlightBySelection[selection] === promise) inFlightBySelection[selection] = null;
3994
4017
  });
3995
4018
  inFlightBySelection[selection] = promise;
@@ -4002,6 +4025,7 @@ async function getUsageLimits(options = {}) {
4002
4025
  */
4003
4026
  async function fetchUsageLimitsUncached({
4004
4027
  home,
4028
+ trackerDir,
4005
4029
  env,
4006
4030
  platform,
4007
4031
  securityRunner,
@@ -4013,6 +4037,10 @@ async function fetchUsageLimitsUncached({
4013
4037
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
4014
4038
  forceRefresh = false,
4015
4039
  devinEnabled = false,
4040
+ // Generation captured by getUsageLimits before the fetch began. A provider
4041
+ // key save bumps it, and this fetch must then stop at returning its own
4042
+ // result: publishing it would restore pre-save data for every later caller.
4043
+ cacheGeneration = usageLimitsCacheGeneration,
4016
4044
  // Selection key fixed by getUsageLimits before the fetch began. Re-deriving it
4017
4045
  // here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
4018
4046
  // post-opt-out slot.
@@ -4079,7 +4107,7 @@ async function fetchUsageLimitsUncached({
4079
4107
  : null;
4080
4108
 
4081
4109
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
4082
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, claudeServiceStatus] = await Promise.all([
4110
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4083
4111
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4084
4112
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4085
4113
  (value) => ({ status: "fulfilled", value }),
@@ -4131,7 +4159,7 @@ async function fetchUsageLimitsUncached({
4131
4159
  // OpenCode Go: authoritative subscription windows come from the dashboard
4132
4160
  // scrape; local opencode.db cost is available only as an explicit estimate.
4133
4161
  // See src/lib/opencode-go-limits.js.
4134
- withProviderTimeout(fetchOpencodeGoLimits({ home, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4162
+ withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4135
4163
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4136
4164
  withProviderTimeout(
4137
4165
  fetchQoderLimits({
@@ -4189,7 +4217,7 @@ async function fetchUsageLimitsUncached({
4189
4217
  // outer race bounds the whole slot: without it three sequential per-request
4190
4218
  // timeouts (whoami, then credits+subscriptions) could hold the aggregate
4191
4219
  // ~2x longer than any sibling provider.
4192
- withProviderTimeout(fetchCommandcodeLimits({ home, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4220
+ withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4193
4221
  .then(
4194
4222
  (value) => ({ status: "fulfilled", value }),
4195
4223
  (reason) => ({ status: "rejected", reason }),
@@ -4204,6 +4232,13 @@ async function fetchUsageLimitsUncached({
4204
4232
  (value) => ({ status: "fulfilled", value }),
4205
4233
  (reason) => ({ status: "rejected", reason }),
4206
4234
  ),
4235
+ // WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
4236
+ // its own local authenticated billing proxy. Abort closes the IPC socket
4237
+ // when the provider budget expires. Reward credits are not a plan window.
4238
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy", providerTimeoutMs)
4239
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4240
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy AI", providerTimeoutMs)
4241
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4207
4242
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4208
4243
  // Only probed for configured accounts — without a token the Claude section
4209
4244
  // never renders, so the reading would have nowhere to go.
@@ -4382,9 +4417,11 @@ async function fetchUsageLimitsUncached({
4382
4417
  stale: false,
4383
4418
  cached_at: new Date(nowMs).toISOString(),
4384
4419
  };
4385
- writeOpencodeGoLimitsCache(opencodeGo, { home, nowMs });
4420
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4421
+ writeOpencodeGoLimitsCache(opencodeGo, { trackerDir, home, nowMs });
4422
+ }
4386
4423
  } else {
4387
- const cached = readOpencodeGoLimitsCache({ home, nowMs });
4424
+ const cached = readOpencodeGoLimitsCache({ trackerDir, home, nowMs });
4388
4425
  if (cached) {
4389
4426
  opencodeGo = cached;
4390
4427
  } else {
@@ -4468,6 +4505,9 @@ async function fetchUsageLimitsUncached({
4468
4505
  qoderCn: withPlanLabel(qoderCn, qoderCn?.plan_label, "Qoder CN"),
4469
4506
  codingPlan: withPlanLabel(codingPlan, codingPlan?.plan_label, "Ark Coding Plan"),
4470
4507
  agentPlan: withPlanLabel(agentPlan, agentPlan?.plan_label, "Ark Agent Plan"),
4508
+ // WorkBuddy preserves provider English names and official edition labels
4509
+ // verbatim (including Free, STANDARD and Pro-Trial).
4510
+ workbuddy: selectWorkbuddyLimits(workbuddy, workbuddyAi),
4471
4511
  };
4472
4512
 
4473
4513
  for (const [providerName, provider] of Object.entries(data)) {
@@ -4491,16 +4531,22 @@ async function fetchUsageLimitsUncached({
4491
4531
  };
4492
4532
  }
4493
4533
 
4494
- cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4495
- data,
4496
- expiresAtMs: cacheExpiresAtMs(data, nowMs),
4497
- };
4534
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4535
+ cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4536
+ data,
4537
+ expiresAtMs: cacheExpiresAtMs(data, nowMs),
4538
+ };
4539
+ }
4498
4540
  return data;
4499
4541
  }
4500
4542
 
4501
- function resetUsageLimitsCache() {
4543
+ function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
4544
+ if (invalidateInFlight) usageLimitsCacheGeneration += 1;
4502
4545
  for (const key of USAGE_LIMITS_SELECTION_KEYS) {
4503
4546
  cacheBySelection[key] = { data: null, expiresAtMs: 0 };
4547
+ // Ordinary resets still let a running fetch serve refresh=1 callers; a
4548
+ // credential replacement must not, so drop the old-key promises too.
4549
+ if (invalidateInFlight) inFlightBySelection[key] = null;
4504
4550
  }
4505
4551
  }
4506
4552
 
@@ -4508,6 +4554,8 @@ module.exports = {
4508
4554
  getUsageLimits,
4509
4555
  normalizePlanLabel,
4510
4556
  resetUsageLimitsCache,
4557
+ OPENCODE_GO_LIMITS_CACHE_FILE,
4558
+ resolveOpencodeGoLimitsCachePath,
4511
4559
  cacheExpiresAtMs,
4512
4560
  runCommand,
4513
4561
  extractGeminiOauthClientCredentials,
@@ -4549,4 +4597,6 @@ module.exports = {
4549
4597
  fetchCommandcodeLimits,
4550
4598
  fetchQoderLimits,
4551
4599
  fetchQoderCnLimits,
4600
+ fetchWorkbuddyLimits,
4601
+ fetchWorkbuddyAiLimits,
4552
4602
  };
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+
3
+ // WorkBuddy's authenticated local proxy. The ticket proves both peers; the
4
+ // desktop daemon adds its own access token to relative billing requests.
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const net = require("node:net");
8
+ const crypto = require("node:crypto");
9
+
10
+ const UNAVAILABLE = "Start WorkBuddy and sign in to read its subscription quota.";
11
+ const MAX_FRAME_BYTES = 1024 * 1024;
12
+ const BILLING_PATHS = new Set([
13
+ "/billing/meter/get-user-resource-summary",
14
+ "/billing/meter/get-user-resource-paid-packages",
15
+ "/billing/meter/get-user-resource-free-packages",
16
+ ]);
17
+
18
+ function readWorkbuddyIpcDiscovery(workbuddyHome) {
19
+ if (!workbuddyHome) return null;
20
+ let fd;
21
+ try {
22
+ fd = fs.openSync(path.join(workbuddyHome, "wbipc", "endpoint.json"),
23
+ fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
24
+ const stat = fs.fstatSync(fd);
25
+ if (!stat.isFile() || stat.size > 16384 || (process.platform !== "win32"
26
+ && (stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0))) {
27
+ throw new Error("Untrusted discovery file.");
28
+ }
29
+ const value = JSON.parse(fs.readFileSync(fd, "utf8"));
30
+ if (typeof value.ticket !== "string" || !/^[\w-]{43}$/.test(value.ticket)
31
+ || typeof value.endpoint !== "string") throw new Error("Invalid discovery file.");
32
+ if (process.platform === "win32") {
33
+ if (!/^\\\\\.\\pipe\\wbipc-[\w-]+$/.test(value.endpoint)) throw new Error("Invalid pipe.");
34
+ } else {
35
+ if (!path.isAbsolute(value.endpoint)) throw new Error("Invalid socket.");
36
+ const socket = fs.lstatSync(value.endpoint);
37
+ if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o077) !== 0) {
38
+ throw new Error("Untrusted socket.");
39
+ }
40
+ }
41
+ return { endpoint: value.endpoint, ticket: value.ticket };
42
+ } catch (error) {
43
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
44
+ // Never expose discovery contents, tickets, paths or JSON parser excerpts.
45
+ throw new Error("WorkBuddy local connection information is unavailable.");
46
+ } finally {
47
+ if (fd !== undefined) fs.closeSync(fd);
48
+ }
49
+ }
50
+
51
+ function computeProof(ticket, role, endpoint, clientNonce, serverNonce) {
52
+ const parts = [];
53
+ for (const field of [role === "server" ? "wbipc-s" : "wbipc-c", "1", endpoint, clientNonce, serverNonce]) {
54
+ const bytes = Buffer.from(field, "utf8");
55
+ const length = Buffer.alloc(4);
56
+ length.writeUInt32BE(bytes.length);
57
+ parts.push(length, bytes);
58
+ }
59
+ return crypto.createHmac("sha256", ticket).update(Buffer.concat(parts)).digest("base64url");
60
+ }
61
+
62
+ function openWorkbuddyIpc({ discovery, signal, timeoutMs = 12000, connectImpl = net.createConnection }) {
63
+ return new Promise((resolve, reject) => {
64
+ const { endpoint, ticket } = discovery;
65
+ const clientNonce = crypto.randomBytes(16).toString("base64url");
66
+ const pending = new Map();
67
+ let socket;
68
+ let phase = "challenge";
69
+ let channel;
70
+ let nextId = 1;
71
+ let buffer = Buffer.alloc(0);
72
+ let closed = false;
73
+ const timer = setTimeout(() => fail(new Error("WorkBuddy quota request timed out.")), timeoutMs);
74
+ const onAbort = () => fail(new Error("WorkBuddy quota request was cancelled."));
75
+
76
+ function fail(error) {
77
+ if (closed) return;
78
+ closed = true;
79
+ clearTimeout(timer);
80
+ signal?.removeEventListener("abort", onAbort);
81
+ reject(error);
82
+ for (const handlers of pending.values()) handlers.reject(error);
83
+ pending.clear();
84
+ socket?.destroy();
85
+ }
86
+ function send(packet) {
87
+ socket.write(JSON.stringify(packet) + "\n");
88
+ }
89
+ function rpc(method, params, mode) {
90
+ if (closed) return Promise.reject(new Error(UNAVAILABLE));
91
+ return new Promise((resolve, reject) => {
92
+ const id = nextId++;
93
+ pending.set(id, { resolve, reject });
94
+ send({ jsonrpc: "2.0", id, method, params, ...(mode ? { mode } : {}) });
95
+ });
96
+ }
97
+ const client = {
98
+ close: () => fail(new Error("WorkBuddy local connection closed.")),
99
+ async postBilling(apiPath, body) {
100
+ if (!BILLING_PATHS.has(apiPath)) throw new Error("Unsupported WorkBuddy quota request.");
101
+ const response = await rpc(`${channel}/http.fetch`, {
102
+ method: "POST",
103
+ path: apiPath,
104
+ headers: { "content-type": "application/json", accept: "application/json" },
105
+ body_b64: Buffer.from(JSON.stringify(body)).toString("base64"),
106
+ }, "call");
107
+ if (response?.status === 401 || response?.status === 403) {
108
+ const error = new Error("WorkBuddy sign-in expired.");
109
+ error.code = "AUTH_EXPIRED";
110
+ throw error;
111
+ }
112
+ if (response?.status !== 200) throw new Error("WorkBuddy quota request failed.");
113
+ try {
114
+ if (typeof response.body_b64 !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(response.body_b64)) {
115
+ throw new Error("Invalid body.");
116
+ }
117
+ return JSON.parse(Buffer.from(response.body_b64, "base64").toString("utf8"));
118
+ } catch {
119
+ throw new Error("WorkBuddy quota response was malformed.");
120
+ }
121
+ },
122
+ };
123
+ function onPacket(packet) {
124
+ if (!packet || typeof packet !== "object") throw new Error("Invalid packet.");
125
+ if (phase === "challenge" && packet.type === "session_challenge" && packet.protocol === 1) {
126
+ if (typeof packet.server_nonce !== "string" || !/^[\w-]{16,128}$/.test(packet.server_nonce)
127
+ || typeof packet.server_proof !== "string") throw new Error("Invalid challenge.");
128
+ const expected = Buffer.from(computeProof(ticket, "server", endpoint, clientNonce, packet.server_nonce));
129
+ const actual = Buffer.from(packet.server_proof);
130
+ if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
131
+ throw new Error("Invalid server proof.");
132
+ }
133
+ phase = "ack";
134
+ send({ type: "session_prove", client_proof: computeProof(ticket, "client", endpoint, clientNonce, packet.server_nonce) });
135
+ } else if (phase === "ack" && packet.type === "session_hello_ack" && packet.protocol === 1
136
+ && Array.isArray(packet.pipes) && packet.pipes.includes("wb.request")) {
137
+ phase = "ready";
138
+ rpc("broker/GetPipe", { pipe: "wb.request" }).then((result) => {
139
+ if (closed) return;
140
+ if (result?.channel !== "c:wb.request" || !Array.isArray(result.methods)
141
+ || !result.methods.includes("http.fetch")) {
142
+ fail(new Error("WorkBuddy billing proxy is unavailable."));
143
+ return;
144
+ }
145
+ channel = result.channel;
146
+ resolve(client);
147
+ }, () => fail(new Error(UNAVAILABLE)));
148
+ } else if (phase === "ready" && packet.jsonrpc === "2.0" && pending.has(packet.id)) {
149
+ const handlers = pending.get(packet.id);
150
+ pending.delete(packet.id);
151
+ if (packet.error) handlers.reject(new Error("WorkBuddy local quota request was rejected."));
152
+ else handlers.resolve(packet.result);
153
+ } else {
154
+ throw new Error("Unexpected packet.");
155
+ }
156
+ }
157
+ signal?.addEventListener("abort", onAbort, { once: true });
158
+ if (signal?.aborted) { onAbort(); return; }
159
+ try {
160
+ socket = connectImpl(endpoint);
161
+ socket.on("connect", () => send({
162
+ type: "session_hello", protocol_min: 1, protocol_max: 1,
163
+ client_nonce: clientNonce,
164
+ ticket_id: crypto.createHash("sha256").update(ticket, "utf8").digest("hex").slice(0, 16),
165
+ client: { kind: "tokentracker", id: "subscription-quota", version: "1" },
166
+ }));
167
+ socket.on("data", (chunk) => {
168
+ if (closed) return;
169
+ buffer = Buffer.concat([buffer, chunk]);
170
+ if (buffer.length > MAX_FRAME_BYTES) { fail(new Error("WorkBuddy quota response was too large.")); return; }
171
+ try {
172
+ let index;
173
+ while ((index = buffer.indexOf(10)) >= 0 && !closed) {
174
+ const line = buffer.subarray(0, index);
175
+ buffer = buffer.subarray(index + 1);
176
+ if (line.length) onPacket(JSON.parse(line.toString("utf8")));
177
+ }
178
+ } catch {
179
+ fail(new Error("WorkBuddy local connection authentication or protocol failed."));
180
+ }
181
+ });
182
+ socket.on("error", () => fail(new Error(UNAVAILABLE)));
183
+ socket.on("close", () => fail(new Error(UNAVAILABLE)));
184
+ socket.on("end", () => fail(new Error(UNAVAILABLE)));
185
+ } catch {
186
+ fail(new Error(UNAVAILABLE));
187
+ }
188
+ });
189
+ }
190
+
191
+ module.exports = { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc };