tokentracker-cli 1.5.6 → 1.5.8
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.md +2 -0
- package/dashboard/dist/assets/{AchievementBadge-NKpY___e.js → AchievementBadge-Ck1DmXtu.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-9nXmSX72.js → AchievementsPage-DjvMoc2i.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DttQciSY.js → AchievementsSection-DvTHE8Vd.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-bvWEdJbe.js → BadgeDetailModal-COa2Rd6M.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-ChuUfph8.js → BotAnimated-CeR3jSce.js} +1 -1
- package/dashboard/dist/assets/{Card-SWtLDXt1.js → Card-bQCEOe_K.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-c2fNl1Rl.js → CloudCheckoutPage-DDH-3QOd.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-CfwDjZMS.js → CloudPage-RygH3R8b.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-tWgPa295.js → CommandPalette-E7YqsdAH.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-D7UaaUdM.js → CommunityStatsModal-B5heTDUE.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BrBcKhfW.js → ConfirmModal-D3vd882N.js} +1 -1
- package/dashboard/dist/assets/{Controls-CcvBkk7W.js → Controls-CK74JkE8.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DwTrlQZQ.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-BkN_pird.js → DateRangePopover-BxVCPuEX.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BbUL7WwT.js → DevicePage-B6x5fBdU.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BXO8SBFr.js → DialogClose-teXmwU1d.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BSCcGkVT.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Du332auj.js → DialogTitle-B3m-31sy.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-CXLYfOt7.js → ElectricLogo-0wyvks4K.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-HSQBOc8B.js → FadeIn-C60pLKXu.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-D5tyQDGE.js → HeaderGithubStar-CCEUzMqo.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BhidhbnD.js → HoverTooltip-D9RtbhSZ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-WMEF1oS3.js → IpCheckPage-BCyW0SWY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BmO-59ZP.js → LandingPage-DCEofDST.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-C2WyniEt.js → LeaderboardPage-CsTbalIm.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-DAZnMRNn.js → LeaderboardProBadge-Crun5RUJ.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-D1BxcloV.js → LeaderboardProfileModal-Bim9cms6.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-RPJDTwpG.js → LeaderboardProfilePage-D9pBE7f4.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-9ZOhKvn5.js → LimitsPage-CFhzwKzB.js} +6 -6
- package/dashboard/dist/assets/{LocalOnlyNotice-CH6w4hRw.js → LocalOnlyNotice-F_qczacX.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-D24HsRjs.js → LoginCard-CQm_T5K2.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-sLvHyi9U.js → LoginModal-BUf4mPCT.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-PIvLEw1Y.js → LoginPage-njdEUQEX.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DYcw_4P1.js → PetPage-COczIU1k.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-D7ZCWq5f.js → PopoverPopup-DxB1k4g7.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-B0kB52K-.js → ProviderIcon-BQfkoaQE.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B_NT4Xfl.js → ResetPasswordPage-PGRVaPTN.js} +1 -1
- package/dashboard/dist/assets/{Select-BtxjjP2Q.js → Select-CQVnpoBO.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Co1M6XRC.js → SelectItemText-DmAn7osh.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-XadfhFnd.js → SelfHostPage-BUURA-Vt.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ZEJyAxsQ.js → ServiceStatusPage-DuzSen0Q.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-DndGq61W.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-D4qHgIxn.js → SettingsPage-CHh0yqsZ.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CkQCk2uF.js → SkillsPage-CCaYXQWN.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Bx1vr4b0.js → TokenGalaxy-CGxUtq9x.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-DQW8FWJO.js → TooltipPopup-B_B4qQD-.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BMNcY3N8.js → TrendMonitor-CdRQTLZL.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-luRTge7O.js → WidgetsPage-DP_w32wh.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B1j3WgXx.js → WrappedPage-C_DCqHUH.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CvvpsW-U.js → __vite-browser-external-Lw3ipvYF.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-DL4fdks6.js → agent-logos-DlnSKc2A.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DiOsjgdu.js → arrow-left-CjLKt_5C.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DBFXV7GU.js → arrow-right-Bh-Ezs8y.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BIvpDQHo.js → arrow-up-right-C87CvEJ5.js} +1 -1
- package/dashboard/dist/assets/{calendar-days--psnXUPW.js → calendar-days-B5VHwV9j.js} +1 -1
- package/dashboard/dist/assets/{check-DLqbsN00.js → check-D2uclldH.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C0P0oLzP.js → chevron-down-Beacyoub.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-NSmb7QNy.js → cloud-checkout-3Y7NDSG1.js} +1 -1
- package/dashboard/dist/assets/{composite-D7ahPHYQ.js → composite-B28yORTL.js} +1 -1
- package/dashboard/dist/assets/{copy-COD7vwd6.js → copy-C8fZJwms.js} +1 -1
- package/dashboard/dist/assets/{download-CpSUX4U-.js → download-IIXYYmyo.js} +1 -1
- package/dashboard/dist/assets/{external-link-DXUjrooT.js → external-link-BEb0dms0.js} +1 -1
- package/dashboard/dist/assets/{flame-Dl3ZT9oo.js → flame-gmfpE-PO.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Ci49THbK.js → format-tokens-CKwy7NXi.js} +1 -1
- package/dashboard/dist/assets/{gift-DTHCN1Of.js → gift-twpInP_O.js} +1 -1
- package/dashboard/dist/assets/{icons-CB9s1e1k.js → icons-DQfkB5-5.js} +1 -1
- package/dashboard/dist/assets/{index-DFdfNKJM.js → index-ChVgw-dM.js} +1 -1
- package/dashboard/dist/assets/{index-Dozwi-Se.js → index-ClwErGKW.js} +1 -1
- package/dashboard/dist/assets/{index-BGT8ZQyn.js → index-D2-6Msd9.js} +1 -1
- package/dashboard/dist/assets/{index-DeX4t_5p.js → index-DzUxfO5e.js} +1 -1
- package/dashboard/dist/assets/{info-CqM2wmSI.js → info-BvndJDmD.js} +1 -1
- package/dashboard/dist/assets/{link-2-Byo7Rj3t.js → link-2-BhU_5JKG.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CMxd78Na.js → loader-circle-BR_62BDE.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CBjnoeck.js → local-usage-transfer-CfoyR0hM.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Oowlk_FC.js → modifiers.esm-CnttlnRD.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-B2EgB_9q.js → monitor-smartphone-D4vzGxrq.js} +1 -1
- package/dashboard/dist/assets/{play-BSuP-UBL.js → play-BVWEKYTF.js} +1 -1
- package/dashboard/dist/assets/{plus-BWemSIuC.js → plus-B9GQ6VvV.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BpzWuFDH.js → provider-display-DCFgR7FO.js} +1 -1
- package/dashboard/dist/assets/{react-cIJ4zx62.js → react-BKUD3UPr.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C_ZMtbgL.js → safePolygon-DPIi3FGf.js} +1 -1
- package/dashboard/dist/assets/{search-BK7eyNds.js → search-BBRJvYNz.js} +1 -1
- package/dashboard/dist/assets/{server-BqQHX-rd.js → server-Cg3zBK6a.js} +1 -1
- package/dashboard/dist/assets/{shield-check-CO5U6gRw.js → shield-check-BBzYplRb.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CMye76ag.js → skills-api-CcAQ6fJX.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-Bvb61Lvj.js → sortable.esm-kNieGfR9.js} +1 -1
- package/dashboard/dist/assets/{store-CICV1KXb.js → store-DQpd_ZKv.js} +1 -1
- package/dashboard/dist/assets/{terminal-BKMWT1Ux.js → terminal-zj_c8_SG.js} +1 -1
- package/dashboard/dist/assets/{trash-D2Oqim4r.js → trash-Sl7eOlF7.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CEqwmjct.js → use-community-stats-Bg-4yvQQ.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-CTuzzu3F.js → use-latest-request-guard-COF23Bfs.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CA2BLWkY.js → use-limits-display-prefs-QOlhCOgQ.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-B54wHSj9.js → use-native-settings-CKDqyirL.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list--PU9ekxY.js → use-ordered-list-mFHPMcF_.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-B4rpC-Zt.js +1 -0
- package/dashboard/dist/assets/{use-transform-BRyR8szg.js → use-transform-CO5wZ0ML.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-sufKdfDY.js → use-usage-limits-CeKjW8jG.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-6oy7kSX8.js → useCurrency-WruYNKxG.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-ZtDdVdJg.js → usePositioner-C3NGxtZa.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-tiOJ-PeE.js → useTokenFormat-ObRZ_T9W.js} +1 -1
- package/dashboard/dist/assets/{zap-Dr1dXUjN.js → zap-CyZ3Tf1D.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +9 -0
- package/src/commands/status.js +12 -6
- package/src/commands/sync.js +22 -4
- package/src/lib/claude-backend.js +291 -0
- package/src/lib/claude-config.js +1 -1
- package/src/lib/cursor-config.js +3 -1
- package/src/lib/devin-limits.js +65 -25
- package/src/lib/diagnostics.js +3 -0
- package/src/lib/doctor.js +13 -0
- package/src/lib/local-api.js +2 -0
- package/src/lib/local-usage-transfer.js +22 -1
- package/src/lib/pricing/index.js +12 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +70 -15
- package/src/lib/session-analytics.js +20 -8
- package/src/lib/trae-cn-config.js +32 -2
- package/src/lib/usage-limits.js +601 -35
- package/dashboard/dist/assets/DashboardPage-DJSgRHGq.js +0 -1
- package/dashboard/dist/assets/DialogDescription-B1HyKP0x.js +0 -1
- package/dashboard/dist/assets/SessionsPage-Cte1taUu.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-CZ8_YWc2.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { preserveLegacyClaudeGroups, preserveLegacyClaudePrefix, createBackendResolver, mergeBackendUsage, claudeLines } = require("./claude-backend");
|
|
1
2
|
const { claudeUserIdentity, collectClaudeForkAliases } = require("./claude-user-identity");
|
|
2
3
|
const fs = require("node:fs/promises");
|
|
3
4
|
const fssync = require("node:fs");
|
|
@@ -352,6 +353,7 @@ async function parseRolloutIncremental({
|
|
|
352
353
|
const projectFreshnessNowMs = Date.now();
|
|
353
354
|
const touchedBuckets = new Set();
|
|
354
355
|
const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
356
|
+
preserveCodexModelBuckets(hourlyState, touchedBuckets);
|
|
355
357
|
const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
|
|
356
358
|
|
|
357
359
|
if (!cursors.files || typeof cursors.files !== "object") {
|
|
@@ -1084,6 +1086,9 @@ async function parseClaudeIncremental({
|
|
|
1084
1086
|
const prevHashes = Array.isArray(cursors.claudeHashes) ? cursors.claudeHashes : [];
|
|
1085
1087
|
const seenMessageHashes = new Set(prevHashes);
|
|
1086
1088
|
const defaultSource = normalizeSourceInput(source) || "claude";
|
|
1089
|
+
preserveLegacyClaudeGroups(files.filter((entry) =>
|
|
1090
|
+
(typeof entry === "string" ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource) === "claude"
|
|
1091
|
+
).map((entry) => typeof entry === "string" ? entry : entry?.path), path.dirname(queuePath));
|
|
1087
1092
|
// Resolve all changed-file lineage before counting: fork files may sort first.
|
|
1088
1093
|
const forkTimes = new Map();
|
|
1089
1094
|
const forkAliases = await collectClaudeForkAliases(files, cursors,
|
|
@@ -1153,6 +1158,7 @@ async function parseClaudeIncremental({
|
|
|
1153
1158
|
const truncated = sameInode && prevOffset > st.size;
|
|
1154
1159
|
const startOffset = sameInode && !truncated ? prevOffset : 0;
|
|
1155
1160
|
|
|
1161
|
+
if (fileSource === "claude") preserveLegacyClaudePrefix(filePath, path.dirname(queuePath), startOffset);
|
|
1156
1162
|
let fileId = null;
|
|
1157
1163
|
if (seenFileIds) {
|
|
1158
1164
|
const cachedFileId =
|
|
@@ -1287,6 +1293,7 @@ async function parseClaudeIncremental({
|
|
|
1287
1293
|
projectKey,
|
|
1288
1294
|
seenMessageHashes,
|
|
1289
1295
|
forkAliases,
|
|
1296
|
+
backendResolver: fileSource === "claude" ? createBackendResolver(filePath, path.dirname(queuePath)) : null,
|
|
1290
1297
|
});
|
|
1291
1298
|
|
|
1292
1299
|
cursors.files[key] = {
|
|
@@ -3134,6 +3141,7 @@ async function parseClaudeFile({
|
|
|
3134
3141
|
projectKey,
|
|
3135
3142
|
seenMessageHashes,
|
|
3136
3143
|
forkAliases,
|
|
3144
|
+
backendResolver,
|
|
3137
3145
|
}) {
|
|
3138
3146
|
const st = fileStat || (await fs.stat(filePath).catch(() => null));
|
|
3139
3147
|
if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
|
|
@@ -3141,15 +3149,14 @@ async function parseClaudeFile({
|
|
|
3141
3149
|
const endOffset = st.size;
|
|
3142
3150
|
if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
|
|
3143
3151
|
|
|
3144
|
-
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
|
|
3145
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
3146
|
-
|
|
3147
3152
|
let eventsAggregated = 0;
|
|
3148
3153
|
// Separator-agnostic: WSL installs are scanned over \\wsl$ UNC paths where
|
|
3149
3154
|
// path.join produces backslashes, and a forward-slash-only check would
|
|
3150
3155
|
// misclassify subagent transcripts as main sessions (#307).
|
|
3151
3156
|
const isMainSession = !/[\\/]subagents[\\/]/.test(filePath);
|
|
3152
|
-
|
|
3157
|
+
let byteOffset = startOffset;
|
|
3158
|
+
for await (const { line, start: lineStart, end } of claudeLines(filePath, startOffset, endOffset)) {
|
|
3159
|
+
byteOffset = end;
|
|
3153
3160
|
if (!line) continue;
|
|
3154
3161
|
|
|
3155
3162
|
// Count user-typed messages as conversations (main sessions only).
|
|
@@ -3211,6 +3218,8 @@ async function parseClaudeFile({
|
|
|
3211
3218
|
|
|
3212
3219
|
if (dedupHash) seenMessageHashes.add(dedupHash);
|
|
3213
3220
|
delta.conversation_count = 0;
|
|
3221
|
+
const backend = backendResolver?.(obj, lineStart, byteOffset);
|
|
3222
|
+
if (backend) delta.backend_usage = [{ ...delta, ...backend }];
|
|
3214
3223
|
|
|
3215
3224
|
const bucketStart = toUtcHalfHourStart(tokenTimestamp);
|
|
3216
3225
|
if (!bucketStart) continue;
|
|
@@ -3232,9 +3241,7 @@ async function parseClaudeFile({
|
|
|
3232
3241
|
eventsAggregated += 1;
|
|
3233
3242
|
}
|
|
3234
3243
|
|
|
3235
|
-
|
|
3236
|
-
stream.close?.();
|
|
3237
|
-
return { endOffset, eventsAggregated };
|
|
3244
|
+
return { endOffset: byteOffset, eventsAggregated };
|
|
3238
3245
|
}
|
|
3239
3246
|
|
|
3240
3247
|
async function parseGeminiFile({
|
|
@@ -3514,6 +3521,40 @@ async function parseOpencodeMessageFile({
|
|
|
3514
3521
|
};
|
|
3515
3522
|
}
|
|
3516
3523
|
|
|
3524
|
+
// Codex models are observed per rollout/turn. A busier sibling agent in the
|
|
3525
|
+
// same half-hour is not evidence for the model of an unnamed request (#927).
|
|
3526
|
+
// Re-emit stored buckets once on upgrade, including idle historical groups:
|
|
3527
|
+
// their totals still retain the original attribution, so no log replay is
|
|
3528
|
+
// needed. Queue readers replace rows by (source, model, hour_start).
|
|
3529
|
+
function preserveCodexModelBuckets(hourlyState, touchedBuckets) {
|
|
3530
|
+
const upgrading = hourlyState.modelAttributionPolicy?.codex !== "observed";
|
|
3531
|
+
if (!upgrading && !Object.keys(hourlyState.groupQueued).some(
|
|
3532
|
+
key => key.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`),
|
|
3533
|
+
)) return;
|
|
3534
|
+
const legacyGroups = new Set();
|
|
3535
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
3536
|
+
const parsed = parseBucketKey(key);
|
|
3537
|
+
if (parsed.source !== DEFAULT_SOURCE || !bucket?.totals) continue;
|
|
3538
|
+
const groupKey = groupBucketKey(parsed.source, parsed.hourStart);
|
|
3539
|
+
const legacy = Object.prototype.hasOwnProperty.call(hourlyState.groupQueued, groupKey);
|
|
3540
|
+
if (legacy) legacyGroups.add(groupKey);
|
|
3541
|
+
if (!upgrading && !legacy) continue;
|
|
3542
|
+
bucket.queuedKey = null;
|
|
3543
|
+
bucket.retractedUnknownKey = null;
|
|
3544
|
+
touchedBuckets.add(key);
|
|
3545
|
+
}
|
|
3546
|
+
for (const groupKey of legacyGroups) {
|
|
3547
|
+
const hourStart = groupKey.slice(DEFAULT_SOURCE.length + BUCKET_SEPARATOR.length);
|
|
3548
|
+
// Legacy groups emitted a combined unknown row for multiple named models.
|
|
3549
|
+
// Replace it with the actual unknown total (or zero) before splitting.
|
|
3550
|
+
const unknown = getHourlyBucket(hourlyState, DEFAULT_SOURCE, DEFAULT_MODEL, hourStart);
|
|
3551
|
+
unknown.queuedKey = null;
|
|
3552
|
+
touchedBuckets.add(bucketKey(DEFAULT_SOURCE, DEFAULT_MODEL, hourStart));
|
|
3553
|
+
delete hourlyState.groupQueued[groupKey];
|
|
3554
|
+
}
|
|
3555
|
+
hourlyState.modelAttributionPolicy = { ...hourlyState.modelAttributionPolicy, codex: "observed" };
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3517
3558
|
async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets }) {
|
|
3518
3559
|
if (!touchedBuckets || touchedBuckets.size === 0) return 0;
|
|
3519
3560
|
|
|
@@ -3607,10 +3648,10 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3607
3648
|
|
|
3608
3649
|
const toAppend = [];
|
|
3609
3650
|
for (const group of groupedBuckets.values()) {
|
|
3610
|
-
//
|
|
3611
|
-
// every model bucket, including unknown,
|
|
3612
|
-
// busiest model in the same half-hour.
|
|
3613
|
-
// so hooks, desktop refreshes
|
|
3651
|
+
// Codex defaults to observed-only attribution; reconstructions can opt
|
|
3652
|
+
// other sources in. Preserve every model bucket, including unknown,
|
|
3653
|
+
// instead of guessing from the busiest model in the same half-hour.
|
|
3654
|
+
// Persist the policy so hooks, desktop refreshes and restarts agree.
|
|
3614
3655
|
if (hourlyState.modelAttributionPolicy?.[group.source] === "observed") {
|
|
3615
3656
|
// A group last aligned under the default policy must not keep the stale
|
|
3616
3657
|
// dominant attribution downstream. Mirror the default path's alignment
|
|
@@ -3662,6 +3703,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3662
3703
|
total_cost_usd: totals.total_cost_usd || 0,
|
|
3663
3704
|
usage_precision: usagePrecision || undefined,
|
|
3664
3705
|
...prioritySubsetOf(totals),
|
|
3706
|
+
...(totals.backend_usage ? { backend_usage: totals.backend_usage } : {}),
|
|
3665
3707
|
conversation_count: totals.conversation_count,
|
|
3666
3708
|
}));
|
|
3667
3709
|
bucket.queuedKey = key;
|
|
@@ -3749,6 +3791,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3749
3791
|
total_cost_usd: totals.total_cost_usd || 0,
|
|
3750
3792
|
usage_precision: usagePrecision || undefined,
|
|
3751
3793
|
...prioritySubsetOf(totals),
|
|
3794
|
+
...(totals.backend_usage ? { backend_usage: totals.backend_usage } : {}),
|
|
3752
3795
|
conversation_count: totals.conversation_count,
|
|
3753
3796
|
}),
|
|
3754
3797
|
);
|
|
@@ -3830,6 +3873,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3830
3873
|
total_cost_usd: unknownBucket.totals.total_cost_usd || 0,
|
|
3831
3874
|
usage_precision: usagePrecision || undefined,
|
|
3832
3875
|
...prioritySubsetOf(unknownBucket.totals),
|
|
3876
|
+
...(unknownBucket.totals.backend_usage ? { backend_usage: unknownBucket.totals.backend_usage } : {}),
|
|
3833
3877
|
conversation_count: unknownBucket.totals.conversation_count,
|
|
3834
3878
|
}),
|
|
3835
3879
|
);
|
|
@@ -3879,6 +3923,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3879
3923
|
billable_total_tokens: group.totals.billable_total_tokens ?? group.totals.total_tokens,
|
|
3880
3924
|
total_cost_usd: group.totals.total_cost_usd || 0,
|
|
3881
3925
|
...prioritySubsetOf(group.totals),
|
|
3926
|
+
...(group.totals.backend_usage ? { backend_usage: group.totals.backend_usage } : {}),
|
|
3882
3927
|
conversation_count: group.totals.conversation_count,
|
|
3883
3928
|
}),
|
|
3884
3929
|
);
|
|
@@ -3926,6 +3971,7 @@ async function enqueueTouchedProjectBuckets({
|
|
|
3926
3971
|
project_ref: projectRef,
|
|
3927
3972
|
project_key: projectKey,
|
|
3928
3973
|
source: bucket.source,
|
|
3974
|
+
...(totals.backend_usage ? { backend_usage: totals.backend_usage } : {}),
|
|
3929
3975
|
hour_start: bucket.hour_start,
|
|
3930
3976
|
input_tokens: totals.input_tokens,
|
|
3931
3977
|
cached_input_tokens: totals.cached_input_tokens,
|
|
@@ -4038,18 +4084,29 @@ function normalizeHourlyState(raw) {
|
|
|
4038
4084
|
const version = Number(state.version || 1);
|
|
4039
4085
|
const rawBuckets = state.buckets && typeof state.buckets === "object" ? state.buckets : {};
|
|
4040
4086
|
const buckets = {};
|
|
4041
|
-
const
|
|
4087
|
+
const existingGroupQueued =
|
|
4088
|
+
state.groupQueued && typeof state.groupQueued === "object" ? state.groupQueued : {};
|
|
4042
4089
|
const modelAttributionPolicy = Object.fromEntries(
|
|
4043
4090
|
Object.entries(state.modelAttributionPolicy || {})
|
|
4044
4091
|
.filter(([, policy]) => policy === "observed"),
|
|
4045
4092
|
);
|
|
4046
4093
|
|
|
4047
4094
|
if (!Number.isFinite(version) || version < 2) {
|
|
4095
|
+
const groupQueued = { ...existingGroupQueued };
|
|
4048
4096
|
for (const [key, value] of Object.entries(rawBuckets)) {
|
|
4049
4097
|
const parsed = parseBucketKey(key);
|
|
4050
4098
|
const hourStart = parsed.hourStart;
|
|
4051
4099
|
if (!hourStart) continue;
|
|
4052
4100
|
const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
4101
|
+
// Older repairs persisted model-aware keys without a schema version.
|
|
4102
|
+
// Preserve them before v1 normalization can collapse sibling models into
|
|
4103
|
+
// unknown; preserveCodexModelBuckets needs the original attribution (#927).
|
|
4104
|
+
const firstSeparator = key.indexOf(BUCKET_SEPARATOR);
|
|
4105
|
+
const hasModel = firstSeparator > 0 && key.indexOf(BUCKET_SEPARATOR, firstSeparator + 1) > 0;
|
|
4106
|
+
if (hasModel) {
|
|
4107
|
+
buckets[bucketKey(source, parsed.model, hourStart)] = value;
|
|
4108
|
+
continue;
|
|
4109
|
+
}
|
|
4053
4110
|
const normalizedKey = bucketKey(source, DEFAULT_MODEL, hourStart);
|
|
4054
4111
|
buckets[normalizedKey] = value;
|
|
4055
4112
|
if (value?.queuedKey) {
|
|
@@ -4073,9 +4130,6 @@ function normalizeHourlyState(raw) {
|
|
|
4073
4130
|
buckets[normalizedKey] = value;
|
|
4074
4131
|
}
|
|
4075
4132
|
|
|
4076
|
-
const existingGroupQueued =
|
|
4077
|
-
state.groupQueued && typeof state.groupQueued === "object" ? state.groupQueued : {};
|
|
4078
|
-
|
|
4079
4133
|
return {
|
|
4080
4134
|
version: 3,
|
|
4081
4135
|
buckets,
|
|
@@ -4529,6 +4583,7 @@ function prioritySubsetOf(totals) {
|
|
|
4529
4583
|
}
|
|
4530
4584
|
|
|
4531
4585
|
function addTotals(target, delta) {
|
|
4586
|
+
mergeBackendUsage(target, delta);
|
|
4532
4587
|
for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
|
|
4533
4588
|
const value = Number(delta?.[field]) || 0;
|
|
4534
4589
|
if (value > 0) target[field] = (Number(target[field]) || 0) + value;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const { createBackendResolver, mergeBackendUsage, claudeLines } = require("./claude-backend");
|
|
2
3
|
|
|
3
4
|
// Metadata-only session analytics sidecar.
|
|
4
5
|
//
|
|
@@ -146,6 +147,7 @@ function tokenTotals(usage) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function addTotals(target, delta) {
|
|
150
|
+
mergeBackendUsage(target, delta);
|
|
149
151
|
for (const key of ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"]) {
|
|
150
152
|
target[key] = finite(target[key]) + finite(delta?.[key]);
|
|
151
153
|
}
|
|
@@ -226,6 +228,7 @@ function normalizeModelUsageRows(rows) {
|
|
|
226
228
|
};
|
|
227
229
|
byModel.set(model, row);
|
|
228
230
|
}
|
|
231
|
+
mergeBackendUsage(row, value);
|
|
229
232
|
for (const field of MODEL_USAGE_SUM_FIELDS) row[field] += finite(value?.[field]);
|
|
230
233
|
row.performance = mergePerformance(row.performance, value?.performance);
|
|
231
234
|
if (Array.isArray(value?.selected_models)) {
|
|
@@ -266,7 +269,7 @@ function normalizeModelUsageRows(rows) {
|
|
|
266
269
|
// most of it the long_context_* counters for sessions with no requests over
|
|
267
270
|
// OPENAI_LONG_CONTEXT_INPUT_THRESHOLD in src/lib/pricing/index.js.
|
|
268
271
|
function serializeModelUsageRow(row) {
|
|
269
|
-
const out = { model: row.model };
|
|
272
|
+
const out = { model: row.model, ...(row.backend_usage ? { backend_usage: row.backend_usage } : {}) };
|
|
270
273
|
for (const field of MODEL_USAGE_SUM_FIELDS) {
|
|
271
274
|
const value = finite(row[field]);
|
|
272
275
|
if (value !== 0) out[field] = value;
|
|
@@ -299,7 +302,7 @@ function repriceSessionRecord(record) {
|
|
|
299
302
|
let hasUnpricedUsage = false;
|
|
300
303
|
for (const row of modelUsage) {
|
|
301
304
|
row.cost_usd = computeRowCost({ source: record.source, ...row });
|
|
302
|
-
row.pricing = getModelPricingInfo(row.model, { source: record.source });
|
|
305
|
+
row.pricing = getModelPricingInfo(row.model, { ...row, source: record.source });
|
|
303
306
|
if (row.total_tokens > 0 && row.pricing.status === "unpriced") hasUnpricedUsage = true;
|
|
304
307
|
}
|
|
305
308
|
record.model_usage = modelUsage;
|
|
@@ -366,7 +369,7 @@ function modelUsageForAggregation(record) {
|
|
|
366
369
|
}
|
|
367
370
|
const priced = rows.map((row) => denseModelUsageRow({
|
|
368
371
|
...row,
|
|
369
|
-
pricing: getModelPricingInfo(row.model, { source: record.source }),
|
|
372
|
+
pricing: getModelPricingInfo(row.model, { ...row, source: record.source }),
|
|
370
373
|
cost_usd: Number.isFinite(Number(row.cost_usd))
|
|
371
374
|
? Number(row.cost_usd)
|
|
372
375
|
: computeRowCost({ source: record.source, ...row }),
|
|
@@ -548,7 +551,7 @@ function isRecoverableSessionReadError(error) {
|
|
|
548
551
|
return ["ENOENT", "EACCES", "EPERM", "EISDIR", "EIO", "ESTALE"].includes(error?.code);
|
|
549
552
|
}
|
|
550
553
|
|
|
551
|
-
async function scanClaudeSession(filePath) {
|
|
554
|
+
async function scanClaudeSession(filePath, { trackerDir, legacyOffsets = {} } = {}) {
|
|
552
555
|
// Native and WSL mounts can disappear independently after discovery. Keep
|
|
553
556
|
// the surviving mirror instead of dropping the whole logical session.
|
|
554
557
|
const filePaths = readableSessionPaths(filePath);
|
|
@@ -600,9 +603,8 @@ async function scanClaudeSession(filePath) {
|
|
|
600
603
|
for (const currentFilePath of filePaths) {
|
|
601
604
|
const currentRecordHashes = new Set();
|
|
602
605
|
try {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
for await (const line of lines) {
|
|
606
|
+
const backend = trackerDir ? createBackendResolver(currentFilePath, trackerDir, { legacyOffset: legacyOffsets[currentFilePath] || 0 }) : null;
|
|
607
|
+
for await (const { line, start, end } of claudeLines(currentFilePath)) {
|
|
606
608
|
if (filePaths.length > 1) {
|
|
607
609
|
const recordHash = crypto.createHash("sha256").update(line).digest("base64url");
|
|
608
610
|
if (priorRecordHashes.has(recordHash)) continue;
|
|
@@ -649,6 +651,8 @@ async function scanClaudeSession(filePath) {
|
|
|
649
651
|
if (candidateModel) model = candidateModel;
|
|
650
652
|
currentTurnModel = model;
|
|
651
653
|
const usage = normalizeClaudeUsage(obj.message.usage);
|
|
654
|
+
const identity = backend && usage.total_tokens > 0 ? backend(obj, start, end) : null;
|
|
655
|
+
if (identity) usage.backend_usage = [{ ...usage, ...identity }];
|
|
652
656
|
addTotals(tokens, usage);
|
|
653
657
|
if (usage.total_tokens > 0) {
|
|
654
658
|
const modelUsage = modelUsageByModel.get(model) || { model, ...emptyTotals(), usage_events: 0 };
|
|
@@ -2343,6 +2347,8 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2343
2347
|
const retryOmpScan = fs.existsSync(incompleteOmpPath);
|
|
2344
2348
|
const incompleteDshPath = `${metaPath}.dsh-incomplete`;
|
|
2345
2349
|
const retryDshScan = fs.existsSync(incompleteDshPath);
|
|
2350
|
+
let provenanceMeta = null;
|
|
2351
|
+
try { provenanceMeta = JSON.parse(fs.readFileSync(metaPath, "utf8")); } catch { /* first scan */ }
|
|
2346
2352
|
let previousMeta = null;
|
|
2347
2353
|
if (!force) {
|
|
2348
2354
|
try {
|
|
@@ -2468,7 +2474,12 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
2468
2474
|
}
|
|
2469
2475
|
if (!rows) {
|
|
2470
2476
|
try {
|
|
2471
|
-
const
|
|
2477
|
+
const legacyOffsets = {};
|
|
2478
|
+
if (entry.source === "claude") {
|
|
2479
|
+
const sizes = provenanceMeta?.files?.[cacheKey]?.stat_key?.split("|") || [];
|
|
2480
|
+
entry.filePath.forEach((file, index) => { legacyOffsets[file] = Number(sizes[index]?.split(":")[0]) || 0; });
|
|
2481
|
+
}
|
|
2482
|
+
const scanned = await entry.scan(entry.filePath, { trackerDir: path.dirname(sidecarPath), legacyOffsets });
|
|
2472
2483
|
rows = Array.isArray(scanned) ? scanned.filter(Boolean) : (scanned ? [scanned] : []);
|
|
2473
2484
|
// Batch sources count the files they had to give up on themselves.
|
|
2474
2485
|
// A dsh batch is one deduplicated inventory, so a single unreadable log
|
|
@@ -2659,6 +2670,7 @@ function scopeSessionUsage(row, from, to, context) {
|
|
|
2659
2670
|
const modelName = previousModelUsage ? previousModelUsage.model : (row.model || bucket.model);
|
|
2660
2671
|
const model = byModel.get(modelName) || {
|
|
2661
2672
|
...(previousModelUsage || {}),
|
|
2673
|
+
backend_usage: undefined,
|
|
2662
2674
|
model: modelName,
|
|
2663
2675
|
...Object.fromEntries(MODEL_USAGE_SUM_FIELDS.filter((field) => field !== "edit_turns").map((field) => [field, 0])),
|
|
2664
2676
|
usage_events: 0,
|
|
@@ -34,6 +34,9 @@ function isTraeCnUsageEnabled(env = process.env) {
|
|
|
34
34
|
}
|
|
35
35
|
const TRAE_CN_APP_DIRS = ["TRAE SOLO CN", "Trae CN"];
|
|
36
36
|
const TRAE_CN_AUTH_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
37
|
+
const TRAE_CN_AUTH_HELP = "Sign in again in the TRAE CN app, then rerun tokentracker sync --source=trae-cn. " +
|
|
38
|
+
"If multiple CN apps are installed, set TOKENTRACKER_TRAE_CN_HOME to the signed-in app data root. " +
|
|
39
|
+
"Readable local auth does not prove API access; TokenTracker cannot refresh the app session.";
|
|
37
40
|
|
|
38
41
|
const TRAE_CN_USAGE_URL = "https://api.trae.cn/trae/api/v1/pay/query_user_usage_group_by_session";
|
|
39
42
|
// usage_type [7] mirrors the constant the official TRAE client sends when its
|
|
@@ -282,6 +285,26 @@ function extractTraeCnToken(auth) {
|
|
|
282
285
|
throw new Error("Trae CN auth data has no token.");
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
// Local-only doctor probe. Return an allowlisted state, never credentials or
|
|
289
|
+
// exception text. Opting out also avoids reading/decrypting the auth payload.
|
|
290
|
+
function inspectTraeCnAuth(options = {}) {
|
|
291
|
+
const enabled = isTraeCnUsageEnabled(options.env ?? process.env);
|
|
292
|
+
const storagePath = resolveTraeCnStoragePath(options);
|
|
293
|
+
const result = { usage_opt_in: enabled, storage_path: storagePath, auth: "unchecked" };
|
|
294
|
+
if (!enabled) return result;
|
|
295
|
+
try {
|
|
296
|
+
const auth = readTraeCnAuthFromStorage(options);
|
|
297
|
+
if (!auth) result.auth = "not-signed-in";
|
|
298
|
+
else {
|
|
299
|
+
extractTraeCnToken(auth);
|
|
300
|
+
result.auth = "readable";
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
result.auth = error?.code === "TRAE_CN_STORAGE_UNREADABLE" ? "unreadable" : "malformed";
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
285
308
|
// ---------------------------------------------------------------------------
|
|
286
309
|
// Usage API
|
|
287
310
|
// ---------------------------------------------------------------------------
|
|
@@ -365,7 +388,7 @@ async function fetchTraeCnUsagePage({
|
|
|
365
388
|
throw new Error("Trae CN usage API request failed.");
|
|
366
389
|
}
|
|
367
390
|
if (response.status === 401 || response.status === 403) {
|
|
368
|
-
throw usageError(`Trae CN usage API returned HTTP ${response.status}
|
|
391
|
+
throw usageError(`Trae CN usage API returned HTTP ${response.status}; authorization was rejected. ${TRAE_CN_AUTH_HELP}`, { status: response.status });
|
|
369
392
|
}
|
|
370
393
|
if (!response.ok) {
|
|
371
394
|
throw usageError(`Trae CN usage API returned HTTP ${response.status}.`, { status: response.status });
|
|
@@ -377,8 +400,13 @@ async function fetchTraeCnUsagePage({
|
|
|
377
400
|
throw new Error("Trae CN usage API returned a non-JSON response.");
|
|
378
401
|
}
|
|
379
402
|
const apiCode = parsed?.data?.code ?? parsed?.code;
|
|
403
|
+
if (apiCode !== undefined &&
|
|
404
|
+
!((typeof apiCode === "number" || (typeof apiCode === "string" && /^\d+$/.test(apiCode))) &&
|
|
405
|
+
Number.isSafeInteger(Number(apiCode)))) {
|
|
406
|
+
throw new Error("Trae CN usage API returned an invalid error code.");
|
|
407
|
+
}
|
|
380
408
|
if (apiCode !== undefined && Number(apiCode) !== 0) {
|
|
381
|
-
throw usageError(`Trae CN usage API returned error code ${apiCode}.`, {
|
|
409
|
+
throw usageError(`Trae CN usage API returned error code ${Number(apiCode)}.`, {
|
|
382
410
|
status: response.status,
|
|
383
411
|
apiCode: Number(apiCode),
|
|
384
412
|
});
|
|
@@ -543,6 +571,8 @@ module.exports = {
|
|
|
543
571
|
isTraeCnUsageEnabled,
|
|
544
572
|
TRAE_CN_APP_DIRS,
|
|
545
573
|
TRAE_CN_AUTH_KEY,
|
|
574
|
+
TRAE_CN_AUTH_HELP,
|
|
575
|
+
inspectTraeCnAuth,
|
|
546
576
|
TRAE_CN_USAGE_URL,
|
|
547
577
|
TRAE_CN_USAGE_TYPE,
|
|
548
578
|
TRAE_CN_USAGE_PAGE_SIZE,
|