tokentracker-cli 0.88.0 → 0.88.2
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 -1
- package/README.ja.md +2 -1
- package/README.ko.md +2 -1
- package/README.md +9 -6
- package/README.zh-CN.md +2 -1
- package/dashboard/dist/assets/{AchievementBadge-C6LBwlRW.js → AchievementBadge-BcCrg832.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-bIFOA_jG.js → AchievementsPage-C95dF_C-.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-54qFstYv.js → AchievementsSection-B-VD8CfN.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DuQD9WfI.js → BadgeDetailModal-CDTJ3tR6.js} +1 -1
- package/dashboard/dist/assets/{Card-BSSB4fdU.js → Card-a0RpvhLn.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-D3CW-PaV.js → ClawdAnimated-CaLKUbmC.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-BDNpgOLw.js → CommandPalette-CQpkX7dK.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DOh00zEU.js → CommunityStatsModal-Cj53g1ky.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BZnKmQvp.js → ConfirmModal-FHZmD91i.js} +1 -1
- package/dashboard/dist/assets/{Controls-CB1IsIKx.js → Controls-BSk2Eph4.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-dGdEyi6L.js → DashboardPage-Ci1jVw-F.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-Pa-RjGEW.js → DevicePage-BnCWVmOI.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BFix8fA1.js → DialogClose-BUhPBSeF.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Dp7bBdfJ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Djq8dbvr.js → DialogTitle-CbNKnmTY.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BbDnYSlD.js → FadeIn-D87p8xfM.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-VK0p8KvI.js → HeaderGithubStar-kE4I1PyE.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CxE_Tdkx.js → HoverTooltip-miw72aLh.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Ce5PrAlg.js → IpCheckPage-D-JoHTz-.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CLcEW-uO.js → LandingPage-Dx_MOVjJ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-CeEVv5Lf.js → LeaderboardAvatar-arnp_cWI.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BRtYEATU.js → LeaderboardPage-zsoFUX5G.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BnUJjdzX.js → LeaderboardProfileModal-CrmSTl9n.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Cp_U5Cu-.js → LeaderboardProfilePage-BSE0fPco.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-Dk83mCC3.js → LimitsPage-BrU9e8AV.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-B0IFxbO0.js → LocalOnlyNotice-BWDZKn00.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BNiVZu18.js → LoginCard-DS8mxxs_.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DsMQpIhI.js → LoginModal-DhINfVDV.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CRrHrKIB.js → LoginPage-BKmuhR74.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DkBEl1o-.js → PetPage-QQk_B1SJ.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-D1Vj1Efz.js → PopoverPopup-DS6ml7IE.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-7UhSc2T5.js → ProviderIcon-DkHp1Hrl.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BHNRVdD_.js → ResetPasswordPage-CvJiufPF.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BV-qV5Kt.js → SegmentedControl-DbYmpdj3.js} +1 -1
- package/dashboard/dist/assets/{Select-BuECb0BN.js → Select-DJcVfYm7.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BGD0xkvB.js → SelectItemText-DUQLkHGQ.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DYlfwLvY.js → ServiceStatusPage-DqPQUM1f.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BJsG4Y6A.js → SessionsPage-CPOSombb.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BWoQPbMV.js → SettingsPage-nIfi1h7D.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DrnjNKaO.js → SkillsPage-BnngQwIu.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Bw1qTfSn.js → TokenGalaxy-CAVmlIYW.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BL8XQ3_A.js → ToolbarRootContext-CyG7xQo2.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CiaCIHjy.js → TrendMonitor-DyuKVbzV.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CpbdEZpd.js → WidgetsPage-72n9-HCD.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BgXFgYTU.js → WrappedPage-Dp1aUOyA.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DzYphO_x.js → __vite-browser-external-8kU62_i_.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-DmIjuARU.js → agent-logos-ChfjzWPV.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BO9KkOdd.js → arrow-up-right-CiTMPVa_.js} +1 -1
- package/dashboard/dist/assets/{check-pWcM1WkS.js → check-OQW1qgf_.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-9BfpWAq6.js → chevron-right-BitImfyV.js} +1 -1
- package/dashboard/dist/assets/{copy-CEJGOfxH.js → copy-BM76c1Ob.js} +1 -1
- package/dashboard/dist/assets/{download-DGn3ZMbn.js → download-DaKmqoH2.js} +1 -1
- package/dashboard/dist/assets/{external-link-Cr0IWKOC.js → external-link-AL639M-l.js} +1 -1
- package/dashboard/dist/assets/{flame-Desm841m.js → flame-BeYD0c6y.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-nrsS_nVp.js → format-tokens-DK1Fdxfq.js} +1 -1
- package/dashboard/dist/assets/{icons-Dz6888iB.js → icons-BtLj_BDi.js} +1 -1
- package/dashboard/dist/assets/{index-Ck7-nAuV.js → index-BHBegQjp.js} +1 -1
- package/dashboard/dist/assets/{index-A1XQdrLM.js → index-BK2Bm5Vp.js} +1 -1
- package/dashboard/dist/assets/{index-D-wYrxD4.js → index-D4kdhgAz.js} +1 -1
- package/dashboard/dist/assets/{info-DYk5fAf_.js → info-CIwDFvrE.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Bnp3QYj9.js → limits-providers-BFkGzLM4.js} +1 -1
- package/dashboard/dist/assets/{link-2-CXFEilGt.js → link-2-xFS-GP5p.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-D9TcNFLI.js → loader-circle-83n_EWic.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Bqi02woy.js → maximize-2-CevrJZKM.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DBig_B4D.js → modifiers.esm-a1oXNZlE.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-B6Xb6Q2B.js → monitor-smartphone-CSP2FeVP.js} +1 -1
- package/dashboard/dist/assets/{provider-display-D9VqWtF5.js → provider-display-CamXa0pj.js} +1 -1
- package/dashboard/dist/assets/{react-fhU3Lic-.js → react-DV1nb3lW.js} +1 -1
- package/dashboard/dist/assets/{search-Dxo_rl9c.js → search-wj8Kxgr5.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BycDG-Gj.js → skills-api-BNpGEfgh.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BgR0p3Td.js → sortable.esm-Xh3nfzFR.js} +1 -1
- package/dashboard/dist/assets/{store-XGqQuDi9.js → store-lBXh9-l6.js} +1 -1
- package/dashboard/dist/assets/{terminal-Btw95Xe9.js → terminal-uNBlGBuA.js} +1 -1
- package/dashboard/dist/assets/{trash-2-MzI5KdIA.js → trash-2-08YOSvJh.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DqXdGI7Z.js → use-community-stats-CMyCuzcM.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DCC_HO5d.js → use-limits-display-prefs-BkqvWQ4X.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BrWqGdXP.js → use-ordered-list-CJ9F2mdG.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CD_DKLbl.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-fF02PDXr.js → use-session-efficiency-pref-Hf4gvuAl.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BU3YFLLq.js → use-transform-BBP_cds4.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Z_aa7E3w.js → use-usage-limits-DsLD0OEm.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BSydrVhG.js → useCurrency-CXngtOdH.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DfukCBjz.js → useScrollLock-Q65zPe5d.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Dh2VNPFE.js → useTokenFormat-CE5zbI_h.js} +1 -1
- package/dashboard/dist/assets/{zap-T24MagjI.js → zap-CHYAc2Be.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 +16 -0
- package/src/commands/status.js +57 -0
- package/src/commands/sync.js +4 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +265 -14
- package/src/lib/session-analytics.js +220 -145
- package/dashboard/dist/assets/DialogDescription-BBVopkKB.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BbtvaYmJ.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -1233,7 +1233,11 @@ async function parseOpencodeIncremental({
|
|
|
1233
1233
|
const size = Number.isFinite(st.size) ? st.size : 0;
|
|
1234
1234
|
const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
|
|
1235
1235
|
const unchanged =
|
|
1236
|
-
prev &&
|
|
1236
|
+
prev &&
|
|
1237
|
+
prev.inode === inode &&
|
|
1238
|
+
prev.size === size &&
|
|
1239
|
+
prev.mtimeMs === mtimeMs &&
|
|
1240
|
+
prev.opencodeForkRepairVersion === 1;
|
|
1237
1241
|
if (unchanged) {
|
|
1238
1242
|
filesProcessed += 1;
|
|
1239
1243
|
if (cb) {
|
|
@@ -1287,6 +1291,7 @@ async function parseOpencodeIncremental({
|
|
|
1287
1291
|
mtimeMs,
|
|
1288
1292
|
lastTotals: result.lastTotals,
|
|
1289
1293
|
messageKey: result.messageKey || null,
|
|
1294
|
+
opencodeForkRepairVersion: 1,
|
|
1290
1295
|
updatedAt: new Date().toISOString(),
|
|
1291
1296
|
};
|
|
1292
1297
|
|
|
@@ -1300,6 +1305,7 @@ async function parseOpencodeIncremental({
|
|
|
1300
1305
|
messageKey: result.messageKey,
|
|
1301
1306
|
totals: result.lastTotals,
|
|
1302
1307
|
fingerprint: result.fingerprint || null,
|
|
1308
|
+
dedupedForkCopy: result.dedupedForkCopy === true,
|
|
1303
1309
|
});
|
|
1304
1310
|
}
|
|
1305
1311
|
|
|
@@ -2630,16 +2636,41 @@ async function parseOpencodeMessageFile({
|
|
|
2630
2636
|
// `Session.fork` re-materialises the parent's prefix under new message ids in
|
|
2631
2637
|
// a new session — count it once (issue #426, see deriveOpencodeMessageFingerprint).
|
|
2632
2638
|
const fingerprint = deriveOpencodeMessageFingerprint({ msg, totals: currentTotals, source });
|
|
2633
|
-
if (
|
|
2634
|
-
|
|
2639
|
+
if (isOpencodeForkCopy(fingerprintIndex, fingerprint, messageKey)) {
|
|
2640
|
+
if (lastTotals && prev?.dedupedForkCopy !== true) {
|
|
2641
|
+
repairCountedOpencodeForkCopy({
|
|
2642
|
+
msg,
|
|
2643
|
+
totals: lastTotals,
|
|
2644
|
+
source,
|
|
2645
|
+
hourlyState,
|
|
2646
|
+
touchedBuckets,
|
|
2647
|
+
projectState,
|
|
2648
|
+
projectTouchedBuckets,
|
|
2649
|
+
projectRef,
|
|
2650
|
+
projectKey,
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return {
|
|
2654
|
+
messageKey,
|
|
2655
|
+
lastTotals: currentTotals,
|
|
2656
|
+
fingerprint,
|
|
2657
|
+
dedupedForkCopy: true,
|
|
2658
|
+
eventsAggregated: 0,
|
|
2659
|
+
shouldUpdate: prev?.dedupedForkCopy !== true || !lastTotals,
|
|
2660
|
+
};
|
|
2635
2661
|
}
|
|
2636
2662
|
|
|
2637
|
-
|
|
2663
|
+
// A formerly deduped copy can become a genuinely distinct in-place update.
|
|
2664
|
+
// Its prior totals were removed from the buckets, so re-add the full current
|
|
2665
|
+
// snapshot instead of only the delta from the suppressed value.
|
|
2666
|
+
const effectiveLastTotals = prev?.dedupedForkCopy === true ? null : lastTotals;
|
|
2667
|
+
const delta = diffGeminiTotals(currentTotals, effectiveLastTotals);
|
|
2638
2668
|
if (!delta || isAllZeroUsage(delta)) {
|
|
2639
2669
|
return {
|
|
2640
2670
|
messageKey,
|
|
2641
2671
|
lastTotals: currentTotals,
|
|
2642
2672
|
fingerprint,
|
|
2673
|
+
dedupedForkCopy: false,
|
|
2643
2674
|
eventsAggregated: 0,
|
|
2644
2675
|
shouldUpdate: true,
|
|
2645
2676
|
};
|
|
@@ -2686,6 +2717,7 @@ async function parseOpencodeMessageFile({
|
|
|
2686
2717
|
messageKey,
|
|
2687
2718
|
lastTotals: currentTotals,
|
|
2688
2719
|
fingerprint,
|
|
2720
|
+
dedupedForkCopy: false,
|
|
2689
2721
|
eventsAggregated: 1,
|
|
2690
2722
|
shouldUpdate: true,
|
|
2691
2723
|
};
|
|
@@ -3276,13 +3308,15 @@ function deriveOpencodeMessageFingerprint({ msg, totals, source }) {
|
|
|
3276
3308
|
return crypto.createHash("sha256").update(raw).digest("base64url").slice(0, 22);
|
|
3277
3309
|
}
|
|
3278
3310
|
|
|
3279
|
-
// `fingerprint -> messageKey` for
|
|
3280
|
-
//
|
|
3281
|
-
//
|
|
3311
|
+
// `fingerprint -> messageKey` for counted messages in this cursor namespace.
|
|
3312
|
+
// Rebuilt per parse run. Pre-#426 entries are fingerprinted as they are read;
|
|
3313
|
+
// the first claims ownership and later cross-session matches are retracted from
|
|
3314
|
+
// persisted buckets once. Tombstoned copies never claim ownership themselves.
|
|
3282
3315
|
function buildOpencodeFingerprintIndex(messageIndex) {
|
|
3283
3316
|
const byFingerprint = new Map();
|
|
3284
3317
|
if (!messageIndex || typeof messageIndex !== "object") return byFingerprint;
|
|
3285
3318
|
for (const [key, entry] of Object.entries(messageIndex)) {
|
|
3319
|
+
if (entry?.dedupedForkCopy === true) continue;
|
|
3286
3320
|
const fingerprint = entry && typeof entry.fingerprint === "string" ? entry.fingerprint : null;
|
|
3287
3321
|
if (fingerprint && !byFingerprint.has(fingerprint)) byFingerprint.set(fingerprint, key);
|
|
3288
3322
|
}
|
|
@@ -3314,13 +3348,25 @@ function isOpencodeForkCopy(fingerprintIndex, fingerprint, messageKey) {
|
|
|
3314
3348
|
// cursor untouched. A falsy `fingerprint` means "unknown" and preserves whatever
|
|
3315
3349
|
// the entry already carried; a new one releases the old claim so a stale
|
|
3316
3350
|
// mid-stream snapshot cannot shadow an unrelated message later.
|
|
3317
|
-
function recordOpencodeMessage({
|
|
3351
|
+
function recordOpencodeMessage({
|
|
3352
|
+
messageIndex,
|
|
3353
|
+
fingerprintIndex,
|
|
3354
|
+
messageKey,
|
|
3355
|
+
totals,
|
|
3356
|
+
fingerprint,
|
|
3357
|
+
dedupedForkCopy = false,
|
|
3358
|
+
}) {
|
|
3318
3359
|
if (!messageIndex || !messageKey) return;
|
|
3319
3360
|
const prev = messageIndex[messageKey];
|
|
3320
3361
|
const prevTotals = prev && typeof prev.lastTotals === "object" ? prev.lastTotals : null;
|
|
3321
3362
|
const prevFingerprint = prev && typeof prev.fingerprint === "string" ? prev.fingerprint : null;
|
|
3322
3363
|
const nextFingerprint = fingerprint || prevFingerprint;
|
|
3323
|
-
|
|
3364
|
+
const prevDeduped = prev?.dedupedForkCopy === true;
|
|
3365
|
+
if (
|
|
3366
|
+
sameGeminiTotals(totals, prevTotals) &&
|
|
3367
|
+
prevFingerprint === nextFingerprint &&
|
|
3368
|
+
prevDeduped === Boolean(dedupedForkCopy)
|
|
3369
|
+
) return;
|
|
3324
3370
|
|
|
3325
3371
|
if (fingerprintIndex && prevFingerprint && prevFingerprint !== nextFingerprint) {
|
|
3326
3372
|
if (fingerprintIndex.get(prevFingerprint) === messageKey) {
|
|
@@ -3329,12 +3375,52 @@ function recordOpencodeMessage({ messageIndex, fingerprintIndex, messageKey, tot
|
|
|
3329
3375
|
}
|
|
3330
3376
|
const entry = { lastTotals: totals, updatedAt: new Date().toISOString() };
|
|
3331
3377
|
if (nextFingerprint) entry.fingerprint = nextFingerprint;
|
|
3378
|
+
if (dedupedForkCopy) entry.dedupedForkCopy = true;
|
|
3332
3379
|
messageIndex[messageKey] = entry;
|
|
3333
|
-
if (
|
|
3380
|
+
if (
|
|
3381
|
+
fingerprintIndex &&
|
|
3382
|
+
nextFingerprint &&
|
|
3383
|
+
!dedupedForkCopy &&
|
|
3384
|
+
!fingerprintIndex.has(nextFingerprint)
|
|
3385
|
+
) {
|
|
3334
3386
|
fingerprintIndex.set(nextFingerprint, messageKey);
|
|
3335
3387
|
}
|
|
3336
3388
|
}
|
|
3337
3389
|
|
|
3390
|
+
function repairCountedOpencodeForkCopy({
|
|
3391
|
+
msg,
|
|
3392
|
+
totals,
|
|
3393
|
+
source,
|
|
3394
|
+
hourlyState,
|
|
3395
|
+
touchedBuckets,
|
|
3396
|
+
projectState,
|
|
3397
|
+
projectTouchedBuckets,
|
|
3398
|
+
projectRef,
|
|
3399
|
+
projectKey,
|
|
3400
|
+
}) {
|
|
3401
|
+
const timestampMs = coerceEpochMs(msg?.time?.completed) || coerceEpochMs(msg?.time?.created);
|
|
3402
|
+
if (!timestampMs) return false;
|
|
3403
|
+
const bucketStart = toUtcHalfHourStart(new Date(timestampMs).toISOString());
|
|
3404
|
+
if (!bucketStart) return false;
|
|
3405
|
+
const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
|
|
3406
|
+
const counted = { ...totals, conversation_count: 1 };
|
|
3407
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
3408
|
+
subtractTotals(bucket.totals, counted);
|
|
3409
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
3410
|
+
if (projectKey && projectState && projectTouchedBuckets) {
|
|
3411
|
+
const projectBucket = getProjectBucket(
|
|
3412
|
+
projectState,
|
|
3413
|
+
projectKey,
|
|
3414
|
+
source,
|
|
3415
|
+
bucketStart,
|
|
3416
|
+
projectRef,
|
|
3417
|
+
);
|
|
3418
|
+
subtractTotals(projectBucket.totals, counted);
|
|
3419
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
3420
|
+
}
|
|
3421
|
+
return true;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3338
3424
|
function getHourlyBucket(state, source, model, hourStart) {
|
|
3339
3425
|
const buckets = state.buckets;
|
|
3340
3426
|
const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|
|
@@ -4390,19 +4476,53 @@ async function parseOpencodeDbIncremental({
|
|
|
4390
4476
|
continue;
|
|
4391
4477
|
}
|
|
4392
4478
|
|
|
4393
|
-
// A fork copy of an already-counted turn contributes nothing
|
|
4394
|
-
//
|
|
4479
|
+
// A fork copy of an already-counted turn contributes nothing. Existing
|
|
4480
|
+
// pre-#426 cursor entries were already added to hourly/project buckets;
|
|
4481
|
+
// retract those once, then persist a tombstone so a later sync is a no-op.
|
|
4395
4482
|
const fingerprint = deriveOpencodeMessageFingerprint({
|
|
4396
4483
|
msg,
|
|
4397
4484
|
totals: currentTotals,
|
|
4398
4485
|
source: defaultSource,
|
|
4399
4486
|
});
|
|
4400
|
-
if (
|
|
4487
|
+
if (isOpencodeForkCopy(fingerprintIndex, fingerprint, messageKey)) {
|
|
4488
|
+
let projectContext = null;
|
|
4489
|
+
if (lastTotals && prev?.dedupedForkCopy !== true && projectEnabled) {
|
|
4490
|
+
projectContext = await resolveProjectContextForDb({
|
|
4491
|
+
msg,
|
|
4492
|
+
dbPath,
|
|
4493
|
+
projectMetaCache,
|
|
4494
|
+
publicRepoCache,
|
|
4495
|
+
publicRepoResolver,
|
|
4496
|
+
projectState,
|
|
4497
|
+
});
|
|
4498
|
+
}
|
|
4499
|
+
if (lastTotals && prev?.dedupedForkCopy !== true) {
|
|
4500
|
+
repairCountedOpencodeForkCopy({
|
|
4501
|
+
msg,
|
|
4502
|
+
totals: lastTotals,
|
|
4503
|
+
source: defaultSource,
|
|
4504
|
+
hourlyState,
|
|
4505
|
+
touchedBuckets,
|
|
4506
|
+
projectState,
|
|
4507
|
+
projectTouchedBuckets,
|
|
4508
|
+
projectRef: projectContext?.projectRef || null,
|
|
4509
|
+
projectKey: projectContext?.projectKey || null,
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
recordOpencodeMessage({
|
|
4513
|
+
messageIndex,
|
|
4514
|
+
fingerprintIndex,
|
|
4515
|
+
messageKey,
|
|
4516
|
+
totals: currentTotals,
|
|
4517
|
+
fingerprint,
|
|
4518
|
+
dedupedForkCopy: true,
|
|
4519
|
+
});
|
|
4401
4520
|
messagesProcessed += 1;
|
|
4402
4521
|
continue;
|
|
4403
4522
|
}
|
|
4404
4523
|
|
|
4405
|
-
const
|
|
4524
|
+
const effectiveLastTotals = prev?.dedupedForkCopy === true ? null : lastTotals;
|
|
4525
|
+
const delta = diffGeminiTotals(currentTotals, effectiveLastTotals);
|
|
4406
4526
|
if (!delta || isAllZeroUsage(delta)) {
|
|
4407
4527
|
// Refresh the index even without a delta: normalization may have changed,
|
|
4408
4528
|
// and pre-#426 entries need their fingerprint backfilled so a fork taken
|
|
@@ -4413,6 +4533,7 @@ async function parseOpencodeDbIncremental({
|
|
|
4413
4533
|
messageKey,
|
|
4414
4534
|
totals: currentTotals,
|
|
4415
4535
|
fingerprint,
|
|
4536
|
+
dedupedForkCopy: false,
|
|
4416
4537
|
});
|
|
4417
4538
|
messagesProcessed += 1;
|
|
4418
4539
|
if (cb) {
|
|
@@ -4476,6 +4597,7 @@ async function parseOpencodeDbIncremental({
|
|
|
4476
4597
|
messageKey,
|
|
4477
4598
|
totals: currentTotals,
|
|
4478
4599
|
fingerprint,
|
|
4600
|
+
dedupedForkCopy: false,
|
|
4479
4601
|
});
|
|
4480
4602
|
messagesProcessed += 1;
|
|
4481
4603
|
eventsAggregated += 1;
|
|
@@ -15998,6 +16120,130 @@ function isCjkCodePoint(code) {
|
|
|
15998
16120
|
);
|
|
15999
16121
|
}
|
|
16000
16122
|
|
|
16123
|
+
// ── Trae SOLO (ByteDance AI IDE) ─────────────────────────────────────────────
|
|
16124
|
+
// https://www.trae.ai
|
|
16125
|
+
//
|
|
16126
|
+
// Trae SOLO is scoped to detection (init) + entitlement display (status):
|
|
16127
|
+
// - init: resolveTraeStoragePath() proves an install exists.
|
|
16128
|
+
// - status: readTraeEntitlementFromStorage() serves the plan/limits snapshot.
|
|
16129
|
+
// Trae SOLO does NOT expose per-request token usage in a readable local format
|
|
16130
|
+
// (session transcripts are SQLCipher-encrypted; memory summaries carry no
|
|
16131
|
+
// token counts), so the token-count-only queue is intentionally never written
|
|
16132
|
+
// for this provider — the cloud hourly table stays clean.
|
|
16133
|
+
function resolveTraePath(env = process.env) {
|
|
16134
|
+
const override = env.TOKENTRACKER_TRAE_HOME;
|
|
16135
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
16136
|
+
return override.trim();
|
|
16137
|
+
}
|
|
16138
|
+
const home = require("node:os").homedir();
|
|
16139
|
+
if (process.platform === "darwin") {
|
|
16140
|
+
return path.join(home, "Library", "Application Support", "TRAE SOLO");
|
|
16141
|
+
}
|
|
16142
|
+
if (process.platform === "win32") {
|
|
16143
|
+
// A Windows box with no APPDATA still keeps Trae under the standard
|
|
16144
|
+
// roaming profile — falling through to the dot-dir below would point at
|
|
16145
|
+
// a location Trae never writes.
|
|
16146
|
+
const appData = typeof env.APPDATA === "string" ? env.APPDATA.trim() : "";
|
|
16147
|
+
return appData
|
|
16148
|
+
? path.join(appData, "TRAE SOLO")
|
|
16149
|
+
: path.join(home, "AppData", "Roaming", "TRAE SOLO");
|
|
16150
|
+
}
|
|
16151
|
+
// Linux and friends: TRAE SOLO ships official builds for macOS/Windows
|
|
16152
|
+
// only, so there is no verified app-data layout to default to. Fall back to
|
|
16153
|
+
// a deterministic home-dir path (best-effort detection);
|
|
16154
|
+
// TOKENTRACKER_TRAE_HOME always wins for unusual installs.
|
|
16155
|
+
return path.join(home, ".trae-solo");
|
|
16156
|
+
}
|
|
16157
|
+
|
|
16158
|
+
function resolveTraeStoragePath(env = process.env) {
|
|
16159
|
+
const traHome = resolveTraePath(env);
|
|
16160
|
+
if (!traHome) return null;
|
|
16161
|
+
const p = path.join(traHome, "User", "globalStorage", "storage.json");
|
|
16162
|
+
return fssync.existsSync(p) ? p : null;
|
|
16163
|
+
}
|
|
16164
|
+
|
|
16165
|
+
/**
|
|
16166
|
+
* Extract the Trae SOLO entitlement snapshot from parsed storage.json
|
|
16167
|
+
* serverData (the value under iCubeServerData://icube.cloudide).
|
|
16168
|
+
* Returns a normalized entitlement object, or null when the serverData
|
|
16169
|
+
* carries no valid entitlementInfo. Shared by the status read path
|
|
16170
|
+
* (readTraeEntitlementFromStorage).
|
|
16171
|
+
*/
|
|
16172
|
+
function normalizeTraeEntitlement(serverData) {
|
|
16173
|
+
let ent;
|
|
16174
|
+
try {
|
|
16175
|
+
ent = typeof serverData === "string" ? JSON.parse(serverData) : serverData;
|
|
16176
|
+
} catch {
|
|
16177
|
+
return null;
|
|
16178
|
+
}
|
|
16179
|
+
// The parsed serverData may legitimately be JSON `null` (e.g. the string
|
|
16180
|
+
// "null") or another non-object — treat it as no valid snapshot.
|
|
16181
|
+
if (!ent || typeof ent !== "object" || Array.isArray(ent)) return null;
|
|
16182
|
+
const entitlementInfo = ent.entitlementInfo;
|
|
16183
|
+
if (!entitlementInfo || typeof entitlementInfo !== "object") return null;
|
|
16184
|
+
const detail = entitlementInfo.detail || {};
|
|
16185
|
+
return {
|
|
16186
|
+
identity: entitlementInfo.identityStr,
|
|
16187
|
+
identity_code: entitlementInfo.identity,
|
|
16188
|
+
has_package: entitlementInfo.hasPackage,
|
|
16189
|
+
is_dollar_billing: entitlementInfo.isDollarUsageBilling,
|
|
16190
|
+
pro_period: entitlementInfo.proPeriod,
|
|
16191
|
+
enable_solo_builder: entitlementInfo.enableSoloBuilder,
|
|
16192
|
+
enable_solo_coder: entitlementInfo.enableSoloCoder,
|
|
16193
|
+
fast_request_per: detail.fastRequestPer,
|
|
16194
|
+
in_waitlist: detail.inWaitlist,
|
|
16195
|
+
};
|
|
16196
|
+
}
|
|
16197
|
+
|
|
16198
|
+
/**
|
|
16199
|
+
* Read the current Trae SOLO entitlement snapshot straight from the Trae
|
|
16200
|
+
* Local State storage.json, without touching the token-count-only queue
|
|
16201
|
+
* (CLAUDE.md privacy contract). Returns a normalized entitlement object
|
|
16202
|
+
* with a captured_at timestamp, or null when storage.json is missing,
|
|
16203
|
+
* unparseable, or carries no valid entitlement snapshot.
|
|
16204
|
+
*/
|
|
16205
|
+
function readTraeEntitlementFromStorage(storagePath) {
|
|
16206
|
+
if (!storagePath) return null;
|
|
16207
|
+
let fd;
|
|
16208
|
+
try {
|
|
16209
|
+
fd = fssync.openSync(storagePath, "r");
|
|
16210
|
+
} catch {
|
|
16211
|
+
return null;
|
|
16212
|
+
}
|
|
16213
|
+
let stat = null;
|
|
16214
|
+
let raw;
|
|
16215
|
+
try {
|
|
16216
|
+
stat = fssync.fstatSync(fd);
|
|
16217
|
+
raw = fssync.readFileSync(fd, "utf8");
|
|
16218
|
+
} catch {
|
|
16219
|
+
return null;
|
|
16220
|
+
} finally {
|
|
16221
|
+
try {
|
|
16222
|
+
fssync.closeSync(fd);
|
|
16223
|
+
} catch {
|
|
16224
|
+
// fd already closed; nothing to do.
|
|
16225
|
+
}
|
|
16226
|
+
}
|
|
16227
|
+
let storage;
|
|
16228
|
+
try {
|
|
16229
|
+
storage = JSON.parse(raw);
|
|
16230
|
+
} catch {
|
|
16231
|
+
return null;
|
|
16232
|
+
}
|
|
16233
|
+
if (!storage || typeof storage !== "object" || Array.isArray(storage)) {
|
|
16234
|
+
return null;
|
|
16235
|
+
}
|
|
16236
|
+
const serverKey = "iCubeServerData://icube.cloudide";
|
|
16237
|
+
const serverData = storage[serverKey];
|
|
16238
|
+
if (!serverData) return null;
|
|
16239
|
+
const entitlement = normalizeTraeEntitlement(serverData);
|
|
16240
|
+
if (!entitlement) return null;
|
|
16241
|
+
return {
|
|
16242
|
+
...entitlement,
|
|
16243
|
+
captured_at: stat ? new Date(stat.mtimeMs).toISOString() : null,
|
|
16244
|
+
};
|
|
16245
|
+
}
|
|
16246
|
+
|
|
16001
16247
|
module.exports = {
|
|
16002
16248
|
listRolloutFiles,
|
|
16003
16249
|
listRolloutFilesDeep,
|
|
@@ -16164,4 +16410,9 @@ module.exports = {
|
|
|
16164
16410
|
parseAntigravityIncremental,
|
|
16165
16411
|
estimateAntigravityTokens,
|
|
16166
16412
|
isCjkCodePoint,
|
|
16413
|
+
|
|
16414
|
+
// Trae SOLO (ByteDance AI IDE)
|
|
16415
|
+
resolveTraePath,
|
|
16416
|
+
resolveTraeStoragePath,
|
|
16417
|
+
readTraeEntitlementFromStorage,
|
|
16167
16418
|
};
|