tokentracker-cli 0.96.3 → 0.97.1

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 (103) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-BX4iBBYM.js → AchievementBadge-BdvqY4Qb.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DRYRuCbh.js → AchievementsPage-CrqvRxnb.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-BM_JWWhJ.js → AchievementsSection-wyLAM9N7.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-DxoxXst2.js → BadgeDetailModal-CquST3dj.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-BVpFxG8u.js → BotAnimated-EXhdUHSg.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BJJs701t.js → Card-Il2WWgOG.js} +1 -1
  7. package/dashboard/dist/assets/{ClawdAnimated-46wPZ_Tc.js → ClawdAnimated-DioF5wa-.js} +2 -2
  8. package/dashboard/dist/assets/{CommandPalette-B_zkc7IU.js → CommandPalette-CaDx06tM.js} +1 -1
  9. package/dashboard/dist/assets/{CommunityStatsModal-M2k5rIf1.js → CommunityStatsModal-BbYWzM07.js} +1 -1
  10. package/dashboard/dist/assets/{ConfirmModal-DBvOTcAQ.js → ConfirmModal-BHWQLUCM.js} +1 -1
  11. package/dashboard/dist/assets/{Controls-BCkjPMSH.js → Controls--LFhExZX.js} +1 -1
  12. package/dashboard/dist/assets/{DashboardPage-3bHhou09.js → DashboardPage-DuqJOpHL.js} +1 -1
  13. package/dashboard/dist/assets/{DevicePage-DFBUce8C.js → DevicePage-RKTirqz0.js} +1 -1
  14. package/dashboard/dist/assets/{DialogClose-kvDd8h3V.js → DialogClose-DPFsTKmJ.js} +1 -1
  15. package/dashboard/dist/assets/DialogDescription-DgaV8Gtk.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-DL5nvD31.js → DialogTitle-DolLpmFL.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-B-Bg6SEy.js → FadeIn-92GGNCPe.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-BExz0hh4.js → HeaderGithubStar-GfXudotT.js} +1 -1
  19. package/dashboard/dist/assets/{HoverTooltip-Dj5zemYT.js → HoverTooltip-n7gbPEsq.js} +1 -1
  20. package/dashboard/dist/assets/{IpCheckPage-DN7FuJ_v.js → IpCheckPage-BaQOTbXi.js} +1 -1
  21. package/dashboard/dist/assets/{LandingPage-9dKgSBRy.js → LandingPage-D3JcvxW_.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardAvatar-00V95XIg.js → LeaderboardAvatar-tVaWJXZ3.js} +1 -1
  23. package/dashboard/dist/assets/{LeaderboardPage-DkKfUt0Z.js → LeaderboardPage-NqHcStnk.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardProfileModal-C9sIHBgN.js → LeaderboardProfileModal-DmvcCVS7.js} +3 -3
  25. package/dashboard/dist/assets/{LeaderboardProfilePage-BAiLMsEb.js → LeaderboardProfilePage-VSv3IlzS.js} +1 -1
  26. package/dashboard/dist/assets/{LimitsPage-BuenTaHa.js → LimitsPage-CtxgT20z.js} +1 -1
  27. package/dashboard/dist/assets/{LocalOnlyNotice-CKhY27Lr.js → LocalOnlyNotice-O9o586G8.js} +1 -1
  28. package/dashboard/dist/assets/{LoginCard-BbvoBx5N.js → LoginCard-JCejigVu.js} +1 -1
  29. package/dashboard/dist/assets/{LoginModal-P7zIkBhO.js → LoginModal-CJ3g7tBc.js} +1 -1
  30. package/dashboard/dist/assets/{LoginPage-Bo0bR26x.js → LoginPage-DPWzPywx.js} +1 -1
  31. package/dashboard/dist/assets/{PetPage-C6i9bSSl.js → PetPage-MSJ3jseK.js} +1 -1
  32. package/dashboard/dist/assets/{PopoverPopup-h0gf36vg.js → PopoverPopup-nL3YOLPO.js} +1 -1
  33. package/dashboard/dist/assets/{ProviderIcon-Dm8fwPdj.js → ProviderIcon-C5RX_xPk.js} +1 -1
  34. package/dashboard/dist/assets/{ResetPasswordPage-DvSJ8kg0.js → ResetPasswordPage-DW_NCwA_.js} +1 -1
  35. package/dashboard/dist/assets/{SegmentedControl-CP7oiGZE.js → SegmentedControl-CS6wQtck.js} +1 -1
  36. package/dashboard/dist/assets/{Select-iwP92x9C.js → Select-YqlqvbT5.js} +1 -1
  37. package/dashboard/dist/assets/{SelectItemText-DzljA15_.js → SelectItemText-BahqsiIc.js} +1 -1
  38. package/dashboard/dist/assets/{ServiceStatusPage-uD4D0Tid.js → ServiceStatusPage-DyhutLUy.js} +1 -1
  39. package/dashboard/dist/assets/{SessionsPage-Cmb1ssNP.js → SessionsPage-wbvi4EHi.js} +1 -1
  40. package/dashboard/dist/assets/{SettingsPage-D8ryg1Pv.js → SettingsPage-HGSwpV1O.js} +2 -2
  41. package/dashboard/dist/assets/SkillsPage-BbVT9oyh.js +1 -0
  42. package/dashboard/dist/assets/{TokenGalaxy-f2djrlUv.js → TokenGalaxy-Cej6MLu6.js} +1 -1
  43. package/dashboard/dist/assets/{ToolbarRootContext-CDxCPYLv.js → ToolbarRootContext-D9pPY73U.js} +1 -1
  44. package/dashboard/dist/assets/{TrendMonitor-C419X22G.js → TrendMonitor-BtBcCjx-.js} +1 -1
  45. package/dashboard/dist/assets/{WidgetsPage-AYq__pOO.js → WidgetsPage-BSNl2WIL.js} +1 -1
  46. package/dashboard/dist/assets/{WrappedPage-Bsm9k5XI.js → WrappedPage-d5oiNV4T.js} +1 -1
  47. package/dashboard/dist/assets/{__vite-browser-external-BDkeh90N.js → __vite-browser-external-CjrP5KfC.js} +12 -12
  48. package/dashboard/dist/assets/{agent-logos-B6Vg-4_1.js → agent-logos-CCVzO3uU.js} +1 -1
  49. package/dashboard/dist/assets/{arrow-up-right-DIQXwaOD.js → arrow-up-right-AKItXMyh.js} +1 -1
  50. package/dashboard/dist/assets/{check-CgD_bV39.js → check-BQw5n0V0.js} +1 -1
  51. package/dashboard/dist/assets/{composite-BIgEqWZD.js → composite-CWz1s64R.js} +1 -1
  52. package/dashboard/dist/assets/{copy-DlM1BHdp.js → copy-CF8XNmv3.js} +1 -1
  53. package/dashboard/dist/assets/{download-CQFG-VIY.js → download-BZvLU_hD.js} +1 -1
  54. package/dashboard/dist/assets/{external-link-BQyDqGxz.js → external-link-bZrcVkeE.js} +1 -1
  55. package/dashboard/dist/assets/{flame-BamzyXzF.js → flame-CMHolW1z.js} +1 -1
  56. package/dashboard/dist/assets/{format-tokens-C4G-_xYt.js → format-tokens-Bs2YotMr.js} +1 -1
  57. package/dashboard/dist/assets/{icons-Ntscvl1a.js → icons-OKyij2y_.js} +1 -1
  58. package/dashboard/dist/assets/{index-DGxgsNoQ.js → index-C8nY1OaT.js} +1 -1
  59. package/dashboard/dist/assets/{index-FRIt5mlv.js → index-CN7xY7j_.js} +1 -1
  60. package/dashboard/dist/assets/{index-CUrcPqgp.js → index-DOwPCQx2.js} +1 -1
  61. package/dashboard/dist/assets/{info-CXytWcZi.js → info-CU7bMBhU.js} +1 -1
  62. package/dashboard/dist/assets/{link-2-Dexoo_jO.js → link-2-wwoUOujz.js} +1 -1
  63. package/dashboard/dist/assets/{loader-circle-Cn0JHc8t.js → loader-circle-BK3Ltc-j.js} +1 -1
  64. package/dashboard/dist/assets/{maximize-2-DLh7gYuP.js → maximize-2-DIPnLqR8.js} +1 -1
  65. package/dashboard/dist/assets/{modifiers.esm-DOh_xtyt.js → modifiers.esm-D_aVVjx5.js} +1 -1
  66. package/dashboard/dist/assets/{monitor-smartphone-CpqS9Ior.js → monitor-smartphone-4NOM9BHb.js} +1 -1
  67. package/dashboard/dist/assets/{plus-bfQV7nyv.js → plus-JqSTJ28Y.js} +1 -1
  68. package/dashboard/dist/assets/{provider-display-Bf9pW06U.js → provider-display-CEFlg9ho.js} +1 -1
  69. package/dashboard/dist/assets/{react-BmFZKA_b.js → react-jS0-5kwE.js} +1 -1
  70. package/dashboard/dist/assets/{search-l3jJ2fzC.js → search-CbBetPN7.js} +1 -1
  71. package/dashboard/dist/assets/{skills-api-CWmLnaxn.js → skills-api-PwUMTdmV.js} +1 -1
  72. package/dashboard/dist/assets/{sortable.esm-DLgyXLHM.js → sortable.esm-BDdw7tkz.js} +1 -1
  73. package/dashboard/dist/assets/{store-JM-a8ElZ.js → store-CFdu9yv_.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-6aY2Sn8P.js → terminal-Dh7Sh6IG.js} +1 -1
  75. package/dashboard/dist/assets/{trash-D5oon5pT.js → trash-B6_OaEVL.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-ZWUMFwzc.js → use-community-stats-D55oU9bu.js} +1 -1
  77. package/dashboard/dist/assets/{use-latest-request-guard-BDaXr4gK.js → use-latest-request-guard-BO89yQ3n.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-yXXToNIc.js → use-limits-display-prefs-C358N7B0.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-C6RG8N6c.js → use-ordered-list-B9ZYDS1K.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-DWHVI51t.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-DpgZO_Lx.js → use-session-efficiency-pref-CwGRDyVq.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-D42P7jkd.js → use-transform-9S4Ruq_s.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-CV6puFwq.js → use-usage-limits-CpnM5V06.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-GWBmOrCR.js → useCurrency-BKJGW_4l.js} +1 -1
  85. package/dashboard/dist/assets/{useTokenFormat-Beybkb0v.js → useTokenFormat-CTB5svOB.js} +1 -1
  86. package/dashboard/dist/assets/{zap-CEq2616W.js → zap-DGdBuT6m.js} +1 -1
  87. package/dashboard/dist/index.html +1 -1
  88. package/dashboard/dist/ip-check.html +1 -1
  89. package/dashboard/dist/leaderboard.html +1 -1
  90. package/dashboard/dist/share.html +1 -1
  91. package/package.json +1 -1
  92. package/src/lib/codex-rollout-parser.js +48 -1
  93. package/src/lib/codex-service-tier.js +49 -0
  94. package/src/lib/local-api.js +5 -0
  95. package/src/lib/pricing/curated-overrides.json +2 -1
  96. package/src/lib/pricing/index.js +61 -3
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +83 -2
  99. package/src/lib/session-analytics.js +11 -0
  100. package/src/lib/skills-manager.js +226 -30
  101. package/dashboard/dist/assets/DialogDescription-BMyOicfz.js +0 -1
  102. package/dashboard/dist/assets/SkillsPage-eXI09q03.js +0 -1
  103. package/dashboard/dist/assets/use-reduced-motion-BqDKim_3.js +0 -1
@@ -21,6 +21,11 @@ const {
21
21
  currentCodexModel,
22
22
  snapshotCodexModelAttributionState,
23
23
  } = require("./codex-model-attribution");
24
+ const {
25
+ CODEX_SERVICE_TIER_MARKER,
26
+ readCodexServiceTier,
27
+ isPriorityServiceTier,
28
+ } = require("./codex-service-tier");
24
29
  const {
25
30
  DEVIN_TABLE_PROBE_SQL,
26
31
  devinUsageSql,
@@ -471,6 +476,7 @@ async function parseRolloutIncremental({
471
476
  ? prev.tokenUsageBaselines || null
472
477
  : null;
473
478
  const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
479
+ const lastServiceTier = sameInode && !truncated ? prev.lastServiceTier || null : null;
474
480
  const modelAttributionState = sameInode && !truncated
475
481
  ? prev.modelAttributionState || null
476
482
  : null;
@@ -551,6 +557,7 @@ async function parseRolloutIncremental({
551
557
  lastTotal,
552
558
  tokenUsageBaselines,
553
559
  lastModel,
560
+ lastServiceTier,
554
561
  modelAttributionState,
555
562
  projectState,
556
563
  projectMetaCache,
@@ -566,6 +573,7 @@ async function parseRolloutIncremental({
566
573
  lastTotal,
567
574
  tokenUsageBaselines,
568
575
  lastModel,
576
+ lastServiceTier,
569
577
  modelAttributionState,
570
578
  hourlyState,
571
579
  touchedBuckets,
@@ -589,6 +597,7 @@ async function parseRolloutIncremental({
589
597
  lastTotal: result.lastTotal,
590
598
  tokenUsageBaselines: result.tokenUsageBaselines,
591
599
  lastModel: result.lastModel,
600
+ lastServiceTier: result.lastServiceTier || null,
592
601
  modelAttributionState: result.modelAttributionState,
593
602
  updatedAt: new Date().toISOString(),
594
603
  };
@@ -2060,6 +2069,7 @@ async function parseRolloutFile({
2060
2069
  lastTotal,
2061
2070
  tokenUsageBaselines,
2062
2071
  lastModel,
2072
+ lastServiceTier,
2063
2073
  modelAttributionState: previousModelAttributionState,
2064
2074
  hourlyState,
2065
2075
  touchedBuckets,
@@ -2086,6 +2096,7 @@ async function parseRolloutFile({
2086
2096
  lastTotal,
2087
2097
  tokenUsageBaselines,
2088
2098
  lastModel,
2099
+ lastServiceTier: typeof lastServiceTier === "string" ? lastServiceTier : null,
2089
2100
  modelAttributionState: previousModelAttributionState,
2090
2101
  eventsAggregated: 0,
2091
2102
  projectFileContexts,
@@ -2098,6 +2109,12 @@ async function parseRolloutFile({
2098
2109
  });
2099
2110
 
2100
2111
  let model = typeof lastModel === "string" ? lastModel : null;
2112
+ // Codex writes thread_settings_applied before the turn_context of the turn it
2113
+ // takes effect on, and token_count rows carry no turn id, so a row's tier is
2114
+ // whatever the last such record said. A session's first turn has none, and
2115
+ // most CLI sessions have none at all — that stays null, and null is billed at
2116
+ // Standard rather than guessed from the current config.
2117
+ let serviceTier = typeof lastServiceTier === "string" ? lastServiceTier : null;
2101
2118
  const modelAttributionState = createCodexModelAttributionState(
2102
2119
  previousModelAttributionState || { model },
2103
2120
  );
@@ -2148,7 +2165,10 @@ async function parseRolloutFile({
2148
2165
  line.includes('"cwd"') ||
2149
2166
  line.includes('"current_date"') ||
2150
2167
  line.includes('"forked_from_id"'));
2151
- if (!maybeTokenCount && !maybeTurnContext && !maybeModelReroute) {
2168
+ const maybeServiceTier =
2169
+ !maybeTokenCount && !maybeModelReroute && !maybeTurnContext &&
2170
+ line.includes(CODEX_SERVICE_TIER_MARKER);
2171
+ if (!maybeTokenCount && !maybeTurnContext && !maybeModelReroute && !maybeServiceTier) {
2152
2172
  if (invalidRecordPolicy === "throw" || !record.terminated) {
2153
2173
  try {
2154
2174
  JSON.parse(line);
@@ -2174,6 +2194,12 @@ async function parseRolloutFile({
2174
2194
  applyCodexModelEvent(modelAttributionState, obj);
2175
2195
  model = currentCodexModel(modelAttributionState) || model;
2176
2196
 
2197
+ const appliedServiceTier = readCodexServiceTier(obj);
2198
+ if (appliedServiceTier) {
2199
+ serviceTier = appliedServiceTier;
2200
+ continue;
2201
+ }
2202
+
2177
2203
  if (
2178
2204
  (obj?.type === "turn_context" || obj?.type === "session_meta") &&
2179
2205
  obj?.payload &&
@@ -2301,6 +2327,9 @@ async function parseRolloutFile({
2301
2327
 
2302
2328
  const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
2303
2329
  addTotals(bucket.totals, delta);
2330
+ // Only the model bucket gets the subset: the project bucket below carries
2331
+ // no model, so no per-model rate can be applied to it anyway.
2332
+ if (isPriorityServiceTier(serviceTier)) addPriorityUsage(bucket.totals, delta);
2304
2333
  touchedBuckets.add(bucketKey(source, model, bucketStart));
2305
2334
  if (currentProjectKey && projectState && projectTouchedBuckets) {
2306
2335
  const projectBucket = getProjectBucket(
@@ -2321,6 +2350,7 @@ async function parseRolloutFile({
2321
2350
  lastTotal: latestTotal,
2322
2351
  tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
2323
2352
  lastModel: model,
2353
+ lastServiceTier: serviceTier,
2324
2354
  modelAttributionState: snapshotCodexModelAttributionState(modelAttributionState),
2325
2355
  eventsAggregated,
2326
2356
  projectFileContexts,
@@ -2333,6 +2363,7 @@ async function scanRolloutProjectFileContexts({
2333
2363
  lastTotal,
2334
2364
  tokenUsageBaselines,
2335
2365
  lastModel,
2366
+ lastServiceTier,
2336
2367
  modelAttributionState,
2337
2368
  projectState,
2338
2369
  projectMetaCache,
@@ -2351,6 +2382,7 @@ async function scanRolloutProjectFileContexts({
2351
2382
  lastTotal,
2352
2383
  tokenUsageBaselines,
2353
2384
  lastModel,
2385
+ lastServiceTier: lastServiceTier || null,
2354
2386
  modelAttributionState,
2355
2387
  eventsAggregated: 0,
2356
2388
  projectFileContexts,
@@ -2428,6 +2460,7 @@ async function scanRolloutProjectFileContexts({
2428
2460
  lastTotal,
2429
2461
  tokenUsageBaselines,
2430
2462
  lastModel,
2463
+ lastServiceTier: lastServiceTier || null,
2431
2464
  modelAttributionState,
2432
2465
  eventsAggregated: 0,
2433
2466
  projectFileContexts,
@@ -3003,6 +3036,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
3003
3036
  billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
3004
3037
  total_cost_usd: totals.total_cost_usd || 0,
3005
3038
  usage_precision: usagePrecision || undefined,
3039
+ ...prioritySubsetOf(totals),
3006
3040
  conversation_count: totals.conversation_count,
3007
3041
  }),
3008
3042
  );
@@ -3083,6 +3117,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
3083
3117
  billable_total_tokens: unknownBucket.totals.billable_total_tokens ?? unknownBucket.totals.total_tokens,
3084
3118
  total_cost_usd: unknownBucket.totals.total_cost_usd || 0,
3085
3119
  usage_precision: usagePrecision || undefined,
3120
+ ...prioritySubsetOf(unknownBucket.totals),
3086
3121
  conversation_count: unknownBucket.totals.conversation_count,
3087
3122
  }),
3088
3123
  );
@@ -3131,6 +3166,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
3131
3166
  total_tokens: group.totals.total_tokens,
3132
3167
  billable_total_tokens: group.totals.billable_total_tokens ?? group.totals.total_tokens,
3133
3168
  total_cost_usd: group.totals.total_cost_usd || 0,
3169
+ ...prioritySubsetOf(group.totals),
3134
3170
  conversation_count: group.totals.conversation_count,
3135
3171
  }),
3136
3172
  );
@@ -3716,7 +3752,43 @@ function initTotals() {
3716
3752
  };
3717
3753
  }
3718
3754
 
3755
+ // Observed-subset markers, same shape as the session sidecar's
3756
+ // long_context_* columns: "how much of the columns above came from requests on
3757
+ // the priority (Astra Fast) service tier". They annotate the existing columns
3758
+ // and are NEVER part of any sum — total_tokens stays
3759
+ // input + output + cache_creation + cache_read + reasoning_output. They are
3760
+ // omitted entirely when zero so no other provider's queue rows change.
3761
+ const PRIORITY_SUBSET_FIELDS = [
3762
+ ["priority_input_tokens", "input_tokens"],
3763
+ ["priority_cached_input_tokens", "cached_input_tokens"],
3764
+ ["priority_cache_creation_input_tokens", "cache_creation_input_tokens"],
3765
+ ["priority_output_tokens", "output_tokens"],
3766
+ ["priority_reasoning_output_tokens", "reasoning_output_tokens"],
3767
+ ];
3768
+
3769
+ // The whole delta belongs to a priority request, so every base column it
3770
+ // carries is priority usage.
3771
+ function addPriorityUsage(target, delta) {
3772
+ for (const [field, base] of PRIORITY_SUBSET_FIELDS) {
3773
+ const value = Number(delta?.[base] || 0);
3774
+ if (value > 0) target[field] = (Number(target[field]) || 0) + value;
3775
+ }
3776
+ }
3777
+
3778
+ function prioritySubsetOf(totals) {
3779
+ const subset = {};
3780
+ for (const [field] of PRIORITY_SUBSET_FIELDS) {
3781
+ const value = Number(totals?.[field]) || 0;
3782
+ if (value > 0) subset[field] = value;
3783
+ }
3784
+ return subset;
3785
+ }
3786
+
3719
3787
  function addTotals(target, delta) {
3788
+ for (const [field] of PRIORITY_SUBSET_FIELDS) {
3789
+ const value = Number(delta?.[field]) || 0;
3790
+ if (value > 0) target[field] = (Number(target[field]) || 0) + value;
3791
+ }
3720
3792
  target.input_tokens += delta.input_tokens || 0;
3721
3793
  target.cached_input_tokens += delta.cached_input_tokens || 0;
3722
3794
  target.cache_creation_input_tokens += delta.cache_creation_input_tokens || 0;
@@ -3731,6 +3803,11 @@ function addTotals(target, delta) {
3731
3803
  }
3732
3804
 
3733
3805
  function subtractTotals(target, totals) {
3806
+ for (const [field] of PRIORITY_SUBSET_FIELDS) {
3807
+ const current = Number(target[field]) || 0;
3808
+ if (current <= 0) continue;
3809
+ target[field] = Math.max(0, current - (Number(totals?.[field]) || 0));
3810
+ }
3734
3811
  target.input_tokens = Math.max(0, target.input_tokens - (totals.input_tokens || 0));
3735
3812
  target.cached_input_tokens = Math.max(
3736
3813
  0,
@@ -3764,7 +3841,7 @@ function subtractTotals(target, totals) {
3764
3841
  }
3765
3842
 
3766
3843
  function totalsKey(totals) {
3767
- return [
3844
+ const base = [
3768
3845
  totals.input_tokens || 0,
3769
3846
  totals.cached_input_tokens || 0,
3770
3847
  totals.cache_creation_input_tokens || 0,
@@ -3775,6 +3852,10 @@ function totalsKey(totals) {
3775
3852
  totals.total_cost_usd || 0,
3776
3853
  totals.conversation_count || 0,
3777
3854
  ].join("|");
3855
+ // Appended only when a priority subset exists, so buckets that never see the
3856
+ // tier keep their previously persisted queuedKey and are not re-enqueued.
3857
+ const priority = PRIORITY_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
3858
+ return priority.some((value) => value > 0) ? `${base}|p:${priority.join(",")}` : base;
3778
3859
  }
3779
3860
 
3780
3861
  function toUtcHalfHourStart(ts) {
@@ -131,9 +131,20 @@ const MODEL_USAGE_SUM_FIELDS = [
131
131
  "long_context_cache_creation_input_tokens",
132
132
  "long_context_output_tokens",
133
133
  "long_context_reasoning_output_tokens",
134
+ "priority_input_tokens",
135
+ "priority_cached_input_tokens",
136
+ "priority_cache_creation_input_tokens",
137
+ "priority_output_tokens",
138
+ "priority_reasoning_output_tokens",
139
+ "priority_long_context_input_tokens",
140
+ "priority_long_context_cached_input_tokens",
141
+ "priority_long_context_cache_creation_input_tokens",
142
+ "priority_long_context_output_tokens",
143
+ "priority_long_context_reasoning_output_tokens",
134
144
  "usage_events",
135
145
  "rerouted_usage_events",
136
146
  "long_context_usage_events",
147
+ "priority_usage_events",
137
148
  // Not a token counter: an edit turn is a turn, and turn_context both marks
138
149
  // where a turn begins and names its model, so an edit turn has exactly one
139
150
  // owner. Splitting it here keeps by_model.tokens_per_edit a ratio over one
@@ -909,7 +909,12 @@ function listInstalledSkills() {
909
909
  });
910
910
  }
911
911
 
912
- async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
912
+ async function installSkill(
913
+ skillInput,
914
+ targetIds = ["claude", "codex"],
915
+ prefetchedTree = null,
916
+ { expectExistingId = null } = {},
917
+ ) {
913
918
  const skill = {
914
919
  key: String(skillInput?.key || ""),
915
920
  name: String(skillInput?.name || ""),
@@ -939,7 +944,9 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
939
944
  );
940
945
  }
941
946
 
942
- const { branch, tree } = await getRepoTree({
947
+ // Bulk update hands in the repo tree it already fetched, so N skills from one
948
+ // repo cost one GitHub call instead of N.
949
+ const { branch, tree } = prefetchedTree || await getRepoTree({
943
950
  owner: skill.repoOwner,
944
951
  name: skill.repoName,
945
952
  branch: skill.repoBranch,
@@ -950,9 +957,10 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
950
957
  if (!files.some((entry) => /(^|\/)SKILL\.md$/i.test(entry.path))) throw new Error("SKILL.md not found in selected directory");
951
958
 
952
959
  const dest = managedSkillPath(installName);
953
- const temp = path.join(dataDir(), "tmp", `${installName}-${Date.now()}`);
954
- removePath(temp);
955
- ensureDir(temp);
960
+ let selectedTargets = targetIds.filter((id) => TARGETS[id]);
961
+ const tempRoot = path.join(dataDir(), "tmp");
962
+ ensureDir(tempRoot);
963
+ const temp = fs.mkdtempSync(path.join(tempRoot, `${installName}-`));
956
964
  try {
957
965
  for (const entry of files) {
958
966
  const relative = entry.path === sourceDir ? path.basename(entry.path) : entry.path.slice(sourceDir.length + 1);
@@ -962,6 +970,17 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
962
970
  ensureDir(path.dirname(out));
963
971
  fs.writeFileSync(out, await fetchText(githubRawUrl(skill.repoOwner, skill.repoName, branch, entry.path)));
964
972
  }
973
+ // An update must not recreate a skill that was uninstalled while we were
974
+ // downloading, nor overwrite whatever took its install directory in the
975
+ // meantime. Checked here because everything from this point to
976
+ // saveRegistry() is synchronous — no await can interleave a change we would
977
+ // then clobber.
978
+ if (expectExistingId) {
979
+ const current = readRegistry().skills.find((entry) => !entry.trashedAt && entry.id === expectExistingId);
980
+ if (!current) throw new Error("Skill is no longer installed");
981
+ // Another page or API caller may have changed target intent during a download.
982
+ selectedTargets = (current.targets || []).filter((id) => TARGETS[id]);
983
+ }
965
984
  removePath(dest);
966
985
  ensureDir(path.dirname(dest));
967
986
  fs.renameSync(temp, dest);
@@ -973,7 +992,6 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
973
992
  const skillMarker = findSkillMarker(dest);
974
993
  const skillMd = skillMarker ? fs.readFileSync(skillMarker, "utf8") : "";
975
994
  const metadata = readSkillMetadata(skillMd, skill.name || installName);
976
- const selectedTargets = targetIds.filter((id) => TARGETS[id]);
977
995
  const installed = {
978
996
  id: `${skill.repoOwner}/${skill.repoName}:${sourceDir}`,
979
997
  key: `${skill.repoOwner}/${skill.repoName}:${sourceDir}`,
@@ -992,9 +1010,13 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
992
1010
  targets: selectedTargets,
993
1011
  };
994
1012
 
995
- registry.skills = registry.skills.filter((entry) => entry.id !== installed.id && entry.directory.toLowerCase() !== installName.toLowerCase());
996
- registry.skills.push(installed);
997
- saveRegistry(registry);
1013
+ // Re-read: the downloads above can take minutes and the registry is shared
1014
+ // with every other mutation path, so the copy fetched before them is stale by
1015
+ // now. Reading here keeps the read/modify/write window down to this block.
1016
+ const latest = readRegistry();
1017
+ latest.skills = latest.skills.filter((entry) => entry.id !== installed.id && entry.directory.toLowerCase() !== installName.toLowerCase());
1018
+ latest.skills.push(installed);
1019
+ saveRegistry(latest);
998
1020
 
999
1021
  for (const id of selectedTargets) syncSkillToTarget(installName, id);
1000
1022
  appendActivity({ action: "install", name: installed.name, directory: installName, targets: selectedTargets, source: `${skill.repoOwner}/${skill.repoName}` });
@@ -1244,20 +1266,89 @@ function updateCachePath() {
1244
1266
  return path.join(dataDir(), "updates-cache.json");
1245
1267
  }
1246
1268
 
1269
+ // checkUpdates() and updateSkills() have to agree on three things: which skills
1270
+ // are candidates, how a cached verdict is keyed, and which tree a skill's
1271
+ // signature comes from. They share these so the badge and the update can never
1272
+ // disagree about the same skill.
1273
+ function managedUpdateCandidates(registry) {
1274
+ return registry.skills.filter(
1275
+ (skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && skill.sourceSignature,
1276
+ );
1277
+ }
1278
+
1279
+ function updateFingerprint(managed) {
1280
+ return managed
1281
+ .map((skill) => `${skill.id}@${skill.sourceSignature}`)
1282
+ .sort()
1283
+ .join("|");
1284
+ }
1285
+
1286
+ // Write the verdicts a round derived, carrying forward the previous map for
1287
+ // anything it could not derive — a repo whose tree failed, everything past a
1288
+ // rate-limited stop. "We could not check" must not render as "nothing is
1289
+ // stale": that clears every badge and with it the way to retry. Returns the
1290
+ // merged map. Entries for skills that are no longer installed are dropped.
1291
+ //
1292
+ // `full` says the round attempted the whole library, which only checkUpdates()
1293
+ // does. An update attempts the ids it was handed, so it may only publish an
1294
+ // entry when the carried map fills in the rest — see the guard below.
1295
+ function writeUpdateCache(verdicts, { full = false } = {}) {
1296
+ const managed = managedUpdateCandidates(readRegistry());
1297
+ const live = new Set(managed.map((skill) => skill.id));
1298
+ const cached = readJson(updateCachePath(), null);
1299
+ const carriedFresh = Boolean(
1300
+ cached &&
1301
+ cached.updates &&
1302
+ typeof cached.updates === "object" &&
1303
+ Number.isFinite(cached.checkedAt) &&
1304
+ Date.now() - cached.checkedAt < UPDATE_CACHE_TTL_MS,
1305
+ );
1306
+ const updates = {};
1307
+ if (carriedFresh) {
1308
+ for (const [id, stale] of Object.entries(cached.updates)) if (live.has(id)) updates[id] = Boolean(stale);
1309
+ }
1310
+ for (const [id, stale] of Object.entries(verdicts)) if (live.has(id)) updates[id] = Boolean(stale);
1311
+
1312
+ // The entry is read back as a verdict for the entire library, and a missing
1313
+ // key reads as "not stale". A partial round that stamped the library
1314
+ // fingerprint would therefore publish silence about skills nobody looked at,
1315
+ // and the next check would hit it. Leave the cache alone instead and let that
1316
+ // check do the work.
1317
+ if (!full && !managed.every((skill) => Object.prototype.hasOwnProperty.call(updates, skill.id))) {
1318
+ return updates;
1319
+ }
1320
+
1321
+ writeJson(updateCachePath(), {
1322
+ fingerprint: updateFingerprint(managed),
1323
+ // A prime inherits the window of the check it built on: it re-derived some
1324
+ // ids, not the library, so it must not restart the hour for the rest.
1325
+ checkedAt: full || !carriedFresh ? Date.now() : cached.checkedAt,
1326
+ updates,
1327
+ });
1328
+ return updates;
1329
+ }
1330
+
1331
+ function groupSkillsByRepo(skills) {
1332
+ const byRepo = new Map();
1333
+ for (const skill of skills) {
1334
+ const branch = skill.repoBranch || "main";
1335
+ const key = `${skill.repoOwner}/${skill.repoName}@${branch}`.toLowerCase();
1336
+ if (!byRepo.has(key)) {
1337
+ byRepo.set(key, { owner: skill.repoOwner, name: skill.repoName, branch, skills: [] });
1338
+ }
1339
+ byRepo.get(key).skills.push(skill);
1340
+ }
1341
+ return Array.from(byRepo.values());
1342
+ }
1343
+
1247
1344
  // Compare each managed GitHub/skills.sh skill's stored source signature against a
1248
1345
  // freshly fetched repo tree. One tree call per repo (skills from the same repo
1249
1346
  // share it), concurrency-limited and cached for an hour so a background check
1250
1347
  // can't trip GitHub's unauthenticated rate limit. Returns { updates: {id:bool} }.
1251
1348
  // Read-only: never mutates the registry or the on-disk skills.
1252
1349
  async function checkUpdates({ force = false } = {}) {
1253
- const registry = readRegistry();
1254
- const managed = registry.skills.filter(
1255
- (skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && skill.sourceSignature,
1256
- );
1257
- const fingerprint = managed
1258
- .map((skill) => `${skill.id}@${skill.sourceSignature}`)
1259
- .sort()
1260
- .join("|");
1350
+ const managed = managedUpdateCandidates(readRegistry());
1351
+ const fingerprint = updateFingerprint(managed);
1261
1352
 
1262
1353
  if (!force) {
1263
1354
  const cached = readJson(updateCachePath(), null);
@@ -1273,18 +1364,8 @@ async function checkUpdates({ force = false } = {}) {
1273
1364
  }
1274
1365
  }
1275
1366
 
1276
- const byRepo = new Map();
1277
- for (const skill of managed) {
1278
- const branch = skill.repoBranch || "main";
1279
- const key = `${skill.repoOwner}/${skill.repoName}@${branch}`.toLowerCase();
1280
- if (!byRepo.has(key)) {
1281
- byRepo.set(key, { owner: skill.repoOwner, name: skill.repoName, branch, skills: [] });
1282
- }
1283
- byRepo.get(key).skills.push(skill);
1284
- }
1285
-
1286
1367
  const updates = {};
1287
- await mapWithConcurrency(Array.from(byRepo.values()), UPDATE_CHECK_CONCURRENCY, async (repo) => {
1368
+ await mapWithConcurrency(groupSkillsByRepo(managed), UPDATE_CHECK_CONCURRENCY, async (repo) => {
1288
1369
  let tree;
1289
1370
  try {
1290
1371
  ({ tree } = await getRepoTree(repo));
@@ -1299,8 +1380,122 @@ async function checkUpdates({ force = false } = {}) {
1299
1380
  });
1300
1381
 
1301
1382
  const checkedAt = Date.now();
1302
- writeJson(updateCachePath(), { fingerprint, checkedAt, updates });
1303
- return { updates, checkedAt, cached: false };
1383
+ return { updates: writeUpdateCache(updates, { full: true }), checkedAt, cached: false };
1384
+ }
1385
+
1386
+ // Re-install managed skills from upstream, grouped by repo like checkUpdates()
1387
+ // so N skills from one repo cost one tree call. Sequential by necessity:
1388
+ // installSkill() read/modify/writes the registry, so parallel installs would
1389
+ // drop each other's entries. Every requested id gets a result row — including
1390
+ // ones that are no longer installed — so updated + skipped + failed always
1391
+ // reconciles with what the caller asked for.
1392
+ async function updateSkills(ids = []) {
1393
+ // An empty or malformed list means "nothing was asked for", never "every
1394
+ // managed skill" — the permissive reading turned one bad request field into a
1395
+ // full re-install of the library.
1396
+ const wanted = new Set(
1397
+ (Array.isArray(ids) ? ids : []).map((id) => String(id || "")).filter(Boolean),
1398
+ );
1399
+ if (!wanted.size) return { results: [], updated: 0, skipped: 0, failed: 0, rateLimited: null };
1400
+ const managed = readRegistry().skills.filter(
1401
+ (skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && wanted.has(skill.id),
1402
+ );
1403
+
1404
+ const results = [];
1405
+ const verdicts = {};
1406
+ let rateLimited = null;
1407
+
1408
+ const known = new Set(managed.map((skill) => skill.id));
1409
+ for (const id of wanted) {
1410
+ if (!known.has(id)) results.push({ id, name: null, ok: false, error: "Skill is no longer installed" });
1411
+ }
1412
+
1413
+ for (const repo of groupSkillsByRepo(managed)) {
1414
+ let branch;
1415
+ let tree;
1416
+ try {
1417
+ ({ branch, tree } = await getRepoTree(repo));
1418
+ } catch (error) {
1419
+ if (error instanceof RateLimitError) {
1420
+ rateLimited = error;
1421
+ break;
1422
+ }
1423
+ for (const skill of repo.skills) {
1424
+ results.push({ id: skill.id, name: skill.name, ok: false, error: error?.message || "Unable to read repository" });
1425
+ }
1426
+ continue;
1427
+ }
1428
+
1429
+ for (const skill of repo.skills) {
1430
+ const sourceDir = skill.sourceDirectory || skill.directory;
1431
+ const fresh = sourceSignatureFromTree(tree, sourceDir);
1432
+ if (skill.sourceSignature && fresh === skill.sourceSignature) {
1433
+ results.push({ id: skill.id, name: skill.name, ok: true, skipped: true });
1434
+ verdicts[skill.id] = false;
1435
+ continue;
1436
+ }
1437
+ // This loop runs for as long as the downloads take, and the registry is
1438
+ // shared with every other mutation path — re-check rather than write back
1439
+ // a skill that was uninstalled while we were working.
1440
+ if (!readRegistry().skills.some((entry) => !entry.trashedAt && entry.id === skill.id)) {
1441
+ results.push({ id: skill.id, name: skill.name, ok: false, error: "Skill is no longer installed" });
1442
+ continue;
1443
+ }
1444
+ try {
1445
+ await installSkill(
1446
+ {
1447
+ key: skill.key,
1448
+ name: skill.name,
1449
+ description: skill.description,
1450
+ // installSkill() wants the repo path, not the flat install name.
1451
+ directory: sourceDir,
1452
+ readmeUrl: skill.readmeUrl,
1453
+ repoOwner: skill.repoOwner,
1454
+ repoName: skill.repoName,
1455
+ repoBranch: branch,
1456
+ },
1457
+ skill.targets || [],
1458
+ { branch, tree },
1459
+ { expectExistingId: skill.id },
1460
+ );
1461
+ results.push({ id: skill.id, name: skill.name, ok: true, skipped: false });
1462
+ if (fresh) verdicts[skill.id] = false;
1463
+ } catch (error) {
1464
+ if (error instanceof RateLimitError) {
1465
+ rateLimited = error;
1466
+ break;
1467
+ }
1468
+ results.push({ id: skill.id, name: skill.name, ok: false, error: error?.message || "Update failed" });
1469
+ if (fresh) verdicts[skill.id] = true;
1470
+ }
1471
+ }
1472
+ if (rateLimited) break;
1473
+ }
1474
+
1475
+ // A rate-limited stop breaks out of both loops, leaving the skill it happened
1476
+ // on and everything after it unreported. Backfill so every requested id comes
1477
+ // back with a row and the counts still reconcile with what was asked for.
1478
+ if (rateLimited) {
1479
+ const reported = new Set(results.map((row) => row.id));
1480
+ const nameById = new Map(managed.map((skill) => [skill.id, skill.name]));
1481
+ for (const id of wanted) {
1482
+ if (reported.has(id)) continue;
1483
+ results.push({ id, name: nameById.get(id) || null, ok: false, error: rateLimited.message });
1484
+ }
1485
+ }
1486
+
1487
+ // Prime the cache from the trees this run already holds, so the refresh that
1488
+ // follows is a cache hit rather than a second tree call per repo. Skipped when
1489
+ // the run derived nothing — there is no fresh verdict to record.
1490
+ if (Object.keys(verdicts).length) writeUpdateCache(verdicts);
1491
+
1492
+ return {
1493
+ results,
1494
+ updated: results.filter((entry) => entry.ok && !entry.skipped).length,
1495
+ skipped: results.filter((entry) => entry.skipped).length,
1496
+ failed: results.filter((entry) => !entry.ok).length,
1497
+ rateLimited: rateLimited ? rateLimited.message : null,
1498
+ };
1304
1499
  }
1305
1500
 
1306
1501
  // skills.sh exposes only /api/search (no leaderboard endpoint), so "Popular" is
@@ -1385,4 +1580,5 @@ module.exports = {
1385
1580
  sourceSignatureFromTree,
1386
1581
  targetList,
1387
1582
  uninstallSkill,
1583
+ updateSkills,
1388
1584
  };
@@ -1 +0,0 @@
1
- import{r as n,v as a}from"./__vite-browser-external-BDkeh90N.js";import{u as p}from"./DialogTitle-DL5nvD31.js";import{u as c}from"./composite-BIgEqWZD.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,i=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};