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.
- package/README.de.md +2 -2
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +11 -3
- package/README.zh-CN.md +3 -3
- package/bin/tracker.js +6 -0
- package/dashboard/dist/assets/{AchievementBadge-DF8q1x6R.js → AchievementBadge-BTqlXUP1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-B8Dg5myo.js → AchievementsPage-DbKUhaY-.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-oPPiyP3y.js → AchievementsSection-El1F-OC7.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-PneMW4B4.js → BadgeDetailModal--8PySLzP.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-GOlSvEiz.js → BotAnimated-DBcOS4vh.js} +1 -1
- package/dashboard/dist/assets/{Card-BT5G0GE0.js → Card-CeGs8TaY.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BNeL5Muo.js → CloudCheckoutPage-DCg3VVAi.js} +1 -1
- package/dashboard/dist/assets/CloudPage-CmeFDrmk.js +2 -0
- package/dashboard/dist/assets/{CommandPalette-CiMSYHNK.js → CommandPalette-CrSNoZW9.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-fK3BE9CW.js → CommunityStatsModal-ynB4j0Qb.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CrUlEHyZ.js → ConfirmModal-DDTUf-My.js} +1 -1
- package/dashboard/dist/assets/{Controls-CbjSwq6k.js → Controls-DXsJjEU1.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-fr4sgbLB.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-E-2Z6imh.js → DateRangePopover-gTiPISL_.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DvHVjCCH.js +1 -0
- package/dashboard/dist/assets/{DialogClose-xJKE22Y6.js → DialogClose-CXwouQe4.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BzbzETUC.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-f8vnnH7z.js → DialogTitle-Cd_k-Nyw.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Ci9WZfHK.js → ElectricLogo-C4vUNTCI.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BXqtyAmn.js → FadeIn-CGM5RBFy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BV5SYJMi.js → HeaderGithubStar-el5-DhVb.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D3gpW3Jh.js → HoverTooltip-C7YaWwJx.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-G7fIWXpL.js → IpCheckPage-EKhbH9hJ.js} +1 -1
- package/dashboard/dist/assets/LandingPage-DJYOv1AM.js +2 -0
- package/dashboard/dist/assets/{LeaderboardPage-D2_wXazL.js → LeaderboardPage-CwzJxA8p.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-3xJ-mH2v.js → LeaderboardProBadge-DYLp9pJY.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-23pH6Ud7.js → LeaderboardProfileModal-DqD2h9dD.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DSezSR58.js → LeaderboardProfilePage-CarYvpYe.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-4T0BpfYS.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Dsk-d0Hr.js → LocalOnlyNotice-CAR-mqy0.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-C7dugIIn.js → LoginCard-Czq-UgAU.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B7RuLsTF.js → LoginModal-D4uxfzZb.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-u32vJQN9.js → LoginPage-Bg3BG0Wv.js} +1 -1
- package/dashboard/dist/assets/{PetPage-Dbc975oU.js → PetPage-BgwBxRyx.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-D-yECvH9.js → PopoverPopup-k3o_Jp2b.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CHtZiJUN.js → ProviderIcon-Bbw6MUSf.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-ctuIsNk0.js → ResetPasswordPage-DIKPVu5K.js} +1 -1
- package/dashboard/dist/assets/{Select-DN4Kq_R1.js → Select-kA0dV5Qw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C-HmeD8L.js → SelectItemText-D4YCIn1x.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-BZOL3JMt.js → SelfHostPage-C3Y9qF3P.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C0sDYa8c.js → ServiceStatusPage-o1G3SJhV.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-l0-FkcJB.js → SessionsPage-CA-efxhU.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-Mr84vxQL.js +5 -0
- package/dashboard/dist/assets/{SkillsPage-Cz6G9d3o.js → SkillsPage-BtRhLSpz.js} +1 -1
- package/dashboard/dist/assets/TokenGalaxy-CcFjCCuC.js +4426 -0
- package/dashboard/dist/assets/TooltipPopup-6aTx3jDf.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-DoywXr8h.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-BKo3DBxT.js → WidgetsPage-BuGSzvR0.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Blm3aPO5.js → WrappedPage-Bm6X-SUZ.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-ClVJNQu3.js +45 -0
- package/dashboard/dist/assets/{agent-logos-DMtDEx7_.js → agent-logos-Sc8Sejgx.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bo9y7VzF.js → arrow-left-Cm1LG2i_.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BLMU8Y5U.js → arrow-right-WP3kIwiy.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B4hXO4sc.js → arrow-up-right-CjyprVMj.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-D9HaEtgo.js → calendar-days-Bm88-Gcb.js} +1 -1
- package/dashboard/dist/assets/{check-BJNGKpX8.js → check-CzIxFg0I.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CoGC_RLK.js → chevron-down-bfGK8629.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-Du5FoHqV.js → cloud-checkout-C9DFQ7yM.js} +1 -1
- package/dashboard/dist/assets/{composite-BfIUPpl8.js → composite-CdExmgTx.js} +1 -1
- package/dashboard/dist/assets/copy-CPPdujn2.js +1 -0
- package/dashboard/dist/assets/{download-C1K_v0C0.js → download-Cl6_dKeK.js} +1 -1
- package/dashboard/dist/assets/{external-link-D-xHhfj5.js → external-link-B0r6Wo4e.js} +1 -1
- package/dashboard/dist/assets/{flame-DXD6uvhM.js → flame-wEezvzXx.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-B9riGvNj.js → format-tokens-vWpBpKpM.js} +1 -1
- package/dashboard/dist/assets/{gift-LPR-nD0J.js → gift-BFjblRqU.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-DLdtLqEt.js → icons-DX3dCNJP.js} +1 -1
- package/dashboard/dist/assets/{index-CXdJ7zMN.js → index-D-TnqdBv.js} +1 -1
- package/dashboard/dist/assets/{index-CC3cXeBa.js → index-D1Zdrpom.js} +1 -1
- package/dashboard/dist/assets/index-DBF1bJlm.js +502 -0
- package/dashboard/dist/assets/{index-BL11MboX.js → index-p8Gt3NFg.js} +1 -1
- package/dashboard/dist/assets/{info-tnXKCyuK.js → info-Cqi971MA.js} +1 -1
- package/dashboard/dist/assets/{link-2-DOk9Tcg1.js → link-2-79sm__cT.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DIl96Mw-.js → loader-circle-CDoVfOUb.js} +1 -1
- package/dashboard/dist/assets/local-usage-transfer-EcXnuwCj.js +1 -0
- package/dashboard/dist/assets/{modifiers.esm-2H_1taJ6.js → modifiers.esm-v5_BKSmY.js} +1 -1
- package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-tnp-03hs.js → monitor-smartphone-CO67DffO.js} +1 -1
- package/dashboard/dist/assets/{play-BGMuGTdN.js → play-CbvuGtc3.js} +1 -1
- package/dashboard/dist/assets/{plus-XETAaErq.js → plus-jc2gXlRM.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BYNT4zPc.js → provider-display-BK3EbRLM.js} +1 -1
- package/dashboard/dist/assets/{react-OW7HALVO.js → react-DMs4C4qS.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-CAfS8-ct.js → safePolygon-cY9H0GBN.js} +1 -1
- package/dashboard/dist/assets/{search-B7ZJhoau.js → search-fqTJoYYA.js} +1 -1
- package/dashboard/dist/assets/{server-CWrP7mtL.js → server-jpBtvTvr.js} +1 -1
- package/dashboard/dist/assets/{shield-check-ip5IQEK3.js → shield-check-DQiBlJbN.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CpyoNTeB.js → skills-api-CN2r_rmt.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-YvzWzPDu.js → sortable.esm-DrS52yOK.js} +1 -1
- package/dashboard/dist/assets/{store-DRK6-JBg.js → store-K2Mk6aMn.js} +1 -1
- package/dashboard/dist/assets/{terminal-DQRoJSvh.js → terminal-Q4SruAlh.js} +1 -1
- package/dashboard/dist/assets/{trash-8h_26atF.js → trash-C1Ob-g8E.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DYlhIXsp.js → use-community-stats-C-Gxpp9D.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-AxJF9SEJ.js → use-latest-request-guard-DK-A4bBp.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-ZauQzxE_.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-BQgB6_PB.js → use-native-settings-BZLEIUK8.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-ogi7Okzf.js → use-ordered-list-De_Mcm_H.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DatTtp2N.js +1 -0
- package/dashboard/dist/assets/{use-transform-BQqoo1et.js → use-transform-XCDD5IrX.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-BncGDhoF.js +1 -0
- package/dashboard/dist/assets/{useCurrency-Bv9g8mpq.js → useCurrency-DXPclxBT.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-CQiZGknc.js → usePositioner-C-E1npCy.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BUVirY0k.js → useTokenFormat-B70xhy9u.js} +1 -1
- package/dashboard/dist/assets/{zap-DR3VD0yY.js → zap-D7gRgGCj.js} +1 -1
- package/dashboard/dist/index.html +5 -5
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/privacy.html +8 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +4 -3
- package/src/cli.js +10 -0
- package/src/commands/sync.js +18 -3
- package/src/lib/codex-rollout-parser.js +29 -2
- package/src/lib/codex-service-tier.js +13 -0
- package/src/lib/commandcode-limits.js +27 -11
- package/src/lib/grok-limits.js +1 -0
- package/src/lib/local-api.js +122 -11
- package/src/lib/local-usage-transfer.js +271 -0
- package/src/lib/opencode-go-limits.js +17 -6
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/index.js +50 -39
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +110 -0
- package/src/lib/rollout.js +27 -3
- package/src/lib/sentry.js +231 -0
- package/src/lib/session-analytics.js +15 -2
- package/src/lib/static-server.js +15 -2
- package/src/lib/usage-limits.js +74 -24
- package/src/lib/workbuddy-ipc.js +191 -0
- package/src/lib/workbuddy-limits.js +564 -0
- package/dashboard/dist/assets/CloudPage-26bhEYqI.js +0 -2
- package/dashboard/dist/assets/DashboardPage-CP_PMMga.js +0 -1
- package/dashboard/dist/assets/DevicePage-B0fLd80M.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +0 -1
- package/dashboard/dist/assets/LandingPage-BQNOdAkR.js +0 -2
- package/dashboard/dist/assets/LimitsPage-BLJsmVVy.js +0 -9
- package/dashboard/dist/assets/SettingsPage-CrczfZiZ.js +0 -5
- package/dashboard/dist/assets/TokenGalaxy-B5IGaLQK.js +0 -4426
- package/dashboard/dist/assets/TooltipPopup-B6FqLWO7.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-B4kJUwmF.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external--HDUIUZr.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
- package/dashboard/dist/assets/host-mode-C2K0MMOs.js +0 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
- package/dashboard/dist/assets/use-limits-display-prefs-DOZzuTwo.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +0 -1
- package/dashboard/dist/assets/use-session-efficiency-pref-DP3hu3Rp.js +0 -1
- 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
|
+
};
|
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/lib/static-server.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|