tokentracker-cli 1.3.4 → 1.3.6

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.
Files changed (147) hide show
  1. package/README.de.md +5 -5
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +6 -5
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/{AchievementBadge-CKjtQp-U.js → AchievementBadge-CHDt55IP.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DpOXrabE.js → AchievementsPage-DpbRHmb8.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DiXyIoa0.js → AchievementsSection-Ck5CiX4P.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-CILTTkPF.js → BadgeDetailModal-BVPqlh0X.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CrBHWiRi.js → BotAnimated-CVsv6kxT.js} +1 -1
  11. package/dashboard/dist/assets/{Card-DUItW3xI.js → Card-C9FjyAwh.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-C_t_PDlG.js → CloudCheckoutPage-ChkjId9l.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-pvJUrhya.js → CloudPage-D1nrt3MG.js} +1 -1
  14. package/dashboard/dist/assets/{CloudUsageExport-DQbDwiyk.js → CloudUsageExport-jXdoiD0e.js} +1 -1
  15. package/dashboard/dist/assets/{CommandPalette-BvHilrbV.js → CommandPalette-DFnEeZzc.js} +1 -1
  16. package/dashboard/dist/assets/{CommunityStatsModal-CizxEYv-.js → CommunityStatsModal-CZDS8gPA.js} +1 -1
  17. package/dashboard/dist/assets/{ConfirmModal-h9nISVy7.js → ConfirmModal-DRhgBoGE.js} +1 -1
  18. package/dashboard/dist/assets/{Controls-BjdIonTX.js → Controls-BmT6-x_L.js} +1 -1
  19. package/dashboard/dist/assets/{DashboardPage-Bxryr4qp.js → DashboardPage-DfwwXZBo.js} +1 -1
  20. package/dashboard/dist/assets/{DateRangePopover-DpKh-0l8.js → DateRangePopover-DuqNLatr.js} +1 -1
  21. package/dashboard/dist/assets/{DevicePage-DUJR9hJC.js → DevicePage-BtI0bWyg.js} +1 -1
  22. package/dashboard/dist/assets/{DialogClose-BmVAmN-o.js → DialogClose-DjnC3P4V.js} +1 -1
  23. package/dashboard/dist/assets/DialogDescription-yy_QHg7O.js +1 -0
  24. package/dashboard/dist/assets/{DialogTitle-BsaIbN8k.js → DialogTitle-B9NNSz5p.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-BpSt2P_W.js → FadeIn-fsVQmJGa.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-Cc_VHB0R.js → HeaderGithubStar-Cg-f1iZc.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-WFd21iP9.js → HoverTooltip-DRIRlzXH.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-C_mUPSRB.js → IpCheckPage-DaAubaJQ.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-DVR8ClZO.js → LandingPage-DjBGIjHk.js} +2 -2
  30. package/dashboard/dist/assets/LeaderboardPage-BkCXqhz7.js +2 -0
  31. package/dashboard/dist/assets/{LeaderboardProBadge-BjX4EFl2.js → LeaderboardProBadge-Ds-wiWcN.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-vbPMNn7W.js → LeaderboardProfileModal-X_rfSQT5.js} +4 -4
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-CqClmuA-.js → LeaderboardProfilePage-rpl1Ozwj.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-BUvDBmgD.js +9 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-B6nDke_u.js → LocalOnlyNotice-LBH1DBrs.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-C28QVjd7.js → LoginCard-B0vRJEcg.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-Bx1btNhH.js → LoginModal-DdzgP43f.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-DVOGNGYI.js → LoginPage-B4VCQxzn.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-nOKryTde.js → PetPage-Cmf6aXc9.js} +1 -1
  40. package/dashboard/dist/assets/{PopoverPopup-Ch6NLnu6.js → PopoverPopup-BzfELnIC.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BRY2br-g.js → ProviderIcon-ClHXNkQ0.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-ko1OQecQ.js → ResetPasswordPage-BeUpNt_6.js} +1 -1
  43. package/dashboard/dist/assets/{Select-Q8mS3_od.js → Select-BHEXdYHu.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-C1id8GDj.js → SelectItemText-aaQK_QIt.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-Dzb2bDiM.js → SelfHostPage-ClNhKnlP.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-ewFwz29J.js → ServiceStatusPage-Byd_s6FS.js} +1 -1
  47. package/dashboard/dist/assets/SessionsPage-u3_b4PO7.js +3 -0
  48. package/dashboard/dist/assets/SettingsPage-DweCAbsy.js +2 -0
  49. package/dashboard/dist/assets/{SkillsPage-CcG77tqU.js → SkillsPage-Bb-PVfux.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D2HDrbQR.js → TokenGalaxy-CubA3PXu.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-C_bF2ZwO.js → TooltipPopup-DMliWhoK.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-BcyoN5ns.js → TrendMonitor-as8idaOy.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-BirK0U_N.js → WidgetsPage-DDPR4v0R.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-thsXBuw6.js → WrappedPage-zZzgYz6s.js} +1 -1
  55. package/dashboard/dist/assets/{__vite-browser-external-Bq8nj-JV.js → __vite-browser-external-DWN9Fb-H.js} +12 -12
  56. package/dashboard/dist/assets/__vite-browser-external-LpFw4zXh.css +1 -0
  57. package/dashboard/dist/assets/agent-logos-CoNhCm9-.js +1 -0
  58. package/dashboard/dist/assets/{arrow-left-c_NOSfoq.js → arrow-left-C5hDp7fc.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-CP7drKCT.js → arrow-right-DEvMmSiA.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-DQ6QfCpo.js → arrow-up-right-DxY0neJ4.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-D_pxFQvT.js → calendar-days-bzh7-lab.js} +1 -1
  62. package/dashboard/dist/assets/{check-HAS31kTT.js → check-CSZvcC3X.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-BIMuk6oN.js → chevron-down-CsiGcG0G.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-COmDwrUT.js → cloud-checkout-z_eDVwm6.js} +1 -1
  65. package/dashboard/dist/assets/{composite-kLwqpqEk.js → composite-Cq0CmNAv.js} +1 -1
  66. package/dashboard/dist/assets/{download-9RgsZIiH.js → download-DqLpubIF.js} +1 -1
  67. package/dashboard/dist/assets/{external-link-C_gYjHGF.js → external-link-B8E8tbB3.js} +1 -1
  68. package/dashboard/dist/assets/{flame-BSsCvgT8.js → flame-CrJDlnro.js} +1 -1
  69. package/dashboard/dist/assets/{format-tokens-N22Wqv1X.js → format-tokens-DPIGGOlt.js} +1 -1
  70. package/dashboard/dist/assets/gift-CCp4Hh50.js +1 -0
  71. package/dashboard/dist/assets/{host-mode-JzrhHX7F.js → host-mode-xzpMLcwP.js} +1 -1
  72. package/dashboard/dist/assets/{icons-pK99mIMv.js → icons-CbB9Y-eV.js} +1 -1
  73. package/dashboard/dist/assets/{index-BhviBroj.js → index-BiK5-tQp.js} +1 -1
  74. package/dashboard/dist/assets/{index-CmD84EJi.js → index-DX7nYrHb.js} +1 -1
  75. package/dashboard/dist/assets/{index-Dk43KNa6.js → index-zuC7FU-t.js} +1 -1
  76. package/dashboard/dist/assets/{info-D-RLFnSK.js → info-B3NKYwWG.js} +1 -1
  77. package/dashboard/dist/assets/{link-2-BiKXW9mV.js → link-2-BRc8d8M9.js} +1 -1
  78. package/dashboard/dist/assets/{loader-circle-KVUaEKV2.js → loader-circle-Cm5Nsgp-.js} +1 -1
  79. package/dashboard/dist/assets/{modifiers.esm-5NK8j3mT.js → modifiers.esm-CYLYBECB.js} +1 -1
  80. package/dashboard/dist/assets/module-BNnaKM__.js +3 -0
  81. package/dashboard/dist/assets/{monitor-smartphone-u9yr-_Es.js → monitor-smartphone--IZ4D84F.js} +1 -1
  82. package/dashboard/dist/assets/{play-D0MaDCOx.js → play-CUrMOLtq.js} +1 -1
  83. package/dashboard/dist/assets/{plus-DTJVtoAJ.js → plus-0hrCDl4r.js} +1 -1
  84. package/dashboard/dist/assets/{provider-display-YhtG0I9Y.js → provider-display-CNK0FN3Q.js} +1 -1
  85. package/dashboard/dist/assets/{react-gIVxucOB.js → react-CCNSVHEA.js} +1 -1
  86. package/dashboard/dist/assets/{safePolygon-C_sPnJ1L.js → safePolygon-DsolkMUX.js} +1 -1
  87. package/dashboard/dist/assets/{search-DdQRwFYV.js → search-DYmYb9xY.js} +1 -1
  88. package/dashboard/dist/assets/{server-M0rmbGWy.js → server-CYcqzdSW.js} +1 -1
  89. package/dashboard/dist/assets/{shield-check-9-sqQKdD.js → shield-check-5zABLIr6.js} +1 -1
  90. package/dashboard/dist/assets/{skills-api-B0qBWnuK.js → skills-api-CD-ojBGj.js} +1 -1
  91. package/dashboard/dist/assets/{sortable.esm-iqbm7x5D.js → sortable.esm-BCn2Ms6R.js} +1 -1
  92. package/dashboard/dist/assets/{store-DstuWd4a.js → store-B22kwLs0.js} +1 -1
  93. package/dashboard/dist/assets/{terminal-Dz0MxHNG.js → terminal-CTv-oTU2.js} +1 -1
  94. package/dashboard/dist/assets/{trash-DBMFOFVz.js → trash-C5MAh3GO.js} +1 -1
  95. package/dashboard/dist/assets/{use-community-stats-CacbAcUR.js → use-community-stats-BrgrxUD2.js} +1 -1
  96. package/dashboard/dist/assets/{use-latest-request-guard-nBuQVZOR.js → use-latest-request-guard-B1_rbvgH.js} +1 -1
  97. package/dashboard/dist/assets/use-limits-display-prefs-BOk5f-HM.js +1 -0
  98. package/dashboard/dist/assets/{use-native-settings-EAqAFx2x.js → use-native-settings-B2gc_m9W.js} +1 -1
  99. package/dashboard/dist/assets/{use-ordered-list-DGLjjU6Q.js → use-ordered-list-N3sGpJ0u.js} +1 -1
  100. package/dashboard/dist/assets/{use-pet-settings-C5MzWHE7.js → use-pet-settings-sWFXgR3m.js} +2 -2
  101. package/dashboard/dist/assets/use-reduced-motion-C3MBYVqm.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-C25eCuz_.js → use-transform-BsaLmw46.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-wL7v-S0r.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-t3PYQi1l.js → useCurrency-CbCnjf-N.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-C59mj6kG.js → usePositioner-MepYlL0A.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-C0xQsBGf.js → useTokenFormat-Bq-rwuKa.js} +1 -1
  107. package/dashboard/dist/assets/{zap-CyH0tvKM.js → zap-1ah5e11g.js} +1 -1
  108. package/dashboard/dist/index.html +14 -12
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/llms.txt +2 -2
  112. package/dashboard/dist/share.html +2 -2
  113. package/package.json +2 -2
  114. package/src/cli.js +1 -1
  115. package/src/commands/init.js +43 -1
  116. package/src/commands/status.js +27 -0
  117. package/src/commands/sync.js +54 -4
  118. package/src/commands/uninstall.js +19 -0
  119. package/src/lib/claude-config.js +47 -15
  120. package/src/lib/codex-rollout-parser.js +11 -3
  121. package/src/lib/command-runner.js +15 -1
  122. package/src/lib/cursor-config.js +2 -1
  123. package/src/lib/gemini-config.js +3 -2
  124. package/src/lib/local-api.js +13 -3
  125. package/src/lib/notify-command.js +38 -0
  126. package/src/lib/openclaw-session-plugin.js +47 -14
  127. package/src/lib/passive-mode.js +12 -0
  128. package/src/lib/pricing/curated-overrides.json +42 -5
  129. package/src/lib/pricing/matcher.js +14 -5
  130. package/src/lib/pricing/seed-snapshot.json +1 -1
  131. package/src/lib/rollout.js +199 -44
  132. package/src/lib/session-analytics.js +653 -60
  133. package/src/lib/session-performance.js +24 -1
  134. package/src/lib/usage-limits.js +183 -20
  135. package/src/lib/zcode-limits.js +218 -18
  136. package/src/lib/zcode-reset-credits.js +66 -0
  137. package/dashboard/dist/assets/DialogDescription-C0Cgq67g.js +0 -1
  138. package/dashboard/dist/assets/LeaderboardPage-D8ydzVpn.js +0 -2
  139. package/dashboard/dist/assets/LimitsPage-DaRggHge.js +0 -8
  140. package/dashboard/dist/assets/SessionsPage-C6Y8kbnM.js +0 -3
  141. package/dashboard/dist/assets/SettingsPage-nztCXEPs.js +0 -2
  142. package/dashboard/dist/assets/__vite-browser-external-BwmeEfv7.css +0 -1
  143. package/dashboard/dist/assets/agent-logos-CK6SzhI7.js +0 -1
  144. package/dashboard/dist/assets/module-WSx8viXK.js +0 -3
  145. package/dashboard/dist/assets/use-limits-display-prefs-fqg8hs6b.js +0 -1
  146. package/dashboard/dist/assets/use-reduced-motion-DK2oZ2uC.js +0 -1
  147. package/dashboard/dist/assets/use-usage-limits-BhqnEai3.js +0 -1
@@ -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
- out.sort((a, b) => a.localeCompare(b));
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.close?.();
2146
+ if (!lines) rl.close();
2147
+ if (!fileHandle) stream?.close?.();
2074
2148
  return {
2075
2149
  endOffset,
2076
2150
  eventsAggregated,
@@ -5053,16 +5127,21 @@ function buildV2Sql(sessionTable, watermark = null) {
5053
5127
  // sessionTable (which session table exists, session_v2 preferred over session
5054
5128
  // via DESC). Returns null when the probe itself fails — callers treat that as
5055
5129
  // v1-only, matching the pre-existing silent-degradation contract.
5056
- function detectOpencodeMessageLayout(dbPath, sqliteOptions = {}) {
5130
+ function detectOpencodeMessageLayout(dbPath, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5057
5131
  let rows;
5058
5132
  try {
5059
5133
  rows = readSqliteJsonRows(
5060
5134
  dbPath,
5061
5135
  `SELECT (SELECT 1 FROM session_message LIMIT 1) AS hasRows,
5062
5136
  (SELECT name FROM sqlite_master WHERE name IN ('session','session_v2') ORDER BY name DESC LIMIT 1) AS sessionTable`,
5063
- { label: "OpenCode", timeout: 10_000, maxBuffer: 1024 * 1024, ...sqliteOptions },
5137
+ { label: "OpenCode", timeout: 10_000, maxBuffer: 1024 * 1024, ...sqliteOptions, throwOnReadFailure: true },
5064
5138
  );
5065
- } catch (_e) {
5139
+ } catch (error) {
5140
+ // A missing session_message table is the normal v1-only miss; with
5141
+ // propagateReadFailure a real read failure must reach the caller — the
5142
+ // session scanner would otherwise skip the v2 generation silently and
5143
+ // cache a zero-token inventory over a readable-in-principle database.
5144
+ if (propagateReadFailure && !isMissingSqliteTableError(error)) throw error;
5066
5145
  return null;
5067
5146
  }
5068
5147
  if (!Array.isArray(rows) || rows.length === 0) return null;
@@ -5082,7 +5161,17 @@ function opencodeMessageProvider(data) {
5082
5161
  return data?.providerID || data?.model?.providerID || "";
5083
5162
  }
5084
5163
 
5085
- function readOpencodeDbMessagesIncremental(dbPath, previousCursor = null, sqliteOptions = {}) {
5164
+ // readSqliteJsonRows wraps its two backend errors into the thrown Error, so a
5165
+ // "table genuinely absent" miss and a "database unreadable" failure look the
5166
+ // same to a caller that only catches. Both backends spell a missing table
5167
+ // "no such table" — use that to keep the normal miss on the tolerant path
5168
+ // while real read failures can propagate.
5169
+ function isMissingSqliteTableError(error) {
5170
+ const messages = [error?.message, error?.cliError?.message, error?.nodeSqliteError?.message];
5171
+ return messages.some((m) => typeof m === "string" && m.includes("no such table"));
5172
+ }
5173
+
5174
+ function readOpencodeDbMessagesIncremental(dbPath, previousCursor = null, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5086
5175
  if (!dbPath || !fssync.existsSync(dbPath)) return { messages: [], cursor: null };
5087
5176
 
5088
5177
  const identity = opencodeDbIdentity(dbPath);
@@ -5149,7 +5238,11 @@ function readOpencodeDbMessagesIncremental(dbPath, previousCursor = null, sqlite
5149
5238
  throwOnReadFailure: true,
5150
5239
  });
5151
5240
  return Array.isArray(rows) ? rows : [];
5152
- } catch (_e) {
5241
+ } catch (error) {
5242
+ // A missing table is the normal "other generation's layout" miss; an
5243
+ // unreadable database is not — with propagateReadFailure the session
5244
+ // scan aborts instead of caching a zero-token inventory over it.
5245
+ if (propagateReadFailure && !isMissingSqliteTableError(error)) throw error;
5153
5246
  return null; // query-level failure (e.g. wrong generation's table)
5154
5247
  }
5155
5248
  };
@@ -5227,7 +5320,7 @@ function readOpencodeDbMessagesIncremental(dbPath, previousCursor = null, sqlite
5227
5320
  // baseline every database carries or degrades to), v2 runs only when the
5228
5321
  // probe confirms session_message has rows — this avoids firing a JOIN
5229
5322
  // against a non-existent session table on type-A databases.
5230
- const probe = detectOpencodeMessageLayout(effectiveDbPath, sqliteOptions);
5323
+ const probe = detectOpencodeMessageLayout(effectiveDbPath, sqliteOptions, { propagateReadFailure });
5231
5324
  const out = [];
5232
5325
  const nextCursor = {
5233
5326
  version: OPENCODE_DB_CURSOR_VERSION,
@@ -5267,8 +5360,8 @@ function readOpencodeDbMessagesIncremental(dbPath, previousCursor = null, sqlite
5267
5360
  }
5268
5361
  }
5269
5362
 
5270
- function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
5271
- return readOpencodeDbMessagesIncremental(dbPath, null, sqliteOptions).messages;
5363
+ function readOpencodeDbMessages(dbPath, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5364
+ return readOpencodeDbMessagesIncremental(dbPath, null, sqliteOptions, { propagateReadFailure }).messages;
5272
5365
  }
5273
5366
 
5274
5367
  // mimocode mirrors the user's Claude Code + claude-mem history into its own
@@ -5384,7 +5477,7 @@ const ZCODE_NATIVE_USAGE_COLUMNS = new Set([
5384
5477
  "cache_read_input_tokens",
5385
5478
  ]);
5386
5479
 
5387
- function detectZcodeNativeUsageLayout(dbPath, sqliteOptions = {}) {
5480
+ function detectZcodeNativeUsageLayout(dbPath, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5388
5481
  if (!dbPath || !fssync.existsSync(dbPath)) return null;
5389
5482
  let rows;
5390
5483
  try {
@@ -5401,7 +5494,11 @@ function detectZcodeNativeUsageLayout(dbPath, sqliteOptions = {}) {
5401
5494
  throwOnReadFailure: true,
5402
5495
  },
5403
5496
  );
5404
- } catch (_error) {
5497
+ } catch (error) {
5498
+ // propagateReadFailure: an unreadable db is a failure, not "no native
5499
+ // layout" — the session scanner must skip the file, not fall back to an
5500
+ // empty legacy read and cache zero tokens.
5501
+ if (propagateReadFailure) throw error;
5405
5502
  return null;
5406
5503
  }
5407
5504
  const modelUsageColumns = new Set(
@@ -5471,7 +5568,13 @@ function buildZcodeNativeUsageSql({ hasSessionDirectory }) {
5471
5568
  ORDER BY mu.started_at ASC, mu.id ASC`;
5472
5569
  }
5473
5570
 
5474
- function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
5571
+ // `propagateReadFailure` (off by default): when the native usage layout is
5572
+ // present but its row query fails, throw instead of returning null. The usage
5573
+ // queue treats that fallthrough as "no native table, use legacy" — correct for
5574
+ // its own resilience — but the session scanner must not cache a zero-token
5575
+ // inventory over a failed read (the browser then hides the sessions entirely
5576
+ // instead of reporting the db as skipped).
5577
+ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5475
5578
  let snapshot = null;
5476
5579
  let effectiveDbPath = dbPath;
5477
5580
  if (isUncPath(dbPath)) {
@@ -5486,7 +5589,7 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
5486
5589
 
5487
5590
  let rows;
5488
5591
  try {
5489
- const layout = detectZcodeNativeUsageLayout(effectiveDbPath, sqliteOptions);
5592
+ const layout = detectZcodeNativeUsageLayout(effectiveDbPath, sqliteOptions, { propagateReadFailure });
5490
5593
  if (!layout) return null;
5491
5594
  rows = readSqliteJsonRows(effectiveDbPath, buildZcodeNativeUsageSql(layout), {
5492
5595
  label: "ZCode",
@@ -5495,7 +5598,8 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
5495
5598
  ...sqliteOptions,
5496
5599
  throwOnReadFailure: true,
5497
5600
  });
5498
- } catch (_error) {
5601
+ } catch (error) {
5602
+ if (propagateReadFailure) throw error;
5499
5603
  return null;
5500
5604
  } finally {
5501
5605
  if (snapshot) snapshot.cleanup();
@@ -5547,10 +5651,10 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
5547
5651
  // table is authoritative from its first completed row onward, but it is not a
5548
5652
  // historical backfill. Keep legacy rows before that boundary so merely adding
5549
5653
  // the new table cannot make older usage disappear from TokenTracker.
5550
- function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
5654
+ function readZcodeDbMessages(dbPath, sqliteOptions = {}, { propagateReadFailure = false } = {}) {
5551
5655
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
5552
- const nativeMessages = readZcodeNativeUsageMessages(dbPath, sqliteOptions);
5553
- const legacyMessages = readOpencodeDbMessages(dbPath, sqliteOptions)
5656
+ const nativeMessages = readZcodeNativeUsageMessages(dbPath, sqliteOptions, { propagateReadFailure });
5657
+ const legacyMessages = readOpencodeDbMessages(dbPath, sqliteOptions, { propagateReadFailure })
5554
5658
  .filter((message) => isZcodeNativeMessage(message.data))
5555
5659
  .map(normalizeZcodeLegacyMessage);
5556
5660
  if (nativeMessages === null) return legacyMessages;
@@ -10451,23 +10555,43 @@ async function parseCodebuddyIncremental({
10451
10555
  // is accepted by workbuddySqliteUsageSnapshot.
10452
10556
  // ─────────────────────────────────────────────────────────────────────────────
10453
10557
 
10454
- function resolveWorkbuddyHome(env = process.env) {
10455
- const home = env.HOME || require("node:os").homedir();
10456
- if (env.WORKBUDDY_HOME) return env.WORKBUDDY_HOME;
10558
+ // WorkBuddy ships two regional builds that keep their data in sibling homes:
10559
+ // ~/.workbuddy — Tencent CN build (models: hy*/glm*/deepseek-*/auto)
10560
+ // ~/.workbuddy-ai — international build (models: gpt-5.6-*, gpt-6-*)
10561
+ // Both write byte-identical JSONL, so every reader below is shared and only
10562
+ // the home directory (and the bucket source key) differs. `variant` is also
10563
+ // the source key written into hourly buckets, so the two builds stay
10564
+ // separable in the dashboard and never share dedup state.
10565
+ const WORKBUDDY_VARIANTS = {
10566
+ workbuddy: { dir: ".workbuddy", envVar: "WORKBUDDY_HOME" },
10567
+ "workbuddy-ai": { dir: ".workbuddy-ai", envVar: "WORKBUDDY_AI_HOME" },
10568
+ };
10569
+
10570
+ function resolveWorkbuddyVariant(variant) {
10571
+ return Object.prototype.hasOwnProperty.call(WORKBUDDY_VARIANTS, variant)
10572
+ ? variant
10573
+ : "workbuddy";
10574
+ }
10575
+
10576
+ function resolveWorkbuddyHome(env = process.env, variant = "workbuddy") {
10577
+ const spec = WORKBUDDY_VARIANTS[resolveWorkbuddyVariant(variant)];
10578
+ const home = env.HOME || env.USERPROFILE || require("node:os").homedir();
10579
+ const configured = expandHome(env[spec.envVar], home);
10580
+ if (configured) return configured;
10457
10581
  if (process.platform === "win32") {
10458
10582
  return pickWin32ProviderPath({
10459
10583
  env,
10460
- nativeValue: path.join(home, ".workbuddy"),
10461
- wslProviderDir: ".workbuddy",
10584
+ nativeValue: path.join(home, spec.dir),
10585
+ wslProviderDir: spec.dir,
10462
10586
  });
10463
10587
  }
10464
- return path.join(home, ".workbuddy");
10588
+ return path.join(home, spec.dir);
10465
10589
  }
10466
10590
 
10467
- function resolveWorkbuddyDefaultModel(env = process.env) {
10591
+ function resolveWorkbuddyDefaultModel(env = process.env, variant = "workbuddy") {
10468
10592
  const fallback = "auto";
10469
10593
  try {
10470
- const workbuddyHome = resolveWorkbuddyHome(env);
10594
+ const workbuddyHome = resolveWorkbuddyHome(env, variant);
10471
10595
  if (!workbuddyHome) return fallback;
10472
10596
  const settingsPath = path.join(workbuddyHome, "settings.json");
10473
10597
  const raw = fssync.readFileSync(settingsPath, "utf8");
@@ -10487,8 +10611,8 @@ function resolveWorkbuddyDefaultModel(env = process.env) {
10487
10611
  // ~/.workbuddy/traces/<pid>/trace_*.json. Those summaries are useful only when
10488
10612
  // the detailed JSONL for a session is absent, so they are returned as typed
10489
10613
  // entries and parsed after JSONL (JSONL remains authoritative when both exist).
10490
- function resolveWorkbuddyProjectFiles(env = process.env) {
10491
- const workbuddyHome = resolveWorkbuddyHome(env);
10614
+ function resolveWorkbuddyProjectFiles(env = process.env, variant = "workbuddy") {
10615
+ const workbuddyHome = resolveWorkbuddyHome(env, variant);
10492
10616
  if (!workbuddyHome) return [];
10493
10617
  const projectsDir = path.join(workbuddyHome, "projects");
10494
10618
  const files = [];
@@ -10597,10 +10721,15 @@ async function parseWorkbuddyIncremental({
10597
10721
  onProgress,
10598
10722
  env,
10599
10723
  defaultModel,
10724
+ source = "workbuddy",
10600
10725
  } = {}) {
10601
10726
  await ensureDir(path.dirname(queuePath));
10727
+ // `source` doubles as the variant selector and the bucket source key, so the
10728
+ // CN install (~/.workbuddy) and the international install (~/.workbuddy-ai)
10729
+ // get independent cursor namespaces and never share dedup state.
10730
+ const variant = resolveWorkbuddyVariant(source);
10602
10731
  const workbuddyState =
10603
- cursors.workbuddy && typeof cursors.workbuddy === "object" ? cursors.workbuddy : {};
10732
+ cursors[variant] && typeof cursors[variant] === "object" ? cursors[variant] : {};
10604
10733
  const seenIds = new Set(
10605
10734
  Array.isArray(workbuddyState.seenIds) ? workbuddyState.seenIds : [],
10606
10735
  );
@@ -10626,20 +10755,20 @@ async function parseWorkbuddyIncremental({
10626
10755
 
10627
10756
  const allFiles = Array.isArray(projectFiles)
10628
10757
  ? projectFiles
10629
- : resolveWorkbuddyProjectFiles(env || process.env);
10758
+ : resolveWorkbuddyProjectFiles(env || process.env, variant);
10630
10759
  const jsonlFiles = allFiles.filter((entry) => typeof entry === "string");
10631
10760
  const traceFiles = allFiles.filter(
10632
10761
  (entry) => entry && typeof entry === "object" && typeof entry.path === "string" && entry.kind === "trace",
10633
10762
  );
10634
10763
  const files = [...jsonlFiles, ...traceFiles];
10635
- const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env);
10764
+ const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env, variant);
10636
10765
 
10637
- const workbuddyHome = resolveWorkbuddyHome(env || process.env);
10766
+ const workbuddyHome = resolveWorkbuddyHome(env || process.env, variant);
10638
10767
  const dbPath = workbuddyHome ? path.join(workbuddyHome, "workbuddy.db") : null;
10639
10768
  const dbExists = Boolean(dbPath && fssync.existsSync(dbPath));
10640
10769
 
10641
10770
  if (files.length === 0 && !dbExists) {
10642
- cursors.workbuddy = {
10771
+ cursors[variant] = {
10643
10772
  ...workbuddyState,
10644
10773
  seenIds: Array.from(seenIds),
10645
10774
  seenTraceIds: Array.from(seenTraceIds),
@@ -10786,9 +10915,9 @@ async function parseWorkbuddyIncremental({
10786
10915
  conversation_count: 1,
10787
10916
  };
10788
10917
 
10789
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
10918
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
10790
10919
  addTotals(bucket.totals, delta);
10791
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
10920
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
10792
10921
  seenIds.add(messageId);
10793
10922
  // Keep legacy entry-id keys alongside the response id so upgrading a
10794
10923
  // cursor cannot replay a row that an older build already counted.
@@ -10897,9 +11026,9 @@ async function parseWorkbuddyIncremental({
10897
11026
  total_tokens: inputTokens + totalCached + totalOutput,
10898
11027
  conversation_count: 1,
10899
11028
  };
10900
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
11029
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
10901
11030
  addTotals(bucket.totals, delta);
10902
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
11031
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
10903
11032
  seenTraceIds.add(traceId);
10904
11033
  tracedSessionIds.add(sessionId);
10905
11034
  recordsProcessed++;
@@ -11032,9 +11161,9 @@ async function parseWorkbuddyIncremental({
11032
11161
  conversation_count: Object.keys(previousTokens).every((key) => toNonNegativeInt(previousTokens[key]) === 0) || isReset ? 1 : 0,
11033
11162
  };
11034
11163
 
11035
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
11164
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
11036
11165
  addTotals(bucket.totals, delta);
11037
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
11166
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
11038
11167
  sqliteSessions[sessionId] = {
11039
11168
  used: usedNow,
11040
11169
  updatedAt: updatedAtRaw,
@@ -11085,7 +11214,7 @@ async function parseWorkbuddyIncremental({
11085
11214
  const updatedAt = new Date().toISOString();
11086
11215
  hourlyState.updatedAt = updatedAt;
11087
11216
  cursors.hourly = hourlyState;
11088
- cursors.workbuddy = {
11217
+ cursors[variant] = {
11089
11218
  ...workbuddyState,
11090
11219
  seenIds: cappedSeen,
11091
11220
  seenTraceIds: cappedSeenTraceIds,
@@ -15972,6 +16101,27 @@ function resolvePiSessionFiles(env = process.env) {
15972
16101
  } catch {
15973
16102
  // ignore — return what we have
15974
16103
  }
16104
+ // Multica launches pi headless with a dedicated flat session dir:
16105
+ // ~/.multica/pi-sessions/<session-id>.jsonl
16106
+ // Scan it too so orchestrated runs are counted alongside TUI sessions.
16107
+ try {
16108
+ const seen = new Set(files);
16109
+ const multicaSessionsDir = path.join(
16110
+ env.HOME || require("node:os").homedir(),
16111
+ ".multica",
16112
+ "pi-sessions",
16113
+ );
16114
+ for (const entry of fssync.readdirSync(multicaSessionsDir, { withFileTypes: true })) {
16115
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
16116
+ const full = path.join(multicaSessionsDir, entry.name);
16117
+ if (!seen.has(full)) {
16118
+ seen.add(full);
16119
+ files.push(full);
16120
+ }
16121
+ }
16122
+ } catch {
16123
+ // ignore — no Multica install
16124
+ }
15975
16125
  files.sort((a, b) => a.localeCompare(b));
15976
16126
  return files;
15977
16127
  }
@@ -24439,6 +24589,8 @@ module.exports = {
24439
24589
  readOpencodeDbMessagesIncremental,
24440
24590
  readMimoDbMessages,
24441
24591
  readZcodeDbMessages,
24592
+ coerceEpochMs,
24593
+ normalizeOpencodeModelFields,
24442
24594
  hasZcodeNativeUsageSchema,
24443
24595
  resolveQoderDbPath,
24444
24596
  resolveQoderDbPaths,
@@ -24473,6 +24625,7 @@ module.exports = {
24473
24625
  parseGeminiIncremental,
24474
24626
  parseOpencodeIncremental,
24475
24627
  parseOpencodeDbIncremental,
24628
+ deriveOpencodeMessageFingerprint,
24476
24629
  parseQoderDbIncremental,
24477
24630
  openclawCursorKey,
24478
24631
  parseOpenclawIncremental,
@@ -24514,6 +24667,8 @@ module.exports = {
24514
24667
  resolveWorkbuddyProjectFiles,
24515
24668
  resolveWorkbuddyDefaultModel,
24516
24669
  parseWorkbuddyIncremental,
24670
+ WORKBUDDY_VARIANTS,
24671
+ resolveWorkbuddyVariant,
24517
24672
  resolveKiroCliSessionFiles,
24518
24673
  resolveKiroCliDbPath,
24519
24674
  parseKiroCliIncremental,