tokentracker-cli 0.84.5 → 0.84.7

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 (120) hide show
  1. package/README.de.md +10 -8
  2. package/README.ja.md +9 -7
  3. package/README.ko.md +9 -7
  4. package/README.md +12 -10
  5. package/README.zh-CN.md +9 -7
  6. package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
  10. package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
  11. package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
  26. package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
  34. package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
  35. package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
  44. package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
  51. package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
  54. package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
  56. package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
  57. package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
  58. package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
  59. package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
  61. package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
  62. package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
  63. package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
  64. package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
  65. package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
  66. package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
  67. package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
  70. package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
  71. package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
  72. package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
  73. package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
  74. package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
  79. package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
  86. package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
  87. package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
  88. package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
  89. package/dashboard/dist/brand-logos/qoder.svg +5 -0
  90. package/dashboard/dist/index.html +18 -15
  91. package/dashboard/dist/ip-check.html +2 -2
  92. package/dashboard/dist/leaderboard.html +2 -2
  93. package/dashboard/dist/llms.txt +3 -2
  94. package/dashboard/dist/share.html +2 -2
  95. package/package.json +2 -2
  96. package/src/commands/init.js +1 -0
  97. package/src/commands/status.js +34 -7
  98. package/src/commands/sync.js +353 -5
  99. package/src/lib/codex-rollout-parser.js +17 -39
  100. package/src/lib/codex-token-usage.js +195 -0
  101. package/src/lib/local-api.js +4 -1
  102. package/src/lib/pet-packages.js +44 -2
  103. package/src/lib/pricing/seed-snapshot.json +1 -1
  104. package/src/lib/provider-status.js +104 -0
  105. package/src/lib/qoder-limits.js +985 -0
  106. package/src/lib/rollout.js +744 -91
  107. package/src/lib/usage-limits.js +243 -43
  108. package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
  109. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
  110. package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
  111. package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
  112. package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
  113. package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
  114. package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
  115. package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
  116. package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
  117. package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
  118. package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
  120. package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
@@ -11,6 +11,12 @@ const path = require("node:path");
11
11
  const readline = require("node:readline");
12
12
 
13
13
  const { listRolloutFiles } = require("./rollout");
14
+ const {
15
+ consumeUsageDelta,
16
+ createUsageDeltaState,
17
+ snapshotUsageBaselines,
18
+ totalsReset,
19
+ } = require("./codex-token-usage");
14
20
  const {
15
21
  emptyTotals,
16
22
  addInto,
@@ -434,42 +440,10 @@ function normalizeUsage(u) {
434
440
  return out;
435
441
  }
436
442
 
437
- function totalsReset(curr, prev) {
438
- const a = Number(curr?.total_tokens);
439
- const b = Number(prev?.total_tokens);
440
- if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
441
- return a < b;
442
- }
443
-
444
443
  function pickDelta(lastUsage, totalUsage, prevTotals) {
445
- const hasLast = lastUsage && typeof lastUsage === "object";
446
- const hasTotal = totalUsage && typeof totalUsage === "object";
447
- const hasPrev = prevTotals && typeof prevTotals === "object";
448
-
449
- if (hasTotal && hasPrev) {
450
- if (totalsReset(totalUsage, prevTotals)) {
451
- const resetUsage = hasLast ? lastUsage : totalUsage;
452
- return normalizeUsage(resetUsage);
453
- }
454
- const delta = {};
455
- for (const k of [
456
- "input_tokens",
457
- "cached_input_tokens",
458
- "cache_creation_input_tokens",
459
- "output_tokens",
460
- "reasoning_output_tokens",
461
- "total_tokens",
462
- ]) {
463
- const a = Number(totalUsage[k]);
464
- const b = Number(prevTotals[k]);
465
- if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
466
- }
467
- return normalizeUsage(delta);
468
- }
469
-
470
- if (hasLast) return normalizeUsage(lastUsage);
471
- if (hasTotal) return normalizeUsage(totalUsage);
472
- return null;
444
+ const state = createUsageDeltaState({ lastTotal: prevTotals });
445
+ const delta = consumeUsageDelta(state, lastUsage, totalUsage);
446
+ return delta ? normalizeUsage(delta) : null;
473
447
  }
474
448
 
475
449
  // ---------------------------------------------------------------------------
@@ -596,7 +570,10 @@ async function parseCodexRolloutFile(filePath, {
596
570
  let provider = isResuming ? resumeState.provider || null : null;
597
571
  let cliVersion = isResuming ? resumeState.cliVersion || null : null;
598
572
 
599
- let prevTotals = isResuming ? resumeState.prevTotals || null : null;
573
+ const usageDeltaState = createUsageDeltaState({
574
+ lastTotal: isResuming ? resumeState.prevTotals || null : null,
575
+ baselines: isResuming ? resumeState.tokenUsageBaselines || null : null,
576
+ });
600
577
  let pendingToolNames = isResuming
601
578
  ? Array.from(resumeState.pendingToolNames || [])
602
579
  : [];
@@ -875,8 +852,8 @@ async function parseCodexRolloutFile(filePath, {
875
852
  const info = tokenCount.info;
876
853
  const lastUsage = info?.last_token_usage;
877
854
  const totalUsage = info?.total_token_usage;
878
- const delta = pickDelta(lastUsage, totalUsage, prevTotals);
879
- if (totalUsage && typeof totalUsage === "object") prevTotals = totalUsage;
855
+ const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
856
+ const delta = rawDelta ? normalizeUsage(rawDelta) : null;
880
857
  const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
881
858
  if (inRequestedRange) {
882
859
  const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
@@ -927,7 +904,8 @@ async function parseCodexRolloutFile(filePath, {
927
904
  model,
928
905
  provider,
929
906
  cliVersion,
930
- prevTotals,
907
+ prevTotals: usageDeltaState.lastTotal,
908
+ tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
931
909
  pendingToolNames,
932
910
  pendingSkills,
933
911
  pendingExecDetails,
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ // A rollout can contain TokenCount events from more than one Codex SessionState
4
+ // (for example, a parent agent and an embedded reviewer). Each SessionState has
5
+ // its own cumulative total, but the event does not carry a stable stream id.
6
+ // Recover the stream from Codex's invariant: total - last = that stream's prior
7
+ // total. Keeping a small LRU of stream heads also makes repeated rate-limit
8
+ // snapshots idempotent when the streams are interleaved.
9
+
10
+ const USAGE_FIELDS = [
11
+ "input_tokens",
12
+ "cached_input_tokens",
13
+ "cache_creation_input_tokens",
14
+ "output_tokens",
15
+ "reasoning_output_tokens",
16
+ "total_tokens",
17
+ ];
18
+ const MAX_USAGE_BASELINES = 32;
19
+
20
+ function canonicalUsage(value) {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
22
+ const out = {};
23
+ let sawNumber = false;
24
+ for (const field of USAGE_FIELDS) {
25
+ const raw = field === "cache_creation_input_tokens"
26
+ ? value.cache_creation_input_tokens ?? value.cache_write_input_tokens
27
+ : value[field];
28
+ const number = Number(raw);
29
+ if (Number.isFinite(number) && number >= 0) {
30
+ out[field] = Math.floor(number);
31
+ sawNumber = true;
32
+ } else {
33
+ out[field] = 0;
34
+ }
35
+ }
36
+ return sawNumber ? out : null;
37
+ }
38
+
39
+ function usageSignature(value) {
40
+ const usage = canonicalUsage(value);
41
+ return usage ? USAGE_FIELDS.map((field) => usage[field]).join(":") : null;
42
+ }
43
+
44
+ function sameUsage(left, right) {
45
+ const leftSignature = usageSignature(left);
46
+ return leftSignature !== null && leftSignature === usageSignature(right);
47
+ }
48
+
49
+ function subtractUsage(totalUsage, lastUsage) {
50
+ const total = canonicalUsage(totalUsage);
51
+ const last = canonicalUsage(lastUsage);
52
+ if (!total || !last) return null;
53
+ const previous = {};
54
+ for (const field of USAGE_FIELDS) {
55
+ if (total[field] < last[field]) return null;
56
+ previous[field] = total[field] - last[field];
57
+ }
58
+ return previous;
59
+ }
60
+
61
+ function diffUsage(totalUsage, previousUsage) {
62
+ const total = canonicalUsage(totalUsage);
63
+ const previous = canonicalUsage(previousUsage);
64
+ if (!total || !previous) return null;
65
+ const delta = {};
66
+ for (const field of USAGE_FIELDS) {
67
+ delta[field] = Math.max(0, total[field] - previous[field]);
68
+ }
69
+ return delta;
70
+ }
71
+
72
+ function totalsReset(totalUsage, previousUsage) {
73
+ const total = canonicalUsage(totalUsage);
74
+ const previous = canonicalUsage(previousUsage);
75
+ return Boolean(total && previous && total.total_tokens < previous.total_tokens);
76
+ }
77
+
78
+ function createUsageDeltaState({ lastTotal = null, baselines = null } = {}) {
79
+ const state = {
80
+ lastTotal: null,
81
+ baselines: [],
82
+ sawDivergentCumulative: false,
83
+ sawInterleaved: false,
84
+ };
85
+ for (const baseline of Array.isArray(baselines) ? baselines : []) {
86
+ touchBaseline(state, baseline);
87
+ }
88
+ if (canonicalUsage(lastTotal)) touchBaseline(state, lastTotal);
89
+ return state;
90
+ }
91
+
92
+ function snapshotUsageBaselines(state) {
93
+ return Array.isArray(state?.baselines)
94
+ ? state.baselines.map((usage) => ({ ...usage }))
95
+ : [];
96
+ }
97
+
98
+ function consumeUsageDelta(state, lastUsage, totalUsage) {
99
+ if (!state || typeof state !== "object") {
100
+ throw new TypeError("usage delta state is required");
101
+ }
102
+ if (!Array.isArray(state.baselines)) state.baselines = [];
103
+
104
+ const last = canonicalUsage(lastUsage);
105
+ const total = canonicalUsage(totalUsage);
106
+ if (!total) return last;
107
+
108
+ const activeSignature = usageSignature(state.lastTotal);
109
+ const duplicateIndex = findBaselineIndex(state, total);
110
+ if (duplicateIndex >= 0) {
111
+ if (
112
+ activeSignature !== null &&
113
+ usageSignature(state.baselines[duplicateIndex]) !== activeSignature
114
+ ) {
115
+ state.sawInterleaved = true;
116
+ }
117
+ touchBaselineAt(state, duplicateIndex, total);
118
+ return null;
119
+ }
120
+
121
+ if (last) {
122
+ const expectedPrevious = subtractUsage(total, last);
123
+ const lineageIndex = findBaselineIndex(state, expectedPrevious);
124
+ if (lineageIndex >= 0) {
125
+ if (
126
+ activeSignature !== null &&
127
+ usageSignature(state.baselines[lineageIndex]) !== activeSignature
128
+ ) {
129
+ state.sawInterleaved = true;
130
+ }
131
+ touchBaselineAt(state, lineageIndex, total);
132
+ return last;
133
+ }
134
+ if (expectedPrevious) {
135
+ if (canonicalUsage(state.lastTotal)) state.sawDivergentCumulative = true;
136
+ touchBaseline(state, total);
137
+ return last;
138
+ }
139
+ }
140
+
141
+ const active = canonicalUsage(state.lastTotal);
142
+ if (active && !totalsReset(total, active)) {
143
+ const delta = diffUsage(total, active);
144
+ if (!last || Number(delta?.total_tokens || 0) <= Number(last.total_tokens || 0)) {
145
+ const activeIndex = findBaselineIndex(state, active);
146
+ touchBaselineAt(state, activeIndex, total);
147
+ return delta;
148
+ }
149
+
150
+ // A cumulative jump larger than last usage cannot come from the normal
151
+ // append_last_usage path. Treat it as a new stream instead of charging the
152
+ // unrelated cumulative gap; a later event will establish its lineage.
153
+ state.sawDivergentCumulative = true;
154
+ }
155
+
156
+ touchBaseline(state, total);
157
+ return last || total;
158
+ }
159
+
160
+ function findBaselineIndex(state, usage) {
161
+ const signature = usageSignature(usage);
162
+ if (signature === null) return -1;
163
+ return state.baselines.findIndex((baseline) => usageSignature(baseline) === signature);
164
+ }
165
+
166
+ function touchBaselineAt(state, index, usage) {
167
+ const canonical = canonicalUsage(usage);
168
+ if (!canonical) return;
169
+ if (Number.isInteger(index) && index >= 0 && index < state.baselines.length) {
170
+ state.baselines.splice(index, 1);
171
+ } else {
172
+ const duplicateIndex = findBaselineIndex(state, canonical);
173
+ if (duplicateIndex >= 0) state.baselines.splice(duplicateIndex, 1);
174
+ }
175
+ state.baselines.push(canonical);
176
+ while (state.baselines.length > MAX_USAGE_BASELINES) state.baselines.shift();
177
+ state.lastTotal = canonical;
178
+ }
179
+
180
+ function touchBaseline(state, usage) {
181
+ touchBaselineAt(state, findBaselineIndex(state, usage), usage);
182
+ }
183
+
184
+ module.exports = {
185
+ MAX_USAGE_BASELINES,
186
+ canonicalUsage,
187
+ consumeUsageDelta,
188
+ createUsageDeltaState,
189
+ diffUsage,
190
+ sameUsage,
191
+ snapshotUsageBaselines,
192
+ subtractUsage,
193
+ totalsReset,
194
+ usageSignature,
195
+ };
@@ -2684,7 +2684,10 @@ function createLocalApiHandler({ queuePath }) {
2684
2684
  json(res, { importable, codexDetected: importable.length > 0 });
2685
2685
  return true;
2686
2686
  }
2687
- json(res, { pets: pets.listInstalledPets() });
2687
+ json(res, {
2688
+ pets: pets.listInstalledPets(),
2689
+ hiddenBuiltinIds: pets.readHiddenBuiltinIds(),
2690
+ });
2688
2691
  return true;
2689
2692
  }
2690
2693
  if (method === "POST") {
@@ -10,6 +10,8 @@ const MAX_PACKAGE_BYTES = 12 * 1024 * 1024;
10
10
  const MAX_MANIFEST_BYTES = 16 * 1024;
11
11
  const MAX_SPRITESHEET_BYTES = 10 * 1024 * 1024;
12
12
  const BUILTIN_IDS = new Set(["clawd", "sprout", "byte", "ember"]);
13
+ const REMOVABLE_BUILTIN_IDS = new Set(["sprout", "byte", "ember"]);
14
+ const HIDDEN_BUILTINS_FILE = ".hidden-builtins.json";
13
15
  // codex-pets.net added `kind` to pet.json long after launch, and its enum may keep
14
16
  // growing — packages published before that (and future kinds) must stay importable,
15
17
  // so kind is optional metadata validated only as a short slug.
@@ -35,6 +37,44 @@ function resolveCodexPetsDir() {
35
37
  );
36
38
  }
37
39
 
40
+ function readHiddenBuiltinIds() {
41
+ const file = path.join(resolvePetsDir(), HIDDEN_BUILTINS_FILE);
42
+ try {
43
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
44
+ if (!Array.isArray(parsed)) return [];
45
+ return [...new Set(parsed
46
+ .map((id) => String(id || "").trim().toLowerCase())
47
+ .filter((id) => REMOVABLE_BUILTIN_IDS.has(id)))].sort();
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ function hideBuiltinPet(id) {
54
+ const normalized = String(id || "").trim().toLowerCase();
55
+ if (normalized === "clawd") throw new Error("Clawd cannot be removed");
56
+ if (!REMOVABLE_BUILTIN_IDS.has(normalized)) throw new Error("Invalid built-in pet id");
57
+
58
+ const root = resolvePetsDir();
59
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
60
+ const hidden = new Set(readHiddenBuiltinIds());
61
+ hidden.add(normalized);
62
+ const destination = path.join(root, HIDDEN_BUILTINS_FILE);
63
+ const stage = path.join(root, `${HIDDEN_BUILTINS_FILE}.${process.pid}-${crypto.randomBytes(6).toString("hex")}`);
64
+ const backup = `${stage}-backup`;
65
+ try {
66
+ fs.writeFileSync(stage, `${JSON.stringify([...hidden].sort(), null, 2)}\n`, { mode: 0o600 });
67
+ if (fs.existsSync(destination)) fs.renameSync(destination, backup);
68
+ fs.renameSync(stage, destination);
69
+ } catch (error) {
70
+ try { fs.rmSync(stage, { force: true }); } catch {}
71
+ if (!fs.existsSync(destination) && fs.existsSync(backup)) fs.renameSync(backup, destination);
72
+ throw error;
73
+ }
74
+ try { fs.rmSync(backup, { force: true }); } catch {}
75
+ return { id: normalized, hidden: true };
76
+ }
77
+
38
78
  function readUInt24LE(buffer, offset) {
39
79
  return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16);
40
80
  }
@@ -368,10 +408,11 @@ async function installFromCodexPets(input, fetchImpl = fetch) {
368
408
 
369
409
  function removeInstalledPet(id) {
370
410
  const normalized = String(id || "").trim().toLowerCase();
371
- if (!PET_ID_RE.test(normalized) || BUILTIN_IDS.has(normalized)) throw new Error("Invalid custom pet id");
411
+ if (!PET_ID_RE.test(normalized)) throw new Error("Invalid pet id");
412
+ if (BUILTIN_IDS.has(normalized)) return hideBuiltinPet(normalized);
372
413
  const root = resolvePetsDir();
373
414
  const destination = path.join(root, normalized);
374
- if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid custom pet path");
415
+ if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid pet path");
375
416
  fs.rmSync(destination, { recursive: true, force: true });
376
417
  petDirectoryCache.delete(destination);
377
418
  return { id: normalized };
@@ -627,6 +668,7 @@ module.exports = {
627
668
  petIdFromCodexPetsUrl,
628
669
  readCodexBuiltinPets,
629
670
  readCodexImportableAsset,
671
+ readHiddenBuiltinIds,
630
672
  readWebpDimensions,
631
673
  removeInstalledPet,
632
674
  resolveCodexPetsDir,