tokentracker-cli 0.79.9 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-B1WOPlu0.js → AchievementBadge-D9jOMhlD.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-9kHEQicS.js → AchievementsPage-DuswZa4g.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-Dw05PYPr.js → AchievementsSection-BJPeE1Uq.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-DaWgiijJ.js → BadgeDetailModal-nNPIsy6i.js} +1 -1
  5. package/dashboard/dist/assets/{Card-DYhrLLM4.js → Card-VENq8eEp.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-BLy2LUKz.js → ClawdAnimated-Cq7TW0vv.js} +1 -1
  7. package/dashboard/dist/assets/{CommunityStatsModal-CWYqfht3.js → CommunityStatsModal-y2VftQuq.js} +1 -1
  8. package/dashboard/dist/assets/{ConfirmModal-DcE2C24Y.js → ConfirmModal-CWgnJ4hm.js} +1 -1
  9. package/dashboard/dist/assets/{Controls-CI7qw0-b.js → Controls-BUQ_iZZq.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-CAlgaBa7.js → DashboardPage-WkiygMmq.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-DinOyaeN.js → DevicePage-Dh3XDtNx.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-CjfSDrLo.js → DialogClose-KTFU_B3F.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DIra288n.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-f7gNmCca.js → DialogTitle-bza2pIek.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-D_5w0Qh0.js → FadeIn-Dm3FVNfr.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-DTZyNKyL.js → HeaderGithubStar-CbSElzLt.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-D8CViF98.js → IpCheckPage-BDON7VCU.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-CqN5KEjV.js → LandingPage-Ck3CwZrt.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-CwUVO4wn.js → LeaderboardAvatar-GVUy0Ynq.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-B12HJ9LA.js → LeaderboardPage-C19Rkt44.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-DFzPpsnV.js → LeaderboardProfileModal-QcEq_o6E.js} +3 -3
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-Z0ZYpNFd.js → LeaderboardProfilePage-pFhI6292.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-BpwioIa-.js → LimitsPage-ChV3Qezd.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-Covi6zdt.js → LocalOnlyNotice-ALvpjznF.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CRqbhwCx.js → LoginPage-CL-QrJxU.js} +1 -1
  26. package/dashboard/dist/assets/{PetPage-BY_yObVj.js → PetPage-DSuvdGgd.js} +1 -1
  27. package/dashboard/dist/assets/{PopoverPopup-B2dE3Ary.js → PopoverPopup-jSDQ87Bi.js} +1 -1
  28. package/dashboard/dist/assets/{ResetPasswordPage-C8IEJbVc.js → ResetPasswordPage-Dk2IDAJq.js} +1 -1
  29. package/dashboard/dist/assets/{Select-BmRLYSqA.js → Select-Cz6WTj7D.js} +1 -1
  30. package/dashboard/dist/assets/{SelectItemText-FC46bk8b.js → SelectItemText-BZyRjWCM.js} +1 -1
  31. package/dashboard/dist/assets/{SettingsPage-DipzOQVg.js → SettingsPage-CANz8HMM.js} +1 -1
  32. package/dashboard/dist/assets/{SkillsPage-CziypUTf.js → SkillsPage-WZkQG_83.js} +1 -1
  33. package/dashboard/dist/assets/{WidgetsPage-CJegJhVR.js → WidgetsPage-BEyJ62NV.js} +1 -1
  34. package/dashboard/dist/assets/{WrappedPage-DcMiKvzf.js → WrappedPage-Ck_dOx_w.js} +1 -1
  35. package/dashboard/dist/assets/{agent-logos-S7bF1LwW.js → agent-logos-DpjHiAbZ.js} +1 -1
  36. package/dashboard/dist/assets/{arrow-up-right-D0yeL56y.js → arrow-up-right-C4iYPBtK.js} +1 -1
  37. package/dashboard/dist/assets/check-BkPUxPZV.js +1 -0
  38. package/dashboard/dist/assets/{copy-D-z9ZtIu.js → copy-CHJ-38K6.js} +1 -1
  39. package/dashboard/dist/assets/{download-Dwxnd3QG.js → download-BSeG8u17.js} +1 -1
  40. package/dashboard/dist/assets/{flame-Bdqk9A75.js → flame-DeHwKcE-.js} +1 -1
  41. package/dashboard/dist/assets/{format-tokens-DNVcCRwm.js → format-tokens-B-MJYJyp.js} +1 -1
  42. package/dashboard/dist/assets/{info-CMLT6oUI.js → info-DA_0xr_o.js} +1 -1
  43. package/dashboard/dist/assets/{link-2-BmiHu726.js → link-2-noUH83Lc.js} +1 -1
  44. package/dashboard/dist/assets/{loader-circle-CXVUQtzm.js → loader-circle-wL7KZtm0.js} +1 -1
  45. package/dashboard/dist/assets/{main-B9m79io-.js → main-BHfJDFcu.js} +3 -3
  46. package/dashboard/dist/assets/{provider-display-BoAesdke.js → provider-display-DxIClOGM.js} +1 -1
  47. package/dashboard/dist/assets/{trash-2-C9Rh2DAL.js → trash-2-B8TPUmxZ.js} +1 -1
  48. package/dashboard/dist/assets/{use-community-stats-aZwYxrXR.js → use-community-stats-CXD3MRRE.js} +1 -1
  49. package/dashboard/dist/assets/{use-limits-display-prefs-tJKR83RT.js → use-limits-display-prefs-C2pJVYTn.js} +1 -1
  50. package/dashboard/dist/assets/{use-native-settings-PjfJPK5v.js → use-native-settings-BeOBLEZa.js} +1 -1
  51. package/dashboard/dist/assets/{use-ordered-list-D1QFmZUY.js → use-ordered-list-kAtW8pJm.js} +1 -1
  52. package/dashboard/dist/assets/{use-quality-per-dollar-pref-4N3Su4wC.js → use-quality-per-dollar-pref-CFZUZM3S.js} +1 -1
  53. package/dashboard/dist/assets/{use-transform-D9nurInL.js → use-transform-B9YwpyKi.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-DapTpi_6.js → use-usage-limits-DSX5iKPE.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-B0DH9ZZA.js → useCurrency-CKg5K4Nj.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-DijV6DYS.js → useScrollLock-DyyY_Vyt.js} +1 -1
  57. package/dashboard/dist/assets/useTokenFormat-FfTjEH_m.js +1 -0
  58. package/dashboard/dist/assets/{zap-BVfhR3Ck.js → zap-B69x9NCT.js} +1 -1
  59. package/dashboard/dist/index.html +1 -1
  60. package/dashboard/dist/share.html +1 -1
  61. package/package.json +1 -1
  62. package/src/commands/init.js +11 -0
  63. package/src/commands/sync.js +160 -6
  64. package/src/lib/local-api.js +11 -5
  65. package/src/lib/openclaw-session-plugin.js +13 -0
  66. package/src/lib/pricing/seed-snapshot.json +1 -1
  67. package/src/lib/rollout.js +63 -2
  68. package/src/lib/usage-limits.js +9 -5
  69. package/dashboard/dist/assets/DialogDescription-DWs-KIUA.js +0 -1
  70. package/dashboard/dist/assets/check-Dn9BLYEf.js +0 -1
  71. package/dashboard/dist/assets/useTokenFormat-nxdG6gi9.js +0 -1
@@ -172,6 +172,18 @@ const CLOUD_CONVERSATIONS_BACKFILL_KEY = "cloudConversationsBackfill_2026_06";
172
172
  // ~/.codex/archived_sessions/ which sync does not scan) — clearing its bucket
173
173
  // would lose that history (ref the v6 ground-truth-repair data-loss incident).
174
174
  const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
175
+ // One-time repair (#169 follow-up): forked Codex rollouts replay the parent
176
+ // session's token history into the child file, and until the same-day burst
177
+ // detector landed in rollout.js the parser counted those replayed rows as live
178
+ // usage (PR #169's date guard only caught cross-day forks). Cursors sit at EOF
179
+ // on every already-parsed file, so the forward fix alone never corrects the
180
+ // inflated history. This re-runs the exact #187 guarded rebuild under a new key
181
+ // — the rebuild re-parses every codex file with the CURRENT (fork-aware)
182
+ // parser, so replay rows are excluded — and inherits all its safety properties
183
+ // (unreproducible-session skip, atomic throwaway rebuild, queue strip, upload
184
+ // offset reset for the cloud overwrite). Pre-gated: installs with no forked
185
+ // rollout on disk carry no fork phantom and mark done without the rebuild.
186
+ const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
175
187
  const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
176
188
  const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
177
189
  const CODEX_COLD_SCAN_AUDIT_MAX_SYNCS = 288;
@@ -405,7 +417,7 @@ async function cmdSync(argv) {
405
417
 
406
418
  if (isFullSourceScan) {
407
419
  await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
408
- await repairCodexRescanInflation({
420
+ const codexRescanRepairRan = await repairCodexRescanInflation({
409
421
  cursors,
410
422
  queuePath,
411
423
  queueStatePath,
@@ -413,6 +425,15 @@ async function cmdSync(argv) {
413
425
  projectQueueStatePath,
414
426
  rolloutFiles,
415
427
  });
428
+ await repairCodexForkReplayInflation({
429
+ cursors,
430
+ queuePath,
431
+ queueStatePath,
432
+ projectQueuePath,
433
+ projectQueueStatePath,
434
+ rolloutFiles,
435
+ legacyRepairRan: codexRescanRepairRan,
436
+ });
416
437
  await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
417
438
  await repairMimoClaudeMislabel({
418
439
  cursors,
@@ -2097,6 +2118,7 @@ module.exports = {
2097
2118
  migrateCursorUnknownBuckets,
2098
2119
  migrateRolloutCumulativeDeltaBuckets,
2099
2120
  repairCodexRescanInflation,
2121
+ repairCodexForkReplayInflation,
2100
2122
  repairDroidDuplicateSessionInflation,
2101
2123
  repairMimoClaudeMislabel,
2102
2124
  reincludeClaudeMemObserverFiles,
@@ -2109,6 +2131,7 @@ module.exports = {
2109
2131
  CURSOR_UNKNOWN_MIGRATION_KEY,
2110
2132
  ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
2111
2133
  CODEX_RESCAN_DEDUP_REPAIR_KEY,
2134
+ CODEX_FORK_REPLAY_REPAIR_KEY,
2112
2135
  DROID_DUP_SESSION_REPAIR_KEY,
2113
2136
  CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
2114
2137
  GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
@@ -3157,6 +3180,11 @@ async function repairCodexRescanInflation({
3157
3180
  projectQueuePath,
3158
3181
  projectQueueStatePath,
3159
3182
  rolloutFiles,
3183
+ // The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
3184
+ // its own key: the rebuild always uses the CURRENT parser, so any parser fix
3185
+ // shipped since the last run is applied to the rebuilt history.
3186
+ migrationKey = CODEX_RESCAN_DEDUP_REPAIR_KEY,
3187
+ uploadNote = "reset_after_codex_rescan_dedup_2026_06",
3160
3188
  }) {
3161
3189
  if (!cursors || typeof cursors !== "object") return false;
3162
3190
  const migrations = (cursors.migrations ||= {});
@@ -3167,7 +3195,7 @@ async function repairCodexRescanInflation({
3167
3195
  // found). Treating the skip sentinel as "done" is what left users like #187
3168
3196
  // permanently stuck on the inflated value after upgrading (the key was truthy
3169
3197
  // so the guard never got a second chance).
3170
- const priorRepair = migrations[CODEX_RESCAN_DEDUP_REPAIR_KEY];
3198
+ const priorRepair = migrations[migrationKey];
3171
3199
  if (priorRepair && !(typeof priorRepair === "object" && priorRepair.skipped)) return false;
3172
3200
 
3173
3201
  // Codex session files THIS sync discovered (source === "codex").
@@ -3203,7 +3231,7 @@ async function repairCodexRescanInflation({
3203
3231
  if (codexFileSet.has(fp)) continue; // exact file re-scanned this run
3204
3232
  const id = codexSessionIdFromPath(fp);
3205
3233
  if (id && scannedSessionIds.has(id)) continue; // same session scanned elsewhere (moved)
3206
- migrations[CODEX_RESCAN_DEDUP_REPAIR_KEY] = {
3234
+ migrations[migrationKey] = {
3207
3235
  skipped: true,
3208
3236
  reason: "codex_session_unreproducible",
3209
3237
  at: new Date().toISOString(),
@@ -3462,7 +3490,7 @@ async function repairCodexRescanInflation({
3462
3490
  }
3463
3491
  uploadState.offset = 0;
3464
3492
  uploadState.updatedAt = new Date().toISOString();
3465
- uploadState.note = "reset_after_codex_rescan_dedup_2026_06";
3493
+ uploadState.note = uploadNote;
3466
3494
  await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
3467
3495
  }
3468
3496
  if (projectRepairEnabled && typeof projectQueueStatePath === "string" && projectQueueStatePath) {
@@ -3474,14 +3502,140 @@ async function repairCodexRescanInflation({
3474
3502
  }
3475
3503
  uploadState.offset = 0;
3476
3504
  uploadState.updatedAt = new Date().toISOString();
3477
- uploadState.note = "reset_after_codex_rescan_dedup_2026_06";
3505
+ uploadState.note = uploadNote;
3478
3506
  await fs.writeFile(projectQueueStatePath, JSON.stringify(uploadState));
3479
3507
  }
3480
3508
 
3481
- migrations[CODEX_RESCAN_DEDUP_REPAIR_KEY] = new Date().toISOString();
3509
+ migrations[migrationKey] = new Date().toISOString();
3482
3510
  return true;
3483
3511
  }
3484
3512
 
3513
+ // #169 follow-up: repair the historical inflation left by same-day forked
3514
+ // Codex rollout replays (see CODEX_FORK_REPLAY_REPAIR_KEY). Delegates to the
3515
+ // #187 guarded rebuild with the fork-repair key.
3516
+ async function repairCodexForkReplayInflation({
3517
+ cursors,
3518
+ queuePath,
3519
+ queueStatePath,
3520
+ projectQueuePath,
3521
+ projectQueueStatePath,
3522
+ rolloutFiles,
3523
+ // True when the #187 repair ran its rebuild earlier in THIS sync: that
3524
+ // rebuild already used the fork-aware parser, so the history is clean and a
3525
+ // second rebuild would be pure waste.
3526
+ legacyRepairRan = false,
3527
+ }) {
3528
+ if (!cursors || typeof cursors !== "object") return false;
3529
+ const migrations = (cursors.migrations ||= {});
3530
+ // Same completed-vs-skipped semantics as the #187 repair: an ISO string is
3531
+ // final, a {skipped:true} sentinel retries (the skip condition can clear).
3532
+ const prior = migrations[CODEX_FORK_REPLAY_REPAIR_KEY];
3533
+ if (prior && !(typeof prior === "object" && prior.skipped)) return false;
3534
+
3535
+ if (legacyRepairRan) {
3536
+ migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
3537
+ return false;
3538
+ }
3539
+
3540
+ // Pre-gate: fork phantom can only exist on installs that have (or had) a
3541
+ // forked rollout. Scanning file heads is cheap; the rebuild re-parses every
3542
+ // codex file. Files forked AFTER the parser fix shipped never accrue
3543
+ // phantom, so a definitive "no forks" verdict may finalize — but ONLY when
3544
+ // this scan can actually stand for the install's whole codex history:
3545
+ // 1. every candidate head was readable (an unreadable candidate could be
3546
+ // the one forked rollout), AND
3547
+ // 2. every codex session recorded in cursors is covered by THIS scan's
3548
+ // file list (same UUID-keyed reproducibility rule as the #187 guard).
3549
+ // A forked rollout can be real yet absent from one run's rolloutFiles —
3550
+ // WSL/native split probing, a temporarily missing ~/.codex, a deleted
3551
+ // file whose phantom is already in history — and finalizing off such a
3552
+ // partial view would permanently foreclose the repair this migration
3553
+ // exists to perform, while the guarded repair itself would only have
3554
+ // deferred retryably.
3555
+ // Either failure defers with the retryable {skipped:true} sentinel.
3556
+ const forkScan = await scanForForkedCodexRollout(rolloutFiles);
3557
+ if (!forkScan.forked) {
3558
+ if (forkScan.indeterminate) {
3559
+ migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
3560
+ skipped: true,
3561
+ reason: "fork_scan_indeterminate",
3562
+ at: new Date().toISOString(),
3563
+ };
3564
+ return false;
3565
+ }
3566
+ const scannedSessionIds = new Set();
3567
+ const scannedPaths = new Set();
3568
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
3569
+ const fp = typeof entry === "string" ? entry : entry?.path;
3570
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
3571
+ if (!fp || src !== "codex") continue;
3572
+ scannedPaths.add(fp);
3573
+ const id = codexSessionIdFromPath(fp);
3574
+ if (id) scannedSessionIds.add(id);
3575
+ }
3576
+ if (cursors.files && typeof cursors.files === "object") {
3577
+ for (const fp of Object.keys(cursors.files)) {
3578
+ if (!isCodexSessionCursorPath(fp)) continue;
3579
+ if (scannedPaths.has(fp)) continue;
3580
+ const id = codexSessionIdFromPath(fp);
3581
+ if (id && scannedSessionIds.has(id)) continue;
3582
+ migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = {
3583
+ skipped: true,
3584
+ reason: "codex_history_not_covered",
3585
+ at: new Date().toISOString(),
3586
+ };
3587
+ return false;
3588
+ }
3589
+ }
3590
+ migrations[CODEX_FORK_REPLAY_REPAIR_KEY] = new Date().toISOString();
3591
+ return false;
3592
+ }
3593
+
3594
+ return repairCodexRescanInflation({
3595
+ cursors,
3596
+ queuePath,
3597
+ queueStatePath,
3598
+ projectQueuePath,
3599
+ projectQueueStatePath,
3600
+ rolloutFiles,
3601
+ migrationKey: CODEX_FORK_REPLAY_REPAIR_KEY,
3602
+ uploadNote: "reset_after_codex_fork_replay_2026_07",
3603
+ });
3604
+ }
3605
+
3606
+ // Scans codex rollout heads for the fork marker. The child session_meta (with
3607
+ // forked_from_id) is the FIRST line of a forked rollout — observed at byte
3608
+ // offset ≤169 across every local sample — so a bounded head read is
3609
+ // sufficient. 64KB leaves ample margin for long base_instructions preceding
3610
+ // it; a substring false positive (the marker quoted in unrelated head content)
3611
+ // only costs one redundant guarded rebuild, never data. Tri-state result:
3612
+ // {forked:true} on a hit, {forked:false, indeterminate:false} only when every
3613
+ // candidate was inspected, {forked:false, indeterminate:true} when any head
3614
+ // read failed — the caller must not finalize "no forks" off a failed read.
3615
+ async function scanForForkedCodexRollout(rolloutFiles) {
3616
+ const HEAD_BYTES = 65536;
3617
+ let indeterminate = false;
3618
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
3619
+ const fp = typeof entry === "string" ? entry : entry?.path;
3620
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
3621
+ if (!fp || src !== "codex") continue;
3622
+ let fh = null;
3623
+ try {
3624
+ fh = await fs.open(fp, "r");
3625
+ const buf = Buffer.alloc(HEAD_BYTES);
3626
+ const { bytesRead } = await fh.read(buf, 0, HEAD_BYTES, 0);
3627
+ if (buf.toString("utf8", 0, bytesRead).includes('"forked_from_id"')) {
3628
+ return { forked: true, indeterminate: false };
3629
+ }
3630
+ } catch (_e) {
3631
+ indeterminate = true;
3632
+ } finally {
3633
+ if (fh) await fh.close().catch(() => {});
3634
+ }
3635
+ }
3636
+ return { forked: false, indeterminate };
3637
+ }
3638
+
3485
3639
  function isCodexSessionCursorPath(filePath) {
3486
3640
  if (typeof filePath !== "string") return false;
3487
3641
  const normalized = filePath.replace(/\\/g, "/");
@@ -1054,15 +1054,17 @@ function createLocalApiHandler({ queuePath }) {
1054
1054
  // sync is on. Cloud sync is a dashboard (WebView) preference persisted in
1055
1055
  // localStorage; the dashboard mirrors it here via POST
1056
1056
  // /functions/tokentracker-cloud-sync-pref so the auth-unaware popover can key
1057
- // off the same flag. Defaults OFF, exactly like the dashboard toggle.
1057
+ // off the same flag. Defaults ON, exactly like the dashboard toggle — every
1058
+ // consumer additionally requires a relayed refresh token, so the default only
1059
+ // takes effect for signed-in users; an explicit {enabled:false} still wins.
1058
1060
  const cloudSyncPrefPath = path.join(trackerDataDir, "cloud-sync-pref.json");
1059
1061
  let cloudSyncPrefCache;
1060
1062
  function getCloudSyncPref() {
1061
1063
  if (cloudSyncPrefCache === undefined) {
1062
1064
  try {
1063
- cloudSyncPrefCache = JSON.parse(fs.readFileSync(cloudSyncPrefPath, "utf8"))?.enabled === true;
1065
+ cloudSyncPrefCache = JSON.parse(fs.readFileSync(cloudSyncPrefPath, "utf8"))?.enabled !== false;
1064
1066
  } catch {
1065
- cloudSyncPrefCache = false;
1067
+ cloudSyncPrefCache = true;
1066
1068
  }
1067
1069
  }
1068
1070
  return cloudSyncPrefCache;
@@ -2579,14 +2581,18 @@ function createLocalApiHandler({ queuePath }) {
2579
2581
  if (p === "/functions/tokentracker-usage-limits") {
2580
2582
  const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
2581
2583
  try {
2582
- const forceRefresh = url.searchParams.get("refresh");
2583
- if (forceRefresh === "1" || forceRefresh === "true") {
2584
+ const refreshParam = url.searchParams.get("refresh");
2585
+ const forceRefresh = refreshParam === "1" || refreshParam === "true";
2586
+ if (forceRefresh) {
2584
2587
  resetUsageLimitsCache();
2585
2588
  }
2586
2589
  const data = await getUsageLimits({
2587
2590
  home: os.homedir(),
2588
2591
  env: process.env,
2589
2592
  platform: process.platform,
2593
+ // Punches through the Claude disk fresh-cache (but not the 429
2594
+ // cooldown) — an explicit user refresh should hit upstream.
2595
+ forceRefresh,
2590
2596
  });
2591
2597
  json(res, data);
2592
2598
  } catch (e) {
@@ -312,6 +312,19 @@ async function removeOpenclawSessionPluginConfig({
312
312
  }
313
313
 
314
314
  function runOpenclawCli(args, env = process.env) {
315
+ // Test-only escape hatch: skip spawning the real `openclaw` CLI. Behaves like
316
+ // the CLI being absent (which is the CI environment anyway), so callers take
317
+ // their graceful skip path. Avoids a ~2s `plugins install --link` per cmdInit
318
+ // on dev machines that happen to have openclaw installed.
319
+ if ((env && env.TOKENTRACKER_SKIP_OPENCLAW_CLI) === "1") {
320
+ return {
321
+ code: 1,
322
+ skippedReason: "openclaw-cli-missing",
323
+ error: "skipped via TOKENTRACKER_SKIP_OPENCLAW_CLI",
324
+ stdout: "",
325
+ stderr: "",
326
+ };
327
+ }
315
328
  let res;
316
329
  try {
317
330
  res = cp.spawnSync("openclaw", args, {