tokentracker-cli 1.1.3 → 1.1.5
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 +20 -4
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-dt_UDU_h.js → AchievementBadge-ClUmGxBJ.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-B5DJ3S9l.js → AchievementsPage-BB0DFE3m.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CDxcyig2.js → AchievementsSection-BMCSk63s.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Cp98zNn2.js → BadgeDetailModal-DzJtvuKO.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CR7MiUpo.js → BotAnimated-BviEasW4.js} +1 -1
- package/dashboard/dist/assets/{Card-C8IdDvlg.js → Card-BoHr-wLc.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-COeTlW8z.js → ClawdAnimated-Bwm8BfqY.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-Co9k2CCc.js → CommandPalette-Rb0Gfcg9.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-C6XQeRgw.js → CommunityStatsModal-Ck7R7Q-d.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Ai4182O-.js → ConfirmModal-52Fu9wOi.js} +1 -1
- package/dashboard/dist/assets/{Controls-BbKzN-Pe.js → Controls-YlGr187A.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-BRim2Rt6.js +1 -0
- package/dashboard/dist/assets/{DevicePage-CZbrtJmb.js → DevicePage-3mmhzfgd.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DMl8tAoX.js → DialogClose-D1KfBYUS.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BhWwQSSg.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BJxpp2t-.js → DialogTitle-DKye2nJK.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-hU_MXl6R.js → FadeIn-mJp_ZFst.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CM5RSxxN.js → HeaderGithubStar-B2R_aqp9.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BZKfQyZQ.js → HoverTooltip-ElY02914.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BjDx-zja.js → IpCheckPage-CNzPUe3x.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CAt8BCDv.js → LandingPage-L25-CxXg.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DiCrakjI.js → LeaderboardAvatar-BT7x5wAP.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Dj3pHv6U.js → LeaderboardPage-bPce9gOU.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-DEWbcP9p.js → LeaderboardProfileModal-DPrFGAp6.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CTbWHQkY.js → LeaderboardProfilePage-j2D1pwYO.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-V-98OUhF.js → LimitsPage-CSED1kBx.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-a4PqtUZY.js → LocalOnlyNotice-D6OERWez.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-oD1A2Ru8.js → LoginCard-HgmRdIn8.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-D8aSmxok.js → LoginModal-BSlEmKOV.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DIjhBv2X.js → LoginPage-HQAhowvh.js} +1 -1
- package/dashboard/dist/assets/{PetPage-D7601BTX.js → PetPage-DnbYfu-z.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-aPnmKopO.js → PopoverPopup-DnyVdfB8.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-rwyfcNlT.js → ProviderIcon-D8kgkZlR.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-AF3SBgW2.js → ResetPasswordPage-ChtXGykP.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-fnlScNMT.js → SegmentedControl-dHUpXqFk.js} +1 -1
- package/dashboard/dist/assets/{Select-DDjC3RjB.js → Select-C0tpjNiq.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-VhBWE2O8.js → SelectItemText-C3G_w14k.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BL3qqEsp.js → ServiceStatusPage-D6x4e1r8.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DXYiuP2x.js → SessionsPage-DZH1QMWd.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DTeA_ahr.js → SettingsPage-DGmDb2R4.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-CtTRwVQH.js → SkillsPage-ChloCHPa.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CifK91V7.js → TokenGalaxy-DOC9wlkv.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-ClGAIYoa.js → ToolbarRootContext-BB_d-qdY.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-D1evFHRM.js → TrendMonitor-DVVzYdc7.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Cq4RYJDE.js → WidgetsPage-DHVSyU0i.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-CoC5RJH7.js → WrappedPage-BDi28nRt.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Bnc5aBhq.js +45 -0
- package/dashboard/dist/assets/{agent-logos-QLdA9OeA.js → agent-logos-BiGvZiLq.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BYLgB3bH.js → arrow-up-right-SvD82rok.js} +1 -1
- package/dashboard/dist/assets/{check-DfnIvqZD.js → check-jL5XErEW.js} +1 -1
- package/dashboard/dist/assets/{composite--JllTpIX.js → composite-DM6geXPA.js} +1 -1
- package/dashboard/dist/assets/{copy-ClKVBIgD.js → copy-Dy_GIzj2.js} +1 -1
- package/dashboard/dist/assets/{download-Di1-UFJX.js → download-BW_FS7nX.js} +1 -1
- package/dashboard/dist/assets/{external-link-D7zPVFLO.js → external-link-2RIovvCT.js} +1 -1
- package/dashboard/dist/assets/{flame-BMJTxjVg.js → flame-CIRJIxxA.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DCZM5Nfn.js → format-tokens-CT8ahEK4.js} +1 -1
- package/dashboard/dist/assets/{icons-I615W2IU.js → icons-7J1WD0C-.js} +1 -1
- package/dashboard/dist/assets/{index-BX3CXVdD.js → index-D3iSSyYr.js} +1 -1
- package/dashboard/dist/assets/{index-CwczM3BZ.js → index-Ddb2A6Mx.js} +1 -1
- package/dashboard/dist/assets/{index-BejnmdPe.js → index-_vJl6sF7.js} +1 -1
- package/dashboard/dist/assets/{info-CFJPny5Q.js → info-BjVWcOOC.js} +1 -1
- package/dashboard/dist/assets/{link-2-D1ydWh_w.js → link-2-CyRWnHWE.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Cq-J-OFg.js → loader-circle-DUpG5zah.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-kIoSq57I.js → maximize-2-BVGCRkFZ.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CLojthuF.js → modifiers.esm-CZI7rED4.js} +1 -1
- package/dashboard/dist/assets/module-WSx8viXK.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-DX2qWHYV.js → monitor-smartphone-DfBtd01T.js} +1 -1
- package/dashboard/dist/assets/{plus-B-JEpo3V.js → plus-CvbLCvjV.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DJDdrTAS.js → provider-display-DC46GLrv.js} +2 -2
- package/dashboard/dist/assets/{react-Ctoe8iLl.js → react-mYr3J8Vw.js} +1 -1
- package/dashboard/dist/assets/{search-BTSQrlW-.js → search-CFaS1rSj.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DKQyM8h7.js → skills-api-B1N6seFa.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-pOEL13xU.js → sortable.esm-BE2GiQaC.js} +1 -1
- package/dashboard/dist/assets/{store-B2RplzvW.js → store-DwBbExah.js} +1 -1
- package/dashboard/dist/assets/{terminal-BH_6_EW_.js → terminal-Bl2BuO9x.js} +1 -1
- package/dashboard/dist/assets/{trash-CC444Mz6.js → trash-D8beb27Y.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CBXLdD_j.js → use-community-stats-DJG1DsXR.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DMuPfZQI.js → use-latest-request-guard-DYK8iyOM.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CmsGTBA4.js → use-limits-display-prefs-BGmcFVX3.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BNJNpXjB.js → use-ordered-list-Cqia_VIq.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CmCk9Sb5.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-g_0ISxe1.js → use-session-efficiency-pref-Mj3OeVGZ.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D36BHcXk.js → use-transform-D4BQIBHx.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Dxw3vzsD.js → use-usage-limits-BFrO7Mtq.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-V2FE4TMo.js → useCurrency-CYJavR2h.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CEOwvzMt.js → useTokenFormat-CKruI7SP.js} +1 -1
- package/dashboard/dist/assets/{zap-BS7ZU76I.js → zap-a82fG76c.js} +1 -1
- package/dashboard/dist/brand-logos/trae.svg +1 -0
- package/dashboard/dist/index.html +13 -11
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +6 -4
- package/src/commands/init.js +9 -7
- package/src/commands/status.js +66 -11
- package/src/commands/sync.js +115 -11
- package/src/lib/codex-context-breakdown.js +3 -1
- package/src/lib/diagnostics.js +20 -1
- package/src/lib/doctor.js +30 -0
- package/src/lib/pricing/curated-overrides.json +7 -0
- package/src/lib/pricing/matcher.js +5 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +197 -18
- package/src/lib/scan-roots.js +277 -0
- package/src/lib/session-analytics.js +98 -16
- package/src/lib/trae-db.js +127 -0
- package/src/lib/trae-snapshot.js +230 -0
- package/src/lib/trae-sqlite.js +143 -0
- package/src/lib/trae-usage.js +121 -0
- package/dashboard/dist/assets/DashboardPage-DTcVGG4e.js +0 -1
- package/dashboard/dist/assets/DialogDescription-haPz7CX9.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BDugA6I0.js +0 -45
- package/dashboard/dist/assets/module-DtWfRKM4.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-CNpG-Yoi.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -47,6 +47,8 @@ const {
|
|
|
47
47
|
buildDevinUsageEvents,
|
|
48
48
|
} = require("./devin-usage");
|
|
49
49
|
const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
|
|
50
|
+
const { resolveTraeDbPaths, readTraeUsageRows } = require("./trae-db");
|
|
51
|
+
const { normalizeTraeUsage, normalizeTraeModel, traeTimestamp } = require("./trae-usage");
|
|
50
52
|
|
|
51
53
|
const DEFAULT_SOURCE = "codex";
|
|
52
54
|
const DEFAULT_MODEL = "unknown";
|
|
@@ -19662,12 +19664,13 @@ function mergeGrokUsagePrecision(current, next) {
|
|
|
19662
19664
|
return "mixed";
|
|
19663
19665
|
}
|
|
19664
19666
|
|
|
19665
|
-
function
|
|
19667
|
+
function clearSourceHourlyBuckets(hourlyState, source) {
|
|
19666
19668
|
if (!hourlyState || typeof hourlyState !== "object") return;
|
|
19669
|
+
const prefix = `${source}${BUCKET_SEPARATOR}`;
|
|
19667
19670
|
const buckets = hourlyState.buckets && typeof hourlyState.buckets === "object" ? hourlyState.buckets : null;
|
|
19668
19671
|
if (buckets) {
|
|
19669
19672
|
for (const key of Object.keys(buckets)) {
|
|
19670
|
-
if (key.startsWith(
|
|
19673
|
+
if (key.startsWith(prefix)) delete buckets[key];
|
|
19671
19674
|
}
|
|
19672
19675
|
}
|
|
19673
19676
|
const groupQueued =
|
|
@@ -19676,12 +19679,12 @@ function clearGrokHourlyBuckets(hourlyState) {
|
|
|
19676
19679
|
: null;
|
|
19677
19680
|
if (groupQueued) {
|
|
19678
19681
|
for (const key of Object.keys(groupQueued)) {
|
|
19679
|
-
if (key.startsWith(
|
|
19682
|
+
if (key.startsWith(prefix)) delete groupQueued[key];
|
|
19680
19683
|
}
|
|
19681
19684
|
}
|
|
19682
19685
|
}
|
|
19683
19686
|
|
|
19684
|
-
async function
|
|
19687
|
+
async function retractStaleSourceQueueRows(queuePath, source, keepKeys) {
|
|
19685
19688
|
if (!queuePath) return 0;
|
|
19686
19689
|
let raw = "";
|
|
19687
19690
|
try {
|
|
@@ -19691,7 +19694,7 @@ async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
|
19691
19694
|
throw error;
|
|
19692
19695
|
}
|
|
19693
19696
|
|
|
19694
|
-
const
|
|
19697
|
+
const latestRows = new Map();
|
|
19695
19698
|
for (const line of raw.split("\n")) {
|
|
19696
19699
|
if (!line.trim()) continue;
|
|
19697
19700
|
let row;
|
|
@@ -19700,21 +19703,21 @@ async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
|
19700
19703
|
} catch {
|
|
19701
19704
|
continue;
|
|
19702
19705
|
}
|
|
19703
|
-
if ((row?.source || "") !==
|
|
19706
|
+
if ((row?.source || "") !== source) continue;
|
|
19704
19707
|
const model = normalizeModelInput(row.model) || DEFAULT_MODEL;
|
|
19705
19708
|
const hourStart = typeof row.hour_start === "string" ? row.hour_start : null;
|
|
19706
19709
|
if (!hourStart) continue;
|
|
19707
|
-
|
|
19710
|
+
latestRows.set(bucketKey(source, model, hourStart), { model, hour_start: hourStart, row });
|
|
19708
19711
|
}
|
|
19709
19712
|
|
|
19710
19713
|
const zero = initTotals();
|
|
19711
19714
|
const lines = [];
|
|
19712
|
-
for (const [key, entry] of
|
|
19715
|
+
for (const [key, entry] of latestRows.entries()) {
|
|
19713
19716
|
if (keepKeys.has(key)) continue;
|
|
19714
19717
|
if (totalsKey(entry.row) === totalsKey(zero)) continue;
|
|
19715
19718
|
lines.push(
|
|
19716
19719
|
JSON.stringify({
|
|
19717
|
-
source
|
|
19720
|
+
source,
|
|
19718
19721
|
model: entry.model,
|
|
19719
19722
|
hour_start: entry.hour_start,
|
|
19720
19723
|
...zero,
|
|
@@ -19814,7 +19817,7 @@ async function parseGrokBuildIncremental({
|
|
|
19814
19817
|
legacySeen: true,
|
|
19815
19818
|
};
|
|
19816
19819
|
}
|
|
19817
|
-
|
|
19820
|
+
clearSourceHourlyBuckets(hourlyState, "grok");
|
|
19818
19821
|
} else {
|
|
19819
19822
|
sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
|
|
19820
19823
|
}
|
|
@@ -20195,7 +20198,7 @@ async function parseGrokBuildIncremental({
|
|
|
20195
20198
|
if (!key.startsWith("grok|") || !bucket?.totals) continue;
|
|
20196
20199
|
keepKeys.add(key);
|
|
20197
20200
|
}
|
|
20198
|
-
const retracted = await
|
|
20201
|
+
const retracted = await retractStaleSourceQueueRows(queuePath, "grok", keepKeys);
|
|
20199
20202
|
bucketsQueued += retracted;
|
|
20200
20203
|
}
|
|
20201
20204
|
|
|
@@ -21687,13 +21690,188 @@ function isCjkCodePoint(code) {
|
|
|
21687
21690
|
// ── Trae SOLO (ByteDance AI IDE) ─────────────────────────────────────────────
|
|
21688
21691
|
// https://www.trae.ai
|
|
21689
21692
|
//
|
|
21690
|
-
//
|
|
21691
|
-
//
|
|
21692
|
-
//
|
|
21693
|
-
//
|
|
21694
|
-
//
|
|
21695
|
-
//
|
|
21696
|
-
|
|
21693
|
+
// International TRAE usage comes from local encrypted chat_turn metadata.
|
|
21694
|
+
//
|
|
21695
|
+
// The ledger keeps one compact tuple per turn, keyed by a 128-bit digest, so a
|
|
21696
|
+
// correction can retract exactly what the turn contributed even after TRAE
|
|
21697
|
+
// deletes its history. total_tokens is the five token columns plus any input
|
|
21698
|
+
// without a cache split, which is counted but unpriced (normalizeTraeUsage).
|
|
21699
|
+
const TRAE_CURSOR_VERSION = 2;
|
|
21700
|
+
const HALF_HOUR_MS = 30 * 60 * 1000;
|
|
21701
|
+
const [T_SESSION, T_MODEL, T_BUCKET, T_MODIFIED, T_STORE, T_INPUT, T_CACHED, T_WRITTEN,
|
|
21702
|
+
T_OUTPUT, T_REASONING, T_CONVERSATIONS, T_ESTIMATED, T_UNPRICED] = Array.from({ length: 13 }, (_, i) => i);
|
|
21703
|
+
|
|
21704
|
+
function packTraeTurn({ session, model, bucketStart, totals, modifiedAt, store }) {
|
|
21705
|
+
return [session, model, Date.parse(bucketStart) / HALF_HOUR_MS, modifiedAt, store,
|
|
21706
|
+
totals.input_tokens, totals.cached_input_tokens, totals.cache_creation_input_tokens,
|
|
21707
|
+
totals.output_tokens, totals.reasoning_output_tokens, totals.conversation_count,
|
|
21708
|
+
totals.usage_precision === "estimated" ? 1 : 0, totals.unpriced_input_tokens || 0];
|
|
21709
|
+
}
|
|
21710
|
+
|
|
21711
|
+
function unpackTraeTotals(turn) {
|
|
21712
|
+
const totals = {
|
|
21713
|
+
input_tokens: turn[T_INPUT],
|
|
21714
|
+
cached_input_tokens: turn[T_CACHED],
|
|
21715
|
+
cache_creation_input_tokens: turn[T_WRITTEN],
|
|
21716
|
+
output_tokens: turn[T_OUTPUT],
|
|
21717
|
+
reasoning_output_tokens: turn[T_REASONING],
|
|
21718
|
+
total_tokens: turn[T_INPUT] + turn[T_CACHED] + turn[T_WRITTEN] + turn[T_OUTPUT] + turn[T_REASONING]
|
|
21719
|
+
+ turn[T_UNPRICED],
|
|
21720
|
+
conversation_count: turn[T_CONVERSATIONS],
|
|
21721
|
+
};
|
|
21722
|
+
if (turn[T_ESTIMATED]) totals.usage_precision = "estimated";
|
|
21723
|
+
return totals;
|
|
21724
|
+
}
|
|
21725
|
+
|
|
21726
|
+
function traeTurnBucketStart(turn) {
|
|
21727
|
+
return new Date(turn[T_BUCKET] * HALF_HOUR_MS).toISOString();
|
|
21728
|
+
}
|
|
21729
|
+
|
|
21730
|
+
async function parseTraeIncremental({
|
|
21731
|
+
dbPaths,
|
|
21732
|
+
cursors,
|
|
21733
|
+
queuePath,
|
|
21734
|
+
env = process.env,
|
|
21735
|
+
onProgress,
|
|
21736
|
+
readUsageRows = readTraeUsageRows,
|
|
21737
|
+
} = {}) {
|
|
21738
|
+
const paths = dbPaths || resolveTraeDbPaths(env);
|
|
21739
|
+
const hourlyState = normalizeHourlyState(cursors.hourly);
|
|
21740
|
+
// Queue writes and reads may fail. Publish cursor changes only after the
|
|
21741
|
+
// append succeeds, including copies of the mutable bucket totals.
|
|
21742
|
+
hourlyState.buckets = Object.fromEntries(Object.entries(hourlyState.buckets).map(([key, bucket]) => [
|
|
21743
|
+
key, key.startsWith("trae|") ? { ...bucket, totals: { ...bucket.totals } } : bucket,
|
|
21744
|
+
]));
|
|
21745
|
+
hourlyState.groupQueued = { ...hourlyState.groupQueued };
|
|
21746
|
+
// A ledger with another TRAE_CURSOR_VERSION cannot retract what it
|
|
21747
|
+
// contributed, so rebuild the TRAE buckets from the stores instead of
|
|
21748
|
+
// adding every turn on top of them.
|
|
21749
|
+
const prior = cursors.trae?.version === TRAE_CURSOR_VERSION ? cursors.trae : null;
|
|
21750
|
+
const rebuilding = !prior && Boolean(cursors.trae);
|
|
21751
|
+
if (rebuilding) clearSourceHourlyBuckets(hourlyState, "trae");
|
|
21752
|
+
const turns = { ...prior?.turns };
|
|
21753
|
+
const databases = { ...prior?.databases };
|
|
21754
|
+
const stores = [...(prior?.stores || [])];
|
|
21755
|
+
const touchedBuckets = new Set();
|
|
21756
|
+
const conversations = new Set(Object.values(turns)
|
|
21757
|
+
.filter((turn) => turn[T_CONVERSATIONS] > 0).map((turn) => turn[T_SESSION]));
|
|
21758
|
+
// 128-bit digests: identifiers are hashed before they are stored.
|
|
21759
|
+
const digest = (value) => crypto.createHash("sha256").update(value).digest().subarray(0, 16).toString("base64url");
|
|
21760
|
+
let recordsProcessed = 0;
|
|
21761
|
+
let eventsAggregated = 0;
|
|
21762
|
+
let recordsSkipped = 0;
|
|
21763
|
+
let estimatedRecords = 0;
|
|
21764
|
+
let unpricedRecords = 0;
|
|
21765
|
+
const errors = [];
|
|
21766
|
+
for (const dbPath of [...new Set(paths)]) {
|
|
21767
|
+
const databaseKey = digest(path.resolve(dbPath));
|
|
21768
|
+
let store = stores.indexOf(databaseKey);
|
|
21769
|
+
if (store < 0) store = stores.push(databaseKey) - 1;
|
|
21770
|
+
let rows;
|
|
21771
|
+
let fingerprint;
|
|
21772
|
+
let finalFingerprint;
|
|
21773
|
+
try {
|
|
21774
|
+
// existsSync also returns false for permission errors. Let stat/read
|
|
21775
|
+
// report inaccessible stores instead of treating them as absent.
|
|
21776
|
+
fssync.statSync(dbPath);
|
|
21777
|
+
fingerprint = devinSqliteFingerprint(dbPath);
|
|
21778
|
+
if (sameSqliteFingerprint(fingerprint, databases[databaseKey])) continue;
|
|
21779
|
+
rows = await readUsageRows(dbPath, { env });
|
|
21780
|
+
finalFingerprint = devinSqliteFingerprint(dbPath);
|
|
21781
|
+
} catch (err) {
|
|
21782
|
+
// One install can be locked, corrupt, or use a different key while
|
|
21783
|
+
// another remains readable. Its prior fingerprint stays retryable;
|
|
21784
|
+
// successful stores still publish together after the queue append.
|
|
21785
|
+
errors.push({ database: dbPath, message: err?.message || String(err) });
|
|
21786
|
+
continue;
|
|
21787
|
+
}
|
|
21788
|
+
for (const row of rows) {
|
|
21789
|
+
recordsProcessed += 1;
|
|
21790
|
+
const totals = normalizeTraeUsage(row.usage, { model: row.model });
|
|
21791
|
+
const timestamp = traeTimestamp(row.created_at);
|
|
21792
|
+
const bucketStart = timestamp && toUtcHalfHourStart(timestamp);
|
|
21793
|
+
const rowId = row.turn_id || row.id;
|
|
21794
|
+
if (!totals || !bucketStart || rowId == null || rowId === "") {
|
|
21795
|
+
recordsSkipped += 1;
|
|
21796
|
+
continue;
|
|
21797
|
+
}
|
|
21798
|
+
const session = digest(String(row.session_id || `${databaseKey}:${rowId}`));
|
|
21799
|
+
// Stable turn ids deduplicate copies across the two international app
|
|
21800
|
+
// stores. Numeric SQLite ids are only unique within their database.
|
|
21801
|
+
const identity = digest(JSON.stringify(row.turn_id
|
|
21802
|
+
? [session, row.turn_id] : [databaseKey, rowId]));
|
|
21803
|
+
const previous = turns[identity];
|
|
21804
|
+
const modifiedAt = Date.parse(traeTimestamp(row.updated_at) || timestamp);
|
|
21805
|
+
// A copied turn in another install can lag behind the original. Never
|
|
21806
|
+
// let an unrelated rescan of that stale store retract newer usage.
|
|
21807
|
+
// Without a newer timestamp, conflicting copies retain their owner.
|
|
21808
|
+
if (previous && previous[T_STORE] !== store && modifiedAt <= previous[T_MODIFIED]) continue;
|
|
21809
|
+
if (!previous && totals.total_tokens === 0) continue;
|
|
21810
|
+
if (totals.usage_precision === "estimated") estimatedRecords += 1;
|
|
21811
|
+
if (totals.unpriced_input_tokens > 0) unpricedRecords += 1;
|
|
21812
|
+
totals.conversation_count = previous
|
|
21813
|
+
? previous[T_CONVERSATIONS]
|
|
21814
|
+
: conversations.has(session) ? 0 : 1;
|
|
21815
|
+
conversations.add(session);
|
|
21816
|
+
const model = normalizeTraeModel(row.model);
|
|
21817
|
+
const next = packTraeTurn({ session, model, bucketStart, totals, modifiedAt, store });
|
|
21818
|
+
if (previous && next.every((value, i) => i === T_MODIFIED || i === T_STORE || value === previous[i])) {
|
|
21819
|
+
turns[identity] = next;
|
|
21820
|
+
continue;
|
|
21821
|
+
}
|
|
21822
|
+
if (previous) {
|
|
21823
|
+
const previousBucket = traeTurnBucketStart(previous);
|
|
21824
|
+
const old = getHourlyBucket(hourlyState, "trae", previous[T_MODEL], previousBucket);
|
|
21825
|
+
subtractTotals(old.totals, unpackTraeTotals(previous));
|
|
21826
|
+
touchedBuckets.add(bucketKey("trae", previous[T_MODEL], previousBucket));
|
|
21827
|
+
}
|
|
21828
|
+
const bucket = getHourlyBucket(hourlyState, "trae", model, bucketStart);
|
|
21829
|
+
addTotals(bucket.totals, totals);
|
|
21830
|
+
touchedBuckets.add(bucketKey("trae", model, bucketStart));
|
|
21831
|
+
turns[identity] = next;
|
|
21832
|
+
eventsAggregated += 1;
|
|
21833
|
+
}
|
|
21834
|
+
// A read that races with a writer must be retried on the next sync.
|
|
21835
|
+
databases[databaseKey] = sameSqliteFingerprint(fingerprint, finalFingerprint)
|
|
21836
|
+
? fingerprint : null;
|
|
21837
|
+
if (typeof onProgress === "function") {
|
|
21838
|
+
onProgress({ recordsProcessed, eventsAggregated, bucketsQueued: touchedBuckets.size });
|
|
21839
|
+
}
|
|
21840
|
+
}
|
|
21841
|
+
// Rebuild precision from the retained contributions, including unchanged
|
|
21842
|
+
// stores. A correction can replace the last estimated turn with reported
|
|
21843
|
+
// usage without changing numeric totals, or move it to another bucket.
|
|
21844
|
+
const bucketPrecisions = new Map();
|
|
21845
|
+
for (const turn of Object.values(turns)) {
|
|
21846
|
+
const key = bucketKey("trae", turn[T_MODEL], traeTurnBucketStart(turn));
|
|
21847
|
+
if (!touchedBuckets.has(key) || unpackTraeTotals(turn).total_tokens === 0) continue;
|
|
21848
|
+
bucketPrecisions.set(key, mergeGrokUsagePrecision(
|
|
21849
|
+
bucketPrecisions.get(key), turn[T_ESTIMATED] ? "estimated" : "reported",
|
|
21850
|
+
));
|
|
21851
|
+
}
|
|
21852
|
+
for (const key of touchedBuckets) {
|
|
21853
|
+
const precision = bucketPrecisions.get(key);
|
|
21854
|
+
hourlyState.buckets[key].usage_precision = precision === "reported" ? null : precision || null;
|
|
21855
|
+
}
|
|
21856
|
+
// A rebuild is all or nothing. Buckets rebuilt from a subset of the stores
|
|
21857
|
+
// would replace shared buckets with partial totals, and each retry would
|
|
21858
|
+
// append every row again. Keep the old hourly state and ledger instead.
|
|
21859
|
+
if (rebuilding && errors.length) {
|
|
21860
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued: 0, recordsSkipped, estimatedRecords, unpricedRecords, errors };
|
|
21861
|
+
}
|
|
21862
|
+
await ensureDir(path.dirname(queuePath));
|
|
21863
|
+
let bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
21864
|
+
cursors.hourly = hourlyState;
|
|
21865
|
+
if (rebuilding) {
|
|
21866
|
+
// The old ledger may have filed a turn under another bucket key (model or
|
|
21867
|
+
// half-hour); retract those rows so the dashboard does not count it twice.
|
|
21868
|
+
const keepKeys = new Set(Object.keys(hourlyState.buckets).filter((key) => key.startsWith("trae|")));
|
|
21869
|
+
bucketsQueued += await retractStaleSourceQueueRows(queuePath, "trae", keepKeys);
|
|
21870
|
+
}
|
|
21871
|
+
cursors.trae = { version: TRAE_CURSOR_VERSION, stores, databases, turns };
|
|
21872
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued, recordsSkipped, estimatedRecords, unpricedRecords, errors };
|
|
21873
|
+
}
|
|
21874
|
+
|
|
21697
21875
|
// Ordered candidate app-dir names. The CN IDE build installs as "Trae CN" on
|
|
21698
21876
|
// Windows; the international build installs as "TRAE SOLO". Both expose the same
|
|
21699
21877
|
// iCubeServerData entitlement key, so either is a valid Trae IDE install.
|
|
@@ -24192,6 +24370,7 @@ async function parseCommandCodeIncremental({
|
|
|
24192
24370
|
}
|
|
24193
24371
|
|
|
24194
24372
|
module.exports = {
|
|
24373
|
+
parseTraeIncremental,
|
|
24195
24374
|
listRolloutFiles,
|
|
24196
24375
|
listRolloutFilesDeep,
|
|
24197
24376
|
codexSessionIdFromPath,
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Persistent extra scan roots for Codex and Claude Code (#657).
|
|
4
|
+
//
|
|
5
|
+
// Agent harnesses run Codex / Claude Code with a private home
|
|
6
|
+
// (CODEX_HOME=..., CLAUDE_CONFIG_DIR=...). Those sessions were invisible to
|
|
7
|
+
// TokenTracker unless the sync happened to be spawned by a process that
|
|
8
|
+
// inherited the same environment: a hook-fired sync saw them, the native app's
|
|
9
|
+
// background refresh did not. Worse, the two producers then disagreed about
|
|
10
|
+
// where a rollout's cursor lives (the cursor store classifies a rollout as a
|
|
11
|
+
// Codex path — per-day shard — only when it sits under one of ITS codexRoots,
|
|
12
|
+
// everything else goes to core), never saw each other's cursor, and re-parsed
|
|
13
|
+
// the same file from byte 0 on every alternation (#639: one 30-minute bucket
|
|
14
|
+
// re-emitted 138 times, 59B tokens for a 1.2B day).
|
|
15
|
+
//
|
|
16
|
+
// So roots come from ONE persistent list that every producer reads, and the
|
|
17
|
+
// cursor store's codexRoots are derived from that same list.
|
|
18
|
+
//
|
|
19
|
+
// Per provider, in order (first occurrence wins; later duplicates by realpath
|
|
20
|
+
// are dropped):
|
|
21
|
+
// 1. the roots the caller already scans (native home, WSL install)
|
|
22
|
+
// 2. the spawning process's implicit root — CODEX_HOME / CLAUDE_CONFIG_DIR
|
|
23
|
+
// 3. config.scanRoots.<provider>[] from ~/.tokentracker/tracker/config.json
|
|
24
|
+
//
|
|
25
|
+
// Config shape:
|
|
26
|
+
// { "scanRoots": { "codex": ["~/.local/share/agent/codex"],
|
|
27
|
+
// "claude": ["~/.local/share/agent/claude", "~/.claude-native"] } }
|
|
28
|
+
|
|
29
|
+
const fs = require("node:fs");
|
|
30
|
+
const os = require("node:os");
|
|
31
|
+
const path = require("node:path");
|
|
32
|
+
|
|
33
|
+
const { readJson } = require("./fs");
|
|
34
|
+
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
35
|
+
|
|
36
|
+
const PROVIDERS = Object.freeze({
|
|
37
|
+
codex: Object.freeze({ envVar: "CODEX_HOME" }),
|
|
38
|
+
claude: Object.freeze({ envVar: "CLAUDE_CONFIG_DIR" }),
|
|
39
|
+
});
|
|
40
|
+
const PROVIDER_NAMES = Object.freeze(Object.keys(PROVIDERS));
|
|
41
|
+
|
|
42
|
+
// Turn a configured root into a stable absolute path. `~` expands against
|
|
43
|
+
// `home`, and a RELATIVE entry is resolved against `home` too — never against
|
|
44
|
+
// process.cwd(), which differs between a hook-fired sync, the CLI and the
|
|
45
|
+
// desktop app and would make producers scan different directories.
|
|
46
|
+
function expandHome(value, home) {
|
|
47
|
+
if (typeof value !== "string") return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (!trimmed) return null;
|
|
50
|
+
if (trimmed === "~") return path.resolve(home);
|
|
51
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
52
|
+
return path.resolve(home, trimmed.slice(2));
|
|
53
|
+
}
|
|
54
|
+
return path.isAbsolute(trimmed) ? path.resolve(trimmed) : path.resolve(home, trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The spawning process's implicit root — CODEX_HOME / CLAUDE_CONFIG_DIR —
|
|
58
|
+
// normalized ONCE, the same way configured entries are: `~` and relative
|
|
59
|
+
// values are anchored to `home`, never to process.cwd(). Every producer
|
|
60
|
+
// (sync discovery, the cursor store's codexRoots, status, diagnostics, the
|
|
61
|
+
// session browser) must use this value, or a relative CODEX_HOME such as
|
|
62
|
+
// `profiles/codex` is scanned under one directory and cursor-classified
|
|
63
|
+
// under another, and the same rollout replays. Returns null when unset.
|
|
64
|
+
function resolveEnvRoot(provider, { env = process.env, home = os.homedir() } = {}) {
|
|
65
|
+
const spec = PROVIDERS[provider];
|
|
66
|
+
if (!spec) return null;
|
|
67
|
+
const raw = env && typeof env[spec.envVar] === "string" ? env[spec.envVar] : "";
|
|
68
|
+
return expandHome(raw, home);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Accepts { codex: string | string[], claude: string | string[] }. Anything
|
|
72
|
+
// else — wrong type, empty strings, unknown providers — is ignored rather than
|
|
73
|
+
// failing the sync: a malformed config must not stop collection.
|
|
74
|
+
function normalizeScanRootsConfig(raw) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const provider of PROVIDER_NAMES) out[provider] = [];
|
|
77
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
78
|
+
for (const provider of PROVIDER_NAMES) {
|
|
79
|
+
const value = raw[provider];
|
|
80
|
+
const list = Array.isArray(value) ? value : (typeof value === "string" ? [value] : []);
|
|
81
|
+
for (const entry of list) {
|
|
82
|
+
if (typeof entry !== "string" || !entry.trim()) continue;
|
|
83
|
+
out[provider].push(entry.trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// { exists, error }: `exists` is true only for a directory that can be
|
|
90
|
+
// LISTED, not merely stat'ed — a stat-able directory whose listing fails
|
|
91
|
+
// (mode 000, ACL) would otherwise be admitted to the scan, yield no files,
|
|
92
|
+
// read as "present" in status/doctor, and slip past the deferred-repair
|
|
93
|
+
// guard in sync. ENOENT and ENOTDIR (or a regular file in a directory's
|
|
94
|
+
// place) mean the root is absent (error null); any other failure (EACCES,
|
|
95
|
+
// ELOOP, EIO, ...) is reported as `error` so callers can say "unreadable"
|
|
96
|
+
// instead of "missing".
|
|
97
|
+
function scanRootDirState(target, deps = {}) {
|
|
98
|
+
const statSync = deps.statSync || fs.statSync;
|
|
99
|
+
const opendirSync = deps.opendirSync || fs.opendirSync;
|
|
100
|
+
try {
|
|
101
|
+
if (!statSync(target).isDirectory()) return { exists: false, error: null };
|
|
102
|
+
// Probe listability without reading entries; large roots stay cheap.
|
|
103
|
+
opendirSync(target).closeSync();
|
|
104
|
+
return { exists: true, error: null };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const code = e && typeof e.code === "string" ? e.code : "EUNKNOWN";
|
|
107
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { exists: false, error: null };
|
|
108
|
+
return { exists: false, error: code };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isDirectorySync(target, deps) {
|
|
113
|
+
return scanRootDirState(target, deps).exists;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function realpathOrSelf(target, deps) {
|
|
117
|
+
const realpathSync = deps.realpathSync || fs.realpathSync;
|
|
118
|
+
try {
|
|
119
|
+
return realpathSync(target);
|
|
120
|
+
} catch {
|
|
121
|
+
return target;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function identityKey(target, deps) {
|
|
126
|
+
const resolved = path.resolve(target);
|
|
127
|
+
const real = realpathOrSelf(resolved, deps);
|
|
128
|
+
return real.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Drop later entries that resolve (realpath) to a directory already listed.
|
|
132
|
+
// Used at the projects/ level too: two Claude profiles may symlink one
|
|
133
|
+
// projects/ dir, and reading it under both spellings would double-parse every
|
|
134
|
+
// file, leaving correctness to the bounded claudeHashes layer.
|
|
135
|
+
function dedupeDirsByRealpath(dirs, deps = {}) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const dir of Array.isArray(dirs) ? dirs : []) {
|
|
139
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
140
|
+
const key = identityKey(dir, deps);
|
|
141
|
+
if (seen.has(key)) continue;
|
|
142
|
+
seen.add(key);
|
|
143
|
+
out.push(dir);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Resolve the full, ordered, deduplicated root list per provider.
|
|
149
|
+
//
|
|
150
|
+
// base: { codex: string[], claude: string[] } — roots the caller scans
|
|
151
|
+
// anyway (native home, WSL install); they keep their position and
|
|
152
|
+
// are labelled origin "native".
|
|
153
|
+
// env: CODEX_HOME / CLAUDE_CONFIG_DIR become origin "env" unless already
|
|
154
|
+
// covered by base.
|
|
155
|
+
// config.scanRoots.<provider>[] become origin "config".
|
|
156
|
+
//
|
|
157
|
+
// Every entry: { path, realPath, exists, error, origin }. `exists` is a
|
|
158
|
+
// readable-directory check and `error` carries a stat error code other than
|
|
159
|
+
// absence (see scanRootDirState); non-existent or unreadable configured roots
|
|
160
|
+
// are kept in the list (so cursor-path classification stays stable while a
|
|
161
|
+
// volume is unmounted) and callers that walk directories skip them via
|
|
162
|
+
// `exists`.
|
|
163
|
+
function resolveScanRoots({
|
|
164
|
+
home = os.homedir(),
|
|
165
|
+
env = process.env,
|
|
166
|
+
config = null,
|
|
167
|
+
base = {},
|
|
168
|
+
deps = {},
|
|
169
|
+
} = {}) {
|
|
170
|
+
const configured = normalizeScanRootsConfig(config && typeof config === "object" ? config.scanRoots : null);
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const provider of PROVIDER_NAMES) {
|
|
173
|
+
const candidates = [];
|
|
174
|
+
for (const root of Array.isArray(base[provider]) ? base[provider] : []) {
|
|
175
|
+
if (typeof root === "string" && root.trim()) candidates.push({ raw: root, origin: "native" });
|
|
176
|
+
}
|
|
177
|
+
const envRoot = resolveEnvRoot(provider, { env, home });
|
|
178
|
+
if (envRoot) candidates.push({ raw: envRoot, origin: "env" });
|
|
179
|
+
for (const root of configured[provider]) candidates.push({ raw: root, origin: "config" });
|
|
180
|
+
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const entries = [];
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
const expanded = expandHome(candidate.raw, home);
|
|
185
|
+
if (!expanded) continue;
|
|
186
|
+
const resolved = expanded;
|
|
187
|
+
const key = identityKey(resolved, deps);
|
|
188
|
+
if (seen.has(key)) continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
const state = scanRootDirState(resolved, deps);
|
|
191
|
+
entries.push({
|
|
192
|
+
path: resolved,
|
|
193
|
+
realPath: realpathOrSelf(resolved, deps),
|
|
194
|
+
exists: state.exists,
|
|
195
|
+
error: state.error,
|
|
196
|
+
origin: candidate.origin,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
out[provider] = entries;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Append `extraDirs` to `baseDirs`, dropping any extra that resolves (realpath)
|
|
205
|
+
// to a directory already present. Base entries are never collapsed against
|
|
206
|
+
// each other: the native + WSL pair is listed on purpose (see sync.js) and
|
|
207
|
+
// their dedup is handled downstream by file-identity hashes.
|
|
208
|
+
function appendUniqueDirs(baseDirs, extraDirs, deps = {}) {
|
|
209
|
+
const out = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const dir of Array.isArray(baseDirs) ? baseDirs : []) {
|
|
212
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
213
|
+
seen.add(identityKey(dir, deps));
|
|
214
|
+
out.push(dir);
|
|
215
|
+
}
|
|
216
|
+
for (const dir of Array.isArray(extraDirs) ? extraDirs : []) {
|
|
217
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
218
|
+
const key = identityKey(dir, deps);
|
|
219
|
+
if (seen.has(key)) continue;
|
|
220
|
+
seen.add(key);
|
|
221
|
+
out.push(dir);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Roots beyond what the caller already scans: the "env" and "config" entries
|
|
227
|
+
// that exist on disk. Paths only, in list order.
|
|
228
|
+
function extraScanRootPaths(entries) {
|
|
229
|
+
return (Array.isArray(entries) ? entries : [])
|
|
230
|
+
.filter((entry) => entry && entry.origin !== "native" && entry.exists)
|
|
231
|
+
.map((entry) => entry.path);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasAnyScanChild(root, children, deps = {}) {
|
|
235
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
236
|
+
if (isDirectorySync(path.join(root, child), deps)) return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Human label for status / doctor output.
|
|
242
|
+
function describeScanRootOrigin(entry, provider) {
|
|
243
|
+
if (!entry || entry.origin === "native") return "native";
|
|
244
|
+
if (entry.origin === "env") return PROVIDERS[provider]?.envVar || "env";
|
|
245
|
+
return "scanRoots";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// "" for a readable root, " (missing)" for an absent one, " (unreadable: CODE)"
|
|
249
|
+
// for a stat failure other than absence.
|
|
250
|
+
function describeScanRootState(entry) {
|
|
251
|
+
if (!entry || entry.exists) return "";
|
|
252
|
+
return entry.error ? ` (unreadable: ${entry.error})` : " (missing)";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// config.scanRoots as persisted in ~/.tokentracker/tracker/config.json for
|
|
256
|
+
// callers that do not already hold the config (the session browser).
|
|
257
|
+
async function loadScanRootsConfig({ home = os.homedir() } = {}) {
|
|
258
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
259
|
+
const config = await readJson(path.join(trackerDir, "config.json")).catch(() => null);
|
|
260
|
+
return normalizeScanRootsConfig(config && typeof config === "object" ? config.scanRoots : null);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
PROVIDER_NAMES,
|
|
265
|
+
appendUniqueDirs,
|
|
266
|
+
dedupeDirsByRealpath,
|
|
267
|
+
describeScanRootOrigin,
|
|
268
|
+
describeScanRootState,
|
|
269
|
+
expandHome,
|
|
270
|
+
extraScanRootPaths,
|
|
271
|
+
hasAnyScanChild,
|
|
272
|
+
loadScanRootsConfig,
|
|
273
|
+
normalizeScanRootsConfig,
|
|
274
|
+
resolveEnvRoot,
|
|
275
|
+
resolveScanRoots,
|
|
276
|
+
scanRootDirState,
|
|
277
|
+
};
|