tokentracker-cli 1.4.9 → 1.4.11

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 (128) hide show
  1. package/README.de.md +3 -3
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/{AchievementBadge-_dRLgxnp.js → AchievementBadge-DALWiLQQ.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DtvlZLlv.js → AchievementsPage-CjWobmn1.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-OgzKkbes.js → AchievementsSection-CTryGWII.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D4Rlm_um.js → BadgeDetailModal-D4lMxewA.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-errJ5IvB.js → BotAnimated-CsLNf9WH.js} +1 -1
  11. package/dashboard/dist/assets/{Card-esELVQcD.js → Card-BOnNTrmF.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-3sH4Dbx9.js → CloudCheckoutPage-CrY15xqT.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-Bi_vgiqE.js → CloudPage-hNSCMzVG.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-I0aze2EJ.js → CommandPalette-CE4UfPXk.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-CEn2g4J9.js → CommunityStatsModal-CLUegC6z.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-D7SfFCaf.js → ConfirmModal-D3wwfiYB.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-Dyb5Vmk_.js → Controls-B_ta_8zw.js} +1 -1
  18. package/dashboard/dist/assets/{DashboardPage-CMHX8zDq.js → DashboardPage-RevVPZFt.js} +1 -1
  19. package/dashboard/dist/assets/{DateRangePopover-CgmxsHVR.js → DateRangePopover-CYud1FKu.js} +1 -1
  20. package/dashboard/dist/assets/DevicePage-D_eSLfM3.js +1 -0
  21. package/dashboard/dist/assets/{DialogClose-DzR109OA.js → DialogClose-Bu1ct4DA.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-GUHQVNGf.js +1 -0
  23. package/dashboard/dist/assets/{DialogTitle-BLH1MgOX.js → DialogTitle-BJxM52J1.js} +1 -1
  24. package/dashboard/dist/assets/{ElectricLogo-DcpBcJIy.js → ElectricLogo-DYVZebgD.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CBAXe_zb.js → FadeIn-CVaq5ALh.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-BZjQ3uJo.js → HeaderGithubStar-BCwYSguq.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-Bq4HuWOZ.js → HoverTooltip-YsG-TdOX.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-Oiah1-YB.js → IpCheckPage-C0qiyZXr.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-Cc1FfkWg.js → LandingPage-BlRJWwvx.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-Bc2j_DQY.js → LeaderboardPage-DcCCKL2q.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-w1olVHVp.js → LeaderboardProBadge-I_oW0KCc.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-mgG7mdl8.js → LeaderboardProfileModal-DM5Vc7MB.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-DqxyooT5.js → LeaderboardProfilePage-DnYBBGNe.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-Cyy3PI8p.js +6 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-DZLgI75v.js → LocalOnlyNotice-083BoEX9.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-DVNU_QtS.js → LoginCard-CN1db2Rj.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-yxi5pCwC.js → LoginModal-Dfi7_ory.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-CW9pu4mf.js → LoginPage-BzMdiYlx.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-DwZsq4nC.js → PetPage-CsE95lfJ.js} +2 -2
  40. package/dashboard/dist/assets/{PopoverPopup-DYGmmoD8.js → PopoverPopup-U7DTTTE0.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BKfj89GN.js → ProviderIcon-CnLfT5d9.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-gQdgCp9s.js → ResetPasswordPage-De2RzCj-.js} +1 -1
  43. package/dashboard/dist/assets/{Select-DvSR56Lj.js → Select-BS5MWHdB.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-DXo30s3O.js → SelectItemText-D3i379K-.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-jA51Nckw.js → SelfHostPage-CTL0dnp5.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-DEkRRPx8.js → ServiceStatusPage-BmEvOk3B.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-J7hvGPCj.js → SessionsPage-DIog3JA_.js} +2 -2
  48. package/dashboard/dist/assets/{SettingsPage-ewH1Mzsm.js → SettingsPage-Cgpiwh-C.js} +3 -3
  49. package/dashboard/dist/assets/{SkillsPage-BaeBHhU7.js → SkillsPage-DpagshiB.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D52pHpzj.js → TokenGalaxy-Do1U_-E3.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-mLpESQcv.js → TooltipPopup-MOAnEqf3.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-ErowR5GE.js → TrendMonitor-B55RUbQm.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-Bf1uuKE7.js → WidgetsPage-CehbGk-6.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-C8qacvpR.js → WrappedPage-Cqt-U0Zx.js} +1 -1
  55. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +1 -0
  56. package/dashboard/dist/assets/{__vite-browser-external-CIpYjfAT.js → __vite-browser-external-CR0vo15z.js} +3 -3
  57. package/dashboard/dist/assets/{agent-logos-tmdKvCcK.js → agent-logos-DuW-DWAl.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-left-CRKi420c.js → arrow-left-DIvQjuji.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-C_o_iQbK.js → arrow-right-BURLycHb.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-N5YqKKnI.js → arrow-up-right-Bl1QMMkY.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-BJh4FDj9.js → calendar-days-CmIWJC3A.js} +1 -1
  62. package/dashboard/dist/assets/{check-BfHGb92g.js → check-DB2ZTWor.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-D1pO8Xgc.js → chevron-down-BflN3NJt.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-Ds13TmYz.js → cloud-checkout-BugQolSC.js} +1 -1
  65. package/dashboard/dist/assets/{composite-DDEsQKPD.js → composite-DJsqXsjA.js} +1 -1
  66. package/dashboard/dist/assets/{copy-CJTosYLe.js → copy-CtB-GPN2.js} +1 -1
  67. package/dashboard/dist/assets/{download-Bkqx3c3y.js → download-x5Botpqc.js} +1 -1
  68. package/dashboard/dist/assets/{external-link-IYUo5LcK.js → external-link-DH9sRkJW.js} +1 -1
  69. package/dashboard/dist/assets/{flame-BD_hiMfM.js → flame-DEcsokoL.js} +1 -1
  70. package/dashboard/dist/assets/{format-tokens-DxwbhPag.js → format-tokens-BH0jc1MG.js} +1 -1
  71. package/dashboard/dist/assets/{gift-DLzFdL-1.js → gift-BZk9muk9.js} +1 -1
  72. package/dashboard/dist/assets/{icons-CbrWeFPS.js → icons-BMf9-bfS.js} +1 -1
  73. package/dashboard/dist/assets/{index-Dj1QnoPF.js → index-BEtrik3j.js} +1 -1
  74. package/dashboard/dist/assets/{index-DfzCE-FL.js → index-Cm-9apOR.js} +1 -1
  75. package/dashboard/dist/assets/{index-AVEWo-aY.js → index-TErx-xPb.js} +1 -1
  76. package/dashboard/dist/assets/{index-3xGpnZnP.js → index-axJYocSc.js} +1 -1
  77. package/dashboard/dist/assets/{info-odlIlCnm.js → info-DWdPAxu8.js} +1 -1
  78. package/dashboard/dist/assets/{link-2-Bt28Ey9r.js → link-2-c3ZZd-w-.js} +1 -1
  79. package/dashboard/dist/assets/{loader-circle-lGnIhIFS.js → loader-circle-BKdtxfky.js} +1 -1
  80. package/dashboard/dist/assets/{local-usage-transfer-DSS0WaL-.js → local-usage-transfer-BFTJyQl3.js} +1 -1
  81. package/dashboard/dist/assets/{modifiers.esm-CitRa_Z1.js → modifiers.esm-C3yIfOiP.js} +1 -1
  82. package/dashboard/dist/assets/{monitor-smartphone-1T0GFcOz.js → monitor-smartphone-DbdL7Mei.js} +1 -1
  83. package/dashboard/dist/assets/{play-d6YYOmED.js → play-qtWt2ArI.js} +1 -1
  84. package/dashboard/dist/assets/{plus-GrGV9Fkm.js → plus-C_-__5vi.js} +1 -1
  85. package/dashboard/dist/assets/{provider-display-D1v3cgDm.js → provider-display-IKHnhvaj.js} +1 -1
  86. package/dashboard/dist/assets/{react-DYCAyQbo.js → react-H6DfeMUm.js} +1 -1
  87. package/dashboard/dist/assets/{safePolygon-iPu060Qe.js → safePolygon-OEQJQTVa.js} +1 -1
  88. package/dashboard/dist/assets/{search-AQhOTwzm.js → search-MZE76qNU.js} +1 -1
  89. package/dashboard/dist/assets/{server-uYTbWPMd.js → server-CUWYLMjG.js} +1 -1
  90. package/dashboard/dist/assets/{shield-check-DB1NVCDf.js → shield-check-lA0Ja0RB.js} +1 -1
  91. package/dashboard/dist/assets/{skills-api-D7X10n0t.js → skills-api-CJJIPvSe.js} +1 -1
  92. package/dashboard/dist/assets/{sortable.esm-B13KhrCd.js → sortable.esm-DTnvt1IN.js} +1 -1
  93. package/dashboard/dist/assets/{store-D6XHiq8o.js → store-CC0YKrxA.js} +1 -1
  94. package/dashboard/dist/assets/{terminal-t37F8qyF.js → terminal-yc2l_-3m.js} +1 -1
  95. package/dashboard/dist/assets/{trash-CDHRwGsy.js → trash-Dl9f-o2h.js} +1 -1
  96. package/dashboard/dist/assets/{use-community-stats--kaGg0EW.js → use-community-stats-CZCKWCO7.js} +1 -1
  97. package/dashboard/dist/assets/{use-latest-request-guard-ChyODWU3.js → use-latest-request-guard-DTrOu0gD.js} +1 -1
  98. package/dashboard/dist/assets/use-limits-display-prefs-DGqP231e.js +1 -0
  99. package/dashboard/dist/assets/{use-native-settings-i3O4GTY1.js → use-native-settings-BEATnxB6.js} +1 -1
  100. package/dashboard/dist/assets/{use-ordered-list-BtewEEX_.js → use-ordered-list-C_HTFP5C.js} +1 -1
  101. package/dashboard/dist/assets/use-reduced-motion-CfCWjE-a.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-NBAyuphu.js → use-transform-DM0QGq_O.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-BnjBG2fM.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-BdsxkAID.js → useCurrency-xQrRFokb.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-BAN79miB.js → usePositioner-DkVdsypM.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-CGEO_mX1.js → useTokenFormat-CvA_kIPe.js} +1 -1
  107. package/dashboard/dist/assets/{zap-BDHWp-gL.js → zap-BJNS4i_u.js} +1 -1
  108. package/dashboard/dist/index.html +5 -5
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/share.html +2 -2
  112. package/package.json +2 -2
  113. package/src/lib/grok-limits.js +1 -0
  114. package/src/lib/omp-session.js +480 -0
  115. package/src/lib/pricing/curated-overrides.json +35 -0
  116. package/src/lib/pricing/matcher.js +44 -15
  117. package/src/lib/pricing/seed-snapshot.json +1 -1
  118. package/src/lib/session-analytics.js +54 -8
  119. package/src/lib/usage-limits.js +20 -8
  120. package/src/lib/workbuddy-ipc.js +191 -0
  121. package/src/lib/workbuddy-limits.js +564 -0
  122. package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +0 -1
  123. package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +0 -1
  124. package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +0 -6
  125. package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +0 -1
  126. package/dashboard/dist/assets/use-limits-display-prefs-katFAxOA.js +0 -1
  127. package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +0 -1
  128. package/dashboard/dist/assets/use-usage-limits-hsv-93TF.js +0 -1
@@ -38,6 +38,7 @@ const { readCursorStateSummary } = require("./cursor-store");
38
38
  const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
39
39
  const wsl = require("./wsl-probe");
40
40
  const { discoverKimiWireFiles, kimiSessionDependencies, readKimiSessions } = require("./kimi-session");
41
+ const { discoverOmpSessionFiles, ompSessionDependencies, readOmpSessions } = require("./omp-session");
41
42
  const {
42
43
  appendUniqueDirs,
43
44
  dedupeDirsByRealpath,
@@ -89,7 +90,8 @@ const {
89
90
  // v25 adds Kimi wire sessions, including official/legacy migration dedup.
90
91
  // v26 retains Codex Ultrafast (service_tier ultrafast) subset columns so
91
92
  // date-filtered session views and cached sidecars keep the 6x premium.
92
- const SIDECAR_VERSION = 26;
93
+ // v27 adds oh-my-pi (omp) sessions from ~/.omp/agent/sessions/**/*.jsonl.
94
+ const SIDECAR_VERSION = 27;
93
95
  const EDIT_TOOLS = new Set([
94
96
  "apply_patch",
95
97
  "edit",
@@ -1948,6 +1950,26 @@ async function scanKimiSessions(filePaths) {
1948
1950
  }));
1949
1951
  }
1950
1952
 
1953
+ async function scanOmpSessions(filePaths) {
1954
+ // Batch the whole OMP inventory so fork copies that reuse entry ids are
1955
+ // counted once across sessions (same contract as the hourly seenIds set).
1956
+ const rows = await readOmpSessions(Array.isArray(filePaths) ? filePaths : [filePaths]);
1957
+ return rows.map(({ identity, files, title, ...row }) => finalizeRecord({
1958
+ ...row,
1959
+ title: cleanSessionTitle(title),
1960
+ version: SIDECAR_VERSION,
1961
+ source: "omp",
1962
+ session_hash: sessionHash("omp", identity),
1963
+ project_key: projectKey(row.project_ref, files[0]),
1964
+ ...sessionStorageUsage(files),
1965
+ }));
1966
+ }
1967
+
1968
+ async function scanOmpSession(filePath) {
1969
+ const rows = await scanOmpSessions([filePath]);
1970
+ return rows[0] || null;
1971
+ }
1972
+
1951
1973
  async function discoverSessionFiles(home, env = process.env, deps = {}) {
1952
1974
  const grokHome = resolveGrokHome(home);
1953
1975
  // config.scanRoots (#657): read from the tracker config unless the caller
@@ -2040,7 +2062,12 @@ async function discoverSessionFiles(home, env = process.env, deps = {}) {
2040
2062
  return roots;
2041
2063
  });
2042
2064
  const kimi = discoverKimiWireFiles(dedupeDirsByRealpath(kimiRoots), incomplete);
2043
- return { claude, codex: groupCodexFiles([...codex, ...archived]), grok, zcode, opencode, kimi, incomplete };
2065
+ const usingMachineHome = path.resolve(home) === path.resolve((deps.homedir || os.homedir)());
2066
+ const ompEnv = usingMachineHome
2067
+ ? { ...env, HOME: home }
2068
+ : { HOME: home, OMP_HOME: path.join(home, ".omp") };
2069
+ const omp = discoverOmpSessionFiles(ompEnv, incomplete);
2070
+ return { claude, codex: groupCodexFiles([...codex, ...archived]), grok, zcode, opencode, kimi, omp, incomplete };
2044
2071
  }
2045
2072
 
2046
2073
  // Warn once per unreadable directory per process: the dashboard polls this
@@ -2144,6 +2171,9 @@ function analyticsEntryStatKey(source, filePath) {
2144
2171
  if (source === "kimi") {
2145
2172
  return `${sessionStat}|metadata:${filesSignature(kimiSessionDependencies(filePaths))}`;
2146
2173
  }
2174
+ if (source === "omp") {
2175
+ return `${sessionStat}|metadata:${filesSignature(ompSessionDependencies([filePaths].flat()))}`;
2176
+ }
2147
2177
  if (source === "grok") {
2148
2178
  const primary = filePaths[0];
2149
2179
  return `${sessionStat}|summary:${sessionFileStatKey(grokSummaryPathFor(primary)) || "missing"}|signals:${sessionFileStatKey(grokSignalsPathFor(primary)) || "missing"}`;
@@ -2185,13 +2215,15 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2185
2215
  const retryOpencodeScan = fs.existsSync(incompleteOpencodePath);
2186
2216
  const incompleteKimiPath = `${metaPath}.kimi-incomplete`;
2187
2217
  const retryKimiScan = fs.existsSync(incompleteKimiPath);
2218
+ const incompleteOmpPath = `${metaPath}.omp-incomplete`;
2219
+ const retryOmpScan = fs.existsSync(incompleteOmpPath);
2188
2220
  let previousMeta = null;
2189
2221
  if (!force) {
2190
2222
  try {
2191
2223
  previousMeta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
2192
2224
  const checkedAt = Date.parse(previousMeta.checked_at || previousMeta.generated_at || "");
2193
2225
  if (
2194
- !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && previousMeta.version === SIDECAR_VERSION &&
2226
+ !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && previousMeta.version === SIDECAR_VERSION &&
2195
2227
  Number.isFinite(checkedAt) &&
2196
2228
  Date.now() - checkedAt < Math.max(0, Number(cacheTtlMs) || 0)
2197
2229
  ) {
@@ -2223,13 +2255,15 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2223
2255
  ...discovered.codex.flat().map(codexTitleIndexPathFor).filter(Boolean),
2224
2256
  ...discovered.kimi,
2225
2257
  ...kimiSessionDependencies(discovered.kimi),
2258
+ ...discovered.omp,
2259
+ ...ompSessionDependencies(discovered.omp),
2226
2260
  ...discovered.grok,
2227
2261
  ...discovered.grok.map(grokSummaryPathFor),
2228
2262
  ...discovered.grok.map(grokSignalsPathFor),
2229
2263
  // db + its WAL sidecars: the trio is one logical file for change detection.
2230
2264
  ...[...(discovered.zcode || []), ...(discovered.opencode || [])].flatMap((dbPath) => [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]),
2231
2265
  ]);
2232
- if (!force && !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
2266
+ if (!force && !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && !retryOmpScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
2233
2267
  await writeAtomic(metaPath, `${JSON.stringify({ ...previousMeta, checked_at: new Date().toISOString() })}\n`);
2234
2268
  return readSidecar(sidecarPath);
2235
2269
  }
@@ -2252,6 +2286,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2252
2286
  ...(discovered.zcode.length ? [{ source: "zcode", filePath: discovered.zcode, scan: scanZcodeSessions }] : []),
2253
2287
  ...(discovered.opencode.length ? [{ source: "opencode", filePath: discovered.opencode, scan: scanOpencodeSessions }] : []),
2254
2288
  ...(discovered.kimi.length ? [{ source: "kimi", filePath: discovered.kimi, scan: scanKimiSessions }] : []),
2289
+ ...(discovered.omp.length ? [{ source: "omp", filePath: discovered.omp, scan: scanOmpSessions }] : []),
2255
2290
  ];
2256
2291
  // Files we could not turn into a row (permission denied, half-written line,
2257
2292
  // vanished mid-scan). Swallowing these silently made sessions disappear with
@@ -2260,6 +2295,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2260
2295
  let incompleteZcodeScan = false;
2261
2296
  let incompleteOpencodeScan = false;
2262
2297
  let incompleteKimiScan = false;
2298
+ let incompleteOmpScan = false;
2263
2299
  for (const entry of entries) {
2264
2300
  const cacheKey = sessionFileCacheKey(entry.source, entry.filePath);
2265
2301
  const statKey = analyticsEntryStatKey(entry.source, entry.filePath);
@@ -2268,11 +2304,12 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2268
2304
  if (entry.source === "zcode") incompleteZcodeScan = true;
2269
2305
  if (entry.source === "opencode") incompleteOpencodeScan = true;
2270
2306
  if (entry.source === "kimi") incompleteKimiScan = true;
2307
+ if (entry.source === "omp") incompleteOmpScan = true;
2271
2308
  continue;
2272
2309
  }
2273
2310
  let rows = null;
2274
- if (!force && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && !(entry.source === "kimi" && retryKimiScan) && previousFiles[cacheKey]?.stat_key === statKey) {
2275
- if (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi") {
2311
+ if (!force && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && !(entry.source === "kimi" && retryKimiScan) && !(entry.source === "omp" && retryOmpScan) && previousFiles[cacheKey]?.stat_key === statKey) {
2312
+ if (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi" || entry.source === "omp") {
2276
2313
  // One db scans to many cached rows; recover them all from the previous
2277
2314
  // pass. An empty recovery means the sidecar lost its rows — rescan.
2278
2315
  const cached = previousRows.filter((row) => row?.source === entry.source);
@@ -2293,6 +2330,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2293
2330
  if (entry.source === "zcode") incompleteZcodeScan = true;
2294
2331
  if (entry.source === "opencode") incompleteOpencodeScan = true;
2295
2332
  if (entry.source === "kimi") incompleteKimiScan = true;
2333
+ if (entry.source === "omp") incompleteOmpScan = true;
2296
2334
  if (!process.env.NODE_TEST_CONTEXT) {
2297
2335
  console.warn(`[session-analytics] skipped ${entry.source} session file: ${error?.message || error}`);
2298
2336
  }
@@ -2306,7 +2344,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2306
2344
  // so key each by the db path plus its session id; a bare session id as a
2307
2345
  // separate path element would be path.resolve()d against the process cwd
2308
2346
  // and never match across runs.
2309
- row._cache_key = (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi") && (row.session_id || row.session_hash)
2347
+ row._cache_key = (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi" || entry.source === "omp") && (row.session_id || row.session_hash)
2310
2348
  ? `${cacheKey}#${sessionHash(entry.source, row.session_id || row.session_hash)}`
2311
2349
  : cacheKey;
2312
2350
  sessions.push(row);
@@ -2330,10 +2368,14 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2330
2368
  await writeAtomic(incompleteKimiPath, "1\n");
2331
2369
  sessions.push(...readSidecar(sidecarPath).filter((row) => row.source === "kimi" && row.version === SIDECAR_VERSION));
2332
2370
  }
2371
+ if (incompleteOmpScan) {
2372
+ await writeAtomic(incompleteOmpPath, "1\n");
2373
+ sessions.push(...readSidecar(sidecarPath).filter((row) => row.source === "omp" && row.version === SIDECAR_VERSION));
2374
+ }
2333
2375
  sessions.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
2334
2376
  // A failed database scan is an incomplete inventory. Keep the last complete
2335
2377
  // snapshot and signature so an unchanged database is retried next build.
2336
- if (incompleteDirs.length === 0 && !incompleteZcodeScan && !incompleteOpencodeScan && !incompleteKimiScan) {
2378
+ if (incompleteDirs.length === 0 && !incompleteZcodeScan && !incompleteOpencodeScan && !incompleteKimiScan && !incompleteOmpScan) {
2337
2379
  const content = sessions.map(serializeSessionRecord).join("\n") + (sessions.length ? "\n" : "");
2338
2380
  await writeAtomic(sidecarPath, content);
2339
2381
  const generatedAt = new Date().toISOString();
@@ -2347,6 +2389,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2347
2389
  await fsp.rm(incompleteZcodePath, { force: true });
2348
2390
  await fsp.rm(incompleteOpencodePath, { force: true });
2349
2391
  await fsp.rm(incompleteKimiPath, { force: true });
2392
+ await fsp.rm(incompleteOmpPath, { force: true });
2350
2393
  }
2351
2394
  // Non-enumerable so the array still behaves exactly like a plain row list
2352
2395
  // for every existing caller (map/filter/JSON of the rows is unaffected).
@@ -2632,6 +2675,7 @@ function resumeCommandFor(source, sessionId) {
2632
2675
  // Grok Build: `grok --resume <session-id>` (also accepts a title). The
2633
2676
  // command must still be run from the session's project_ref cwd.
2634
2677
  if (source === "grok") return `grok --resume ${id}`;
2678
+ if (source === "omp") return `omp --resume ${id}`;
2635
2679
  return null;
2636
2680
  }
2637
2681
 
@@ -2973,6 +3017,8 @@ module.exports = {
2973
3017
  scanZcodeSessions,
2974
3018
  scanOpencodeSessions,
2975
3019
  scanKimiSessions,
3020
+ scanOmpSessions,
3021
+ scanOmpSession,
2976
3022
  buildSessionAnalytics,
2977
3023
  summarizeSessions,
2978
3024
  listSessionsForBrowser,
@@ -35,6 +35,7 @@ const { fetchDevinLimits } = require("./devin-limits");
35
35
  const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
36
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
37
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
38
+ const { fetchWorkbuddyLimits, fetchWorkbuddyAiLimits, selectWorkbuddyLimits } = require("./workbuddy-limits");
38
39
  const { fetchProviderServiceStatus } = require("./provider-status");
39
40
  const { resolveKimiProfile } = require("./kimi-profile");
40
41
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
@@ -749,13 +750,12 @@ function normalizeCursorUsageSummary(body) {
749
750
  }
750
751
 
751
752
  /**
752
- * Cursor's Sand "Grok Bot" RPC reports the shared Super weekly pool (same
753
- * reset and about the same percent as Grok billing's creditUsagePercent).
754
- * Once the Grok card lists a Grok Bot product share, that Sand bar would
755
- * show the whole pool a second time under Cursor. Hide it only in that
756
- * case. A real separate Sand quota (different reset, or a percent more than
757
- * a point away from the pool) stays. Accounts with no Grok Bot product row
758
- * keep the Sand bar.
753
+ * Cursor's Sand RPC reports Grok Bot usage. grok.com shows that as a weekly
754
+ * Grok Bot limit of its own, separate from the SuperGrok pool and with its own
755
+ * reset, so the Sand bar normally stays. It is hidden only when it is
756
+ * indistinguishable from the Grok pool, so the same numbers are not drawn
757
+ * twice: Grok billing lists a GrokBot product share, and the two windows reset
758
+ * at the same instant and sit within a point of each other.
759
759
  */
760
760
  function cursorSandDuplicatesGrokPool(cursor, grok) {
761
761
  const sand = cursor?.quaternary_window;
@@ -4107,7 +4107,7 @@ async function fetchUsageLimitsUncached({
4107
4107
  : null;
4108
4108
 
4109
4109
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
4110
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, claudeServiceStatus] = await Promise.all([
4110
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4111
4111
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4112
4112
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4113
4113
  (value) => ({ status: "fulfilled", value }),
@@ -4232,6 +4232,13 @@ async function fetchUsageLimitsUncached({
4232
4232
  (value) => ({ status: "fulfilled", value }),
4233
4233
  (reason) => ({ status: "rejected", reason }),
4234
4234
  ),
4235
+ // WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
4236
+ // its own local authenticated billing proxy. Abort closes the IPC socket
4237
+ // when the provider budget expires. Reward credits are not a plan window.
4238
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy", providerTimeoutMs)
4239
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4240
+ withAbortableProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy AI", providerTimeoutMs)
4241
+ .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4235
4242
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4236
4243
  // Only probed for configured accounts — without a token the Claude section
4237
4244
  // never renders, so the reading would have nowhere to go.
@@ -4498,6 +4505,9 @@ async function fetchUsageLimitsUncached({
4498
4505
  qoderCn: withPlanLabel(qoderCn, qoderCn?.plan_label, "Qoder CN"),
4499
4506
  codingPlan: withPlanLabel(codingPlan, codingPlan?.plan_label, "Ark Coding Plan"),
4500
4507
  agentPlan: withPlanLabel(agentPlan, agentPlan?.plan_label, "Ark Agent Plan"),
4508
+ // WorkBuddy preserves provider English names and official edition labels
4509
+ // verbatim (including Free, STANDARD and Pro-Trial).
4510
+ workbuddy: selectWorkbuddyLimits(workbuddy, workbuddyAi),
4501
4511
  };
4502
4512
 
4503
4513
  for (const [providerName, provider] of Object.entries(data)) {
@@ -4587,4 +4597,6 @@ module.exports = {
4587
4597
  fetchCommandcodeLimits,
4588
4598
  fetchQoderLimits,
4589
4599
  fetchQoderCnLimits,
4600
+ fetchWorkbuddyLimits,
4601
+ fetchWorkbuddyAiLimits,
4590
4602
  };
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+
3
+ // WorkBuddy's authenticated local proxy. The ticket proves both peers; the
4
+ // desktop daemon adds its own access token to relative billing requests.
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const net = require("node:net");
8
+ const crypto = require("node:crypto");
9
+
10
+ const UNAVAILABLE = "Start WorkBuddy and sign in to read its subscription quota.";
11
+ const MAX_FRAME_BYTES = 1024 * 1024;
12
+ const BILLING_PATHS = new Set([
13
+ "/billing/meter/get-user-resource-summary",
14
+ "/billing/meter/get-user-resource-paid-packages",
15
+ "/billing/meter/get-user-resource-free-packages",
16
+ ]);
17
+
18
+ function readWorkbuddyIpcDiscovery(workbuddyHome) {
19
+ if (!workbuddyHome) return null;
20
+ let fd;
21
+ try {
22
+ fd = fs.openSync(path.join(workbuddyHome, "wbipc", "endpoint.json"),
23
+ fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
24
+ const stat = fs.fstatSync(fd);
25
+ if (!stat.isFile() || stat.size > 16384 || (process.platform !== "win32"
26
+ && (stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0))) {
27
+ throw new Error("Untrusted discovery file.");
28
+ }
29
+ const value = JSON.parse(fs.readFileSync(fd, "utf8"));
30
+ if (typeof value.ticket !== "string" || !/^[\w-]{43}$/.test(value.ticket)
31
+ || typeof value.endpoint !== "string") throw new Error("Invalid discovery file.");
32
+ if (process.platform === "win32") {
33
+ if (!/^\\\\\.\\pipe\\wbipc-[\w-]+$/.test(value.endpoint)) throw new Error("Invalid pipe.");
34
+ } else {
35
+ if (!path.isAbsolute(value.endpoint)) throw new Error("Invalid socket.");
36
+ const socket = fs.lstatSync(value.endpoint);
37
+ if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o077) !== 0) {
38
+ throw new Error("Untrusted socket.");
39
+ }
40
+ }
41
+ return { endpoint: value.endpoint, ticket: value.ticket };
42
+ } catch (error) {
43
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
44
+ // Never expose discovery contents, tickets, paths or JSON parser excerpts.
45
+ throw new Error("WorkBuddy local connection information is unavailable.");
46
+ } finally {
47
+ if (fd !== undefined) fs.closeSync(fd);
48
+ }
49
+ }
50
+
51
+ function computeProof(ticket, role, endpoint, clientNonce, serverNonce) {
52
+ const parts = [];
53
+ for (const field of [role === "server" ? "wbipc-s" : "wbipc-c", "1", endpoint, clientNonce, serverNonce]) {
54
+ const bytes = Buffer.from(field, "utf8");
55
+ const length = Buffer.alloc(4);
56
+ length.writeUInt32BE(bytes.length);
57
+ parts.push(length, bytes);
58
+ }
59
+ return crypto.createHmac("sha256", ticket).update(Buffer.concat(parts)).digest("base64url");
60
+ }
61
+
62
+ function openWorkbuddyIpc({ discovery, signal, timeoutMs = 12000, connectImpl = net.createConnection }) {
63
+ return new Promise((resolve, reject) => {
64
+ const { endpoint, ticket } = discovery;
65
+ const clientNonce = crypto.randomBytes(16).toString("base64url");
66
+ const pending = new Map();
67
+ let socket;
68
+ let phase = "challenge";
69
+ let channel;
70
+ let nextId = 1;
71
+ let buffer = Buffer.alloc(0);
72
+ let closed = false;
73
+ const timer = setTimeout(() => fail(new Error("WorkBuddy quota request timed out.")), timeoutMs);
74
+ const onAbort = () => fail(new Error("WorkBuddy quota request was cancelled."));
75
+
76
+ function fail(error) {
77
+ if (closed) return;
78
+ closed = true;
79
+ clearTimeout(timer);
80
+ signal?.removeEventListener("abort", onAbort);
81
+ reject(error);
82
+ for (const handlers of pending.values()) handlers.reject(error);
83
+ pending.clear();
84
+ socket?.destroy();
85
+ }
86
+ function send(packet) {
87
+ socket.write(JSON.stringify(packet) + "\n");
88
+ }
89
+ function rpc(method, params, mode) {
90
+ if (closed) return Promise.reject(new Error(UNAVAILABLE));
91
+ return new Promise((resolve, reject) => {
92
+ const id = nextId++;
93
+ pending.set(id, { resolve, reject });
94
+ send({ jsonrpc: "2.0", id, method, params, ...(mode ? { mode } : {}) });
95
+ });
96
+ }
97
+ const client = {
98
+ close: () => fail(new Error("WorkBuddy local connection closed.")),
99
+ async postBilling(apiPath, body) {
100
+ if (!BILLING_PATHS.has(apiPath)) throw new Error("Unsupported WorkBuddy quota request.");
101
+ const response = await rpc(`${channel}/http.fetch`, {
102
+ method: "POST",
103
+ path: apiPath,
104
+ headers: { "content-type": "application/json", accept: "application/json" },
105
+ body_b64: Buffer.from(JSON.stringify(body)).toString("base64"),
106
+ }, "call");
107
+ if (response?.status === 401 || response?.status === 403) {
108
+ const error = new Error("WorkBuddy sign-in expired.");
109
+ error.code = "AUTH_EXPIRED";
110
+ throw error;
111
+ }
112
+ if (response?.status !== 200) throw new Error("WorkBuddy quota request failed.");
113
+ try {
114
+ if (typeof response.body_b64 !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(response.body_b64)) {
115
+ throw new Error("Invalid body.");
116
+ }
117
+ return JSON.parse(Buffer.from(response.body_b64, "base64").toString("utf8"));
118
+ } catch {
119
+ throw new Error("WorkBuddy quota response was malformed.");
120
+ }
121
+ },
122
+ };
123
+ function onPacket(packet) {
124
+ if (!packet || typeof packet !== "object") throw new Error("Invalid packet.");
125
+ if (phase === "challenge" && packet.type === "session_challenge" && packet.protocol === 1) {
126
+ if (typeof packet.server_nonce !== "string" || !/^[\w-]{16,128}$/.test(packet.server_nonce)
127
+ || typeof packet.server_proof !== "string") throw new Error("Invalid challenge.");
128
+ const expected = Buffer.from(computeProof(ticket, "server", endpoint, clientNonce, packet.server_nonce));
129
+ const actual = Buffer.from(packet.server_proof);
130
+ if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
131
+ throw new Error("Invalid server proof.");
132
+ }
133
+ phase = "ack";
134
+ send({ type: "session_prove", client_proof: computeProof(ticket, "client", endpoint, clientNonce, packet.server_nonce) });
135
+ } else if (phase === "ack" && packet.type === "session_hello_ack" && packet.protocol === 1
136
+ && Array.isArray(packet.pipes) && packet.pipes.includes("wb.request")) {
137
+ phase = "ready";
138
+ rpc("broker/GetPipe", { pipe: "wb.request" }).then((result) => {
139
+ if (closed) return;
140
+ if (result?.channel !== "c:wb.request" || !Array.isArray(result.methods)
141
+ || !result.methods.includes("http.fetch")) {
142
+ fail(new Error("WorkBuddy billing proxy is unavailable."));
143
+ return;
144
+ }
145
+ channel = result.channel;
146
+ resolve(client);
147
+ }, () => fail(new Error(UNAVAILABLE)));
148
+ } else if (phase === "ready" && packet.jsonrpc === "2.0" && pending.has(packet.id)) {
149
+ const handlers = pending.get(packet.id);
150
+ pending.delete(packet.id);
151
+ if (packet.error) handlers.reject(new Error("WorkBuddy local quota request was rejected."));
152
+ else handlers.resolve(packet.result);
153
+ } else {
154
+ throw new Error("Unexpected packet.");
155
+ }
156
+ }
157
+ signal?.addEventListener("abort", onAbort, { once: true });
158
+ if (signal?.aborted) { onAbort(); return; }
159
+ try {
160
+ socket = connectImpl(endpoint);
161
+ socket.on("connect", () => send({
162
+ type: "session_hello", protocol_min: 1, protocol_max: 1,
163
+ client_nonce: clientNonce,
164
+ ticket_id: crypto.createHash("sha256").update(ticket, "utf8").digest("hex").slice(0, 16),
165
+ client: { kind: "tokentracker", id: "subscription-quota", version: "1" },
166
+ }));
167
+ socket.on("data", (chunk) => {
168
+ if (closed) return;
169
+ buffer = Buffer.concat([buffer, chunk]);
170
+ if (buffer.length > MAX_FRAME_BYTES) { fail(new Error("WorkBuddy quota response was too large.")); return; }
171
+ try {
172
+ let index;
173
+ while ((index = buffer.indexOf(10)) >= 0 && !closed) {
174
+ const line = buffer.subarray(0, index);
175
+ buffer = buffer.subarray(index + 1);
176
+ if (line.length) onPacket(JSON.parse(line.toString("utf8")));
177
+ }
178
+ } catch {
179
+ fail(new Error("WorkBuddy local connection authentication or protocol failed."));
180
+ }
181
+ });
182
+ socket.on("error", () => fail(new Error(UNAVAILABLE)));
183
+ socket.on("close", () => fail(new Error(UNAVAILABLE)));
184
+ socket.on("end", () => fail(new Error(UNAVAILABLE)));
185
+ } catch {
186
+ fail(new Error(UNAVAILABLE));
187
+ }
188
+ });
189
+ }
190
+
191
+ module.exports = { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc };