tokentracker-cli 1.5.1 → 1.5.3
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-mB7HZSGE.js → AchievementBadge-uuDGO9Mq.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DLzeq_eo.js → AchievementsPage-BCTVT7Jo.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DrXQyDns.js → AchievementsSection-B6fJSJSr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-WrgKRybi.js → BadgeDetailModal-FjUN__9G.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DFRaixVK.js → BotAnimated-B-KqY_Wr.js} +1 -1
- package/dashboard/dist/assets/{Card-bFq2tlO8.js → Card-DS8T8piP.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-gwhGxavy.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BD4iBYeZ.js → CloudPage-BstgVBaV.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_KlPl0U.js → CommandPalette-DtVsYZgc.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Cl1Bgv8M.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-NWoYevYC.js → ConfirmModal-Lfdh2vr7.js} +1 -1
- package/dashboard/dist/assets/{Controls-D2-D2cfA.js → Controls-DpwQ7pkq.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-8EMVXuH6.js → DateRangePopover-CrWgrzT2.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DbP8d6bM.js → DevicePage-DbK5KWJS.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CEstgLa1.js → DialogClose-Dd2B_o03.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DP5TLLbz.js → DialogTitle-3JQBVha3.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Uvi3yMum.js → ElectricLogo-LYAqG8pG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dpzihg6f.js → FadeIn-DhDvUor7.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DllqemKr.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Dc-TAH6l.js → HoverTooltip-BXhHAkP7.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-jd1D5ZKX.js → IpCheckPage-ChPdDeMl.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CB1ZVw-a.js → LandingPage-DnJ7Y9zw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Bt2cRRmE.js → LeaderboardPage-BF9isLzu.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-BVlyVOFf.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-CwQFRJg_.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CeqcTbUG.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2_HoCVD.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-nw9JOc2d.js → LoginCard-g_Gbnzhs.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BHNLTK1D.js → LoginModal-DkJrI6b7.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-LI2GZKhM.js → LoginPage-CqCKSo0n.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CK6sPeI0.js → PetPage-DbF7ROsj.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-BsXchEVa.js → PopoverPopup-4WuH41ZT.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-vgCEy0dh.js → ProviderIcon-6j0A2KD9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B4CPj2vX.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
- package/dashboard/dist/assets/{Select-BkAy6P2r.js → Select-BBEpLeiG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C0RNqDrD.js → SelectItemText-Cn_U_rrO.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-B-_w-Z-a.js → SelfHostPage-2WZc6Rml.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-CmWXBqD2.js → SessionsPage-CWS0iQ1L.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-CA70zP-t.js → SettingsPage-rXNr96i-.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CNamhfzU.js → SkillsPage-CudTvbZL.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CXrVBSk6.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-CR4MqElX.js → TooltipPopup-CsLKXhWC.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DQfcrEve.js → TrendMonitor-8onUTbkk.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BBXxtZbP.js → WidgetsPage-CcwE0lz8.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C8i2KlMF.js → WrappedPage-Cj9LLYqP.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
- package/dashboard/dist/assets/{agent-logos-CxQb8vir.js → agent-logos-ChBA1nAH.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-D6zkcLVt.js → arrow-left-Bb5mfoPc.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BGb-XVn_.js → arrow-right-D1Ti2XUI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-XTStagDJ.js → arrow-up-right-B_CjA_y8.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-Cz3NGZiS.js → calendar-days-DdfrAqie.js} +1 -1
- package/dashboard/dist/assets/{check-1z_vmHPD.js → check-29zGw1MK.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CkSBY3bE.js → chevron-down-DAzlFX7g.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-pD5C4r_S.js → cloud-checkout-C2SeCjFM.js} +1 -1
- package/dashboard/dist/assets/{composite-BtDzx3a2.js → composite-DIf735Hq.js} +1 -1
- package/dashboard/dist/assets/{copy-Ctv_wxuD.js → copy-BOuOM0HQ.js} +1 -1
- package/dashboard/dist/assets/{download-DKbKMGSR.js → download-50tQRpGe.js} +1 -1
- package/dashboard/dist/assets/{external-link-g9cU017t.js → external-link-BhlQClVe.js} +1 -1
- package/dashboard/dist/assets/{flame-DZq3kXUt.js → flame-CeGfA-0L.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CXQfx0PC.js → format-tokens-Cq7YiAkJ.js} +1 -1
- package/dashboard/dist/assets/{gift-CKlKjGP6.js → gift-Do-UFhiT.js} +1 -1
- package/dashboard/dist/assets/{icons-xPGMnBjl.js → icons-C6EZ_HmP.js} +1 -1
- package/dashboard/dist/assets/{index-fVbP5wOv.js → index-BFrbVqsm.js} +1 -1
- package/dashboard/dist/assets/{index-CDBOKcDh.js → index-BWhbBAd9.js} +1 -1
- package/dashboard/dist/assets/{index-Dl0oFLNr.js → index-D9kwYWLX.js} +1 -1
- package/dashboard/dist/assets/{index-BwOjMtB2.js → index-c0crpe9Z.js} +1 -1
- package/dashboard/dist/assets/{info-D9xNIuGh.js → info-B4MXmsNE.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bk5wYt2n.js → link-2-DuWCslzl.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CcN3pHWb.js → loader-circle-C2I3NZAO.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CYcy1ici.js → local-usage-transfer-BQasvsUF.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbpEzkJe.js → modifiers.esm-DbeNS6FT.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-WkWv1qIS.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
- package/dashboard/dist/assets/{play-2yZGWMes.js → play-CwgcDJC2.js} +1 -1
- package/dashboard/dist/assets/{plus-mOF8tG1F.js → plus-B796wsUO.js} +1 -1
- package/dashboard/dist/assets/{provider-display-7gaOvgMp.js → provider-display-Cw26RFDm.js} +1 -1
- package/dashboard/dist/assets/{react-CdPiPDhk.js → react-d7Bk1-_K.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C7FJLAlh.js → safePolygon-CDE0dNv_.js} +1 -1
- package/dashboard/dist/assets/{search-C63nCrjo.js → search-DtKvsdRs.js} +1 -1
- package/dashboard/dist/assets/{server-BaG1rnuv.js → server-gAURHUyz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-D_4-Cm6L.js → shield-check-DFVdDTwh.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DfbzwpDv.js → skills-api-2rdPUrJI.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D-v8FRX5.js → sortable.esm-DFlfwnJr.js} +1 -1
- package/dashboard/dist/assets/{store-Dv_FYrXK.js → store-BKPCk_de.js} +1 -1
- package/dashboard/dist/assets/{terminal-FQi6MaYt.js → terminal-BtWFakvS.js} +1 -1
- package/dashboard/dist/assets/{trash-TbK96dEz.js → trash-bpr9zTAM.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DGoWVZuk.js → use-community-stats-DMnGgLne.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Dg6uSaYt.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-DIk0AF3b.js → use-native-settings-OthqiF6g.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CUqV3Zgl.js → use-ordered-list-DpAtr0R-.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
- package/dashboard/dist/assets/{use-transform-D_-w22c8.js → use-transform-BRz9dz_2.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
- package/dashboard/dist/assets/{useCurrency-B_AuLCsL.js → useCurrency-CAA-tQ27.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DfxvIif8.js → usePositioner-Cj2DPmwM.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-bbO13V4s.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
- package/dashboard/dist/assets/{zap-k07-EXTK.js → zap-B8Fg6PaC.js} +1 -1
- package/dashboard/dist/index.html +6 -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 +22 -13
- package/src/lib/codex-rollout-parser.js +43 -6
- package/src/lib/codex-usage-record.js +21 -0
- package/src/lib/deepseek-status.js +157 -0
- package/src/lib/local-api.js +44 -4
- package/src/lib/pricing/curated-overrides.json +94 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +15 -2
- package/src/lib/qoder-limits.js +95 -23
- package/src/lib/rollout.js +68 -14
- package/src/lib/session-analytics.js +10 -3
- package/src/lib/usage-limits.js +187 -43
- package/src/lib/workbuddy-limits.js +292 -27
- package/src/lib/zcode-limits.js +457 -42
- package/dashboard/dist/assets/DashboardPage-vITMnPXx.js +0 -1
- package/dashboard/dist/assets/DialogDescription-0wcygxtH.js +0 -1
- package/dashboard/dist/assets/LimitsPage-Bp4ZORmR.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-5YevsGGD.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BEEzY7mq.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-Djbdp7fi.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DMCok-vZ.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-h7aGsIQr.js +0 -1
|
@@ -12,16 +12,25 @@
|
|
|
12
12
|
// WorkBuddy adds its own credentials, including encrypted desktop sessions.
|
|
13
13
|
// Legacy plaintext credentials remain a fallback when no local IPC exists.
|
|
14
14
|
//
|
|
15
|
-
// Only
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Only two windows are returned, mirroring the desktop app's own split:
|
|
16
|
+
// the subscription plan package (the row the app calls 套餐基础积分, with that
|
|
17
|
+
// package's cycle end as the reset) and one pooled resource-package window
|
|
18
|
+
// for every non-plan credit group (套餐赠送积分 / 购买积分 / 平台奖励积分).
|
|
19
|
+
// Display names are English regardless of the backend's localized names.
|
|
19
20
|
|
|
20
21
|
const fs = require("node:fs");
|
|
22
|
+
const os = require("node:os");
|
|
21
23
|
const path = require("node:path");
|
|
22
24
|
const { resolveWorkbuddyHome } = require("./rollout");
|
|
23
25
|
const { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc } = require("./workbuddy-ipc");
|
|
24
26
|
|
|
27
|
+
const WORKBUDDY_LIMITS_CACHE_FILE = "workbuddy-usage-limits-cache.json";
|
|
28
|
+
// Windows without a usable reset_at — the pooled resource window by design,
|
|
29
|
+
// plus a plan row whose cycle end never parsed — cannot be checked against a
|
|
30
|
+
// quota rollover, so they are only served while the snapshot is young enough
|
|
31
|
+
// to trust.
|
|
32
|
+
const WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
33
|
+
|
|
25
34
|
const PLAN_PRIORITY = Object.freeze({
|
|
26
35
|
TCACA_code_027_0FCGVA6vSa: 4, // flagship
|
|
27
36
|
TCACA_code_026_BaESVICNoi: 3, // advanced
|
|
@@ -278,8 +287,8 @@ function planWindow(row) {
|
|
|
278
287
|
};
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
//
|
|
282
|
-
// editions
|
|
290
|
+
// Legacy merge for the pre-split unified row. The local app now publishes
|
|
291
|
+
// both editions side by side; cloud mirrors of this module still merge.
|
|
283
292
|
function selectWorkbuddyLimits(china, international) {
|
|
284
293
|
const editions = [china, international].filter(Boolean);
|
|
285
294
|
const usable = (value) => value.configured === true && !value.error;
|
|
@@ -289,9 +298,10 @@ function selectWorkbuddyLimits(china, international) {
|
|
|
289
298
|
|| { configured: false };
|
|
290
299
|
}
|
|
291
300
|
|
|
292
|
-
// Pick the one subscription-plan package. Reward rows are ignored
|
|
293
|
-
// several plan packages exist, the
|
|
294
|
-
// matching the desktop app's
|
|
301
|
+
// Pick the one subscription-plan package. Reward rows are ignored here — they
|
|
302
|
+
// feed the resource-pool window instead. When several plan packages exist, the
|
|
303
|
+
// highest tier that still has credit wins, matching the desktop app's
|
|
304
|
+
// pickActivePlanBase.
|
|
295
305
|
function selectWorkbuddyPlanQuota(rows, identity = {}, variantId = "workbuddy") {
|
|
296
306
|
const merged = mergePackageRows(rows.filter(Boolean));
|
|
297
307
|
const subscriptionPackageCode = identity.code || "";
|
|
@@ -309,6 +319,93 @@ function selectWorkbuddyPlanQuota(rows, identity = {}, variantId = "workbuddy")
|
|
|
309
319
|
};
|
|
310
320
|
}
|
|
311
321
|
|
|
322
|
+
// The desktop app's billing meter splits an account into CreditGroups:
|
|
323
|
+
// plan_base (套餐基础积分) plus the non-plan pools plan_bonus (套餐赠送积分),
|
|
324
|
+
// addon (购买积分) and benefit (平台奖励积分). The app shows those pools as
|
|
325
|
+
// resource packages on top of the plan, so mirror that split: one window for
|
|
326
|
+
// the plan cycle, one pooled window for everything else. Groups the summary
|
|
327
|
+
// does not quantify are ignored; a pool with no credit at all stays null so
|
|
328
|
+
// the UI drops it like a missing window.
|
|
329
|
+
function collectResourceGroups(summary) {
|
|
330
|
+
const data = unwrapBillingBody(summary);
|
|
331
|
+
if (!data) return null;
|
|
332
|
+
const groups = [
|
|
333
|
+
data.CreditGroups,
|
|
334
|
+
data.creditGroups,
|
|
335
|
+
data.credit_groups,
|
|
336
|
+
].find(Array.isArray);
|
|
337
|
+
if (groups) {
|
|
338
|
+
const pool = { total: 0, used: 0, remain: 0, remainDerived: 0, seen: false, usedSeen: false, remainSeen: false };
|
|
339
|
+
for (const group of groups) {
|
|
340
|
+
if (!group || typeof group !== "object") continue;
|
|
341
|
+
const kind = firstString(group, ["Kind", "kind"]);
|
|
342
|
+
if (kind && kind.toLowerCase() === "plan_base") continue;
|
|
343
|
+
const total = firstCount(group, ["CycleTotalCapacity", "total"]);
|
|
344
|
+
const used = firstCount(group, ["CycleUsedCapacity", "used"]);
|
|
345
|
+
const remain = firstCount(group, ["CycleRemainCapacity", "remain"]);
|
|
346
|
+
if (total === null && used === null && remain === null) continue;
|
|
347
|
+
pool.seen = true;
|
|
348
|
+
// A group that omits its usage is still counted: derive it from the
|
|
349
|
+
// capacity identity, the way planWindow does for the plan row.
|
|
350
|
+
const knownUsed = used ?? (total !== null && remain !== null ? Math.max(0, total - remain) : null);
|
|
351
|
+
pool.usedSeen = pool.usedSeen || knownUsed !== null;
|
|
352
|
+
pool.remainSeen = pool.remainSeen || remain !== null;
|
|
353
|
+
pool.total += total ?? 0;
|
|
354
|
+
pool.used += knownUsed ?? 0;
|
|
355
|
+
if (remain === null) pool.remainDerived += Math.max(0, (total ?? 0) - (used ?? 0));
|
|
356
|
+
else pool.remain += remain;
|
|
357
|
+
}
|
|
358
|
+
return pool.seen ? pool : null;
|
|
359
|
+
}
|
|
360
|
+
// Older summaries predate CreditGroups. Their package rows carry the same
|
|
361
|
+
// pools one row per package — every row that cannot be the subscription
|
|
362
|
+
// plan is resource credit.
|
|
363
|
+
const identity = readSubscriptionIdentity(summary);
|
|
364
|
+
const rows = collectPackageRows(summary)
|
|
365
|
+
.filter((row) => !isSelectablePlan(row, identity.code));
|
|
366
|
+
if (rows.length === 0) return null;
|
|
367
|
+
const derivedUsed = (row) =>
|
|
368
|
+
row.used ?? (row.total !== null && row.remain !== null ? Math.max(0, row.total - row.remain) : null);
|
|
369
|
+
return {
|
|
370
|
+
total: rows.reduce((sum, row) => sum + (row.total ?? 0), 0),
|
|
371
|
+
used: rows.reduce((sum, row) => sum + (derivedUsed(row) ?? 0), 0),
|
|
372
|
+
remain: rows.reduce((sum, row) => sum + (row.remain ?? 0), 0),
|
|
373
|
+
remainDerived: rows.reduce(
|
|
374
|
+
(sum, row) => sum + (row.remain === null ? Math.max(0, (row.total ?? 0) - (row.used ?? 0)) : 0),
|
|
375
|
+
0,
|
|
376
|
+
),
|
|
377
|
+
seen: true,
|
|
378
|
+
usedSeen: rows.some((row) => derivedUsed(row) !== null),
|
|
379
|
+
remainSeen: rows.some((row) => row.remain !== null),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Resource credits have no shared cycle: benefit and add-on packages carry
|
|
384
|
+
// their own grants, so the pooled window has no reset. An empty pool (0/0)
|
|
385
|
+
// yields no window instead of a full or zeroed bar.
|
|
386
|
+
function resourceWindow(pool) {
|
|
387
|
+
if (!pool || !(pool.total > 0) || !Number.isFinite(pool.total)) return null;
|
|
388
|
+
// No group reported either side of the meter: usage is unknowable, not
|
|
389
|
+
// zero — hide the pool instead of rendering a full bar.
|
|
390
|
+
if (!pool.usedSeen && !pool.remainSeen) return null;
|
|
391
|
+
const used = pool.usedSeen || pool.used > 0
|
|
392
|
+
? pool.used
|
|
393
|
+
: Math.max(0, pool.total - (pool.remain + pool.remainDerived));
|
|
394
|
+
const usedPercent = clampPercent((used / pool.total) * 100);
|
|
395
|
+
if (usedPercent === null) return null;
|
|
396
|
+
const remaining = pool.remainSeen
|
|
397
|
+
? pool.remain + pool.remainDerived
|
|
398
|
+
: Math.max(0, pool.total - used);
|
|
399
|
+
if (!Number.isFinite(remaining)) return null;
|
|
400
|
+
return {
|
|
401
|
+
used_percent: usedPercent,
|
|
402
|
+
reset_at: null,
|
|
403
|
+
used_credits: used,
|
|
404
|
+
limit_credits: pool.total,
|
|
405
|
+
remaining_credits: remaining,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
312
409
|
function englishPlanLabel(row, identity, variantId) {
|
|
313
410
|
// An actual English subscription/package name takes precedence over the
|
|
314
411
|
// client's edition fallback. Unknown localized tiers stay unnamed.
|
|
@@ -362,12 +459,16 @@ function readAuthFile(filePath) {
|
|
|
362
459
|
const auth = parsed?.auth;
|
|
363
460
|
if (!auth || typeof auth !== "object") return null;
|
|
364
461
|
const domain = isNonEmptyString(auth.domain) ? auth.domain.trim() : "";
|
|
462
|
+
// The app records the signed-in account next to its (often encrypted)
|
|
463
|
+
// session. The uid stays readable while the app is closed, so it is the
|
|
464
|
+
// one account signal that survives into the UNAVAILABLE state.
|
|
465
|
+
const uid = isNonEmptyString(parsed.account?.uid) ? parsed.account.uid.trim() : "";
|
|
365
466
|
const token = auth.accessToken;
|
|
366
|
-
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false };
|
|
467
|
+
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false, uid };
|
|
367
468
|
if (token && typeof token === "object" && token.$wbEncrypted === 1) {
|
|
368
|
-
return { token: null, domain, encrypted: true };
|
|
469
|
+
return { token: null, domain, encrypted: true, uid };
|
|
369
470
|
}
|
|
370
|
-
return { token: null, domain, encrypted: false };
|
|
471
|
+
return { token: null, domain, encrypted: false, uid };
|
|
371
472
|
}
|
|
372
473
|
|
|
373
474
|
function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
@@ -375,15 +476,120 @@ function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
|
375
476
|
? authFiles
|
|
376
477
|
: authSearchDirs(env, workbuddyHome).flatMap((dir) => variant.authFileNames.map((name) => path.join(dir, name)));
|
|
377
478
|
let encryptedForVariant = false;
|
|
479
|
+
let encryptedUid = "";
|
|
378
480
|
for (const filePath of files) {
|
|
379
481
|
if (!filePath) continue;
|
|
380
482
|
const session = readAuthFile(filePath);
|
|
381
483
|
if (!session) continue;
|
|
382
484
|
if (!hostAllowed(hostOf(session.domain), variant.allowedHostSuffixes)) continue;
|
|
383
|
-
if (session.token) return { token: session.token, encrypted: false, matched: true };
|
|
384
|
-
if (session.encrypted)
|
|
485
|
+
if (session.token) return { token: session.token, encrypted: false, matched: true, uid: session.uid };
|
|
486
|
+
if (session.encrypted) {
|
|
487
|
+
encryptedForVariant = true;
|
|
488
|
+
// First encrypted candidate wins: when the app is closed this record is
|
|
489
|
+
// what a live read would authenticate with, so its account is the one
|
|
490
|
+
// the cached windows must match.
|
|
491
|
+
encryptedUid = encryptedUid || session.uid;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { token: null, encrypted: encryptedForVariant, matched: encryptedForVariant, uid: encryptedUid };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Last-good disk cache for WorkBuddy quota windows (house pattern: codex /
|
|
499
|
+
// ark / qoder / zcode, issue #952). A live read — IPC or API — persists the
|
|
500
|
+
// windows per edition; when the live chain later fails, the stale windows are
|
|
501
|
+
// served with data-age markers instead of a bare "Start WorkBuddy and sign
|
|
502
|
+
// in". The steady state this covers is the app being closed: wbipc discovery
|
|
503
|
+
// is gone and the encrypted session cannot be read.
|
|
504
|
+
//
|
|
505
|
+
// Editions never borrow each other's data: workbuddy CN and workbuddyAi are
|
|
506
|
+
// separate partitions of one file, matching the edition-local socket
|
|
507
|
+
// discipline. Partitions are also scoped to the login that produced the
|
|
508
|
+
// windows, so an account switch followed by a live failure cannot serve the
|
|
509
|
+
// previous account's bars.
|
|
510
|
+
//
|
|
511
|
+
// The cache identity comes from the session selection itself (resolveSession):
|
|
512
|
+
// the record whose token supplies — or, while the app is closed, would supply
|
|
513
|
+
// — the live read. It is the app's own account uid, readable after the app
|
|
514
|
+
// closes when the encrypted session token is not, and it is an account id,
|
|
515
|
+
// not a credential: nothing secret is persisted or hashed. An account the
|
|
516
|
+
// records do not name ("unknown") matches only its own entries.
|
|
517
|
+
|
|
518
|
+
function workbuddyLimitsCachePath({ home } = {}) {
|
|
519
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", WORKBUDDY_LIMITS_CACHE_FILE);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// A window whose reset_at has already passed is stale data, not a usable
|
|
523
|
+
// fallback — the quota has rolled over, so drop it. Undated windows (the
|
|
524
|
+
// resource pool, or a plan row without a readable cycle end) cannot be
|
|
525
|
+
// checked against a reset and are bounded by the snapshot-age TTL instead.
|
|
526
|
+
function workbuddyCachedWindow(window, { cachedAtMs, nowMs } = {}) {
|
|
527
|
+
if (!window || typeof window !== "object") return null;
|
|
528
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
529
|
+
if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
|
|
530
|
+
return nowMs - cachedAtMs <= WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Read the last-good windows for one edition, scoped to the login that wrote them. */
|
|
534
|
+
function readWorkbuddyLimitsCache({ home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
535
|
+
// An explicit home keeps the cache hermetic: callers that never said where
|
|
536
|
+
// the user's tracker state lives (tests, odd embedders) must not touch the
|
|
537
|
+
// developer's real cache file.
|
|
538
|
+
if (!home || !variantId || !identity) return null;
|
|
539
|
+
let parsed;
|
|
540
|
+
try {
|
|
541
|
+
parsed = JSON.parse(fs.readFileSync(workbuddyLimitsCachePath({ home }), "utf8"));
|
|
542
|
+
} catch (_error) {
|
|
543
|
+
return null;
|
|
385
544
|
}
|
|
386
|
-
|
|
545
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
546
|
+
const raw = parsed[variantId];
|
|
547
|
+
if (!raw || typeof raw !== "object") return null;
|
|
548
|
+
if (raw.identity !== identity) return null;
|
|
549
|
+
const cachedAtMs = Date.parse(raw.cached_at || "");
|
|
550
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
551
|
+
const context = { cachedAtMs, nowMs };
|
|
552
|
+
const primaryWindow = workbuddyCachedWindow(raw.primary_window, context);
|
|
553
|
+
const secondaryWindow = workbuddyCachedWindow(raw.secondary_window, context);
|
|
554
|
+
// A partition whose windows all rolled or expired no longer describes the
|
|
555
|
+
// account's quota — withholding it lets the live error stay visible.
|
|
556
|
+
if (!primaryWindow && !secondaryWindow) return null;
|
|
557
|
+
return {
|
|
558
|
+
configured: true,
|
|
559
|
+
error: null,
|
|
560
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
561
|
+
primary_window: primaryWindow,
|
|
562
|
+
secondary_window: secondaryWindow,
|
|
563
|
+
cached: true,
|
|
564
|
+
stale: true,
|
|
565
|
+
cached_at: raw.cached_at,
|
|
566
|
+
source: "disk-cache",
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Merge one edition's partition into the disk cache; best-effort, never throws. */
|
|
571
|
+
function writeWorkbuddyLimitsCache(limits, { home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
572
|
+
if (!home || !variantId || !identity || !limits?.configured || limits.error) return;
|
|
573
|
+
if (!limits.primary_window && !limits.secondary_window) return;
|
|
574
|
+
const cachePath = workbuddyLimitsCachePath({ home });
|
|
575
|
+
let payload = {};
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
578
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
579
|
+
} catch (_error) {}
|
|
580
|
+
payload[variantId] = {
|
|
581
|
+
identity,
|
|
582
|
+
plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
|
|
583
|
+
primary_window: limits.primary_window || null,
|
|
584
|
+
secondary_window: limits.secondary_window || null,
|
|
585
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
586
|
+
};
|
|
587
|
+
try {
|
|
588
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
589
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
590
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
591
|
+
fs.renameSync(tmpPath, cachePath);
|
|
592
|
+
} catch (_error) {}
|
|
387
593
|
}
|
|
388
594
|
|
|
389
595
|
function readProductEndpoint(workbuddyHome) {
|
|
@@ -479,6 +685,7 @@ async function fetchPlanQuota(post, variantId) {
|
|
|
479
685
|
configured: true, error: null,
|
|
480
686
|
plan_label: quota?.plan_label || englishPlanLabel({ code: identity.code }, identity, variantId),
|
|
481
687
|
primary_window: quota?.primary_window || null,
|
|
688
|
+
secondary_window: resourceWindow(collectResourceGroups(summary)),
|
|
482
689
|
};
|
|
483
690
|
}
|
|
484
691
|
|
|
@@ -494,19 +701,24 @@ function scopedEnv(env, home) {
|
|
|
494
701
|
return next;
|
|
495
702
|
}
|
|
496
703
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
704
|
+
// Live chain for one edition, unchanged by the cache layer: the IPC proxy
|
|
705
|
+
// while the app publishes it, then the API source with a resolvable session.
|
|
706
|
+
// The session is selected by the caller (fetchVariant) so the cache identity
|
|
707
|
+
// and the live read cannot disagree about the account. Throws propagate to
|
|
708
|
+
// the cache wrapper below; the { configured: true, error: UNAVAILABLE }
|
|
709
|
+
// result comes back when the app is closed and no session can be read.
|
|
710
|
+
async function fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session }) {
|
|
503
711
|
const discovery = readWorkbuddyIpcDiscovery(workbuddyHome);
|
|
504
712
|
if (discovery) {
|
|
505
713
|
// Discovery is edition-local. Never fall back to the other edition's
|
|
506
714
|
// socket or allow an explicitly redirected product to be mislabelled.
|
|
507
715
|
const productEndpoint = readProductEndpoint(workbuddyHome);
|
|
508
716
|
if (productEndpoint && !originAllowed(productEndpoint, variant)) {
|
|
509
|
-
|
|
717
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
718
|
+
// Tagged so the cache wrapper surfaces the mislabelled host instead of
|
|
719
|
+
// stale bars from whichever login wrote them.
|
|
720
|
+
error.code = "EDITION_MISMATCH";
|
|
721
|
+
throw error;
|
|
510
722
|
}
|
|
511
723
|
const client = await openWorkbuddyIpc({
|
|
512
724
|
discovery, signal: opts.signal, timeoutMs: opts.timeoutMs, connectImpl: opts.connectImpl,
|
|
@@ -517,10 +729,6 @@ async function fetchVariant(variantId, opts = {}) {
|
|
|
517
729
|
client.close();
|
|
518
730
|
}
|
|
519
731
|
}
|
|
520
|
-
const explicitToken = resolveToken(variant, env);
|
|
521
|
-
const session = explicitToken
|
|
522
|
-
? { token: explicitToken, encrypted: false, matched: true }
|
|
523
|
-
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
524
732
|
if (!session.token) {
|
|
525
733
|
if (session.encrypted || installed) return { configured: true, error: UNAVAILABLE };
|
|
526
734
|
if (!installed) return { configured: false };
|
|
@@ -535,12 +743,64 @@ async function fetchVariant(variantId, opts = {}) {
|
|
|
535
743
|
workbuddyHome: opts.origin ? null : workbuddyHome,
|
|
536
744
|
});
|
|
537
745
|
if (!originAllowed(origin, variant)) {
|
|
538
|
-
|
|
746
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
747
|
+
error.code = "EDITION_MISMATCH";
|
|
748
|
+
throw error;
|
|
539
749
|
}
|
|
540
750
|
return { ...await fetchPlanQuota((apiPath, body) => postBilling(fetchImpl,
|
|
541
751
|
`${origin}${apiPath}`, session.token, body, opts.signal), variantId), source: "api" };
|
|
542
752
|
}
|
|
543
753
|
|
|
754
|
+
async function fetchVariant(variantId, opts = {}) {
|
|
755
|
+
const variant = variantById(variantId);
|
|
756
|
+
if (!variant) return { configured: false };
|
|
757
|
+
const env = scopedEnv(opts.env || process.env, opts.home);
|
|
758
|
+
const workbuddyHome = opts.workbuddyHome || resolveWorkbuddyHome(env, variant.source);
|
|
759
|
+
const installed = directoryExists(workbuddyHome);
|
|
760
|
+
// Sampled when used, not when the fetch starts: the live attempt can spend
|
|
761
|
+
// the whole provider budget before a cache read or write happens, and a
|
|
762
|
+
// window whose reset rolled in that span must not be served. An injected
|
|
763
|
+
// clock (tests) keeps every use on the same instant.
|
|
764
|
+
const nowMs = () => (Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now());
|
|
765
|
+
// One session selection feeds both the live attempt and the cache identity:
|
|
766
|
+
// cached windows are stamped with (and served only to) the account whose
|
|
767
|
+
// record supplies — or, while the app is closed, would supply — the session
|
|
768
|
+
// token. An env-token session names no record; "unknown" matches only its
|
|
769
|
+
// own entries.
|
|
770
|
+
const explicitToken = resolveToken(variant, env);
|
|
771
|
+
const session = explicitToken
|
|
772
|
+
? { token: explicitToken, encrypted: false, matched: true, uid: "" }
|
|
773
|
+
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
774
|
+
const identity = (explicitToken ? "" : session.uid) || "unknown";
|
|
775
|
+
let live;
|
|
776
|
+
try {
|
|
777
|
+
live = await fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session });
|
|
778
|
+
} catch (error) {
|
|
779
|
+
// Re-login stays actionable (opencode-go auth_error / codex reauth
|
|
780
|
+
// precedent) and a mislabelled host is a configuration fault, not
|
|
781
|
+
// transient data loss: neither may be masked by stale bars.
|
|
782
|
+
if (error?.code === "AUTH_EXPIRED" || error?.code === "EDITION_MISMATCH") throw error;
|
|
783
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
784
|
+
if (cached) return cached;
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
if (live.configured === true && !live.error) {
|
|
788
|
+
// Live read is current as of now: persist it so a later failure serves
|
|
789
|
+
// stale bars instead of a vanished card.
|
|
790
|
+
writeWorkbuddyLimitsCache(live, { home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
791
|
+
return live;
|
|
792
|
+
}
|
|
793
|
+
if (live.configured === true && live.error) {
|
|
794
|
+
// The steady state the cache exists for (UNAVAILABLE: app closed, session
|
|
795
|
+
// unreadable) and transient API/IPC failures. A never-signed-in install
|
|
796
|
+
// ({ configured: false }) stays untouched — an edition that produced no
|
|
797
|
+
// windows here has nothing to fall back to.
|
|
798
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
799
|
+
if (cached) return cached;
|
|
800
|
+
}
|
|
801
|
+
return live;
|
|
802
|
+
}
|
|
803
|
+
|
|
544
804
|
function fetchWorkbuddyLimits(opts) {
|
|
545
805
|
return fetchVariant("workbuddy", opts);
|
|
546
806
|
}
|
|
@@ -556,9 +816,14 @@ module.exports = {
|
|
|
556
816
|
selectWorkbuddyPlanQuota,
|
|
557
817
|
selectWorkbuddyLimits,
|
|
558
818
|
collectPackageRows,
|
|
819
|
+
collectResourceGroups,
|
|
820
|
+
resourceWindow,
|
|
559
821
|
readSubscriptionIdentity,
|
|
560
822
|
originAllowed,
|
|
561
823
|
hostAllowed,
|
|
824
|
+
readWorkbuddyLimitsCache,
|
|
825
|
+
writeWorkbuddyLimitsCache,
|
|
826
|
+
workbuddyLimitsCachePath,
|
|
562
827
|
fetchWorkbuddyLimits,
|
|
563
828
|
fetchWorkbuddyAiLimits,
|
|
564
829
|
};
|