tokentracker-cli 1.3.4 → 1.3.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 -5
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +6 -5
- package/README.zh-CN.md +3 -3
- package/dashboard/dist/assets/{AchievementBadge-CKjtQp-U.js → AchievementBadge-Dazs3pEa.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DpOXrabE.js → AchievementsPage-LWnxZvmw.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DiXyIoa0.js → AchievementsSection-BLqczmU9.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CILTTkPF.js → BadgeDetailModal-B_n4aj2S.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CrBHWiRi.js → BotAnimated-KDWI_YcZ.js} +1 -1
- package/dashboard/dist/assets/{Card-DUItW3xI.js → Card-BYoxd4OC.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-C_t_PDlG.js → CloudCheckoutPage-BR2_B03P.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-pvJUrhya.js → CloudPage-DL5RkJKK.js} +1 -1
- package/dashboard/dist/assets/{CloudUsageExport-DQbDwiyk.js → CloudUsageExport-C_DqZKo1.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-BvHilrbV.js → CommandPalette-DkCaIQI4.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CizxEYv-.js → CommunityStatsModal-pX9OePi4.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-h9nISVy7.js → ConfirmModal-tkgZwz4u.js} +1 -1
- package/dashboard/dist/assets/{Controls-BjdIonTX.js → Controls-MTUhGWy8.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-Bxryr4qp.js → DashboardPage-SOx-A7lH.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-DpKh-0l8.js → DateRangePopover-B-BMOm3O.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DUJR9hJC.js → DevicePage-DSK9lm7v.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BmVAmN-o.js → DialogClose-uVUNu4rg.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DBqGGWNj.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BsaIbN8k.js → DialogTitle-B-VifiA3.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BpSt2P_W.js → FadeIn-B6lrj4uo.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cc_VHB0R.js → HeaderGithubStar-Cx5zOhXa.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-WFd21iP9.js → HoverTooltip-DY1ENf69.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C_mUPSRB.js → IpCheckPage-D6TEq64d.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DVR8ClZO.js → LandingPage-OdZUfq--.js} +2 -2
- package/dashboard/dist/assets/LeaderboardPage-DCK5xcAt.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProBadge-BjX4EFl2.js → LeaderboardProBadge-CBXJ1u_m.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-vbPMNn7W.js → LeaderboardProfileModal-Ccdzb7QK.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CqClmuA-.js → LeaderboardProfilePage-CFXlZ0fL.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-5krjm-g9.js +9 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B6nDke_u.js → LocalOnlyNotice-Rk1B8PaZ.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-C28QVjd7.js → LoginCard-DTTIm8vk.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-Bx1btNhH.js → LoginModal-BNcDdjfr.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DVOGNGYI.js → LoginPage-CQgXP7Ee.js} +1 -1
- package/dashboard/dist/assets/{PetPage-nOKryTde.js → PetPage-Cw_kAPRs.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Ch6NLnu6.js → PopoverPopup-NYiGE8A2.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BRY2br-g.js → ProviderIcon-Bp7iFJ_m.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-ko1OQecQ.js → ResetPasswordPage-Cf9kAWDW.js} +1 -1
- package/dashboard/dist/assets/{Select-Q8mS3_od.js → Select-kIcvQFAo.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C1id8GDj.js → SelectItemText-BpIsAg3b.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-Dzb2bDiM.js → SelfHostPage-Ch2vvSwI.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ewFwz29J.js → ServiceStatusPage-hoYBvGbu.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-BzRB6q-Z.js +3 -0
- package/dashboard/dist/assets/SettingsPage-DRl2uX-i.js +2 -0
- package/dashboard/dist/assets/{SkillsPage-CcG77tqU.js → SkillsPage-CkqThTZL.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-D2HDrbQR.js → TokenGalaxy-eLG-UIZY.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-C_bF2ZwO.js → TooltipPopup-bnMJyilQ.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BcyoN5ns.js → TrendMonitor-X7-aGzIv.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BirK0U_N.js → WidgetsPage-CGznIzrW.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-thsXBuw6.js → WrappedPage-p31-uUoF.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Bq8nj-JV.js → __vite-browser-external-2g_4s3ot.js} +12 -12
- package/dashboard/dist/assets/__vite-browser-external-LpFw4zXh.css +1 -0
- package/dashboard/dist/assets/agent-logos-2TVT3fIE.js +1 -0
- package/dashboard/dist/assets/{arrow-left-c_NOSfoq.js → arrow-left-DL0hAp_7.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-CP7drKCT.js → arrow-right-DFCpnV_9.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DQ6QfCpo.js → arrow-up-right-DLvfE0Wu.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-D_pxFQvT.js → calendar-days-B0aDoKSG.js} +1 -1
- package/dashboard/dist/assets/{check-HAS31kTT.js → check-C_cfrxiz.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BIMuk6oN.js → chevron-down-Ck8ml43e.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-COmDwrUT.js → cloud-checkout-PYnwNfdH.js} +1 -1
- package/dashboard/dist/assets/{composite-kLwqpqEk.js → composite-INW-ELWX.js} +1 -1
- package/dashboard/dist/assets/{download-9RgsZIiH.js → download-B7uokeIo.js} +1 -1
- package/dashboard/dist/assets/{external-link-C_gYjHGF.js → external-link-CI3crg4K.js} +1 -1
- package/dashboard/dist/assets/{flame-BSsCvgT8.js → flame-DCzvFKV1.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-N22Wqv1X.js → format-tokens-CSTEYK2m.js} +1 -1
- package/dashboard/dist/assets/gift-dEHDFMc6.js +1 -0
- package/dashboard/dist/assets/{host-mode-JzrhHX7F.js → host-mode-r2SW9Mdv.js} +1 -1
- package/dashboard/dist/assets/{icons-pK99mIMv.js → icons-q-3zvEkh.js} +1 -1
- package/dashboard/dist/assets/{index-CmD84EJi.js → index-BV7qwUw4.js} +1 -1
- package/dashboard/dist/assets/{index-Dk43KNa6.js → index-BbrsgHf4.js} +1 -1
- package/dashboard/dist/assets/{index-BhviBroj.js → index-CyHYj9XI.js} +1 -1
- package/dashboard/dist/assets/{info-D-RLFnSK.js → info-BBdY9hV4.js} +1 -1
- package/dashboard/dist/assets/{link-2-BiKXW9mV.js → link-2-CLPePfXs.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-KVUaEKV2.js → loader-circle-ETrjzfmn.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-5NK8j3mT.js → modifiers.esm-DHqnp2ol.js} +1 -1
- package/dashboard/dist/assets/module-BNnaKM__.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-u9yr-_Es.js → monitor-smartphone-D1kdzrdb.js} +1 -1
- package/dashboard/dist/assets/{play-D0MaDCOx.js → play-Cxp-ronD.js} +1 -1
- package/dashboard/dist/assets/{plus-DTJVtoAJ.js → plus-4n94VzDK.js} +1 -1
- package/dashboard/dist/assets/{provider-display-YhtG0I9Y.js → provider-display-CRlbeO7t.js} +1 -1
- package/dashboard/dist/assets/{react-gIVxucOB.js → react-DlHjGQFN.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C_sPnJ1L.js → safePolygon-CVZ27a2_.js} +1 -1
- package/dashboard/dist/assets/{search-DdQRwFYV.js → search-BDPr56_h.js} +1 -1
- package/dashboard/dist/assets/{server-M0rmbGWy.js → server-Bm7fHvVa.js} +1 -1
- package/dashboard/dist/assets/{shield-check-9-sqQKdD.js → shield-check-DtT8ACzI.js} +1 -1
- package/dashboard/dist/assets/{skills-api-B0qBWnuK.js → skills-api-Dc1-2qu6.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-iqbm7x5D.js → sortable.esm-B-qN-Xk2.js} +1 -1
- package/dashboard/dist/assets/{store-DstuWd4a.js → store-gHeBU8dQ.js} +1 -1
- package/dashboard/dist/assets/{terminal-Dz0MxHNG.js → terminal-D4VZbvhg.js} +1 -1
- package/dashboard/dist/assets/{trash-DBMFOFVz.js → trash-BZtLAFQy.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CacbAcUR.js → use-community-stats-BdqndrbB.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-nBuQVZOR.js → use-latest-request-guard-DhGg1cDS.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-D3HOcRFv.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-EAqAFx2x.js → use-native-settings-BniKwA_n.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DGLjjU6Q.js → use-ordered-list-b09H7EHq.js} +1 -1
- package/dashboard/dist/assets/{use-pet-settings-C5MzWHE7.js → use-pet-settings-CPZdlq_e.js} +2 -2
- package/dashboard/dist/assets/use-reduced-motion-D1gcKJUo.js +1 -0
- package/dashboard/dist/assets/{use-transform-C25eCuz_.js → use-transform-CIdxjmI8.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-eJlU65yY.js +1 -0
- package/dashboard/dist/assets/{useCurrency-t3PYQi1l.js → useCurrency-CM22CmAq.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-C59mj6kG.js → usePositioner-B3l2ulTw.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-C0xQsBGf.js → useTokenFormat-D7nzgxKw.js} +1 -1
- package/dashboard/dist/assets/{zap-CyH0tvKM.js → zap-Daxct1WV.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 +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +43 -1
- package/src/commands/status.js +27 -0
- package/src/commands/sync.js +54 -4
- package/src/commands/uninstall.js +19 -0
- package/src/lib/claude-config.js +47 -15
- package/src/lib/codex-rollout-parser.js +11 -3
- package/src/lib/command-runner.js +15 -1
- package/src/lib/cursor-config.js +2 -1
- package/src/lib/gemini-config.js +3 -2
- package/src/lib/local-api.js +13 -3
- package/src/lib/notify-command.js +38 -0
- package/src/lib/openclaw-session-plugin.js +47 -14
- package/src/lib/passive-mode.js +12 -0
- package/src/lib/pricing/curated-overrides.json +42 -5
- package/src/lib/pricing/matcher.js +14 -5
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +150 -28
- package/src/lib/session-analytics.js +108 -9
- package/src/lib/usage-limits.js +183 -20
- package/src/lib/zcode-limits.js +218 -18
- package/src/lib/zcode-reset-credits.js +66 -0
- package/dashboard/dist/assets/DialogDescription-C0Cgq67g.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-D8ydzVpn.js +0 -2
- package/dashboard/dist/assets/LimitsPage-DaRggHge.js +0 -8
- package/dashboard/dist/assets/SessionsPage-C6Y8kbnM.js +0 -3
- package/dashboard/dist/assets/SettingsPage-nztCXEPs.js +0 -2
- package/dashboard/dist/assets/__vite-browser-external-BwmeEfv7.css +0 -1
- package/dashboard/dist/assets/agent-logos-CK6SzhI7.js +0 -1
- package/dashboard/dist/assets/module-WSx8viXK.js +0 -3
- package/dashboard/dist/assets/use-limits-display-prefs-fqg8hs6b.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DK2oZ2uC.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-BhqnEai3.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -12,6 +12,7 @@ const { physicalJsonlRecords } = require("./jsonl-lines");
|
|
|
12
12
|
const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
13
13
|
const wsl = require("./wsl-probe");
|
|
14
14
|
const { resolveInstallPaths } = require("./install-resolver");
|
|
15
|
+
const { expandHome } = require("./scan-roots");
|
|
15
16
|
const {
|
|
16
17
|
consumeUsageDelta,
|
|
17
18
|
createUsageDeltaState,
|
|
@@ -1634,9 +1635,19 @@ async function resolveOpenclawSessionFiles(env = process.env, deps = {}) {
|
|
|
1634
1635
|
out.push(full);
|
|
1635
1636
|
}
|
|
1636
1637
|
}
|
|
1638
|
+
for (const dbPath of [
|
|
1639
|
+
path.join(agentsDir, agent.name, "agent", "openclaw-agent.sqlite"),
|
|
1640
|
+
path.join(agentsDir, agent.name, "openclaw-agent.sqlite"),
|
|
1641
|
+
]) {
|
|
1642
|
+
if ((await fs.stat(dbPath).catch(() => null))?.isFile()) {
|
|
1643
|
+
out.push(dbPath);
|
|
1644
|
+
break; // alternate layouts of one agent database, not two sources
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1637
1647
|
}
|
|
1638
1648
|
}
|
|
1639
|
-
|
|
1649
|
+
// Import archives must seed dedup before the corresponding SQLite history.
|
|
1650
|
+
out.sort((a, b) => Number(a.endsWith(".sqlite")) - Number(b.endsWith(".sqlite")) || a.localeCompare(b));
|
|
1640
1651
|
return out;
|
|
1641
1652
|
}
|
|
1642
1653
|
|
|
@@ -1699,6 +1710,67 @@ async function parseOpenclawIncremental({
|
|
|
1699
1710
|
const previousOffset = Number(prev?.offset || 0);
|
|
1700
1711
|
let accepted = null;
|
|
1701
1712
|
let openedRegularFile = false;
|
|
1713
|
+
if (filePath.endsWith(".sqlite")) {
|
|
1714
|
+
const sessionFilter = typeof entry?.sessionId === "string"
|
|
1715
|
+
? ` AND e.session_id = '${entry.sessionId.replace(/'/g, "''")}'` : "";
|
|
1716
|
+
const rows = await readSqliteJsonRowsAsync(filePath, `
|
|
1717
|
+
SELECT e.session_id, json_object(
|
|
1718
|
+
'type', json_extract(e.event_json, '$.type'),
|
|
1719
|
+
'id', json_extract(e.event_json, '$.id'),
|
|
1720
|
+
'timestamp', json_extract(e.event_json, '$.timestamp'),
|
|
1721
|
+
'message', json_object(
|
|
1722
|
+
'role', json_extract(e.event_json, '$.message.role'),
|
|
1723
|
+
'model', json_extract(e.event_json, '$.message.model'),
|
|
1724
|
+
'provider', json_extract(e.event_json, '$.message.provider'),
|
|
1725
|
+
'api', json_extract(e.event_json, '$.message.api'),
|
|
1726
|
+
'timestamp', json_extract(e.event_json, '$.message.timestamp'),
|
|
1727
|
+
'responseId', json_extract(e.event_json, '$.message.responseId'),
|
|
1728
|
+
'usage', json_extract(e.event_json, '$.message.usage')
|
|
1729
|
+
)) AS event
|
|
1730
|
+
FROM transcript_events e JOIN session_windows w ON w.session_id = e.session_id
|
|
1731
|
+
WHERE json_valid(e.event_json) AND COALESCE(w.reason, '') != 'compaction'
|
|
1732
|
+
AND json_extract(e.event_json, '$.message.role') = 'assistant'
|
|
1733
|
+
AND json_extract(e.event_json, '$.message.usage') IS NOT NULL
|
|
1734
|
+
${sessionFilter}
|
|
1735
|
+
ORDER BY e.session_id, e.seq
|
|
1736
|
+
`, { readOnly: true, throwOnReadFailure: true, label: "OpenClaw" });
|
|
1737
|
+
const sessions = new Map();
|
|
1738
|
+
for (const row of rows) {
|
|
1739
|
+
if (!sessions.has(row.session_id)) sessions.set(row.session_id, []);
|
|
1740
|
+
sessions.get(row.session_id).push(row.event);
|
|
1741
|
+
}
|
|
1742
|
+
for (const [sessionId, lines] of sessions) {
|
|
1743
|
+
const sessionCursorKey = `${key}#${sessionId}`;
|
|
1744
|
+
const prior = { ...(stagedFiles[sessionCursorKey]?.usageEvents || {}) };
|
|
1745
|
+
const dbDir = path.dirname(filePath);
|
|
1746
|
+
const agentDir = path.basename(dbDir) === "agent" && path.basename(path.dirname(path.dirname(dbDir))) === "agents"
|
|
1747
|
+
? path.dirname(dbDir) : dbDir;
|
|
1748
|
+
const agentPrefix = openclawCursorKey(`${agentDir}${path.sep}`);
|
|
1749
|
+
// SQLite imports retain transcript identities. Carry forward counts
|
|
1750
|
+
// from this session's legacy transcript, including reset archives.
|
|
1751
|
+
for (const [legacyPath, cursor] of Object.entries(stagedFiles)) {
|
|
1752
|
+
const basename = path.basename(legacyPath);
|
|
1753
|
+
if (cursor.provider !== "openclaw" || !openclawCursorKey(legacyPath).startsWith(agentPrefix)) continue;
|
|
1754
|
+
if (!basename.startsWith(`${sessionId}.jsonl`) &&
|
|
1755
|
+
!(basename.startsWith(`${sessionId}-topic-`) && basename.includes(".jsonl"))) continue;
|
|
1756
|
+
for (const [identity, count] of Object.entries(cursor.usageEvents || {})) {
|
|
1757
|
+
prior[identity] = Math.max(prior[identity] || 0, count);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const result = await parseOpenclawSessionFile({
|
|
1761
|
+
lines, previousUsageEvents: prior, appendOnly: false,
|
|
1762
|
+
hourlyState, touchedBuckets, source: fileSource,
|
|
1763
|
+
});
|
|
1764
|
+
stagedFiles[sessionCursorKey] = {
|
|
1765
|
+
provider: "openclaw", usageEvents: result.usageEvents,
|
|
1766
|
+
hasRealUsage: Object.keys(result.usageEvents).length > 0,
|
|
1767
|
+
updatedAt: new Date().toISOString(),
|
|
1768
|
+
};
|
|
1769
|
+
eventsAggregated += result.eventsAggregated;
|
|
1770
|
+
}
|
|
1771
|
+
filesProcessed += 1;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1702
1774
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1703
1775
|
const fileHandle = await fs.open(filePath, "r").catch(() => null);
|
|
1704
1776
|
if (!fileHandle) break;
|
|
@@ -1913,6 +1985,7 @@ function mergeOpenclawAttemptBuckets(
|
|
|
1913
1985
|
}
|
|
1914
1986
|
|
|
1915
1987
|
async function parseOpenclawSessionFile({
|
|
1988
|
+
lines,
|
|
1916
1989
|
filePath,
|
|
1917
1990
|
fileHandle,
|
|
1918
1991
|
startOffset,
|
|
@@ -1928,7 +2001,8 @@ async function parseOpenclawSessionFile({
|
|
|
1928
2001
|
projectState,
|
|
1929
2002
|
projectTouchedBuckets,
|
|
1930
2003
|
}) {
|
|
1931
|
-
const st = fileHandle ? await fileHandle.stat() : await fs.stat(filePath);
|
|
2004
|
+
const st = lines ? { size: lines.length } : fileHandle ? await fileHandle.stat() : await fs.stat(filePath);
|
|
2005
|
+
startOffset = startOffset || 0;
|
|
1932
2006
|
const endOffset =
|
|
1933
2007
|
Number.isFinite(endOffsetLimit) && endOffsetLimit >= 0
|
|
1934
2008
|
? Math.min(st.size, endOffsetLimit)
|
|
@@ -1963,14 +2037,14 @@ async function parseOpenclawSessionFile({
|
|
|
1963
2037
|
};
|
|
1964
2038
|
}
|
|
1965
2039
|
|
|
1966
|
-
const stream = fssync.createReadStream(filePath, {
|
|
2040
|
+
const stream = lines ? null : fssync.createReadStream(filePath, {
|
|
1967
2041
|
fd: fileHandle?.fd,
|
|
1968
2042
|
autoClose: !fileHandle,
|
|
1969
2043
|
encoding: "utf8",
|
|
1970
2044
|
start: startOffset,
|
|
1971
2045
|
end: endOffset - 1,
|
|
1972
2046
|
});
|
|
1973
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2047
|
+
const rl = lines || readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1974
2048
|
|
|
1975
2049
|
let eventsAggregated = 0;
|
|
1976
2050
|
const scanCounts = appendOnly ? { ...priorCounts } : {};
|
|
@@ -2069,8 +2143,8 @@ async function parseOpenclawSessionFile({
|
|
|
2069
2143
|
if (!collectOnly) eventsAggregated += 1;
|
|
2070
2144
|
}
|
|
2071
2145
|
|
|
2072
|
-
rl.close();
|
|
2073
|
-
if (!fileHandle) stream
|
|
2146
|
+
if (!lines) rl.close();
|
|
2147
|
+
if (!fileHandle) stream?.close?.();
|
|
2074
2148
|
return {
|
|
2075
2149
|
endOffset,
|
|
2076
2150
|
eventsAggregated,
|
|
@@ -10451,23 +10525,43 @@ async function parseCodebuddyIncremental({
|
|
|
10451
10525
|
// is accepted by workbuddySqliteUsageSnapshot.
|
|
10452
10526
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10453
10527
|
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10528
|
+
// WorkBuddy ships two regional builds that keep their data in sibling homes:
|
|
10529
|
+
// ~/.workbuddy — Tencent CN build (models: hy*/glm*/deepseek-*/auto)
|
|
10530
|
+
// ~/.workbuddy-ai — international build (models: gpt-5.6-*, gpt-6-*)
|
|
10531
|
+
// Both write byte-identical JSONL, so every reader below is shared and only
|
|
10532
|
+
// the home directory (and the bucket source key) differs. `variant` is also
|
|
10533
|
+
// the source key written into hourly buckets, so the two builds stay
|
|
10534
|
+
// separable in the dashboard and never share dedup state.
|
|
10535
|
+
const WORKBUDDY_VARIANTS = {
|
|
10536
|
+
workbuddy: { dir: ".workbuddy", envVar: "WORKBUDDY_HOME" },
|
|
10537
|
+
"workbuddy-ai": { dir: ".workbuddy-ai", envVar: "WORKBUDDY_AI_HOME" },
|
|
10538
|
+
};
|
|
10539
|
+
|
|
10540
|
+
function resolveWorkbuddyVariant(variant) {
|
|
10541
|
+
return Object.prototype.hasOwnProperty.call(WORKBUDDY_VARIANTS, variant)
|
|
10542
|
+
? variant
|
|
10543
|
+
: "workbuddy";
|
|
10544
|
+
}
|
|
10545
|
+
|
|
10546
|
+
function resolveWorkbuddyHome(env = process.env, variant = "workbuddy") {
|
|
10547
|
+
const spec = WORKBUDDY_VARIANTS[resolveWorkbuddyVariant(variant)];
|
|
10548
|
+
const home = env.HOME || env.USERPROFILE || require("node:os").homedir();
|
|
10549
|
+
const configured = expandHome(env[spec.envVar], home);
|
|
10550
|
+
if (configured) return configured;
|
|
10457
10551
|
if (process.platform === "win32") {
|
|
10458
10552
|
return pickWin32ProviderPath({
|
|
10459
10553
|
env,
|
|
10460
|
-
nativeValue: path.join(home,
|
|
10461
|
-
wslProviderDir:
|
|
10554
|
+
nativeValue: path.join(home, spec.dir),
|
|
10555
|
+
wslProviderDir: spec.dir,
|
|
10462
10556
|
});
|
|
10463
10557
|
}
|
|
10464
|
-
return path.join(home,
|
|
10558
|
+
return path.join(home, spec.dir);
|
|
10465
10559
|
}
|
|
10466
10560
|
|
|
10467
|
-
function resolveWorkbuddyDefaultModel(env = process.env) {
|
|
10561
|
+
function resolveWorkbuddyDefaultModel(env = process.env, variant = "workbuddy") {
|
|
10468
10562
|
const fallback = "auto";
|
|
10469
10563
|
try {
|
|
10470
|
-
const workbuddyHome = resolveWorkbuddyHome(env);
|
|
10564
|
+
const workbuddyHome = resolveWorkbuddyHome(env, variant);
|
|
10471
10565
|
if (!workbuddyHome) return fallback;
|
|
10472
10566
|
const settingsPath = path.join(workbuddyHome, "settings.json");
|
|
10473
10567
|
const raw = fssync.readFileSync(settingsPath, "utf8");
|
|
@@ -10487,8 +10581,8 @@ function resolveWorkbuddyDefaultModel(env = process.env) {
|
|
|
10487
10581
|
// ~/.workbuddy/traces/<pid>/trace_*.json. Those summaries are useful only when
|
|
10488
10582
|
// the detailed JSONL for a session is absent, so they are returned as typed
|
|
10489
10583
|
// entries and parsed after JSONL (JSONL remains authoritative when both exist).
|
|
10490
|
-
function resolveWorkbuddyProjectFiles(env = process.env) {
|
|
10491
|
-
const workbuddyHome = resolveWorkbuddyHome(env);
|
|
10584
|
+
function resolveWorkbuddyProjectFiles(env = process.env, variant = "workbuddy") {
|
|
10585
|
+
const workbuddyHome = resolveWorkbuddyHome(env, variant);
|
|
10492
10586
|
if (!workbuddyHome) return [];
|
|
10493
10587
|
const projectsDir = path.join(workbuddyHome, "projects");
|
|
10494
10588
|
const files = [];
|
|
@@ -10597,10 +10691,15 @@ async function parseWorkbuddyIncremental({
|
|
|
10597
10691
|
onProgress,
|
|
10598
10692
|
env,
|
|
10599
10693
|
defaultModel,
|
|
10694
|
+
source = "workbuddy",
|
|
10600
10695
|
} = {}) {
|
|
10601
10696
|
await ensureDir(path.dirname(queuePath));
|
|
10697
|
+
// `source` doubles as the variant selector and the bucket source key, so the
|
|
10698
|
+
// CN install (~/.workbuddy) and the international install (~/.workbuddy-ai)
|
|
10699
|
+
// get independent cursor namespaces and never share dedup state.
|
|
10700
|
+
const variant = resolveWorkbuddyVariant(source);
|
|
10602
10701
|
const workbuddyState =
|
|
10603
|
-
cursors
|
|
10702
|
+
cursors[variant] && typeof cursors[variant] === "object" ? cursors[variant] : {};
|
|
10604
10703
|
const seenIds = new Set(
|
|
10605
10704
|
Array.isArray(workbuddyState.seenIds) ? workbuddyState.seenIds : [],
|
|
10606
10705
|
);
|
|
@@ -10626,20 +10725,20 @@ async function parseWorkbuddyIncremental({
|
|
|
10626
10725
|
|
|
10627
10726
|
const allFiles = Array.isArray(projectFiles)
|
|
10628
10727
|
? projectFiles
|
|
10629
|
-
: resolveWorkbuddyProjectFiles(env || process.env);
|
|
10728
|
+
: resolveWorkbuddyProjectFiles(env || process.env, variant);
|
|
10630
10729
|
const jsonlFiles = allFiles.filter((entry) => typeof entry === "string");
|
|
10631
10730
|
const traceFiles = allFiles.filter(
|
|
10632
10731
|
(entry) => entry && typeof entry === "object" && typeof entry.path === "string" && entry.kind === "trace",
|
|
10633
10732
|
);
|
|
10634
10733
|
const files = [...jsonlFiles, ...traceFiles];
|
|
10635
|
-
const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env);
|
|
10734
|
+
const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env, variant);
|
|
10636
10735
|
|
|
10637
|
-
const workbuddyHome = resolveWorkbuddyHome(env || process.env);
|
|
10736
|
+
const workbuddyHome = resolveWorkbuddyHome(env || process.env, variant);
|
|
10638
10737
|
const dbPath = workbuddyHome ? path.join(workbuddyHome, "workbuddy.db") : null;
|
|
10639
10738
|
const dbExists = Boolean(dbPath && fssync.existsSync(dbPath));
|
|
10640
10739
|
|
|
10641
10740
|
if (files.length === 0 && !dbExists) {
|
|
10642
|
-
cursors
|
|
10741
|
+
cursors[variant] = {
|
|
10643
10742
|
...workbuddyState,
|
|
10644
10743
|
seenIds: Array.from(seenIds),
|
|
10645
10744
|
seenTraceIds: Array.from(seenTraceIds),
|
|
@@ -10786,9 +10885,9 @@ async function parseWorkbuddyIncremental({
|
|
|
10786
10885
|
conversation_count: 1,
|
|
10787
10886
|
};
|
|
10788
10887
|
|
|
10789
|
-
const bucket = getHourlyBucket(hourlyState,
|
|
10888
|
+
const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
|
|
10790
10889
|
addTotals(bucket.totals, delta);
|
|
10791
|
-
touchedBuckets.add(bucketKey(
|
|
10890
|
+
touchedBuckets.add(bucketKey(variant, model, bucketStart));
|
|
10792
10891
|
seenIds.add(messageId);
|
|
10793
10892
|
// Keep legacy entry-id keys alongside the response id so upgrading a
|
|
10794
10893
|
// cursor cannot replay a row that an older build already counted.
|
|
@@ -10897,9 +10996,9 @@ async function parseWorkbuddyIncremental({
|
|
|
10897
10996
|
total_tokens: inputTokens + totalCached + totalOutput,
|
|
10898
10997
|
conversation_count: 1,
|
|
10899
10998
|
};
|
|
10900
|
-
const bucket = getHourlyBucket(hourlyState,
|
|
10999
|
+
const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
|
|
10901
11000
|
addTotals(bucket.totals, delta);
|
|
10902
|
-
touchedBuckets.add(bucketKey(
|
|
11001
|
+
touchedBuckets.add(bucketKey(variant, model, bucketStart));
|
|
10903
11002
|
seenTraceIds.add(traceId);
|
|
10904
11003
|
tracedSessionIds.add(sessionId);
|
|
10905
11004
|
recordsProcessed++;
|
|
@@ -11032,9 +11131,9 @@ async function parseWorkbuddyIncremental({
|
|
|
11032
11131
|
conversation_count: Object.keys(previousTokens).every((key) => toNonNegativeInt(previousTokens[key]) === 0) || isReset ? 1 : 0,
|
|
11033
11132
|
};
|
|
11034
11133
|
|
|
11035
|
-
const bucket = getHourlyBucket(hourlyState,
|
|
11134
|
+
const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
|
|
11036
11135
|
addTotals(bucket.totals, delta);
|
|
11037
|
-
touchedBuckets.add(bucketKey(
|
|
11136
|
+
touchedBuckets.add(bucketKey(variant, model, bucketStart));
|
|
11038
11137
|
sqliteSessions[sessionId] = {
|
|
11039
11138
|
used: usedNow,
|
|
11040
11139
|
updatedAt: updatedAtRaw,
|
|
@@ -11085,7 +11184,7 @@ async function parseWorkbuddyIncremental({
|
|
|
11085
11184
|
const updatedAt = new Date().toISOString();
|
|
11086
11185
|
hourlyState.updatedAt = updatedAt;
|
|
11087
11186
|
cursors.hourly = hourlyState;
|
|
11088
|
-
cursors
|
|
11187
|
+
cursors[variant] = {
|
|
11089
11188
|
...workbuddyState,
|
|
11090
11189
|
seenIds: cappedSeen,
|
|
11091
11190
|
seenTraceIds: cappedSeenTraceIds,
|
|
@@ -15972,6 +16071,27 @@ function resolvePiSessionFiles(env = process.env) {
|
|
|
15972
16071
|
} catch {
|
|
15973
16072
|
// ignore — return what we have
|
|
15974
16073
|
}
|
|
16074
|
+
// Multica launches pi headless with a dedicated flat session dir:
|
|
16075
|
+
// ~/.multica/pi-sessions/<session-id>.jsonl
|
|
16076
|
+
// Scan it too so orchestrated runs are counted alongside TUI sessions.
|
|
16077
|
+
try {
|
|
16078
|
+
const seen = new Set(files);
|
|
16079
|
+
const multicaSessionsDir = path.join(
|
|
16080
|
+
env.HOME || require("node:os").homedir(),
|
|
16081
|
+
".multica",
|
|
16082
|
+
"pi-sessions",
|
|
16083
|
+
);
|
|
16084
|
+
for (const entry of fssync.readdirSync(multicaSessionsDir, { withFileTypes: true })) {
|
|
16085
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
16086
|
+
const full = path.join(multicaSessionsDir, entry.name);
|
|
16087
|
+
if (!seen.has(full)) {
|
|
16088
|
+
seen.add(full);
|
|
16089
|
+
files.push(full);
|
|
16090
|
+
}
|
|
16091
|
+
}
|
|
16092
|
+
} catch {
|
|
16093
|
+
// ignore — no Multica install
|
|
16094
|
+
}
|
|
15975
16095
|
files.sort((a, b) => a.localeCompare(b));
|
|
15976
16096
|
return files;
|
|
15977
16097
|
}
|
|
@@ -24514,6 +24634,8 @@ module.exports = {
|
|
|
24514
24634
|
resolveWorkbuddyProjectFiles,
|
|
24515
24635
|
resolveWorkbuddyDefaultModel,
|
|
24516
24636
|
parseWorkbuddyIncremental,
|
|
24637
|
+
WORKBUDDY_VARIANTS,
|
|
24638
|
+
resolveWorkbuddyVariant,
|
|
24517
24639
|
resolveKiroCliSessionFiles,
|
|
24518
24640
|
resolveKiroCliDbPath,
|
|
24519
24641
|
parseKiroCliIncremental,
|
|
@@ -70,7 +70,7 @@ const {
|
|
|
70
70
|
// long-context subset) and folds delivery signals into the token parser's
|
|
71
71
|
// single pass instead of parsing every Codex file twice.
|
|
72
72
|
// v14 adds request-level estimated performance and Claude model usage.
|
|
73
|
-
const SIDECAR_VERSION =
|
|
73
|
+
const SIDECAR_VERSION = 16;
|
|
74
74
|
const EDIT_TOOLS = new Set([
|
|
75
75
|
"apply_patch",
|
|
76
76
|
"edit",
|
|
@@ -123,12 +123,23 @@ function addTotals(target, delta) {
|
|
|
123
123
|
for (const key of ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"]) {
|
|
124
124
|
target[key] = finite(target[key]) + finite(delta?.[key]);
|
|
125
125
|
}
|
|
126
|
+
for (const key of MODEL_USAGE_SUM_FIELDS) {
|
|
127
|
+
if (!/^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key)) continue;
|
|
128
|
+
if (delta?.[key]) target[key] = finite(target[key]) + finite(delta[key]);
|
|
129
|
+
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
function emptyTotals() {
|
|
129
133
|
return tokenTotals({});
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
function mergeUsageMetadata(target, usage) {
|
|
137
|
+
for (const field of ["selected_models", "reroute_reasons"]) {
|
|
138
|
+
if (Array.isArray(usage[field])) target[field] = [...new Set([...(target[field] || []), ...usage[field]])];
|
|
139
|
+
}
|
|
140
|
+
if (usage.model_attribution === "effective") target.model_attribution = "effective";
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
const MODEL_USAGE_SUM_FIELDS = [
|
|
133
144
|
"input_tokens",
|
|
134
145
|
"cached_input_tokens",
|
|
@@ -241,6 +252,11 @@ function serializeSessionRecord(row) {
|
|
|
241
252
|
}
|
|
242
253
|
|
|
243
254
|
function repriceSessionRecord(record) {
|
|
255
|
+
if (Array.isArray(record.usage_buckets)) {
|
|
256
|
+
record.usage_buckets = record.usage_buckets.map((bucket) => ({
|
|
257
|
+
...bucket, cost_usd: computeRowCost({ source: record.source, ...bucket }),
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
244
260
|
const modelUsage = normalizeModelUsageRows(record?.model_usage);
|
|
245
261
|
if (modelUsage.length > 0) {
|
|
246
262
|
let hasUnpricedUsage = false;
|
|
@@ -469,6 +485,19 @@ function finalizeRecord(record) {
|
|
|
469
485
|
return record;
|
|
470
486
|
}
|
|
471
487
|
|
|
488
|
+
function sessionStorageUsage(filePaths) {
|
|
489
|
+
let bytes = 0;
|
|
490
|
+
const allocated = process.platform !== "win32";
|
|
491
|
+
for (const file of new Set(filePaths)) {
|
|
492
|
+
try {
|
|
493
|
+
const stat = fs.statSync(file);
|
|
494
|
+
if (!stat.isFile()) continue;
|
|
495
|
+
bytes += allocated && Number.isFinite(stat.blocks) ? stat.blocks * 512 : stat.size;
|
|
496
|
+
} catch { /* deleted or unreadable while scanning */ }
|
|
497
|
+
}
|
|
498
|
+
return { session_store_bytes: bytes, session_store_metric: allocated ? "allocated" : "logical" };
|
|
499
|
+
}
|
|
500
|
+
|
|
472
501
|
function readableSessionPaths(filePath) {
|
|
473
502
|
const candidates = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
474
503
|
const readable = candidates.filter((value) => sessionFileStatKey(value));
|
|
@@ -623,6 +652,7 @@ async function scanClaudeSession(filePath) {
|
|
|
623
652
|
// wrote one — the UI then falls back to the project name.
|
|
624
653
|
title: aiTitle,
|
|
625
654
|
source: "claude",
|
|
655
|
+
...sessionStorageUsage(filePaths),
|
|
626
656
|
project_key: projectKey(cwd, primaryFilePath),
|
|
627
657
|
project_ref: cwd || null,
|
|
628
658
|
model,
|
|
@@ -735,6 +765,7 @@ async function scanCodexSession(filePath) {
|
|
|
735
765
|
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
736
766
|
const signalCollector = createCodexDeliverySignalCollector();
|
|
737
767
|
const performanceCollector = createCodexPerformanceCollector();
|
|
768
|
+
const usageBuckets = new Map();
|
|
738
769
|
const parsed = await parseCodexRolloutFile(filePaths, {
|
|
739
770
|
seenTokenEvents: new Set(),
|
|
740
771
|
collectBreakdowns: false,
|
|
@@ -743,7 +774,20 @@ async function scanCodexSession(filePath) {
|
|
|
743
774
|
signalCollector.consume(obj, model);
|
|
744
775
|
performanceCollector.consume(obj, model);
|
|
745
776
|
},
|
|
746
|
-
onUsage
|
|
777
|
+
onUsage(event) {
|
|
778
|
+
if (event.origin !== "compaction") performanceCollector.consumeUsage(event);
|
|
779
|
+
if (!event.delta || event.delta.total_tokens <= 0) return;
|
|
780
|
+
const ms = Date.parse(event.timestamp);
|
|
781
|
+
if (!Number.isFinite(ms)) return;
|
|
782
|
+
// Minute buckets preserve every current IANA local-midnight boundary
|
|
783
|
+
// and every integer tz_offset_minutes accepted by the local API.
|
|
784
|
+
const timestamp = new Date(Math.floor(ms / 60_000) * 60_000).toISOString();
|
|
785
|
+
const key = `${timestamp}\u0000${event.model}`;
|
|
786
|
+
const bucket = usageBuckets.get(key) || { timestamp, model: event.model || "unknown", ...emptyTotals() };
|
|
787
|
+
addTotals(bucket, event.delta);
|
|
788
|
+
mergeUsageMetadata(bucket, event.delta);
|
|
789
|
+
usageBuckets.set(key, bucket);
|
|
790
|
+
},
|
|
747
791
|
});
|
|
748
792
|
const signals = signalCollector.finish();
|
|
749
793
|
const performance = performanceCollector.finish();
|
|
@@ -798,10 +842,12 @@ async function scanCodexSession(filePath) {
|
|
|
798
842
|
// Local-only: stripped in summarizeSessions before any cloud/CSV export.
|
|
799
843
|
title,
|
|
800
844
|
source: "codex",
|
|
845
|
+
...sessionStorageUsage(filePaths),
|
|
801
846
|
project_key: projectKey(parsed.cwd, primaryFilePath),
|
|
802
847
|
project_ref: parsed.cwd || null,
|
|
803
848
|
model,
|
|
804
849
|
model_usage: modelUsage,
|
|
850
|
+
usage_buckets: [...usageBuckets.values()],
|
|
805
851
|
performance: performance.performance,
|
|
806
852
|
...signals.bounds,
|
|
807
853
|
turns: signals.turns || finite(parsed.turnCount),
|
|
@@ -1079,6 +1125,7 @@ async function scanGrokSession(filePath) {
|
|
|
1079
1125
|
// Local-only: stripped in summarizeSessions before any cloud/CSV export.
|
|
1080
1126
|
title,
|
|
1081
1127
|
source: "grok",
|
|
1128
|
+
...sessionStorageUsage([filePath, grokSummaryPathFor(filePath), grokSignalsPathFor(filePath)]),
|
|
1082
1129
|
project_key: projectKey(cwd, filePath),
|
|
1083
1130
|
project_ref: cwd || null,
|
|
1084
1131
|
model: model || "unknown",
|
|
@@ -1617,17 +1664,55 @@ function buildSessionAnalytics(options = {}) {
|
|
|
1617
1664
|
// exactly the long/resumed ones, which also sort to the top because the sort
|
|
1618
1665
|
// key is ended_at. On real data a 7-day window lost 6 sessions, the largest
|
|
1619
1666
|
// 224M tokens. Treat the window as an interval intersection instead.
|
|
1620
|
-
function
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1667
|
+
function sessionUsageDay(timestamp, { timeZone, offsetMinutes } = {}) {
|
|
1668
|
+
const date = new Date(timestamp);
|
|
1669
|
+
if (!Number.isFinite(date.getTime())) return "";
|
|
1670
|
+
if (timeZone) {
|
|
1671
|
+
try {
|
|
1672
|
+
const parts = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date);
|
|
1673
|
+
const part = (name) => parts.find((p) => p.type === name)?.value;
|
|
1674
|
+
return `${part("year")}-${part("month")}-${part("day")}`;
|
|
1675
|
+
} catch { /* fall back to the caller's offset or UTC */ }
|
|
1676
|
+
}
|
|
1677
|
+
return new Date(date.getTime() + (Number.isFinite(offsetMinutes) ? offsetMinutes * 60_000 : 0)).toISOString().slice(0, 10);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function withinDayRange(row, from, to, context) {
|
|
1681
|
+
const startDay = sessionUsageDay(row?.started_at || row?.ended_at || "", context);
|
|
1682
|
+
const endDay = sessionUsageDay(row?.ended_at || row?.started_at || "", context);
|
|
1623
1683
|
if (from && (!endDay || endDay < from)) return false;
|
|
1624
1684
|
if (to && (!startDay || startDay > to)) return false;
|
|
1625
1685
|
return true;
|
|
1626
1686
|
}
|
|
1627
1687
|
|
|
1628
|
-
function
|
|
1688
|
+
function scopeSessionUsage(row, from, to, context) {
|
|
1689
|
+
if ((!from && !to) || !Array.isArray(row.usage_buckets)) return { ...row };
|
|
1690
|
+
const buckets = row.usage_buckets.filter((bucket) => {
|
|
1691
|
+
const day = sessionUsageDay(bucket.timestamp, context);
|
|
1692
|
+
return (!from || day >= from) && (!to || day <= to);
|
|
1693
|
+
});
|
|
1694
|
+
const tokens = emptyTotals();
|
|
1695
|
+
const byModel = new Map();
|
|
1696
|
+
for (const bucket of buckets) {
|
|
1697
|
+
addTotals(tokens, bucket);
|
|
1698
|
+
const model = byModel.get(bucket.model) || { model: bucket.model, ...emptyTotals(), usage_events: 0 };
|
|
1699
|
+
addTotals(model, bucket);
|
|
1700
|
+
mergeUsageMetadata(model, bucket);
|
|
1701
|
+
if (!bucket.usage_events) model.usage_events += 1;
|
|
1702
|
+
byModel.set(bucket.model, model);
|
|
1703
|
+
}
|
|
1704
|
+
return repriceSessionRecord({ ...row, tokens, total_tokens: tokens.total_tokens,
|
|
1705
|
+
model_usage: [...byModel.values()], usage_buckets: buckets, cost_usd: 0,
|
|
1706
|
+
lifetime_total_tokens: row.total_tokens, lifetime_cost_usd: row.cost_usd });
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function summarizeSessions(sessions, { from = "", to = "", includeSessions = true, timeZone, offsetMinutes } = {}) {
|
|
1710
|
+
const context = { timeZone, offsetMinutes };
|
|
1629
1711
|
const filtered = annotateCodexThreadUsage((sessions || [])
|
|
1630
|
-
.filter((row) => withinDayRange(row, from, to))
|
|
1712
|
+
.filter((row) => withinDayRange(row, from, to, context))
|
|
1713
|
+
// Efficiency is a session-lifetime metric: both tokens and edit turns
|
|
1714
|
+
// must describe that same population. The browser scopes consumption
|
|
1715
|
+
// separately; do not divide in-range tokens by lifetime edit counts here.
|
|
1631
1716
|
.map((row) => ({ ...row })));
|
|
1632
1717
|
const byModel = new Map();
|
|
1633
1718
|
const subagents = new Map();
|
|
@@ -1838,12 +1923,19 @@ function toSessionBrowserRow(row) {
|
|
|
1838
1923
|
parent_link_conflict: Boolean(row.parent_link_conflict),
|
|
1839
1924
|
title: row.title || null,
|
|
1840
1925
|
source: row.source,
|
|
1926
|
+
...(Number.isFinite(row.session_store_bytes) ? {
|
|
1927
|
+
session_store_bytes: row.session_store_bytes,
|
|
1928
|
+
session_store_metric: row.session_store_metric,
|
|
1929
|
+
bytes_per_1k_tokens: (row.lifetime_total_tokens ?? row.total_tokens) > 0
|
|
1930
|
+
? row.session_store_bytes * 1000 / (row.lifetime_total_tokens ?? row.total_tokens) : null,
|
|
1931
|
+
} : {}),
|
|
1841
1932
|
project_key: row.project_key,
|
|
1842
1933
|
// project_ref (the local cwd) only ever travels over the local API so the
|
|
1843
1934
|
// browser can show where a session ran and compose a resume command.
|
|
1844
1935
|
project_ref: row.project_ref || null,
|
|
1845
1936
|
model: row.model,
|
|
1846
1937
|
model_usage: modelUsageForAggregation(row),
|
|
1938
|
+
...(Array.isArray(row.usage_buckets) ? { usage_buckets: row.usage_buckets } : {}),
|
|
1847
1939
|
performance: normalizePerformance(row.performance),
|
|
1848
1940
|
started_at: row.started_at || null,
|
|
1849
1941
|
ended_at: row.ended_at || null,
|
|
@@ -1914,11 +2006,15 @@ function mergeSessionFragments(rows) {
|
|
|
1914
2006
|
cur.performance = mergePerformance(cur.performance, row.performance);
|
|
1915
2007
|
cur.subagent_calls = finite(cur.subagent_calls) + finite(row.subagent_calls);
|
|
1916
2008
|
addTotals(cur.tokens, row.tokens);
|
|
2009
|
+
if (Array.isArray(row.usage_buckets)) {
|
|
2010
|
+
cur.usage_buckets = [...(cur.usage_buckets || []), ...row.usage_buckets];
|
|
2011
|
+
}
|
|
1917
2012
|
cur.model_usage = normalizeModelUsageRows([
|
|
1918
2013
|
...(cur.model_usage || []),
|
|
1919
2014
|
...(row.model_usage || []),
|
|
1920
2015
|
]);
|
|
1921
2016
|
cur.total_tokens = finite(cur.total_tokens) + finite(row.total_tokens);
|
|
2017
|
+
if (Number.isFinite(row.session_store_bytes)) cur.session_store_bytes = finite(cur.session_store_bytes) + row.session_store_bytes;
|
|
1922
2018
|
cur.cost_usd = finite(cur.cost_usd) + finite(row.cost_usd);
|
|
1923
2019
|
cur.provider_cost_usd = finite(cur.provider_cost_usd) + finite(row.provider_cost_usd);
|
|
1924
2020
|
cur.usage_events = finite(cur.usage_events) + finite(row.usage_events);
|
|
@@ -2095,7 +2191,8 @@ function annotateCodexThreadUsage(rows) {
|
|
|
2095
2191
|
// summarizeSessions (cloud/CSV safe), this retains session_id + project_ref so
|
|
2096
2192
|
// the UI can offer one-click resume. Callers must only expose it over the
|
|
2097
2193
|
// local API.
|
|
2098
|
-
function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}) {
|
|
2194
|
+
function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0, timeZone, offsetMinutes } = {}) {
|
|
2195
|
+
const context = { timeZone, offsetMinutes };
|
|
2099
2196
|
const filtered = annotateCodexThreadUsage(mergeSessionFragments(sessions)
|
|
2100
2197
|
// Only sessions that actually spent tokens are worth listing. This drops
|
|
2101
2198
|
// two kinds of noise: non-session logs under ~/.claude/projects
|
|
@@ -2103,7 +2200,9 @@ function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}
|
|
|
2103
2200
|
// the model replied. Both render as "unknown · 0 tokens · $0.00" rows with
|
|
2104
2201
|
// no model, no cost and nothing to analyze.
|
|
2105
2202
|
.filter((row) => finite(row.total_tokens) > 0)
|
|
2106
|
-
.filter((row) => withinDayRange(row, from, to))
|
|
2203
|
+
.filter((row) => withinDayRange(row, from, to, context))
|
|
2204
|
+
.map((row) => scopeSessionUsage(row, from, to, context))
|
|
2205
|
+
.filter((row) => !Array.isArray(row.usage_buckets) || finite(row.total_tokens) > 0));
|
|
2107
2206
|
filtered.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
|
|
2108
2207
|
const cap = Number(limit) > 0 ? Number(limit) : 0;
|
|
2109
2208
|
const limited = cap > 0 ? filtered.slice(0, cap) : filtered;
|