tokentracker-cli 1.5.2 → 1.5.4
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/dashboard/dist/assets/{AchievementBadge-B5kmt0VD.js → AchievementBadge-CKNb_cwg.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-u9P8DXJI.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-BSCUJ4DR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-C7D2WbQA.js} +1 -1
- package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-B4IqAhai.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-DDEZfmZg.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-CER_AklA.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-Cbb74eib.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
- package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DaGOfcoz.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-DSS1Ira_.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-6zPLYydh.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-DdCQ36LK.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-BjP217Y1.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-Dhq8PM4h.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-CGNbED4E.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-CQZTgyQy.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-Bz09HXLn.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DC-lZsYR.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-D8MpLM_t.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-aF01udyR.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-BaroD0Ct.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-0jgyMGVM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-BUx8U58i.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-CpOjtJqg.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-BYjfer-M.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-NtYa2h0u.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
- package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-CYP95o5y.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-BCDfusOM.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.js → SelfHostPage-BmVc2rIT.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
- package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-DfnMCAuA.js → SettingsPage-CCMbq5wc.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-C4O4fbaQ.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-D2d738xr.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-BJfFYesG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-CElakFSU.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-Cvd1LbJm.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-wRC1tE_a.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Br-KAXh6.css → __vite-browser-external-CMBWGkOq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-Ba57HF3w.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-B1bKnxXL.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-DrOlKzX5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-hlYpo3yw.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-CSY6SEsI.js} +1 -1
- package/dashboard/dist/assets/{check--EZH8RTp.js → check-xrP1gLw5.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-6-zT5n9d.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-BZymZD4K.js} +1 -1
- package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-C9izWafa.js} +1 -1
- package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-Dkas5wZX.js} +1 -1
- package/dashboard/dist/assets/{download-CWHq03Y2.js → download-CagGRs0_.js} +1 -1
- package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-DRl10hxd.js} +1 -1
- package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CkvmsTFB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-BtwhO3mV.js} +1 -1
- package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-YzjXkMQx.js} +1 -1
- package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-CHhKYs0u.js} +1 -1
- package/dashboard/dist/assets/{index-DqCtgU0G.js → index-B0MQZTrM.js} +1 -1
- package/dashboard/dist/assets/{index-fNKH4U7j.js → index-C2DK8av-.js} +1 -1
- package/dashboard/dist/assets/{index-CKhcCLNC.js → index-CJknN9Jd.js} +1 -1
- package/dashboard/dist/assets/{index-DFF9xo6f.js → index-DojzoMqd.js} +1 -1
- package/dashboard/dist/assets/{info-B4YzRR4y.js → info-Torep-Ti.js} +1 -1
- package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DE5Yu-z6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-BL8LxD9n.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-8X1InWix.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-D60-6UQa.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C-IFPjqk.js} +1 -1
- package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-n6uWrdH9.js} +1 -1
- package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B2f6TaNj.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-DWqCEcsP.js} +1 -1
- package/dashboard/dist/assets/{react-BgG7_MY0.js → react-B-zfi_j6.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-C2PsvI-v.js} +1 -1
- package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DZOaTLF9.js} +1 -1
- package/dashboard/dist/assets/{server-DOEvwlEU.js → server-DIp4Qfsv.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-Cs2M8-iT.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-CoYy6Tov.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-B5_hjIfs.js} +1 -1
- package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BLXqtJzb.js} +1 -1
- package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-CEMKX3ba.js} +1 -1
- package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-DOWYaXjK.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-BjRI1Bzz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-s_CIC8Bb.js → use-native-settings-Cg5Zb4q_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.js → use-ordered-list-BF-wIvmG.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
- package/dashboard/dist/assets/{use-transform-CVeMFlS6.js → use-transform-CbJ1IcNc.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-Dgg3jpC4.js +1 -0
- package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-3oBHWfSe.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-D0YrIW0A.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-DZAHWE_R.js} +1 -1
- package/dashboard/dist/assets/{zap-D7VRNTwk.js → zap-kLub6Jiv.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +25 -15
- package/src/lib/accept-encoding.js +59 -0
- package/src/lib/cursor-config.js +3 -1
- package/src/lib/deepseek-limits.js +280 -0
- package/src/lib/deepseek-status.js +157 -0
- package/src/lib/grok-limits.js +40 -1
- package/src/lib/local-api.js +248 -10
- package/src/lib/opencode-status.js +88 -0
- package/src/lib/pricing/index.js +182 -79
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +2 -0
- package/src/lib/provider-status.js +27 -2
- package/src/lib/qoder-limits.js +17 -2
- package/src/lib/session-analytics.js +194 -29
- package/src/lib/static-server.js +74 -2
- package/src/lib/usage-limits.js +714 -53
- package/src/lib/workbuddy-limits.js +291 -26
- package/src/lib/zcode-limits.js +143 -35
- package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
- package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
- package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
- package/dashboard/dist/assets/SessionsPage-BymccEEz.js +0 -3
- package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
- package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DRZMI20u.js +0 -1
|
@@ -19,10 +19,18 @@
|
|
|
19
19
|
// Display names are English regardless of the backend's localized names.
|
|
20
20
|
|
|
21
21
|
const fs = require("node:fs");
|
|
22
|
+
const os = require("node:os");
|
|
22
23
|
const path = require("node:path");
|
|
23
24
|
const { resolveWorkbuddyHome } = require("./rollout");
|
|
24
25
|
const { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc } = require("./workbuddy-ipc");
|
|
25
26
|
|
|
27
|
+
const WORKBUDDY_LIMITS_CACHE_FILE = "workbuddy-usage-limits-cache.json";
|
|
28
|
+
// Windows without a usable reset_at — the pooled resource window unless it
|
|
29
|
+
// carries a readable expiring_at, plus a plan row whose cycle end never
|
|
30
|
+
// parsed — cannot be checked against a quota rollover, so they are only
|
|
31
|
+
// served while the snapshot is young enough to trust.
|
|
32
|
+
const WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
33
|
+
|
|
26
34
|
const PLAN_PRIORITY = Object.freeze({
|
|
27
35
|
TCACA_code_027_0FCGVA6vSa: 4, // flagship
|
|
28
36
|
TCACA_code_026_BaESVICNoi: 3, // advanced
|
|
@@ -187,6 +195,9 @@ function normalizePackageRow(row) {
|
|
|
187
195
|
used,
|
|
188
196
|
cycleEnd: firstString(row, ["CycleEndTime", "cycleEnd", "cycle_end"])
|
|
189
197
|
|| firstString(row, ["DeductionEndTime", "expireAt"]),
|
|
198
|
+
// Set once a grant has terminated (spent or revoked); active grants
|
|
199
|
+
// carry an empty string.
|
|
200
|
+
terminatedAt: firstString(row, ["ExpiredTime", "expiredTime"]),
|
|
190
201
|
};
|
|
191
202
|
}
|
|
192
203
|
|
|
@@ -398,6 +409,59 @@ function resourceWindow(pool) {
|
|
|
398
409
|
};
|
|
399
410
|
}
|
|
400
411
|
|
|
412
|
+
// Resource packages die individually: paid add-ons and reward grants each
|
|
413
|
+
// carry their own cycle end, so the pooled window can advertise when its
|
|
414
|
+
// credits start to lapse — "by <day>, <credits> expire" beats a bare date
|
|
415
|
+
// because a date alone cannot say how much is at stake. Grants are bucketed
|
|
416
|
+
// by local calendar day and only the earliest day that still holds credit is
|
|
417
|
+
// reported; later days belong in a per-grant breakdown, not the pooled bar.
|
|
418
|
+
// Rows that could be the subscription plan are not resource credit (the same
|
|
419
|
+
// rule collectResourceGroups applies), and a grant with no remaining credit,
|
|
420
|
+
// a past end, an unreadable end, or a termination stamp has nothing to lose.
|
|
421
|
+
function collectExpiringResourceCredits(rows, identity = {}, nowMs = Date.now()) {
|
|
422
|
+
const grants = new Map();
|
|
423
|
+
for (const row of rows) {
|
|
424
|
+
if (!row || isSelectablePlan(row, identity.code)) continue;
|
|
425
|
+
if (row.terminatedAt) continue;
|
|
426
|
+
const remain = row.remain
|
|
427
|
+
?? (row.total !== null && row.used !== null ? Math.max(0, row.total - row.used) : null);
|
|
428
|
+
if (remain === null || remain <= 0) continue;
|
|
429
|
+
const endMs = Date.parse(normalizeResetAt(row.cycleEnd) || "");
|
|
430
|
+
if (!Number.isFinite(endMs) || endMs <= nowMs) continue;
|
|
431
|
+
// The summary and the paid/free pages list the same grant; one code with
|
|
432
|
+
// one end time is one grant, and the later listing wins the way
|
|
433
|
+
// mergePackageRows merges.
|
|
434
|
+
grants.set(`${row.code}\u0000${endMs}`, { endMs, remain });
|
|
435
|
+
}
|
|
436
|
+
let earliest = null;
|
|
437
|
+
for (const grant of grants.values()) {
|
|
438
|
+
const day = localDayNumber(grant.endMs);
|
|
439
|
+
if (!earliest || day < earliest.day) {
|
|
440
|
+
earliest = { day, endMs: grant.endMs, firstMs: grant.endMs, credits: grant.remain };
|
|
441
|
+
} else if (day === earliest.day) {
|
|
442
|
+
// Grants lapsing the same day merge; the moment to advertise is the
|
|
443
|
+
// last one, when the day's final grant dies, while the first one marks
|
|
444
|
+
// when the pool's numbers actually start to drift.
|
|
445
|
+
earliest.endMs = Math.max(earliest.endMs, grant.endMs);
|
|
446
|
+
earliest.firstMs = Math.min(earliest.firstMs, grant.endMs);
|
|
447
|
+
earliest.credits += grant.remain;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!earliest) return null;
|
|
451
|
+
return {
|
|
452
|
+
at: new Date(earliest.endMs).toISOString(),
|
|
453
|
+
firstAt: new Date(earliest.firstMs).toISOString(),
|
|
454
|
+
credits: earliest.credits,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Year*10000 + month*100 + day, local time — an ordering key that survives
|
|
459
|
+
// unpadded string comparison and matches how the end times parse.
|
|
460
|
+
function localDayNumber(ms) {
|
|
461
|
+
const date = new Date(ms);
|
|
462
|
+
return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
|
|
463
|
+
}
|
|
464
|
+
|
|
401
465
|
function englishPlanLabel(row, identity, variantId) {
|
|
402
466
|
// An actual English subscription/package name takes precedence over the
|
|
403
467
|
// client's edition fallback. Unknown localized tiers stay unnamed.
|
|
@@ -451,12 +515,16 @@ function readAuthFile(filePath) {
|
|
|
451
515
|
const auth = parsed?.auth;
|
|
452
516
|
if (!auth || typeof auth !== "object") return null;
|
|
453
517
|
const domain = isNonEmptyString(auth.domain) ? auth.domain.trim() : "";
|
|
518
|
+
// The app records the signed-in account next to its (often encrypted)
|
|
519
|
+
// session. The uid stays readable while the app is closed, so it is the
|
|
520
|
+
// one account signal that survives into the UNAVAILABLE state.
|
|
521
|
+
const uid = isNonEmptyString(parsed.account?.uid) ? parsed.account.uid.trim() : "";
|
|
454
522
|
const token = auth.accessToken;
|
|
455
|
-
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false };
|
|
523
|
+
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false, uid };
|
|
456
524
|
if (token && typeof token === "object" && token.$wbEncrypted === 1) {
|
|
457
|
-
return { token: null, domain, encrypted: true };
|
|
525
|
+
return { token: null, domain, encrypted: true, uid };
|
|
458
526
|
}
|
|
459
|
-
return { token: null, domain, encrypted: false };
|
|
527
|
+
return { token: null, domain, encrypted: false, uid };
|
|
460
528
|
}
|
|
461
529
|
|
|
462
530
|
function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
@@ -464,15 +532,130 @@ function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
|
464
532
|
? authFiles
|
|
465
533
|
: authSearchDirs(env, workbuddyHome).flatMap((dir) => variant.authFileNames.map((name) => path.join(dir, name)));
|
|
466
534
|
let encryptedForVariant = false;
|
|
535
|
+
let encryptedUid = "";
|
|
467
536
|
for (const filePath of files) {
|
|
468
537
|
if (!filePath) continue;
|
|
469
538
|
const session = readAuthFile(filePath);
|
|
470
539
|
if (!session) continue;
|
|
471
540
|
if (!hostAllowed(hostOf(session.domain), variant.allowedHostSuffixes)) continue;
|
|
472
|
-
if (session.token) return { token: session.token, encrypted: false, matched: true };
|
|
473
|
-
if (session.encrypted)
|
|
541
|
+
if (session.token) return { token: session.token, encrypted: false, matched: true, uid: session.uid };
|
|
542
|
+
if (session.encrypted) {
|
|
543
|
+
encryptedForVariant = true;
|
|
544
|
+
// First encrypted candidate wins: when the app is closed this record is
|
|
545
|
+
// what a live read would authenticate with, so its account is the one
|
|
546
|
+
// the cached windows must match.
|
|
547
|
+
encryptedUid = encryptedUid || session.uid;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return { token: null, encrypted: encryptedForVariant, matched: encryptedForVariant, uid: encryptedUid };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// Last-good disk cache for WorkBuddy quota windows (house pattern: codex /
|
|
555
|
+
// ark / qoder / zcode, issue #952). A live read — IPC or API — persists the
|
|
556
|
+
// windows per edition; when the live chain later fails, the stale windows are
|
|
557
|
+
// served with data-age markers instead of a bare "Start WorkBuddy and sign
|
|
558
|
+
// in". The steady state this covers is the app being closed: wbipc discovery
|
|
559
|
+
// is gone and the encrypted session cannot be read.
|
|
560
|
+
//
|
|
561
|
+
// Editions never borrow each other's data: workbuddy CN and workbuddyAi are
|
|
562
|
+
// separate partitions of one file, matching the edition-local socket
|
|
563
|
+
// discipline. Partitions are also scoped to the login that produced the
|
|
564
|
+
// windows, so an account switch followed by a live failure cannot serve the
|
|
565
|
+
// previous account's bars.
|
|
566
|
+
//
|
|
567
|
+
// The cache identity comes from the session selection itself (resolveSession):
|
|
568
|
+
// the record whose token supplies — or, while the app is closed, would supply
|
|
569
|
+
// — the live read. It is the app's own account uid, readable after the app
|
|
570
|
+
// closes when the encrypted session token is not, and it is an account id,
|
|
571
|
+
// not a credential: nothing secret is persisted or hashed. An account the
|
|
572
|
+
// records do not name ("unknown") matches only its own entries.
|
|
573
|
+
|
|
574
|
+
function workbuddyLimitsCachePath({ home } = {}) {
|
|
575
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", WORKBUDDY_LIMITS_CACHE_FILE);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// A window whose reset_at has already passed is stale data, not a usable
|
|
579
|
+
// fallback — the quota has rolled over, so drop it. Undated windows (the
|
|
580
|
+
// resource pool without a readable expiring_at, or a plan row without a
|
|
581
|
+
// readable cycle end) cannot be checked against a reset and are bounded by
|
|
582
|
+
// the snapshot-age TTL instead.
|
|
583
|
+
function workbuddyCachedWindow(window, { cachedAtMs, nowMs } = {}) {
|
|
584
|
+
if (!window || typeof window !== "object") return null;
|
|
585
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
586
|
+
if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
|
|
587
|
+
// The resource pool has no reset but may carry its earliest grant expiry.
|
|
588
|
+
// expiring_first_at is the first moment the advertised credits start to
|
|
589
|
+
// lapse — from then on the pool's numbers no longer describe the account,
|
|
590
|
+
// so the snapshot is trusted exactly until then; expiring_at (the day's
|
|
591
|
+
// end) is only the display anchor. The same discipline as a reset date,
|
|
592
|
+
// not the undated TTL.
|
|
593
|
+
const expiringAtMs = Date.parse(window.expiring_first_at || "");
|
|
594
|
+
const anchorMs = Number.isFinite(expiringAtMs) ? expiringAtMs : Date.parse(window.expiring_at || "");
|
|
595
|
+
if (Number.isFinite(anchorMs)) return anchorMs > nowMs ? window : null;
|
|
596
|
+
return nowMs - cachedAtMs <= WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Read the last-good windows for one edition, scoped to the login that wrote them. */
|
|
600
|
+
function readWorkbuddyLimitsCache({ home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
601
|
+
// An explicit home keeps the cache hermetic: callers that never said where
|
|
602
|
+
// the user's tracker state lives (tests, odd embedders) must not touch the
|
|
603
|
+
// developer's real cache file.
|
|
604
|
+
if (!home || !variantId || !identity) return null;
|
|
605
|
+
let parsed;
|
|
606
|
+
try {
|
|
607
|
+
parsed = JSON.parse(fs.readFileSync(workbuddyLimitsCachePath({ home }), "utf8"));
|
|
608
|
+
} catch (_error) {
|
|
609
|
+
return null;
|
|
474
610
|
}
|
|
475
|
-
|
|
611
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
612
|
+
const raw = parsed[variantId];
|
|
613
|
+
if (!raw || typeof raw !== "object") return null;
|
|
614
|
+
if (raw.identity !== identity) return null;
|
|
615
|
+
const cachedAtMs = Date.parse(raw.cached_at || "");
|
|
616
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
617
|
+
const context = { cachedAtMs, nowMs };
|
|
618
|
+
const primaryWindow = workbuddyCachedWindow(raw.primary_window, context);
|
|
619
|
+
const secondaryWindow = workbuddyCachedWindow(raw.secondary_window, context);
|
|
620
|
+
// A partition whose windows all rolled or expired no longer describes the
|
|
621
|
+
// account's quota — withholding it lets the live error stay visible.
|
|
622
|
+
if (!primaryWindow && !secondaryWindow) return null;
|
|
623
|
+
return {
|
|
624
|
+
configured: true,
|
|
625
|
+
error: null,
|
|
626
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
627
|
+
primary_window: primaryWindow,
|
|
628
|
+
secondary_window: secondaryWindow,
|
|
629
|
+
cached: true,
|
|
630
|
+
stale: true,
|
|
631
|
+
cached_at: raw.cached_at,
|
|
632
|
+
source: "disk-cache",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Merge one edition's partition into the disk cache; best-effort, never throws. */
|
|
637
|
+
function writeWorkbuddyLimitsCache(limits, { home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
638
|
+
if (!home || !variantId || !identity || !limits?.configured || limits.error) return;
|
|
639
|
+
if (!limits.primary_window && !limits.secondary_window) return;
|
|
640
|
+
const cachePath = workbuddyLimitsCachePath({ home });
|
|
641
|
+
let payload = {};
|
|
642
|
+
try {
|
|
643
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
644
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
645
|
+
} catch (_error) {}
|
|
646
|
+
payload[variantId] = {
|
|
647
|
+
identity,
|
|
648
|
+
plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
|
|
649
|
+
primary_window: limits.primary_window || null,
|
|
650
|
+
secondary_window: limits.secondary_window || null,
|
|
651
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
652
|
+
};
|
|
653
|
+
try {
|
|
654
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
655
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
656
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
657
|
+
fs.renameSync(tmpPath, cachePath);
|
|
658
|
+
} catch (_error) {}
|
|
476
659
|
}
|
|
477
660
|
|
|
478
661
|
function readProductEndpoint(workbuddyHome) {
|
|
@@ -539,18 +722,27 @@ async function postBilling(fetchImpl, url, token, body = {}, signal) {
|
|
|
539
722
|
return response.json();
|
|
540
723
|
}
|
|
541
724
|
|
|
542
|
-
async function fetchPlanQuota(post, variantId) {
|
|
725
|
+
async function fetchPlanQuota(post, variantId, nowMs) {
|
|
543
726
|
const prefix = "/billing/meter/";
|
|
544
727
|
const summary = await post(`${prefix}get-user-resource-summary`, {});
|
|
545
728
|
if (!unwrapBillingBody(summary)) throw new Error("WorkBuddy quota request was rejected.");
|
|
546
729
|
const identity = readSubscriptionIdentity(summary);
|
|
730
|
+
const summaryRows = collectPackageRows(summary);
|
|
547
731
|
const planCodes = [...new Set([...Object.keys(PLAN_PRIORITY),
|
|
548
732
|
...(identity.code && !isRewardCode(identity.code) ? [identity.code] : [])])];
|
|
733
|
+
// Resource grants ride along in the same pages query. The summary carries
|
|
734
|
+
// their aggregated pool row without a cycle end, so the per-grant rows —
|
|
735
|
+
// where each grant's own CycleEndTime lives — only come back when their
|
|
736
|
+
// package codes are requested; a plan-code-only listing never sees them.
|
|
737
|
+
const resourceCodes = summaryRows
|
|
738
|
+
.filter((row) => !isSelectablePlan(row, identity.code))
|
|
739
|
+
.map((row) => row.code);
|
|
740
|
+
const packageCodes = [...new Set([...planCodes, ...resourceCodes])];
|
|
549
741
|
const packageRows = await Promise.all(["paid", "free"].map(async (kind) => {
|
|
550
742
|
const rows = [];
|
|
551
743
|
for (let page = 1; page <= 10; page++) {
|
|
552
744
|
const payload = await post(`${prefix}get-user-resource-${kind}-packages`, {
|
|
553
|
-
PageNumber: page, PageSize: 100, PackageCodes:
|
|
745
|
+
PageNumber: page, PageSize: 100, PackageCodes: packageCodes,
|
|
554
746
|
Status: [0, 3], ...(kind === "paid" ? { NeedRenewInfo: true } : {}),
|
|
555
747
|
});
|
|
556
748
|
const data = unwrapBillingBody(payload);
|
|
@@ -561,14 +753,28 @@ async function fetchPlanQuota(post, variantId) {
|
|
|
561
753
|
}
|
|
562
754
|
throw new Error("WorkBuddy quota package list was incomplete.");
|
|
563
755
|
}));
|
|
564
|
-
const quota = selectWorkbuddyPlanQuota([
|
|
565
|
-
|
|
566
|
-
|
|
756
|
+
const quota = selectWorkbuddyPlanQuota([...summaryRows, ...packageRows.flat()], identity, variantId);
|
|
757
|
+
const secondary = resourceWindow(collectResourceGroups(summary));
|
|
758
|
+
if (secondary) {
|
|
759
|
+
// The pooled bar cannot say which credits lapse first; the grant rows
|
|
760
|
+
// carry each grant's cycle end. The summary's own pool row has no cycle
|
|
761
|
+
// end and skips the expiry bucket, so grants are not double-counted. A
|
|
762
|
+
// pool without a window is dropped wholesale, so expiry has nothing to
|
|
763
|
+
// attach to.
|
|
764
|
+
const expiring = collectExpiringResourceCredits(
|
|
765
|
+
[...summaryRows, ...packageRows.flat()], identity, nowMs,
|
|
766
|
+
);
|
|
767
|
+
if (expiring) {
|
|
768
|
+
secondary.expiring_at = expiring.at;
|
|
769
|
+
secondary.expiring_first_at = expiring.firstAt;
|
|
770
|
+
secondary.expiring_credits = expiring.credits;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
567
773
|
return {
|
|
568
774
|
configured: true, error: null,
|
|
569
775
|
plan_label: quota?.plan_label || englishPlanLabel({ code: identity.code }, identity, variantId),
|
|
570
776
|
primary_window: quota?.primary_window || null,
|
|
571
|
-
secondary_window:
|
|
777
|
+
secondary_window: secondary,
|
|
572
778
|
};
|
|
573
779
|
}
|
|
574
780
|
|
|
@@ -584,33 +790,35 @@ function scopedEnv(env, home) {
|
|
|
584
790
|
return next;
|
|
585
791
|
}
|
|
586
792
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
793
|
+
// Live chain for one edition, unchanged by the cache layer: the IPC proxy
|
|
794
|
+
// while the app publishes it, then the API source with a resolvable session.
|
|
795
|
+
// The session is selected by the caller (fetchVariant) so the cache identity
|
|
796
|
+
// and the live read cannot disagree about the account. Throws propagate to
|
|
797
|
+
// the cache wrapper below; the { configured: true, error: UNAVAILABLE }
|
|
798
|
+
// result comes back when the app is closed and no session can be read.
|
|
799
|
+
async function fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session }) {
|
|
593
800
|
const discovery = readWorkbuddyIpcDiscovery(workbuddyHome);
|
|
594
801
|
if (discovery) {
|
|
595
802
|
// Discovery is edition-local. Never fall back to the other edition's
|
|
596
803
|
// socket or allow an explicitly redirected product to be mislabelled.
|
|
597
804
|
const productEndpoint = readProductEndpoint(workbuddyHome);
|
|
598
805
|
if (productEndpoint && !originAllowed(productEndpoint, variant)) {
|
|
599
|
-
|
|
806
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
807
|
+
// Tagged so the cache wrapper surfaces the mislabelled host instead of
|
|
808
|
+
// stale bars from whichever login wrote them.
|
|
809
|
+
error.code = "EDITION_MISMATCH";
|
|
810
|
+
throw error;
|
|
600
811
|
}
|
|
601
812
|
const client = await openWorkbuddyIpc({
|
|
602
813
|
discovery, signal: opts.signal, timeoutMs: opts.timeoutMs, connectImpl: opts.connectImpl,
|
|
603
814
|
});
|
|
604
815
|
try {
|
|
605
|
-
return { ...await fetchPlanQuota((apiPath, body) => client.postBilling(apiPath, body), variantId
|
|
816
|
+
return { ...await fetchPlanQuota((apiPath, body) => client.postBilling(apiPath, body), variantId,
|
|
817
|
+
Number.isFinite(opts.nowMs) ? opts.nowMs : undefined), source: "ipc" };
|
|
606
818
|
} finally {
|
|
607
819
|
client.close();
|
|
608
820
|
}
|
|
609
821
|
}
|
|
610
|
-
const explicitToken = resolveToken(variant, env);
|
|
611
|
-
const session = explicitToken
|
|
612
|
-
? { token: explicitToken, encrypted: false, matched: true }
|
|
613
|
-
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
614
822
|
if (!session.token) {
|
|
615
823
|
if (session.encrypted || installed) return { configured: true, error: UNAVAILABLE };
|
|
616
824
|
if (!installed) return { configured: false };
|
|
@@ -625,10 +833,63 @@ async function fetchVariant(variantId, opts = {}) {
|
|
|
625
833
|
workbuddyHome: opts.origin ? null : workbuddyHome,
|
|
626
834
|
});
|
|
627
835
|
if (!originAllowed(origin, variant)) {
|
|
628
|
-
|
|
836
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
837
|
+
error.code = "EDITION_MISMATCH";
|
|
838
|
+
throw error;
|
|
629
839
|
}
|
|
630
840
|
return { ...await fetchPlanQuota((apiPath, body) => postBilling(fetchImpl,
|
|
631
|
-
`${origin}${apiPath}`, session.token, body, opts.signal), variantId
|
|
841
|
+
`${origin}${apiPath}`, session.token, body, opts.signal), variantId,
|
|
842
|
+
Number.isFinite(opts.nowMs) ? opts.nowMs : undefined), source: "api" };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function fetchVariant(variantId, opts = {}) {
|
|
846
|
+
const variant = variantById(variantId);
|
|
847
|
+
if (!variant) return { configured: false };
|
|
848
|
+
const env = scopedEnv(opts.env || process.env, opts.home);
|
|
849
|
+
const workbuddyHome = opts.workbuddyHome || resolveWorkbuddyHome(env, variant.source);
|
|
850
|
+
const installed = directoryExists(workbuddyHome);
|
|
851
|
+
// Sampled when used, not when the fetch starts: the live attempt can spend
|
|
852
|
+
// the whole provider budget before a cache read or write happens, and a
|
|
853
|
+
// window whose reset rolled in that span must not be served. An injected
|
|
854
|
+
// clock (tests) keeps every use on the same instant.
|
|
855
|
+
const nowMs = () => (Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now());
|
|
856
|
+
// One session selection feeds both the live attempt and the cache identity:
|
|
857
|
+
// cached windows are stamped with (and served only to) the account whose
|
|
858
|
+
// record supplies — or, while the app is closed, would supply — the session
|
|
859
|
+
// token. An env-token session names no record; "unknown" matches only its
|
|
860
|
+
// own entries.
|
|
861
|
+
const explicitToken = resolveToken(variant, env);
|
|
862
|
+
const session = explicitToken
|
|
863
|
+
? { token: explicitToken, encrypted: false, matched: true, uid: "" }
|
|
864
|
+
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
865
|
+
const identity = (explicitToken ? "" : session.uid) || "unknown";
|
|
866
|
+
let live;
|
|
867
|
+
try {
|
|
868
|
+
live = await fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session });
|
|
869
|
+
} catch (error) {
|
|
870
|
+
// Re-login stays actionable (opencode-go auth_error / codex reauth
|
|
871
|
+
// precedent) and a mislabelled host is a configuration fault, not
|
|
872
|
+
// transient data loss: neither may be masked by stale bars.
|
|
873
|
+
if (error?.code === "AUTH_EXPIRED" || error?.code === "EDITION_MISMATCH") throw error;
|
|
874
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
875
|
+
if (cached) return cached;
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
if (live.configured === true && !live.error) {
|
|
879
|
+
// Live read is current as of now: persist it so a later failure serves
|
|
880
|
+
// stale bars instead of a vanished card.
|
|
881
|
+
writeWorkbuddyLimitsCache(live, { home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
882
|
+
return live;
|
|
883
|
+
}
|
|
884
|
+
if (live.configured === true && live.error) {
|
|
885
|
+
// The steady state the cache exists for (UNAVAILABLE: app closed, session
|
|
886
|
+
// unreadable) and transient API/IPC failures. A never-signed-in install
|
|
887
|
+
// ({ configured: false }) stays untouched — an edition that produced no
|
|
888
|
+
// windows here has nothing to fall back to.
|
|
889
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
890
|
+
if (cached) return cached;
|
|
891
|
+
}
|
|
892
|
+
return live;
|
|
632
893
|
}
|
|
633
894
|
|
|
634
895
|
function fetchWorkbuddyLimits(opts) {
|
|
@@ -647,10 +908,14 @@ module.exports = {
|
|
|
647
908
|
selectWorkbuddyLimits,
|
|
648
909
|
collectPackageRows,
|
|
649
910
|
collectResourceGroups,
|
|
911
|
+
collectExpiringResourceCredits,
|
|
650
912
|
resourceWindow,
|
|
651
913
|
readSubscriptionIdentity,
|
|
652
914
|
originAllowed,
|
|
653
915
|
hostAllowed,
|
|
916
|
+
readWorkbuddyLimitsCache,
|
|
917
|
+
writeWorkbuddyLimitsCache,
|
|
918
|
+
workbuddyLimitsCachePath,
|
|
654
919
|
fetchWorkbuddyLimits,
|
|
655
920
|
fetchWorkbuddyAiLimits,
|
|
656
921
|
};
|