tokentracker-cli 1.4.11 → 1.4.13
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 +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +6 -4
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-DALWiLQQ.js → AchievementBadge-6wnHO-Hp.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CjWobmn1.js → AchievementsPage-BpHA9auG.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CTryGWII.js → AchievementsSection-ySXWiLC3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D4lMxewA.js → BadgeDetailModal-DuxcntYu.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CsLNf9WH.js → BotAnimated-Dg1gSql-.js} +1 -1
- package/dashboard/dist/assets/{Card-BOnNTrmF.js → Card-73qYdlEv.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-CrY15xqT.js → CloudCheckoutPage-C7-9p9zk.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-hNSCMzVG.js → CloudPage-CKcDcf6O.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-CE4UfPXk.js → CommandPalette-Cq7TSYUV.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CLUegC6z.js → CommunityStatsModal-C0gj8QaO.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-D3wwfiYB.js → ConfirmModal-Cuf2JDA9.js} +1 -1
- package/dashboard/dist/assets/{Controls-B_ta_8zw.js → Controls-COaS9RLV.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-Beg--N9T.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-CYud1FKu.js → DateRangePopover-rNSRXWkr.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-D_eSLfM3.js → DevicePage-jZ9S-taT.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-Bu1ct4DA.js → DialogClose-CEYsCJ6-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BAaD46Xg.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BJxM52J1.js → DialogTitle-CnaGDa7P.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DYVZebgD.js → ElectricLogo-CM0HfX6y.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CVaq5ALh.js → FadeIn-CngXKUqj.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BCwYSguq.js → HeaderGithubStar-DQn0rAVr.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-YsG-TdOX.js → HoverTooltip-By7PDA5D.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C0qiyZXr.js → IpCheckPage-BvrRGJNy.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BlRJWwvx.js → LandingPage-C0CnNfi_.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-DcCCKL2q.js → LeaderboardPage-iUXnjGy-.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-I_oW0KCc.js → LeaderboardProBadge-CrV2bYaH.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DM5Vc7MB.js → LeaderboardProfileModal-iGgVOvl7.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DnYBBGNe.js → LeaderboardProfilePage-DmiiHi5U.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-Bpp0LYGk.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-083BoEX9.js → LocalOnlyNotice-0pQm9-gI.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CN1db2Rj.js → LoginCard-OaDObE8j.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-Dfi7_ory.js → LoginModal-C0dRPusa.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BzMdiYlx.js → LoginPage-D6CoX42w.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CsE95lfJ.js → PetPage-Ch5t4JT_.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-U7DTTTE0.js → PopoverPopup-UtHXvmNC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CnLfT5d9.js → ProviderIcon-CRlMy2AY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-De2RzCj-.js → ResetPasswordPage-DijAhwkr.js} +1 -1
- package/dashboard/dist/assets/{Select-BS5MWHdB.js → Select-D1FvgdDV.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D3i379K-.js → SelectItemText-Conq3RQN.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-CTL0dnp5.js → SelfHostPage-CLUgPEFh.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BmEvOk3B.js → ServiceStatusPage-D7KRjgOE.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-CWNrbLT4.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-Cgpiwh-C.js → SettingsPage-Bh_PuEXU.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-DpagshiB.js → SkillsPage-DxZuztPd.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Do1U_-E3.js → TokenGalaxy-BE-dqlDy.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-MOAnEqf3.js → TooltipPopup-DQrTDsmN.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-B55RUbQm.js → TrendMonitor-Cal2kaMh.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CehbGk-6.js → WidgetsPage-Cvv2NXRK.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Cqt-U0Zx.js → WrappedPage-H99MjZkq.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +1 -0
- package/dashboard/dist/assets/{__vite-browser-external-CR0vo15z.js → __vite-browser-external-I1de5Swt.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-DuW-DWAl.js → agent-logos-BUBtw9FF.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DIvQjuji.js → arrow-left-C99UO2S4.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BURLycHb.js → arrow-right-BMFV-A6L.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Bl1QMMkY.js → arrow-up-right-CReo0rju.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CmIWJC3A.js → calendar-days-CiEobm13.js} +1 -1
- package/dashboard/dist/assets/{check-DB2ZTWor.js → check-BBOwGLur.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BflN3NJt.js → chevron-down-C08QHnpk.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BugQolSC.js → cloud-checkout-NOmtAy01.js} +1 -1
- package/dashboard/dist/assets/{composite-DJsqXsjA.js → composite-C5GvW04X.js} +1 -1
- package/dashboard/dist/assets/{copy-CtB-GPN2.js → copy-DgY9F9FR.js} +1 -1
- package/dashboard/dist/assets/{download-x5Botpqc.js → download-B7xuZSZ2.js} +1 -1
- package/dashboard/dist/assets/{external-link-DH9sRkJW.js → external-link-1nRJjSL1.js} +1 -1
- package/dashboard/dist/assets/{flame-DEcsokoL.js → flame-CR1qiptA.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BH0jc1MG.js → format-tokens-DNLvupK2.js} +1 -1
- package/dashboard/dist/assets/{gift-BZk9muk9.js → gift-DUUpsPxb.js} +1 -1
- package/dashboard/dist/assets/{icons-BMf9-bfS.js → icons-qY8ZAPUu.js} +1 -1
- package/dashboard/dist/assets/{index-BEtrik3j.js → index-7VLgsnkO.js} +1 -1
- package/dashboard/dist/assets/{index-Cm-9apOR.js → index-BvNIpRP0.js} +1 -1
- package/dashboard/dist/assets/{index-TErx-xPb.js → index-CfdotR_D.js} +1 -1
- package/dashboard/dist/assets/{index-axJYocSc.js → index-Tl9T3Nm3.js} +1 -1
- package/dashboard/dist/assets/{info-DWdPAxu8.js → info-DnT3wtaz.js} +1 -1
- package/dashboard/dist/assets/{link-2-c3ZZd-w-.js → link-2-CjxRD2lW.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-BKdtxfky.js → loader-circle-BYCsfrhr.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-BFTJyQl3.js → local-usage-transfer-DBfMvvqx.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-C3yIfOiP.js → modifiers.esm-DG_gVjqU.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DbdL7Mei.js → monitor-smartphone-CJUH6nwc.js} +1 -1
- package/dashboard/dist/assets/{play-qtWt2ArI.js → play-DY4d-C2J.js} +1 -1
- package/dashboard/dist/assets/{plus-C_-__5vi.js → plus-Dydx6cB7.js} +1 -1
- package/dashboard/dist/assets/{provider-display-IKHnhvaj.js → provider-display-DCtG1A8I.js} +2 -2
- package/dashboard/dist/assets/{react-H6DfeMUm.js → react-D9IQudPE.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-OEQJQTVa.js → safePolygon-CxfZ_vVn.js} +1 -1
- package/dashboard/dist/assets/{search-MZE76qNU.js → search-DBso9ppI.js} +1 -1
- package/dashboard/dist/assets/{server-CUWYLMjG.js → server-Bb_KzpvJ.js} +1 -1
- package/dashboard/dist/assets/{shield-check-lA0Ja0RB.js → shield-check-cnqpE0zI.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CJJIPvSe.js → skills-api-DMbIT8CJ.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DTnvt1IN.js → sortable.esm-2Wnda4fq.js} +1 -1
- package/dashboard/dist/assets/{store-CC0YKrxA.js → store-CGDaAZE0.js} +1 -1
- package/dashboard/dist/assets/{terminal-yc2l_-3m.js → terminal-yZ69kiqV.js} +1 -1
- package/dashboard/dist/assets/{trash-Dl9f-o2h.js → trash-DytHiAPM.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CZCKWCO7.js → use-community-stats-DkR1XWs8.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DTrOu0gD.js → use-latest-request-guard-D_4XuEoM.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DGqP231e.js → use-limits-display-prefs-BadNsZG8.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BEATnxB6.js → use-native-settings-BfxfnQAn.js} +1 -1
- package/dashboard/dist/assets/use-ordered-list-BHn-XC6u.js +1 -0
- package/dashboard/dist/assets/use-reduced-motion-BsG6Qxa-.js +1 -0
- package/dashboard/dist/assets/{use-transform-DM0QGq_O.js → use-transform-C0qKlh_s.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BnjBG2fM.js → use-usage-limits-CQugaku6.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-xQrRFokb.js → useCurrency-ChAc4UHj.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DkVdsypM.js → usePositioner-CGtwrNEt.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CvA_kIPe.js → useTokenFormat-CjvWdxnV.js} +1 -1
- package/dashboard/dist/assets/{zap-BJNS4i_u.js → zap-WTlrDMoe.js} +1 -1
- package/dashboard/dist/index.html +14 -12
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +1 -0
- package/src/commands/status.js +10 -0
- package/src/commands/sync.js +24 -0
- package/src/lib/local-usage-transfer.js +1 -1
- package/src/lib/pricing/curated-overrides.json +78 -0
- package/src/lib/pricing/index.js +2 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +408 -37
- package/src/lib/trae-cn-config.js +36 -20
- package/dashboard/dist/assets/DashboardPage-RevVPZFt.js +0 -1
- package/dashboard/dist/assets/DialogDescription-GUHQVNGf.js +0 -1
- package/dashboard/dist/assets/LimitsPage-Cyy3PI8p.js +0 -6
- package/dashboard/dist/assets/SessionsPage-DIog3JA_.js +0 -3
- package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +0 -1
- package/dashboard/dist/assets/use-ordered-list-C_HTFP5C.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-CfCWjE-a.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -6770,13 +6770,35 @@ async function parseQoderDbIncremental({
|
|
|
6770
6770
|
//
|
|
6771
6771
|
// Qoder 2026-08 (app 0.1.2+) migrated from SharedClientCache/cache/db/local.db
|
|
6772
6772
|
// to Electron main.sqlite + ~/.qoder/projects/<slug>/<sessionId>.jsonl.
|
|
6773
|
-
//
|
|
6774
|
-
//
|
|
6775
|
-
//
|
|
6776
|
-
//
|
|
6777
|
-
//
|
|
6778
|
-
//
|
|
6779
|
-
//
|
|
6773
|
+
//
|
|
6774
|
+
// The transcript's message.usage shape has drifted at least once, and BOTH
|
|
6775
|
+
// shapes occur in real user history:
|
|
6776
|
+
// - app 0.1.8 / agent 1.1.44: Anthropic-style precise token fields, on the
|
|
6777
|
+
// main transcript AND on subagent transcripts
|
|
6778
|
+
// (see test/qoder-new-parser.test.js "Real CN record shape").
|
|
6779
|
+
// - app 0.4.x, model "qfmodel": every row is
|
|
6780
|
+
// {input_tokens:0, output_tokens:0, cache_*:0, credits:>0, billable:false}.
|
|
6781
|
+
// The old comment here claimed the credit-billed shape was `{credits, billable:
|
|
6782
|
+
// true}`; that assumption is what made the gate below drop 100% of modern Qoder
|
|
6783
|
+
// rows, so Qoder vanished from the dashboard instead of showing up as
|
|
6784
|
+
// "0 tokens / N requests". `billable` means "charged to the account", NOT
|
|
6785
|
+
// "a call happened" — free-tier models report false while still reporting
|
|
6786
|
+
// credits. Admission therefore needs all three signals, and dropping any of
|
|
6787
|
+
// them regresses a shape that used to count.
|
|
6788
|
+
//
|
|
6789
|
+
// Non-goals: credit→token conversion stays out. There is no first-party credit
|
|
6790
|
+
// → token (or → USD) rate, so fabricating one would push an invented number
|
|
6791
|
+
// into totals, trends, achievements and the cloud leaderboard. Rows without
|
|
6792
|
+
// authoritative tokens contribute conversation activity only, and
|
|
6793
|
+
// total_cost_usd stays 0. Transcript `usage.credits` is deliberately NOT copied
|
|
6794
|
+
// into bucket totals — there is no first-party credit→USD rate to book cost
|
|
6795
|
+
// from, and carrying it forward invites someone to divide by it later.
|
|
6796
|
+
//
|
|
6797
|
+
// conversation_count here follows this provider's existing per-response
|
|
6798
|
+
// convention (`: 1` per admitted main-transcript row). That field is NOT
|
|
6799
|
+
// comparable across providers — same rule as the Claude branch's isMainSession
|
|
6800
|
+
// (#307) and Claude Science's `isRoot ? 1 : 0` (see the Claude Science parser).
|
|
6801
|
+
//
|
|
6780
6802
|
// Old local.db is kept as a legacy fallback; both sources now use distinct
|
|
6781
6803
|
// cursor namespaces (qoder vs qoderNew) via disjoint messageKey prefixes
|
|
6782
6804
|
// (row: vs jsonl:) but upload under the same source="qoder".
|
|
@@ -6870,17 +6892,47 @@ function qoderNewMessageKey(record, filePath, lineIndex = 0) {
|
|
|
6870
6892
|
return `jsonl:${fallbackSuffix}|${record?.uuid || ""}`;
|
|
6871
6893
|
}
|
|
6872
6894
|
|
|
6895
|
+
// 8.64e15 is the ECMAScript TimeClip limit. A larger finite number (nanosecond
|
|
6896
|
+
// epochs, clock garbage) passes coerceEpochMs untouched, and `new Date(ms)`
|
|
6897
|
+
// yields Invalid Date whose .toISOString() THROWS RangeError — outside the
|
|
6898
|
+
// per-line JSON.parse try/catch, aborting the whole source on one bad row and
|
|
6899
|
+
// silently removing Qoder from every later sync (warnProviderParseFailure is
|
|
6900
|
+
// quiet in auto syncs). Clamp to a drop instead.
|
|
6901
|
+
const MAX_QODER_TIMESTAMP_MS = 8.64e15;
|
|
6902
|
+
|
|
6873
6903
|
function qoderNewTimestampMs(record) {
|
|
6874
|
-
|
|
6904
|
+
const ms = coerceEpochMs(record?.timestamp)
|
|
6875
6905
|
|| coerceEpochMs(record?.message?.timestamp)
|
|
6876
6906
|
|| parseIsoTimestampMs(record?.timestamp)
|
|
6877
6907
|
|| parseIsoTimestampMs(record?.message?.timestamp)
|
|
6878
6908
|
|| 0;
|
|
6909
|
+
return ms > MAX_QODER_TIMESTAMP_MS ? 0 : ms;
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6912
|
+
// Nominal credits for one transcript row. `credits` and `original_credits`
|
|
6913
|
+
// coexist (the latter is the pre-discount value), and either alone can be 0,
|
|
6914
|
+
// so take the larger instead of `credits ?? original_credits` — `??` does not
|
|
6915
|
+
// skip a present-but-zero `credits`, which would drop real activity.
|
|
6916
|
+
//
|
|
6917
|
+
// The epsilon floor is load-bearing: the activity gate below only needs "some
|
|
6918
|
+
// credits were reported", and upstream float noise like 1e-9 would otherwise
|
|
6919
|
+
// admit rows that carry no usage at all. Real Qoder credits observed in the
|
|
6920
|
+
// wild start around 0.2 (2026-10 CN corpus), so 1e-6 never suppresses genuine
|
|
6921
|
+
// activity.
|
|
6922
|
+
const MIN_QODER_CREDIT_ACTIVITY = 1e-6;
|
|
6923
|
+
|
|
6924
|
+
function readQoderNominalCredits(usage) {
|
|
6925
|
+
let max = 0;
|
|
6926
|
+
for (const raw of [usage?.credits, usage?.original_credits]) {
|
|
6927
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
6928
|
+
if (raw > max) max = raw;
|
|
6929
|
+
}
|
|
6930
|
+
return max >= MIN_QODER_CREDIT_ACTIVITY ? max : 0;
|
|
6879
6931
|
}
|
|
6880
6932
|
|
|
6881
6933
|
function normalizeQoderNewTokens(usage) {
|
|
6882
6934
|
if (!usage || typeof usage !== "object") return null;
|
|
6883
|
-
const credits =
|
|
6935
|
+
const credits = readQoderNominalCredits(usage);
|
|
6884
6936
|
let input = Number(usage.input_tokens ?? 0);
|
|
6885
6937
|
let cached = Number(usage.cache_read_input_tokens ?? usage.cached_tokens ?? 0);
|
|
6886
6938
|
let cacheCreation = Number(usage.cache_creation_input_tokens ?? 0);
|
|
@@ -6907,16 +6959,31 @@ function normalizeQoderNewTokens(usage) {
|
|
|
6907
6959
|
total_tokens: inp + cach + cc + out,
|
|
6908
6960
|
billable_total_tokens: inp + cach + cc + out,
|
|
6909
6961
|
credits: Number.isFinite(credits) && credits > 0 ? credits : 0,
|
|
6962
|
+
// `credits` dies at the totals boundary — parseQoderNewIncremental never
|
|
6963
|
+
// copies it into bucket totals or the cursor, and must not start: there
|
|
6964
|
+
// is no first-party credit→token or credit→USD rate to divide it by.
|
|
6910
6965
|
usage_precision: null,
|
|
6911
6966
|
};
|
|
6912
6967
|
}
|
|
6913
|
-
// Credit-only usage without authoritative token fields
|
|
6914
|
-
//
|
|
6915
|
-
//
|
|
6916
|
-
//
|
|
6968
|
+
// Credit-only usage without authoritative token fields yields null: no token
|
|
6969
|
+
// delta, because there is no first-party credit→token rate. The caller still
|
|
6970
|
+
// counts it as conversation activity when nominal credits prove the call
|
|
6971
|
+
// really happened.
|
|
6917
6972
|
return null;
|
|
6918
6973
|
}
|
|
6919
6974
|
|
|
6975
|
+
// Subagent transcripts are internal turns of the same conversation, so they
|
|
6976
|
+
// must not add to conversation activity — same rule the Claude branch enforces
|
|
6977
|
+
// with isMainSession (#307) and Claude Science encodes with `isRoot ? 1 : 0`.
|
|
6978
|
+
// A row that carries neither tokens nor activity is still tracked in the cursor:
|
|
6979
|
+
// a streamed rewrite can hand it real tokens later, and the cursor entry is what
|
|
6980
|
+
// makes that transition subtract/add correctly. It must not touch an hourly or
|
|
6981
|
+
// project bucket though, because getHourlyBucket() creates buckets on demand and
|
|
6982
|
+
// would publish an all-zero row.
|
|
6983
|
+
function qoderRowCountsAsUsage(totals) {
|
|
6984
|
+
return (totals?.total_tokens || 0) > 0 || (totals?.conversation_count || 0) > 0;
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6920
6987
|
async function parseQoderNewIncremental({
|
|
6921
6988
|
sessionFiles,
|
|
6922
6989
|
cursors,
|
|
@@ -6977,6 +7044,9 @@ async function parseQoderNewIncremental({
|
|
|
6977
7044
|
} catch (_e) {
|
|
6978
7045
|
continue;
|
|
6979
7046
|
}
|
|
7047
|
+
// Separator-agnostic: WSL installs are walked over \\wsl$ UNC paths where
|
|
7048
|
+
// path.join emits backslashes (#307).
|
|
7049
|
+
const isMainSession = !/[\\/]subagents[\\/]/.test(filePath);
|
|
6980
7050
|
const lines = raw.split("\n");
|
|
6981
7051
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
6982
7052
|
const line = lines[lineIdx];
|
|
@@ -6994,9 +7064,19 @@ async function parseQoderNewIncremental({
|
|
|
6994
7064
|
const usage = msg.usage;
|
|
6995
7065
|
if (!usage || typeof usage !== "object") continue;
|
|
6996
7066
|
const base = normalizeQoderNewTokens(usage);
|
|
6997
|
-
//
|
|
7067
|
+
// Three independent admission signals, kept as a superset of the previous
|
|
7068
|
+
// behaviour — dropping any one regresses rows that used to count:
|
|
7069
|
+
// base authoritative tokens (app 0.1.8 / agent 1.1.44 shape)
|
|
7070
|
+
// isBillable billable is absent or true (the older credit-billed shape)
|
|
7071
|
+
// nominalCredits credits > 0 even though billable is false: Qoder 0.4.x
|
|
7072
|
+
// reports billable=false for its free model while still
|
|
7073
|
+
// reporting real credits, and gate-on-billable alone
|
|
7074
|
+
// discarded every row so the whole source vanished.
|
|
7075
|
+
// Rows that are not billable AND carry no credits stay dropped: that is
|
|
7076
|
+
// the synthetic / cancelled / empty-call case we must not count.
|
|
6998
7077
|
const isBillable = usage.billable !== false;
|
|
6999
|
-
|
|
7078
|
+
const nominalCredits = readQoderNominalCredits(usage);
|
|
7079
|
+
if (!base && !isBillable && nominalCredits <= 0) continue;
|
|
7000
7080
|
const timestampMs = qoderNewTimestampMs(record);
|
|
7001
7081
|
if (!timestampMs) continue;
|
|
7002
7082
|
const bucketStart = toUtcHalfHourStart(new Date(timestampMs).toISOString());
|
|
@@ -7014,7 +7094,7 @@ async function parseQoderNewIncremental({
|
|
|
7014
7094
|
billable_total_tokens: base.billable_total_tokens,
|
|
7015
7095
|
total_cost_usd: 0,
|
|
7016
7096
|
usage_precision: base.usage_precision || undefined,
|
|
7017
|
-
conversation_count: 1,
|
|
7097
|
+
conversation_count: isMainSession ? 1 : 0,
|
|
7018
7098
|
} : {
|
|
7019
7099
|
input_tokens: 0,
|
|
7020
7100
|
cached_input_tokens: 0,
|
|
@@ -7024,7 +7104,7 @@ async function parseQoderNewIncremental({
|
|
|
7024
7104
|
total_tokens: 0,
|
|
7025
7105
|
billable_total_tokens: 0,
|
|
7026
7106
|
total_cost_usd: 0,
|
|
7027
|
-
conversation_count: 1,
|
|
7107
|
+
conversation_count: isMainSession ? 1 : 0,
|
|
7028
7108
|
};
|
|
7029
7109
|
let projectKey = null;
|
|
7030
7110
|
let projectRef = null;
|
|
@@ -7043,6 +7123,16 @@ async function parseQoderNewIncremental({
|
|
|
7043
7123
|
projectRef = context?.projectRef || null;
|
|
7044
7124
|
}
|
|
7045
7125
|
}
|
|
7126
|
+
// Same sessionId+msgId can appear in both the main transcript and a
|
|
7127
|
+
// subagent transcript (mirrored id). currentByKey is last-wins and
|
|
7128
|
+
// listQoderNewSessionFiles orders by localeCompare, so an unguarded
|
|
7129
|
+
// subagent copy (conversation_count 0) could silently replace the main
|
|
7130
|
+
// row's activity depending on file order. Precedence: a main-session
|
|
7131
|
+
// record always wins a subagent copy; within the same class, last-wins
|
|
7132
|
+
// stands so a streamed rewrite (the later, final token counts) is what
|
|
7133
|
+
// gets booked — first-wins here would under-count subagent tokens.
|
|
7134
|
+
const existing = currentByKey.get(messageKey);
|
|
7135
|
+
if (existing?.isMainSession && !isMainSession) continue;
|
|
7046
7136
|
currentByKey.set(messageKey, {
|
|
7047
7137
|
totals,
|
|
7048
7138
|
bucketStart,
|
|
@@ -7050,6 +7140,7 @@ async function parseQoderNewIncremental({
|
|
|
7050
7140
|
projectKey,
|
|
7051
7141
|
projectRef,
|
|
7052
7142
|
filePath,
|
|
7143
|
+
isMainSession,
|
|
7053
7144
|
});
|
|
7054
7145
|
}
|
|
7055
7146
|
if (cb && (fileIdx % 50 === 0 || fileIdx === files.length - 1)) {
|
|
@@ -7077,7 +7168,7 @@ async function parseQoderNewIncremental({
|
|
|
7077
7168
|
&& (prev.projectKey || null) === (cur.projectKey || null)
|
|
7078
7169
|
&& (prev.totals?.total_cost_usd || 0) === (cur.totals.total_cost_usd || 0);
|
|
7079
7170
|
if (unchanged) continue;
|
|
7080
|
-
if (prev.totals && prev.bucketStart && prev.model) {
|
|
7171
|
+
if (prev.totals && prev.bucketStart && prev.model && qoderRowCountsAsUsage(prev.totals)) {
|
|
7081
7172
|
const oldBucket = getHourlyBucket(hourlyState, sourceKey, prev.model, prev.bucketStart);
|
|
7082
7173
|
subtractTotals(oldBucket.totals, prev.totals);
|
|
7083
7174
|
touchedBuckets.add(bucketKey(sourceKey, prev.model, prev.bucketStart));
|
|
@@ -7088,16 +7179,17 @@ async function parseQoderNewIncremental({
|
|
|
7088
7179
|
}
|
|
7089
7180
|
}
|
|
7090
7181
|
if (cur) {
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7182
|
+
if (qoderRowCountsAsUsage(cur.totals)) {
|
|
7183
|
+
const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
|
|
7184
|
+
addTotals(bucket.totals, cur.totals);
|
|
7185
|
+
if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
|
|
7186
|
+
touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
|
|
7187
|
+
if (projectEnabled && cur.projectKey) {
|
|
7188
|
+
const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
|
|
7189
|
+
addTotals(projectBucket.totals, cur.totals);
|
|
7190
|
+
if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
|
|
7191
|
+
projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
|
|
7192
|
+
}
|
|
7101
7193
|
}
|
|
7102
7194
|
qoderState.messages[key] = {
|
|
7103
7195
|
totals: cur.totals,
|
|
@@ -7118,15 +7210,17 @@ async function parseQoderNewIncremental({
|
|
|
7118
7210
|
// Add brand-new keys
|
|
7119
7211
|
for (const [key, cur] of currentByKey.entries()) {
|
|
7120
7212
|
if (qoderState.messages[key]) continue;
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7213
|
+
if (qoderRowCountsAsUsage(cur.totals)) {
|
|
7214
|
+
const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
|
|
7215
|
+
addTotals(bucket.totals, cur.totals);
|
|
7216
|
+
if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
|
|
7217
|
+
touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
|
|
7218
|
+
if (projectEnabled && cur.projectKey) {
|
|
7219
|
+
const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
|
|
7220
|
+
addTotals(projectBucket.totals, cur.totals);
|
|
7221
|
+
if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
|
|
7222
|
+
projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
|
|
7223
|
+
}
|
|
7130
7224
|
}
|
|
7131
7225
|
qoderState.messages[key] = {
|
|
7132
7226
|
totals: cur.totals,
|
|
@@ -14107,6 +14201,279 @@ async function parseLmstudioIncremental({
|
|
|
14107
14201
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
14108
14202
|
}
|
|
14109
14203
|
|
|
14204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14205
|
+
// OpenSquilla
|
|
14206
|
+
//
|
|
14207
|
+
// Read only the finalized usage ledger's model rows. Transcript entries and
|
|
14208
|
+
// message content are not needed for token attribution and are never selected.
|
|
14209
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14210
|
+
|
|
14211
|
+
const OPENSQUILLA_SOURCE = "opensquilla";
|
|
14212
|
+
|
|
14213
|
+
function resolveOpenSquillaPath(value, home) {
|
|
14214
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
14215
|
+
if (!raw) return null;
|
|
14216
|
+
if (raw === "~") return path.resolve(home);
|
|
14217
|
+
if (raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
14218
|
+
return path.resolve(home, raw.slice(2));
|
|
14219
|
+
}
|
|
14220
|
+
return path.resolve(raw);
|
|
14221
|
+
}
|
|
14222
|
+
|
|
14223
|
+
function resolveOpenSquillaDbPaths(env = process.env, deps = {}) {
|
|
14224
|
+
const home = deps.home || env.HOME || env.USERPROFILE || os.homedir();
|
|
14225
|
+
const platform = deps.platform || process.platform;
|
|
14226
|
+
const dbOverride = resolveOpenSquillaPath(env.TOKENTRACKER_OPENSQUILLA_DB, home);
|
|
14227
|
+
if (dbOverride) return [dbOverride];
|
|
14228
|
+
|
|
14229
|
+
const gatewayStateDir = resolveOpenSquillaPath(env.OPENSQUILLA_GATEWAY_STATE_DIR, home);
|
|
14230
|
+
if (gatewayStateDir) return [path.join(gatewayStateDir, "sessions.db")];
|
|
14231
|
+
|
|
14232
|
+
const stateHome = resolveOpenSquillaPath(env.OPENSQUILLA_STATE_DIR, home);
|
|
14233
|
+
if (stateHome) return [path.join(stateHome, "state", "sessions.db")];
|
|
14234
|
+
|
|
14235
|
+
const profileHomeOverride = typeof env.OPENSQUILLA_HOME === "string"
|
|
14236
|
+
? env.OPENSQUILLA_HOME.trim()
|
|
14237
|
+
: "";
|
|
14238
|
+
const profileOverride = typeof env.OPENSQUILLA_PROFILE === "string"
|
|
14239
|
+
? env.OPENSQUILLA_PROFILE.trim()
|
|
14240
|
+
: "";
|
|
14241
|
+
if (profileHomeOverride || profileOverride) {
|
|
14242
|
+
const profilesRoot = resolveOpenSquillaPath(
|
|
14243
|
+
profileHomeOverride || path.join(home, ".opensquilla", "profiles"),
|
|
14244
|
+
home,
|
|
14245
|
+
);
|
|
14246
|
+
const profile = profileOverride || "default";
|
|
14247
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(profile)) return [];
|
|
14248
|
+
return [path.join(profilesRoot, profile, "state", "sessions.db")];
|
|
14249
|
+
}
|
|
14250
|
+
|
|
14251
|
+
const paths = [path.join(home, ".opensquilla", "state", "sessions.db")];
|
|
14252
|
+
let userDataRoot;
|
|
14253
|
+
if (platform === "win32") {
|
|
14254
|
+
userDataRoot = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
14255
|
+
} else if (platform === "darwin") {
|
|
14256
|
+
userDataRoot = path.join(home, "Library", "Application Support");
|
|
14257
|
+
} else {
|
|
14258
|
+
userDataRoot = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
14259
|
+
}
|
|
14260
|
+
paths.unshift(path.join(
|
|
14261
|
+
userDataRoot,
|
|
14262
|
+
"@opensquilla",
|
|
14263
|
+
"desktop-electron",
|
|
14264
|
+
"opensquilla",
|
|
14265
|
+
"state",
|
|
14266
|
+
"sessions.db",
|
|
14267
|
+
));
|
|
14268
|
+
|
|
14269
|
+
const seen = new Set();
|
|
14270
|
+
return paths.filter((dbPath) => {
|
|
14271
|
+
const resolved = path.resolve(dbPath);
|
|
14272
|
+
const key = platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
14273
|
+
if (seen.has(key)) return false;
|
|
14274
|
+
seen.add(key);
|
|
14275
|
+
return true;
|
|
14276
|
+
});
|
|
14277
|
+
}
|
|
14278
|
+
|
|
14279
|
+
async function readOpenSquillaUsageRows(dbPath, sqliteOptions = {}) {
|
|
14280
|
+
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
14281
|
+
const options = {
|
|
14282
|
+
label: "OpenSquilla",
|
|
14283
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
14284
|
+
timeout: 30_000,
|
|
14285
|
+
...sqliteOptions,
|
|
14286
|
+
readOnly: true,
|
|
14287
|
+
throwOnReadFailure: true,
|
|
14288
|
+
};
|
|
14289
|
+
const tables = new Set(
|
|
14290
|
+
(await readSqliteJsonRowsAsync(
|
|
14291
|
+
dbPath,
|
|
14292
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('usage_events','usage_event_items')",
|
|
14293
|
+
options,
|
|
14294
|
+
)).map((row) => row?.name).filter(Boolean),
|
|
14295
|
+
);
|
|
14296
|
+
if (!tables.has("usage_events") || !tables.has("usage_event_items")) return [];
|
|
14297
|
+
const sql = [
|
|
14298
|
+
"SELECT e.event_id, e.completed_at_ms, i.ordinal,",
|
|
14299
|
+
"COALESCE(i.model, e.model) AS model, i.input_tokens, i.output_tokens,",
|
|
14300
|
+
"i.reasoning_tokens, i.cache_read_tokens, i.cache_write_tokens, i.total_tokens",
|
|
14301
|
+
"FROM usage_events AS e",
|
|
14302
|
+
"JOIN usage_event_items AS i ON i.event_id = e.event_id",
|
|
14303
|
+
"WHERE e.status = 'finalized' AND e.completed_at_ms IS NOT NULL",
|
|
14304
|
+
"ORDER BY e.completed_at_ms, e.event_id, i.ordinal",
|
|
14305
|
+
].join(" ");
|
|
14306
|
+
return readSqliteJsonRowsAsync(dbPath, sql, options);
|
|
14307
|
+
}
|
|
14308
|
+
|
|
14309
|
+
function normalizeOpenSquillaUsageRow(row) {
|
|
14310
|
+
const eventId = typeof row?.event_id === "string" ? row.event_id.trim() : "";
|
|
14311
|
+
if (!eventId || row?.ordinal == null) return null;
|
|
14312
|
+
|
|
14313
|
+
// OpenSquilla stores input as a cache-inclusive total and reasoning as an
|
|
14314
|
+
// output subset; normalize both before writing TokenTracker queue rows.
|
|
14315
|
+
const totals = normalizeLocalStudioTokens({
|
|
14316
|
+
input_tokens: row.input_tokens,
|
|
14317
|
+
output_tokens: row.output_tokens,
|
|
14318
|
+
reasoning_tokens: row.reasoning_tokens,
|
|
14319
|
+
cached_tokens: row.cache_read_tokens,
|
|
14320
|
+
cache_creation_input_tokens: row.cache_write_tokens,
|
|
14321
|
+
total_tokens: row.total_tokens,
|
|
14322
|
+
});
|
|
14323
|
+
const timestampMs = coerceEpochMs(row.completed_at_ms);
|
|
14324
|
+
const bucketStart = timestampMs
|
|
14325
|
+
? toUtcHalfHourStart(new Date(timestampMs).toISOString())
|
|
14326
|
+
: null;
|
|
14327
|
+
if (!totals || !bucketStart) return null;
|
|
14328
|
+
|
|
14329
|
+
return {
|
|
14330
|
+
key: "opensquilla:" + eventId + ":" + String(toNonNegativeInt(row.ordinal)),
|
|
14331
|
+
model: normalizeModelInput(row.model) || DEFAULT_MODEL,
|
|
14332
|
+
bucketStart,
|
|
14333
|
+
totals,
|
|
14334
|
+
};
|
|
14335
|
+
}
|
|
14336
|
+
|
|
14337
|
+
async function parseOpenSquillaIncremental({
|
|
14338
|
+
dbPaths,
|
|
14339
|
+
cursors,
|
|
14340
|
+
queuePath,
|
|
14341
|
+
onProgress,
|
|
14342
|
+
env,
|
|
14343
|
+
sqliteOptions,
|
|
14344
|
+
} = {}) {
|
|
14345
|
+
await ensureDir(path.dirname(queuePath));
|
|
14346
|
+
const priorState = cursors.opensquilla && typeof cursors.opensquilla === "object"
|
|
14347
|
+
? cursors.opensquilla
|
|
14348
|
+
: {};
|
|
14349
|
+
// Copy before mutate so a failed queue append leaves committed cursors intact.
|
|
14350
|
+
const messages = priorState.messages && typeof priorState.messages === "object"
|
|
14351
|
+
? { ...priorState.messages }
|
|
14352
|
+
: {};
|
|
14353
|
+
const fingerprints = priorState.fingerprints && typeof priorState.fingerprints === "object"
|
|
14354
|
+
? { ...priorState.fingerprints }
|
|
14355
|
+
: {};
|
|
14356
|
+
const requestedPaths = Array.isArray(dbPaths) && dbPaths.length
|
|
14357
|
+
? dbPaths
|
|
14358
|
+
: resolveOpenSquillaDbPaths(env || process.env);
|
|
14359
|
+
const seenPaths = new Set();
|
|
14360
|
+
const availablePaths = requestedPaths.filter((dbPath) => {
|
|
14361
|
+
if (typeof dbPath !== "string" || !dbPath.trim() || !fssync.existsSync(dbPath)) return false;
|
|
14362
|
+
const resolved = path.resolve(dbPath);
|
|
14363
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
14364
|
+
if (seenPaths.has(key)) return false;
|
|
14365
|
+
seenPaths.add(key);
|
|
14366
|
+
return true;
|
|
14367
|
+
});
|
|
14368
|
+
// No currently readable DB is treated as temporary unavailability — keep
|
|
14369
|
+
// prior observations rather than wiping totals.
|
|
14370
|
+
if (availablePaths.length === 0) {
|
|
14371
|
+
cursors.opensquilla = { ...priorState, messages, fingerprints, updatedAt: new Date().toISOString() };
|
|
14372
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
14373
|
+
}
|
|
14374
|
+
|
|
14375
|
+
const pathFingerprints = new Map();
|
|
14376
|
+
let databaseChanged = false;
|
|
14377
|
+
const currentPathKeys = new Set();
|
|
14378
|
+
for (const dbPath of availablePaths) {
|
|
14379
|
+
const key = path.resolve(dbPath);
|
|
14380
|
+
currentPathKeys.add(key);
|
|
14381
|
+
const fingerprint = sqliteSidecarFingerprint(dbPath);
|
|
14382
|
+
pathFingerprints.set(key, fingerprint);
|
|
14383
|
+
if (!fingerprints[key] || !sameSqliteFingerprint(fingerprint, fingerprints[key])) {
|
|
14384
|
+
databaseChanged = true;
|
|
14385
|
+
}
|
|
14386
|
+
}
|
|
14387
|
+
// A removed Desktop/CLI DB must also trigger reconciliation so orphaned
|
|
14388
|
+
// observations can be retracted (fingerprint-only checks would miss this).
|
|
14389
|
+
for (const key of Object.keys(fingerprints)) {
|
|
14390
|
+
if (!currentPathKeys.has(key)) {
|
|
14391
|
+
databaseChanged = true;
|
|
14392
|
+
break;
|
|
14393
|
+
}
|
|
14394
|
+
}
|
|
14395
|
+
if (!databaseChanged) {
|
|
14396
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
14397
|
+
}
|
|
14398
|
+
|
|
14399
|
+
// The Desktop profile is first in the default path list. If a migrated CLI
|
|
14400
|
+
// profile retains the same event IDs, prefer the active Desktop copy and
|
|
14401
|
+
// count each model row only once.
|
|
14402
|
+
const rowsByEventItem = new Map();
|
|
14403
|
+
for (const dbPath of availablePaths) {
|
|
14404
|
+
const dbRows = await readOpenSquillaUsageRows(dbPath, sqliteOptions);
|
|
14405
|
+
for (const row of dbRows) {
|
|
14406
|
+
const eventId = typeof row?.event_id === "string" ? row.event_id.trim() : "";
|
|
14407
|
+
if (!eventId || row?.ordinal == null) continue;
|
|
14408
|
+
const key = eventId + ":" + String(toNonNegativeInt(row.ordinal));
|
|
14409
|
+
if (!rowsByEventItem.has(key)) rowsByEventItem.set(key, row);
|
|
14410
|
+
}
|
|
14411
|
+
}
|
|
14412
|
+
const rows = Array.from(rowsByEventItem.values());
|
|
14413
|
+
// structuredClone so a failed enqueueTouchedBuckets cannot leave mutated
|
|
14414
|
+
// totals committed while opensquilla.messages stays unchanged (retry would
|
|
14415
|
+
// subtract again). Same pattern as TRAE / session reconcilers.
|
|
14416
|
+
const hourlyState = structuredClone(normalizeHourlyState(cursors?.hourly));
|
|
14417
|
+
const touchedBuckets = new Set();
|
|
14418
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
14419
|
+
let eventsAggregated = 0;
|
|
14420
|
+
const currentKeys = new Set();
|
|
14421
|
+
for (let index = 0; index < rows.length; index++) {
|
|
14422
|
+
const event = normalizeOpenSquillaUsageRow(rows[index]);
|
|
14423
|
+
if (event) currentKeys.add(event.key);
|
|
14424
|
+
if (event && reconcilePassiveUsageEvent({
|
|
14425
|
+
event,
|
|
14426
|
+
source: OPENSQUILLA_SOURCE,
|
|
14427
|
+
messages,
|
|
14428
|
+
hourlyState,
|
|
14429
|
+
touchedBuckets,
|
|
14430
|
+
})) eventsAggregated += 1;
|
|
14431
|
+
if (cb) {
|
|
14432
|
+
cb({
|
|
14433
|
+
index: index + 1,
|
|
14434
|
+
total: rows.length,
|
|
14435
|
+
recordsProcessed: index + 1,
|
|
14436
|
+
eventsAggregated,
|
|
14437
|
+
bucketsQueued: touchedBuckets.size,
|
|
14438
|
+
});
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
|
|
14442
|
+
// Retract observations that disappeared with a removed database (or were
|
|
14443
|
+
// deleted from the remaining DB). Overlapping event IDs keep Desktop-first
|
|
14444
|
+
// totals via reconcilePassiveUsageEvent above.
|
|
14445
|
+
for (const key of Object.keys(messages)) {
|
|
14446
|
+
if (!key.startsWith("opensquilla:") || currentKeys.has(key)) continue;
|
|
14447
|
+
const previous = messages[key];
|
|
14448
|
+
if (previous?.model && previous?.bucketStart && previous?.totals) {
|
|
14449
|
+
const oldBucket = getHourlyBucket(
|
|
14450
|
+
hourlyState,
|
|
14451
|
+
OPENSQUILLA_SOURCE,
|
|
14452
|
+
previous.model,
|
|
14453
|
+
previous.bucketStart,
|
|
14454
|
+
);
|
|
14455
|
+
subtractTotals(oldBucket.totals, previous.totals);
|
|
14456
|
+
touchedBuckets.add(bucketKey(OPENSQUILLA_SOURCE, previous.model, previous.bucketStart));
|
|
14457
|
+
}
|
|
14458
|
+
delete messages[key];
|
|
14459
|
+
eventsAggregated += 1;
|
|
14460
|
+
}
|
|
14461
|
+
|
|
14462
|
+
const nextFingerprints = {};
|
|
14463
|
+
for (const dbPath of availablePaths) {
|
|
14464
|
+
const key = path.resolve(dbPath);
|
|
14465
|
+
const before = pathFingerprints.get(key);
|
|
14466
|
+
const after = sqliteSidecarFingerprint(dbPath);
|
|
14467
|
+
nextFingerprints[key] = sameSqliteFingerprint(before, after) ? after : before;
|
|
14468
|
+
}
|
|
14469
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
14470
|
+
const updatedAt = new Date().toISOString();
|
|
14471
|
+
hourlyState.updatedAt = updatedAt;
|
|
14472
|
+
cursors.hourly = hourlyState;
|
|
14473
|
+
cursors.opensquilla = { messages, fingerprints: nextFingerprints, updatedAt };
|
|
14474
|
+
return { recordsProcessed: rows.length, eventsAggregated, bucketsQueued };
|
|
14475
|
+
}
|
|
14476
|
+
|
|
14110
14477
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14111
14478
|
// Unsloth Studio
|
|
14112
14479
|
//
|
|
@@ -25865,6 +26232,10 @@ module.exports = {
|
|
|
25865
26232
|
normalizeLocalStudioTokens,
|
|
25866
26233
|
readLmstudioFileRecords,
|
|
25867
26234
|
parseLmstudioIncremental,
|
|
26235
|
+
resolveOpenSquillaDbPaths,
|
|
26236
|
+
readOpenSquillaUsageRows,
|
|
26237
|
+
normalizeOpenSquillaUsageRow,
|
|
26238
|
+
parseOpenSquillaIncremental,
|
|
25868
26239
|
resolveUnslothDbPath,
|
|
25869
26240
|
readUnslothUsageRows,
|
|
25870
26241
|
normalizeUnslothUsageRow,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* TRAE Work CN (国内版) config + usage fetch. Phase A: macOS + Windows + test-injected
|
|
5
|
-
* paths only. Resolves the CN
|
|
5
|
+
* paths only. Resolves the CN app data directory (TRAE Work CN or the classic
|
|
6
|
+
* Trae CN client), reads the `iCubeAuthInfo://icube.cloudide`
|
|
6
7
|
* blob from User/globalStorage/storage.json (plaintext JSON or Base64 tc v5),
|
|
7
8
|
* and talks to the TRAE CN usage API. JWTs / refresh tokens are never
|
|
8
9
|
* persisted, never logged, and never put into error messages; a
|
|
@@ -31,7 +32,7 @@ function isTraeCnUsageEnabled(env = process.env) {
|
|
|
31
32
|
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
32
33
|
return normalized === "1" || normalized === "true";
|
|
33
34
|
}
|
|
34
|
-
const
|
|
35
|
+
const TRAE_CN_APP_DIRS = ["TRAE SOLO CN", "Trae CN"];
|
|
35
36
|
const TRAE_CN_AUTH_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
36
37
|
|
|
37
38
|
const TRAE_CN_USAGE_URL = "https://api.trae.cn/trae/api/v1/pay/query_user_usage_group_by_session";
|
|
@@ -77,30 +78,46 @@ const TRAE_CN_KG = Buffer.from([
|
|
|
77
78
|
// Path resolution
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
|
|
80
|
-
function
|
|
81
|
-
const override = env[TRAE_CN_HOME_ENV];
|
|
82
|
-
if (typeof override === "string" && override.trim()) {
|
|
83
|
-
return path.resolve(override.trim());
|
|
84
|
-
}
|
|
81
|
+
function traeCnDataParent({ env = process.env, home = os.homedir(), platform = process.platform } = {}) {
|
|
85
82
|
if (platform === "darwin") {
|
|
86
|
-
return path.join(home, "Library", "Application Support"
|
|
83
|
+
return path.join(home, "Library", "Application Support");
|
|
87
84
|
}
|
|
88
85
|
if (platform === "win32") {
|
|
89
|
-
// Windows keeps the CN
|
|
90
|
-
// macOS Application Support layout (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
: path.join(home, "AppData", "Roaming");
|
|
95
|
-
return path.join(appData, TRAE_CN_APP_DIR);
|
|
86
|
+
// Windows keeps the CN app under the roaming profile, mirroring the
|
|
87
|
+
// macOS Application Support layout (<parent>/<app dir>/User/...).
|
|
88
|
+
return typeof env.APPDATA === "string" && env.APPDATA.trim()
|
|
89
|
+
? env.APPDATA.trim()
|
|
90
|
+
: path.join(home, "AppData", "Roaming");
|
|
96
91
|
}
|
|
97
|
-
// No verified CN
|
|
92
|
+
// No verified CN app-data layout on other platforms yet.
|
|
98
93
|
return null;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
function resolveTraeCnStoragePath(options = {}) {
|
|
102
|
-
const
|
|
103
|
-
|
|
97
|
+
const env = options.env ?? process.env;
|
|
98
|
+
const override = env[TRAE_CN_HOME_ENV];
|
|
99
|
+
if (typeof override === "string" && override.trim()) {
|
|
100
|
+
return path.join(path.resolve(override.trim()), "User", "globalStorage", "storage.json");
|
|
101
|
+
}
|
|
102
|
+
const parent = traeCnDataParent({ env, home: options.home ?? os.homedir(), platform: options.platform ?? process.platform });
|
|
103
|
+
if (!parent) return null;
|
|
104
|
+
for (const dir of TRAE_CN_APP_DIRS) {
|
|
105
|
+
const candidate = path.join(parent, dir, "User", "globalStorage", "storage.json");
|
|
106
|
+
try {
|
|
107
|
+
// Any successful stat means this client dir is the discovery hit —
|
|
108
|
+
// including a non-file path (e.g. a directory named storage.json).
|
|
109
|
+
// Hand that to the reader so EISDIR surfaces as TRAE_CN_STORAGE_UNREADABLE
|
|
110
|
+
// instead of silently skipping to the next client or reporting absent.
|
|
111
|
+
fs.statSync(candidate);
|
|
112
|
+
return candidate;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// Permission failures must reach the reader's diagnostics, not
|
|
115
|
+
// masquerade as an absent installation and silently drop an entire
|
|
116
|
+
// usage history (same discovery policy as trae-db.js).
|
|
117
|
+
if (err.code !== "ENOENT" && err.code !== "ENOTDIR") return candidate;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
// ---------------------------------------------------------------------------
|
|
@@ -524,7 +541,7 @@ module.exports = {
|
|
|
524
541
|
TRAE_CN_HOME_ENV,
|
|
525
542
|
TRAE_CN_USAGE_ENV,
|
|
526
543
|
isTraeCnUsageEnabled,
|
|
527
|
-
|
|
544
|
+
TRAE_CN_APP_DIRS,
|
|
528
545
|
TRAE_CN_AUTH_KEY,
|
|
529
546
|
TRAE_CN_USAGE_URL,
|
|
530
547
|
TRAE_CN_USAGE_TYPE,
|
|
@@ -532,7 +549,6 @@ module.exports = {
|
|
|
532
549
|
TRAE_CN_USAGE_MAX_PAGES,
|
|
533
550
|
TRAE_CN_USAGE_DELAY_MS,
|
|
534
551
|
TRAE_CN_MAGIC,
|
|
535
|
-
resolveTraeCnHome,
|
|
536
552
|
resolveTraeCnStoragePath,
|
|
537
553
|
traeCnHardcodedKdfSecret,
|
|
538
554
|
deriveTraeCnKeyIv,
|