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.
- package/README.de.md +2 -2
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +11 -3
- package/README.zh-CN.md +3 -3
- package/bin/tracker.js +6 -0
- package/dashboard/dist/assets/{AchievementBadge-DF8q1x6R.js → AchievementBadge-BTqlXUP1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-B8Dg5myo.js → AchievementsPage-DbKUhaY-.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-oPPiyP3y.js → AchievementsSection-El1F-OC7.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-PneMW4B4.js → BadgeDetailModal--8PySLzP.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-GOlSvEiz.js → BotAnimated-DBcOS4vh.js} +1 -1
- package/dashboard/dist/assets/{Card-BT5G0GE0.js → Card-CeGs8TaY.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BNeL5Muo.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
- package/dashboard/dist/assets/CloudPage-CmeFDrmk.js +2 -0
- package/dashboard/dist/assets/{CommandPalette-CiMSYHNK.js → CommandPalette-CrSNoZW9.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-fK3BE9CW.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CrUlEHyZ.js → ConfirmModal-DDTUf-My.js} +1 -1
- package/dashboard/dist/assets/{Controls-CbjSwq6k.js → Controls-DXsJjEU1.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-fr4sgbLB.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-E-2Z6imh.js → DateRangePopover-gTiPISL_.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
- package/dashboard/dist/assets/{DialogClose-xJKE22Y6.js → DialogClose-CXwouQe4.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-f8vnnH7z.js → DialogTitle-Cd_k-Nyw.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Ci9WZfHK.js → ElectricLogo-C4vUNTCI.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BXqtyAmn.js → FadeIn-CGM5RBFy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BV5SYJMi.js → HeaderGithubStar-el5-DhVb.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D3gpW3Jh.js → HoverTooltip-C7YaWwJx.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-G7fIWXpL.js → IpCheckPage-EKhbH9hJ.js} +1 -1
- package/dashboard/dist/assets/LandingPage-DJYOv1AM.js +2 -0
- package/dashboard/dist/assets/{LeaderboardPage-D2_wXazL.js → LeaderboardPage-CwzJxA8p.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-3xJ-mH2v.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-23pH6Ud7.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DSezSR58.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Dsk-d0Hr.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-C7dugIIn.js → LoginCard-Czq-UgAU.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B7RuLsTF.js → LoginModal-D4uxfzZb.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-u32vJQN9.js → LoginPage-Bg3BG0Wv.js} +1 -1
- package/dashboard/dist/assets/{PetPage-Dbc975oU.js → PetPage-BgwBxRyx.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-D-yECvH9.js → PopoverPopup-k3o_Jp2b.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CHtZiJUN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-ctuIsNk0.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
- package/dashboard/dist/assets/{Select-DN4Kq_R1.js → Select-kA0dV5Qw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C-HmeD8L.js → SelectItemText-D4YCIn1x.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-BZOL3JMt.js → SelfHostPage-C3Y9qF3P.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C0sDYa8c.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-l0-FkcJB.js → SessionsPage-CA-efxhU.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-Mr84vxQL.js +5 -0
- package/dashboard/dist/assets/{SkillsPage-Cz6G9d3o.js → SkillsPage-BtRhLSpz.js} +1 -1
- package/dashboard/dist/assets/TokenGalaxy-CcFjCCuC.js +4426 -0
- package/dashboard/dist/assets/TooltipPopup-6aTx3jDf.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-DoywXr8h.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-BKo3DBxT.js → WidgetsPage-BuGSzvR0.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Blm3aPO5.js → WrappedPage-Bm6X-SUZ.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-ClVJNQu3.js +45 -0
- package/dashboard/dist/assets/{agent-logos-DMtDEx7_.js → agent-logos-Sc8Sejgx.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bo9y7VzF.js → arrow-left-Cm1LG2i_.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BLMU8Y5U.js → arrow-right-WP3kIwiy.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B4hXO4sc.js → arrow-up-right-CjyprVMj.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-D9HaEtgo.js → calendar-days-Bm88-Gcb.js} +1 -1
- package/dashboard/dist/assets/{check-BJNGKpX8.js → check-CzIxFg0I.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CoGC_RLK.js → chevron-down-bfGK8629.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-Du5FoHqV.js → cloud-checkout-C9DFQ7yM.js} +1 -1
- package/dashboard/dist/assets/{composite-BfIUPpl8.js → composite-CdExmgTx.js} +1 -1
- package/dashboard/dist/assets/copy-CPPdujn2.js +1 -0
- package/dashboard/dist/assets/{download-C1K_v0C0.js → download-Cl6_dKeK.js} +1 -1
- package/dashboard/dist/assets/{external-link-D-xHhfj5.js → external-link-B0r6Wo4e.js} +1 -1
- package/dashboard/dist/assets/{flame-DXD6uvhM.js → flame-wEezvzXx.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-B9riGvNj.js → format-tokens-vWpBpKpM.js} +1 -1
- package/dashboard/dist/assets/{gift-LPR-nD0J.js → gift-BFjblRqU.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-DLdtLqEt.js → icons-DX3dCNJP.js} +1 -1
- package/dashboard/dist/assets/{index-CXdJ7zMN.js → index-D-TnqdBv.js} +1 -1
- package/dashboard/dist/assets/{index-CC3cXeBa.js → index-D1Zdrpom.js} +1 -1
- package/dashboard/dist/assets/index-DBF1bJlm.js +502 -0
- package/dashboard/dist/assets/{index-BL11MboX.js → index-p8Gt3NFg.js} +1 -1
- package/dashboard/dist/assets/{info-tnXKCyuK.js → info-Cqi971MA.js} +1 -1
- package/dashboard/dist/assets/{link-2-DOk9Tcg1.js → link-2-79sm__cT.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DIl96Mw-.js → loader-circle-CDoVfOUb.js} +1 -1
- package/dashboard/dist/assets/local-usage-transfer-EcXnuwCj.js +1 -0
- package/dashboard/dist/assets/{modifiers.esm-2H_1taJ6.js → modifiers.esm-v5_BKSmY.js} +1 -1
- package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-tnp-03hs.js → monitor-smartphone-CO67DffO.js} +1 -1
- package/dashboard/dist/assets/{play-BGMuGTdN.js → play-CbvuGtc3.js} +1 -1
- package/dashboard/dist/assets/{plus-XETAaErq.js → plus-jc2gXlRM.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BYNT4zPc.js → provider-display-BK3EbRLM.js} +1 -1
- package/dashboard/dist/assets/{react-OW7HALVO.js → react-DMs4C4qS.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-CAfS8-ct.js → safePolygon-cY9H0GBN.js} +1 -1
- package/dashboard/dist/assets/{search-B7ZJhoau.js → search-fqTJoYYA.js} +1 -1
- package/dashboard/dist/assets/{server-CWrP7mtL.js → server-jpBtvTvr.js} +1 -1
- package/dashboard/dist/assets/{shield-check-ip5IQEK3.js → shield-check-DQiBlJbN.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CpyoNTeB.js → skills-api-CN2r_rmt.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-YvzWzPDu.js → sortable.esm-DrS52yOK.js} +1 -1
- package/dashboard/dist/assets/{store-DRK6-JBg.js → store-K2Mk6aMn.js} +1 -1
- package/dashboard/dist/assets/{terminal-DQRoJSvh.js → terminal-Q4SruAlh.js} +1 -1
- package/dashboard/dist/assets/{trash-8h_26atF.js → trash-C1Ob-g8E.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DYlhIXsp.js → use-community-stats-C-Gxpp9D.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-AxJF9SEJ.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-BQgB6_PB.js → use-native-settings-BZLEIUK8.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-ogi7Okzf.js → use-ordered-list-De_Mcm_H.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
- package/dashboard/dist/assets/{use-transform-BQqoo1et.js → use-transform-XCDD5IrX.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
- package/dashboard/dist/assets/{useCurrency-Bv9g8mpq.js → useCurrency-DXPclxBT.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-CQiZGknc.js → usePositioner-C-E1npCy.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BUVirY0k.js → useTokenFormat-B70xhy9u.js} +1 -1
- package/dashboard/dist/assets/{zap-DR3VD0yY.js → zap-D7gRgGCj.js} +1 -1
- package/dashboard/dist/index.html +5 -5
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/privacy.html +8 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +4 -3
- package/src/cli.js +10 -0
- package/src/commands/sync.js +18 -3
- package/src/lib/codex-rollout-parser.js +29 -2
- package/src/lib/codex-service-tier.js +13 -0
- package/src/lib/commandcode-limits.js +27 -11
- package/src/lib/grok-limits.js +1 -0
- package/src/lib/local-api.js +122 -11
- package/src/lib/local-usage-transfer.js +271 -0
- package/src/lib/opencode-go-limits.js +17 -6
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/index.js +50 -39
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +110 -0
- package/src/lib/rollout.js +27 -3
- package/src/lib/sentry.js +231 -0
- package/src/lib/session-analytics.js +15 -2
- package/src/lib/static-server.js +15 -2
- package/src/lib/usage-limits.js +74 -24
- package/src/lib/workbuddy-ipc.js +191 -0
- package/src/lib/workbuddy-limits.js +564 -0
- package/dashboard/dist/assets/CloudPage-26bhEYqI.js +0 -2
- package/dashboard/dist/assets/DashboardPage-CP_PMMga.js +0 -1
- package/dashboard/dist/assets/DevicePage-B0fLd80M.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +0 -1
- package/dashboard/dist/assets/LandingPage-BQNOdAkR.js +0 -2
- package/dashboard/dist/assets/LimitsPage-BLJsmVVy.js +0 -9
- package/dashboard/dist/assets/SettingsPage-CrczfZiZ.js +0 -5
- package/dashboard/dist/assets/TokenGalaxy-B5IGaLQK.js +0 -4426
- package/dashboard/dist/assets/TooltipPopup-B6FqLWO7.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-B4kJUwmF.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external--HDUIUZr.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
- package/dashboard/dist/assets/host-mode-C2K0MMOs.js +0 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
- package/dashboard/dist/assets/use-limits-display-prefs-DOZzuTwo.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +0 -1
- package/dashboard/dist/assets/use-session-efficiency-pref-DP3hu3Rp.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DAf6O4aZ.js +0 -1
package/src/lib/usage-limits.js
CHANGED
|
@@ -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
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
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 };
|