tokentracker-cli 1.4.7 → 1.4.10

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 (154) hide show
  1. package/README.de.md +2 -2
  2. package/README.ja.md +2 -2
  3. package/README.ko.md +2 -2
  4. package/README.md +11 -3
  5. package/README.zh-CN.md +3 -3
  6. package/bin/tracker.js +6 -0
  7. package/dashboard/dist/assets/{AchievementBadge-DF8q1x6R.js → AchievementBadge-BTqlXUP1.js} +1 -1
  8. package/dashboard/dist/assets/{AchievementsPage-B8Dg5myo.js → AchievementsPage-DbKUhaY-.js} +2 -2
  9. package/dashboard/dist/assets/{AchievementsSection-oPPiyP3y.js → AchievementsSection-El1F-OC7.js} +1 -1
  10. package/dashboard/dist/assets/{BadgeDetailModal-PneMW4B4.js → BadgeDetailModal--8PySLzP.js} +1 -1
  11. package/dashboard/dist/assets/{BotAnimated-GOlSvEiz.js → BotAnimated-DBcOS4vh.js} +1 -1
  12. package/dashboard/dist/assets/{Card-BT5G0GE0.js → Card-CeGs8TaY.js} +1 -1
  13. package/dashboard/dist/assets/{CloudCheckoutPage-BNeL5Muo.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
  14. package/dashboard/dist/assets/CloudPage-CmeFDrmk.js +2 -0
  15. package/dashboard/dist/assets/{CommandPalette-CiMSYHNK.js → CommandPalette-CrSNoZW9.js} +1 -1
  16. package/dashboard/dist/assets/{CommunityStatsModal-fK3BE9CW.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
  17. package/dashboard/dist/assets/{ConfirmModal-CrUlEHyZ.js → ConfirmModal-DDTUf-My.js} +1 -1
  18. package/dashboard/dist/assets/{Controls-CbjSwq6k.js → Controls-DXsJjEU1.js} +1 -1
  19. package/dashboard/dist/assets/DashboardPage-fr4sgbLB.js +1 -0
  20. package/dashboard/dist/assets/{DateRangePopover-E-2Z6imh.js → DateRangePopover-gTiPISL_.js} +1 -1
  21. package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
  22. package/dashboard/dist/assets/{DialogClose-xJKE22Y6.js → DialogClose-CXwouQe4.js} +1 -1
  23. package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
  24. package/dashboard/dist/assets/{DialogTitle-f8vnnH7z.js → DialogTitle-Cd_k-Nyw.js} +1 -1
  25. package/dashboard/dist/assets/{ElectricLogo-Ci9WZfHK.js → ElectricLogo-C4vUNTCI.js} +1 -1
  26. package/dashboard/dist/assets/{FadeIn-BXqtyAmn.js → FadeIn-CGM5RBFy.js} +1 -1
  27. package/dashboard/dist/assets/{HeaderGithubStar-BV5SYJMi.js → HeaderGithubStar-el5-DhVb.js} +1 -1
  28. package/dashboard/dist/assets/{HoverTooltip-D3gpW3Jh.js → HoverTooltip-C7YaWwJx.js} +1 -1
  29. package/dashboard/dist/assets/{IpCheckPage-G7fIWXpL.js → IpCheckPage-EKhbH9hJ.js} +1 -1
  30. package/dashboard/dist/assets/LandingPage-DJYOv1AM.js +2 -0
  31. package/dashboard/dist/assets/{LeaderboardPage-D2_wXazL.js → LeaderboardPage-CwzJxA8p.js} +2 -2
  32. package/dashboard/dist/assets/{LeaderboardProBadge-3xJ-mH2v.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
  33. package/dashboard/dist/assets/{LeaderboardProfileModal-23pH6Ud7.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
  34. package/dashboard/dist/assets/{LeaderboardProfilePage-DSezSR58.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
  35. package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
  36. package/dashboard/dist/assets/{LocalOnlyNotice-Dsk-d0Hr.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
  37. package/dashboard/dist/assets/{LoginCard-C7dugIIn.js → LoginCard-Czq-UgAU.js} +1 -1
  38. package/dashboard/dist/assets/{LoginModal-B7RuLsTF.js → LoginModal-D4uxfzZb.js} +1 -1
  39. package/dashboard/dist/assets/{LoginPage-u32vJQN9.js → LoginPage-Bg3BG0Wv.js} +1 -1
  40. package/dashboard/dist/assets/{PetPage-Dbc975oU.js → PetPage-BgwBxRyx.js} +2 -2
  41. package/dashboard/dist/assets/{PopoverPopup-D-yECvH9.js → PopoverPopup-k3o_Jp2b.js} +1 -1
  42. package/dashboard/dist/assets/{ProviderIcon-CHtZiJUN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
  43. package/dashboard/dist/assets/{ResetPasswordPage-ctuIsNk0.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
  44. package/dashboard/dist/assets/{Select-DN4Kq_R1.js → Select-kA0dV5Qw.js} +1 -1
  45. package/dashboard/dist/assets/{SelectItemText-C-HmeD8L.js → SelectItemText-D4YCIn1x.js} +1 -1
  46. package/dashboard/dist/assets/{SelfHostPage-BZOL3JMt.js → SelfHostPage-C3Y9qF3P.js} +1 -1
  47. package/dashboard/dist/assets/{ServiceStatusPage-C0sDYa8c.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
  48. package/dashboard/dist/assets/{SessionsPage-l0-FkcJB.js → SessionsPage-CA-efxhU.js} +3 -3
  49. package/dashboard/dist/assets/SettingsPage-Mr84vxQL.js +5 -0
  50. package/dashboard/dist/assets/{SkillsPage-Cz6G9d3o.js → SkillsPage-BtRhLSpz.js} +1 -1
  51. package/dashboard/dist/assets/TokenGalaxy-CcFjCCuC.js +4426 -0
  52. package/dashboard/dist/assets/TooltipPopup-6aTx3jDf.js +1 -0
  53. package/dashboard/dist/assets/TrendMonitor-DoywXr8h.js +19 -0
  54. package/dashboard/dist/assets/{WidgetsPage-BKo3DBxT.js → WidgetsPage-BuGSzvR0.js} +1 -1
  55. package/dashboard/dist/assets/{WrappedPage-Blm3aPO5.js → WrappedPage-Bm6X-SUZ.js} +1 -1
  56. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
  57. package/dashboard/dist/assets/__vite-browser-external-ClVJNQu3.js +45 -0
  58. package/dashboard/dist/assets/{agent-logos-DMtDEx7_.js → agent-logos-Sc8Sejgx.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-left-Bo9y7VzF.js → arrow-left-Cm1LG2i_.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-right-BLMU8Y5U.js → arrow-right-WP3kIwiy.js} +1 -1
  61. package/dashboard/dist/assets/{arrow-up-right-B4hXO4sc.js → arrow-up-right-CjyprVMj.js} +1 -1
  62. package/dashboard/dist/assets/{calendar-days-D9HaEtgo.js → calendar-days-Bm88-Gcb.js} +1 -1
  63. package/dashboard/dist/assets/{check-BJNGKpX8.js → check-CzIxFg0I.js} +1 -1
  64. package/dashboard/dist/assets/{chevron-down-CoGC_RLK.js → chevron-down-bfGK8629.js} +1 -1
  65. package/dashboard/dist/assets/{cloud-checkout-Du5FoHqV.js → cloud-checkout-C9DFQ7yM.js} +1 -1
  66. package/dashboard/dist/assets/{composite-BfIUPpl8.js → composite-CdExmgTx.js} +1 -1
  67. package/dashboard/dist/assets/copy-CPPdujn2.js +1 -0
  68. package/dashboard/dist/assets/{download-C1K_v0C0.js → download-Cl6_dKeK.js} +1 -1
  69. package/dashboard/dist/assets/{external-link-D-xHhfj5.js → external-link-B0r6Wo4e.js} +1 -1
  70. package/dashboard/dist/assets/{flame-DXD6uvhM.js → flame-wEezvzXx.js} +1 -1
  71. package/dashboard/dist/assets/{format-tokens-B9riGvNj.js → format-tokens-vWpBpKpM.js} +1 -1
  72. package/dashboard/dist/assets/{gift-LPR-nD0J.js → gift-BFjblRqU.js} +1 -1
  73. package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
  74. package/dashboard/dist/assets/{icons-DLdtLqEt.js → icons-DX3dCNJP.js} +1 -1
  75. package/dashboard/dist/assets/{index-CXdJ7zMN.js → index-D-TnqdBv.js} +1 -1
  76. package/dashboard/dist/assets/{index-CC3cXeBa.js → index-D1Zdrpom.js} +1 -1
  77. package/dashboard/dist/assets/index-DBF1bJlm.js +502 -0
  78. package/dashboard/dist/assets/{index-BL11MboX.js → index-p8Gt3NFg.js} +1 -1
  79. package/dashboard/dist/assets/{info-tnXKCyuK.js → info-Cqi971MA.js} +1 -1
  80. package/dashboard/dist/assets/{link-2-DOk9Tcg1.js → link-2-79sm__cT.js} +1 -1
  81. package/dashboard/dist/assets/{loader-circle-DIl96Mw-.js → loader-circle-CDoVfOUb.js} +1 -1
  82. package/dashboard/dist/assets/local-usage-transfer-EcXnuwCj.js +1 -0
  83. package/dashboard/dist/assets/{modifiers.esm-2H_1taJ6.js → modifiers.esm-v5_BKSmY.js} +1 -1
  84. package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
  85. package/dashboard/dist/assets/{monitor-smartphone-tnp-03hs.js → monitor-smartphone-CO67DffO.js} +1 -1
  86. package/dashboard/dist/assets/{play-BGMuGTdN.js → play-CbvuGtc3.js} +1 -1
  87. package/dashboard/dist/assets/{plus-XETAaErq.js → plus-jc2gXlRM.js} +1 -1
  88. package/dashboard/dist/assets/{provider-display-BYNT4zPc.js → provider-display-BK3EbRLM.js} +1 -1
  89. package/dashboard/dist/assets/{react-OW7HALVO.js → react-DMs4C4qS.js} +1 -1
  90. package/dashboard/dist/assets/{safePolygon-CAfS8-ct.js → safePolygon-cY9H0GBN.js} +1 -1
  91. package/dashboard/dist/assets/{search-B7ZJhoau.js → search-fqTJoYYA.js} +1 -1
  92. package/dashboard/dist/assets/{server-CWrP7mtL.js → server-jpBtvTvr.js} +1 -1
  93. package/dashboard/dist/assets/{shield-check-ip5IQEK3.js → shield-check-DQiBlJbN.js} +1 -1
  94. package/dashboard/dist/assets/{skills-api-CpyoNTeB.js → skills-api-CN2r_rmt.js} +1 -1
  95. package/dashboard/dist/assets/{sortable.esm-YvzWzPDu.js → sortable.esm-DrS52yOK.js} +1 -1
  96. package/dashboard/dist/assets/{store-DRK6-JBg.js → store-K2Mk6aMn.js} +1 -1
  97. package/dashboard/dist/assets/{terminal-DQRoJSvh.js → terminal-Q4SruAlh.js} +1 -1
  98. package/dashboard/dist/assets/{trash-8h_26atF.js → trash-C1Ob-g8E.js} +1 -1
  99. package/dashboard/dist/assets/{use-community-stats-DYlhIXsp.js → use-community-stats-C-Gxpp9D.js} +1 -1
  100. package/dashboard/dist/assets/{use-latest-request-guard-AxJF9SEJ.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
  101. package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
  102. package/dashboard/dist/assets/{use-native-settings-BQgB6_PB.js → use-native-settings-BZLEIUK8.js} +1 -1
  103. package/dashboard/dist/assets/{use-ordered-list-ogi7Okzf.js → use-ordered-list-De_Mcm_H.js} +1 -1
  104. package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
  105. package/dashboard/dist/assets/{use-transform-BQqoo1et.js → use-transform-XCDD5IrX.js} +1 -1
  106. package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
  107. package/dashboard/dist/assets/{useCurrency-Bv9g8mpq.js → useCurrency-DXPclxBT.js} +1 -1
  108. package/dashboard/dist/assets/{usePositioner-CQiZGknc.js → usePositioner-C-E1npCy.js} +1 -1
  109. package/dashboard/dist/assets/{useTokenFormat-BUVirY0k.js → useTokenFormat-B70xhy9u.js} +1 -1
  110. package/dashboard/dist/assets/{zap-DR3VD0yY.js → zap-D7gRgGCj.js} +1 -1
  111. package/dashboard/dist/index.html +5 -5
  112. package/dashboard/dist/ip-check.html +2 -2
  113. package/dashboard/dist/leaderboard.html +2 -2
  114. package/dashboard/dist/privacy.html +8 -2
  115. package/dashboard/dist/share.html +2 -2
  116. package/package.json +4 -3
  117. package/src/cli.js +10 -0
  118. package/src/commands/sync.js +18 -3
  119. package/src/lib/codex-rollout-parser.js +29 -2
  120. package/src/lib/codex-service-tier.js +13 -0
  121. package/src/lib/commandcode-limits.js +27 -11
  122. package/src/lib/grok-limits.js +1 -0
  123. package/src/lib/local-api.js +122 -11
  124. package/src/lib/local-usage-transfer.js +271 -0
  125. package/src/lib/opencode-go-limits.js +17 -6
  126. package/src/lib/pricing/curated-overrides.json +24 -0
  127. package/src/lib/pricing/index.js +50 -39
  128. package/src/lib/pricing/seed-snapshot.json +1 -1
  129. package/src/lib/provider-api-key-store.js +110 -0
  130. package/src/lib/rollout.js +27 -3
  131. package/src/lib/sentry.js +231 -0
  132. package/src/lib/session-analytics.js +15 -2
  133. package/src/lib/static-server.js +15 -2
  134. package/src/lib/usage-limits.js +74 -24
  135. package/src/lib/workbuddy-ipc.js +191 -0
  136. package/src/lib/workbuddy-limits.js +564 -0
  137. package/dashboard/dist/assets/CloudPage-26bhEYqI.js +0 -2
  138. package/dashboard/dist/assets/DashboardPage-CP_PMMga.js +0 -1
  139. package/dashboard/dist/assets/DevicePage-B0fLd80M.js +0 -1
  140. package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +0 -1
  141. package/dashboard/dist/assets/LandingPage-BQNOdAkR.js +0 -2
  142. package/dashboard/dist/assets/LimitsPage-BLJsmVVy.js +0 -9
  143. package/dashboard/dist/assets/SettingsPage-CrczfZiZ.js +0 -5
  144. package/dashboard/dist/assets/TokenGalaxy-B5IGaLQK.js +0 -4426
  145. package/dashboard/dist/assets/TooltipPopup-B6FqLWO7.js +0 -1
  146. package/dashboard/dist/assets/TrendMonitor-B4kJUwmF.js +0 -19
  147. package/dashboard/dist/assets/__vite-browser-external--HDUIUZr.js +0 -45
  148. package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
  149. package/dashboard/dist/assets/host-mode-C2K0MMOs.js +0 -1
  150. package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
  151. package/dashboard/dist/assets/use-limits-display-prefs-DOZzuTwo.js +0 -1
  152. package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +0 -1
  153. package/dashboard/dist/assets/use-session-efficiency-pref-DP3hu3Rp.js +0 -1
  154. package/dashboard/dist/assets/use-usage-limits-DAf6O4aZ.js +0 -1
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ // Provider API keys saved from the dashboard's Limits panel.
4
+ //
5
+ // The launch environment is a shared, machine-wide namespace: a
6
+ // `launchctl setenv OPENCODE_GO_API_KEY` reaches every launchd app on the
7
+ // machine (that is how a global key silently unlocked another app's field in
8
+ // #837). Keys entered in the panel are therefore persisted here instead, in a
9
+ // 0600 JSON file under the tracker data dir next to cloud-device-token.json.
10
+ //
11
+ // The environment variable still wins when set, so headless installs and
12
+ // wrapper-launched apps keep working; the store is the durable default for
13
+ // users who never touch a terminal.
14
+
15
+ const fssync = require("node:fs");
16
+ const path = require("node:path");
17
+ const { ensureDir, writeFileAtomic, chmod600IfPossible } = require("./fs");
18
+
19
+ const PROVIDER_KEY_FILES = {
20
+ "opencode-go": "opencode-go-api-key.json",
21
+ "command-code": "command-code-api-key.json",
22
+ };
23
+
24
+ const PROVIDER_ENV_NAMES = {
25
+ "opencode-go": "OPENCODE_GO_API_KEY",
26
+ "command-code": "COMMAND_CODE_API_KEY",
27
+ };
28
+
29
+ const MIN_API_KEY_LENGTH = 8;
30
+ const MAX_API_KEY_LENGTH = 4096;
31
+
32
+ function isProviderId(provider) {
33
+ return typeof provider === "string" && Object.prototype.hasOwnProperty.call(PROVIDER_KEY_FILES, provider);
34
+ }
35
+
36
+ function envNameForProvider(provider) {
37
+ return isProviderId(provider) ? PROVIDER_ENV_NAMES[provider] : null;
38
+ }
39
+
40
+ function resolveProviderKeyDir({ trackerDir, home } = {}) {
41
+ if (typeof trackerDir === "string" && trackerDir.trim()) return trackerDir.trim();
42
+ if (typeof home === "string" && home.trim()) {
43
+ return path.join(home.trim(), ".tokentracker", "tracker");
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function resolveProviderKeyPath({ provider, trackerDir, home } = {}) {
49
+ if (!isProviderId(provider)) return null;
50
+ const dir = resolveProviderKeyDir({ trackerDir, home });
51
+ if (!dir) return null;
52
+ return path.join(dir, PROVIDER_KEY_FILES[provider]);
53
+ }
54
+
55
+ // Keys are opaque bearer tokens. Reject anything that cannot be pasted into an
56
+ // Authorization header verbatim (internal whitespace/control characters) and
57
+ // keep an upper bound so a mis-pasted blob cannot bloat the auth file.
58
+ function normalizeApiKey(value) {
59
+ if (typeof value !== "string") return "";
60
+ const trimmed = value.trim();
61
+ if (trimmed.length < MIN_API_KEY_LENGTH || trimmed.length > MAX_API_KEY_LENGTH) return "";
62
+ if (/[\s\u0000-\u001f\u007f]/.test(trimmed)) return "";
63
+ return trimmed;
64
+ }
65
+
66
+ // Never throws: a missing, unreadable, or malformed store must degrade to
67
+ // "no stored key" so a limits fetch can still fall back to env/cookie/CLI.
68
+ function readStoredApiKey({ provider, trackerDir, home } = {}) {
69
+ const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
70
+ if (!filePath) return "";
71
+ let raw;
72
+ try {
73
+ raw = fssync.readFileSync(filePath, "utf8");
74
+ } catch {
75
+ return "";
76
+ }
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(raw);
80
+ } catch {
81
+ return "";
82
+ }
83
+ return normalizeApiKey(parsed?.apiKey);
84
+ }
85
+
86
+ async function writeStoredApiKey({ provider, trackerDir, home, apiKey } = {}) {
87
+ const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
88
+ if (!filePath) throw new Error("No tracker data directory available for the API key.");
89
+ const normalized = normalizeApiKey(apiKey);
90
+ if (!normalized) throw new Error("Invalid API key.");
91
+ await ensureDir(path.dirname(filePath));
92
+ const contents = `${JSON.stringify({ apiKey: normalized, updatedAt: new Date().toISOString() }, null, 2)}\n`;
93
+ await writeFileAtomic(filePath, contents, { mode: 0o600 });
94
+ // writeFileAtomic only applies `mode` to newly created files; an existing
95
+ // store with looser permissions must still end up private.
96
+ await chmod600IfPossible(filePath);
97
+ return filePath;
98
+ }
99
+
100
+ module.exports = {
101
+ PROVIDER_KEY_FILES,
102
+ PROVIDER_ENV_NAMES,
103
+ isProviderId,
104
+ envNameForProvider,
105
+ resolveProviderKeyDir,
106
+ resolveProviderKeyPath,
107
+ normalizeApiKey,
108
+ readStoredApiKey,
109
+ writeStoredApiKey,
110
+ };
@@ -28,6 +28,7 @@ const {
28
28
  CODEX_SERVICE_TIER_MARKER,
29
29
  readCodexServiceTier,
30
30
  isPriorityServiceTier,
31
+ isUltrafastServiceTier,
31
32
  } = require("./codex-service-tier");
32
33
  const {
33
34
  awaitsCompactedLine,
@@ -2649,6 +2650,7 @@ async function parseRolloutRecords({
2649
2650
  const bucket = getHourlyBucket(hourlyState, source, event.model, event.bucketStart);
2650
2651
  addTotals(bucket.totals, event.delta);
2651
2652
  if (isPriorityServiceTier(event.serviceTier)) addPriorityUsage(bucket.totals, event.delta);
2653
+ if (isUltrafastServiceTier(event.serviceTier)) addUltrafastUsage(bucket.totals, event.delta);
2652
2654
  touchedBuckets.add(bucketKey(source, event.model, event.bucketStart));
2653
2655
  if (event.projectKey && projectState && projectTouchedBuckets) {
2654
2656
  const projectBucket = getProjectBucket(
@@ -2903,6 +2905,7 @@ async function parseRolloutRecords({
2903
2905
  // Only the model bucket gets the subset: the project bucket below carries
2904
2906
  // no model, so no per-model rate can be applied to it anyway.
2905
2907
  if (isPriorityServiceTier(serviceTier)) addPriorityUsage(bucket.totals, delta);
2908
+ if (isUltrafastServiceTier(serviceTier)) addUltrafastUsage(bucket.totals, delta);
2906
2909
  touchedBuckets.add(bucketKey(source, model, bucketStart));
2907
2910
  if (currentProjectKey && projectState && projectTouchedBuckets) {
2908
2911
  const projectBucket = getProjectBucket(
@@ -4422,6 +4425,16 @@ const PRIORITY_SUBSET_FIELDS = [
4422
4425
  ["priority_reasoning_output_tokens", "reasoning_output_tokens"],
4423
4426
  ];
4424
4427
 
4428
+ const ULTRAFAST_SUBSET_FIELDS = [
4429
+ ["ultrafast_input_tokens", "input_tokens"],
4430
+ ["ultrafast_cached_input_tokens", "cached_input_tokens"],
4431
+ ["ultrafast_cache_creation_input_tokens", "cache_creation_input_tokens"],
4432
+ ["ultrafast_output_tokens", "output_tokens"],
4433
+ ["ultrafast_reasoning_output_tokens", "reasoning_output_tokens"],
4434
+ ];
4435
+
4436
+ const SPEED_TIER_SUBSET_FIELDS = [...PRIORITY_SUBSET_FIELDS, ...ULTRAFAST_SUBSET_FIELDS];
4437
+
4425
4438
  // The whole delta belongs to a priority request, so every base column it
4426
4439
  // carries is priority usage.
4427
4440
  function addPriorityUsage(target, delta) {
@@ -4431,9 +4444,16 @@ function addPriorityUsage(target, delta) {
4431
4444
  }
4432
4445
  }
4433
4446
 
4447
+ function addUltrafastUsage(target, delta) {
4448
+ for (const [field, base] of ULTRAFAST_SUBSET_FIELDS) {
4449
+ const value = Number(delta?.[base] || 0);
4450
+ if (value > 0) target[field] = (Number(target[field]) || 0) + value;
4451
+ }
4452
+ }
4453
+
4434
4454
  function prioritySubsetOf(totals) {
4435
4455
  const subset = {};
4436
- for (const [field] of PRIORITY_SUBSET_FIELDS) {
4456
+ for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
4437
4457
  const value = Number(totals?.[field]) || 0;
4438
4458
  if (value > 0) subset[field] = value;
4439
4459
  }
@@ -4441,7 +4461,7 @@ function prioritySubsetOf(totals) {
4441
4461
  }
4442
4462
 
4443
4463
  function addTotals(target, delta) {
4444
- for (const [field] of PRIORITY_SUBSET_FIELDS) {
4464
+ for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
4445
4465
  const value = Number(delta?.[field]) || 0;
4446
4466
  if (value > 0) target[field] = (Number(target[field]) || 0) + value;
4447
4467
  }
@@ -4511,7 +4531,11 @@ function totalsKey(totals) {
4511
4531
  // Appended only when a priority subset exists, so buckets that never see the
4512
4532
  // tier keep their previously persisted queuedKey and are not re-enqueued.
4513
4533
  const priority = PRIORITY_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
4514
- return priority.some((value) => value > 0) ? `${base}|p:${priority.join(",")}` : base;
4534
+ const ultrafast = ULTRAFAST_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
4535
+ let key = base;
4536
+ if (priority.some((value) => value > 0)) key += `|p:${priority.join(",")}`;
4537
+ if (ultrafast.some((value) => value > 0)) key += `|u:${ultrafast.join(",")}`;
4538
+ return key;
4515
4539
  }
4516
4540
 
4517
4541
  function toUtcHalfHourStart(ts) {
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Optional Sentry error reporting for the Node CLI / EmbeddedServer runtime.
5
+ *
6
+ * Privacy contract (keep in sync with docs/PRIVACY.md and dashboard/src/lib/sentry.js):
7
+ * - Off unless TOKENTRACKER_SENTRY_DSN (or SENTRY_DSN) is set.
8
+ * - Honors the same telemetry opt-out as the daily heartbeat
9
+ * (TOKENTRACKER_NO_TELEMETRY / DO_NOT_TRACK / config.json `"telemetry": false`).
10
+ * - Fail-closed in the node test runner.
11
+ * - Never intended to carry prompts, code, or provider secrets; beforeSend
12
+ * scrubs common secret patterns from event payloads.
13
+ */
14
+
15
+ const path = require("node:path");
16
+ const pkg = require("../../package.json");
17
+ const { readJson } = require("./fs");
18
+ const { isTelemetryDisabled } = require("./telemetry");
19
+
20
+ const SECRET_PATTERN =
21
+ /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|cookie|secret|password|dsn)[=:\s]+["']?[^\s"'\\]{8,}/gi;
22
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
23
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}\.[A-Za-z0-9._-]{10,}\b/g;
24
+
25
+ let initialized = false;
26
+
27
+ function resolveDsn(env = process.env) {
28
+ const raw = env?.TOKENTRACKER_SENTRY_DSN || env?.SENTRY_DSN || "";
29
+ return typeof raw === "string" ? raw.trim() : "";
30
+ }
31
+
32
+ function resolveEnvironment(env = process.env) {
33
+ const raw = typeof env?.TOKENTRACKER_SENTRY_ENVIRONMENT === "string"
34
+ ? env.TOKENTRACKER_SENTRY_ENVIRONMENT.trim()
35
+ : "";
36
+ if (raw) return raw;
37
+ if (env?.NODE_ENV === "production") return "production";
38
+ return "development";
39
+ }
40
+
41
+ function resolveShell(env = process.env) {
42
+ const raw = typeof env?.TOKENTRACKER_APP_SHELL === "string"
43
+ ? env.TOKENTRACKER_APP_SHELL.trim().toLowerCase()
44
+ : "";
45
+ if (raw === "macos" || raw === "windows" || raw === "linux" || raw === "cli") return raw;
46
+ return "cli";
47
+ }
48
+
49
+ function scrubString(value) {
50
+ if (typeof value !== "string" || !value) return value;
51
+ return value
52
+ .replace(BEARER_PATTERN, "Bearer [Filtered]")
53
+ .replace(JWT_PATTERN, "[Filtered]")
54
+ .replace(SECRET_PATTERN, "[Filtered]");
55
+ }
56
+
57
+ function scrubValue(value, depth = 0) {
58
+ if (depth > 6 || value == null) return value;
59
+ if (typeof value === "string") return scrubString(value);
60
+ if (Array.isArray(value)) return value.map((item) => scrubValue(item, depth + 1));
61
+ if (typeof value === "object") {
62
+ const out = {};
63
+ for (const [key, nested] of Object.entries(value)) {
64
+ const lower = key.toLowerCase();
65
+ if (
66
+ lower.includes("authorization") ||
67
+ lower.includes("cookie") ||
68
+ lower.includes("password") ||
69
+ lower.includes("secret") ||
70
+ lower.includes("token") ||
71
+ lower.includes("api_key") ||
72
+ lower.includes("apikey")
73
+ ) {
74
+ out[key] = "[Filtered]";
75
+ } else {
76
+ out[key] = scrubValue(nested, depth + 1);
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function truncateMaybe(value, max = 240) {
85
+ if (typeof value !== "string") return value;
86
+ if (value.length <= max) return value;
87
+ return `${value.slice(0, max)}…[truncated]`;
88
+ }
89
+
90
+ function scrubEvent(event) {
91
+ if (!event || typeof event !== "object") return event;
92
+ const next = { ...event };
93
+ if (next.message) next.message = truncateMaybe(scrubString(next.message));
94
+ if (next.extra) next.extra = scrubValue(next.extra);
95
+ if (next.contexts) next.contexts = scrubValue(next.contexts);
96
+ if (next.request) next.request = scrubValue(next.request);
97
+ // Console breadcrumbs can carry parser / JSON.parse excerpts of local logs
98
+ // (prompt text). Drop them entirely; keep only non-console crumbs and scrub.
99
+ if (Array.isArray(next.breadcrumbs)) {
100
+ next.breadcrumbs = next.breadcrumbs
101
+ .filter((crumb) => crumb && crumb.category !== "console")
102
+ .map((crumb) => scrubValue(crumb));
103
+ }
104
+ if (next.exception?.values) {
105
+ next.exception = {
106
+ ...next.exception,
107
+ values: next.exception.values.map((item) => ({
108
+ ...item,
109
+ value: truncateMaybe(scrubString(item?.value)),
110
+ })),
111
+ };
112
+ }
113
+ // Never attach IP / user PII by default.
114
+ if (next.user) {
115
+ next.user = { id: typeof next.user.id === "string" ? next.user.id : undefined };
116
+ }
117
+ return next;
118
+ }
119
+
120
+ /**
121
+ * Initialise Sentry once for this process. Never throws.
122
+ * @returns {Promise<{ enabled: boolean, reason?: string }>}
123
+ */
124
+ async function initSentry({
125
+ env = process.env,
126
+ trackerDir,
127
+ version = pkg.version,
128
+ } = {}) {
129
+ try {
130
+ if (initialized) return { enabled: true, reason: "already-initialized" };
131
+ if (env?.NODE_TEST_CONTEXT !== undefined) {
132
+ return { enabled: false, reason: "test-env" };
133
+ }
134
+
135
+ const dsn = resolveDsn(env);
136
+ if (!dsn) return { enabled: false, reason: "no-dsn" };
137
+
138
+ let config = {};
139
+ if (trackerDir) {
140
+ config = (await readJson(path.join(trackerDir, "config.json"))) || {};
141
+ }
142
+ if (isTelemetryDisabled({ env, config })) {
143
+ return { enabled: false, reason: "opt-out" };
144
+ }
145
+
146
+ // Honor the same outbound proxy / fail-closed policy as the rest of the CLI.
147
+ // Sentry's Node transport uses node:https (not undici), so pass proxy explicitly.
148
+ const {
149
+ readPersistedProxyConfig,
150
+ resolveEffectiveProxySource,
151
+ } = require("./proxy-env");
152
+ const proxyConfig = readPersistedProxyConfig();
153
+ const proxyResolved = resolveEffectiveProxySource({ env, proxyConfig });
154
+ if (proxyResolved?.source === "blocked") {
155
+ return { enabled: false, reason: "proxy-blocked" };
156
+ }
157
+
158
+ // Lazy require keeps unit tests that never call init from paying the
159
+ // module graph cost at import time of unrelated modules.
160
+ const Sentry = require("@sentry/node");
161
+ Sentry.init({
162
+ dsn,
163
+ environment: resolveEnvironment(env),
164
+ release: `tokentracker@${String(version || "").trim() || "0.0.0"}`,
165
+ sendDefaultPii: false,
166
+ tracesSampleRate: 0,
167
+ // Errors only: drop process-session + console; replace Http so request
168
+ // sessions stay off (Http defaults sessions:true and bypasses beforeSend).
169
+ integrations(defaults) {
170
+ const filtered = defaults.filter((integration) => {
171
+ const name = integration?.name || "";
172
+ return name !== "ProcessSession" && name !== "Console" && name !== "Http";
173
+ });
174
+ filtered.push(Sentry.httpIntegration({ sessions: false }));
175
+ return filtered;
176
+ },
177
+ transportOptions: proxyResolved?.proxyUrl
178
+ ? { proxy: proxyResolved.proxyUrl }
179
+ : undefined,
180
+ beforeSend(event) {
181
+ return scrubEvent(event);
182
+ },
183
+ initialScope: {
184
+ tags: {
185
+ shell: resolveShell(env),
186
+ runtime: "node",
187
+ },
188
+ },
189
+ });
190
+ initialized = true;
191
+ return { enabled: true };
192
+ } catch {
193
+ return { enabled: false, reason: "init-failed" };
194
+ }
195
+ }
196
+
197
+ function captureException(error, context) {
198
+ if (!initialized) return;
199
+ try {
200
+ const Sentry = require("@sentry/node");
201
+ if (context) {
202
+ Sentry.withScope((scope) => {
203
+ if (context.tags) {
204
+ for (const [key, value] of Object.entries(context.tags)) {
205
+ scope.setTag(key, String(value));
206
+ }
207
+ }
208
+ if (context.extra) scope.setExtras(scrubValue(context.extra));
209
+ Sentry.captureException(error);
210
+ });
211
+ return;
212
+ }
213
+ Sentry.captureException(error);
214
+ } catch {
215
+ /* never break the CLI */
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ initSentry,
221
+ captureException,
222
+ resolveDsn,
223
+ resolveEnvironment,
224
+ resolveShell,
225
+ scrubEvent,
226
+ scrubString,
227
+ // test helpers
228
+ _resetForTests() {
229
+ initialized = false;
230
+ },
231
+ };
@@ -87,7 +87,9 @@ const {
87
87
  // v23 excludes database updates and user records from request durations.
88
88
  // v24 keeps OpenCode usage dates at event time and fork history on its ancestor.
89
89
  // v25 adds Kimi wire sessions, including official/legacy migration dedup.
90
- const SIDECAR_VERSION = 25;
90
+ // v26 retains Codex Ultrafast (service_tier ultrafast) subset columns so
91
+ // date-filtered session views and cached sidecars keep the 6x premium.
92
+ const SIDECAR_VERSION = 26;
91
93
  const EDIT_TOOLS = new Set([
92
94
  "apply_patch",
93
95
  "edit",
@@ -141,7 +143,7 @@ function addTotals(target, delta) {
141
143
  target[key] = finite(target[key]) + finite(delta?.[key]);
142
144
  }
143
145
  for (const key of MODEL_USAGE_SUM_FIELDS) {
144
- if (!/^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key)) continue;
146
+ if (!/^(?:long_context_|priority_|ultrafast_)|^(?:usage_events|rerouted_usage_events|long_context_usage_events|priority_usage_events|ultrafast_usage_events)$/.test(key)) continue;
145
147
  if (delta?.[key]) target[key] = finite(target[key]) + finite(delta[key]);
146
148
  }
147
149
  }
@@ -179,10 +181,21 @@ const MODEL_USAGE_SUM_FIELDS = [
179
181
  "priority_long_context_cache_creation_input_tokens",
180
182
  "priority_long_context_output_tokens",
181
183
  "priority_long_context_reasoning_output_tokens",
184
+ "ultrafast_input_tokens",
185
+ "ultrafast_cached_input_tokens",
186
+ "ultrafast_cache_creation_input_tokens",
187
+ "ultrafast_output_tokens",
188
+ "ultrafast_reasoning_output_tokens",
189
+ "ultrafast_long_context_input_tokens",
190
+ "ultrafast_long_context_cached_input_tokens",
191
+ "ultrafast_long_context_cache_creation_input_tokens",
192
+ "ultrafast_long_context_output_tokens",
193
+ "ultrafast_long_context_reasoning_output_tokens",
182
194
  "usage_events",
183
195
  "rerouted_usage_events",
184
196
  "long_context_usage_events",
185
197
  "priority_usage_events",
198
+ "ultrafast_usage_events",
186
199
  // Not a token counter: an edit turn is a turn, and turn_context both marks
187
200
  // where a turn begins and names its model, so an edit turn has exactly one
188
201
  // owner. Splitting it here keeps by_model.tokens_per_edit a ratio over one
@@ -33,11 +33,22 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
33
33
  // prevent directory traversal
34
34
  if (!filePath.startsWith(baseDir)) return false;
35
35
 
36
+ // Watch for the client going away from the start: if the browser cancels the
37
+ // request during the awaits below, "close" fires before the stream exists and
38
+ // a listener added later never runs, leaking the FileHandle (fatal on Node 25+).
39
+ let clientGone = res.destroyed;
40
+ let stream = null;
41
+ res.once("close", () => {
42
+ clientGone = true;
43
+ if (stream) stream.destroy();
44
+ });
45
+
36
46
  let handle;
37
47
  try {
38
48
  handle = await fsPromises.open(filePath, "r");
39
49
  const stat = await handle.stat();
40
50
  if (!stat.isFile()) return false;
51
+ if (clientGone) return true; // finally closes the handle
41
52
 
42
53
  const ext = path.extname(filePath).toLowerCase();
43
54
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -47,21 +58,23 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
47
58
  const source = await handle.readFile("utf8");
48
59
  const content = source.replace(/<head(?:\s[^>]*)?>/i,
49
60
  "$&\n<script>window.__TOKENTRACKER_RUNTIME_CONFIG__={configurationError:\"runtime_config_unavailable\"};</script>\n<script src=\"/api/runtime-config.js\"></script>");
61
+ if (clientGone) return true;
50
62
  res.writeHead(200, { "Content-Type": contentType, "Content-Length": Buffer.byteLength(content), "Cache-Control": "no-store" });
51
63
  res.end(content);
52
64
  return true;
53
65
  }
54
66
 
67
+ if (clientGone) return true;
68
+
55
69
  res.writeHead(200, {
56
70
  "Content-Type": contentType,
57
71
  "Content-Length": stat.size,
58
72
  "Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
59
73
  });
60
74
 
61
- const stream = handle.createReadStream();
75
+ stream = handle.createReadStream();
62
76
  handle = null; // The stream owns and closes this descriptor.
63
77
  stream.on("error", (error) => res.destroy(error));
64
- res.on("close", () => stream.destroy());
65
78
  stream.pipe(res);
66
79
  return true;
67
80
  } catch (_e) {