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.
Files changed (145) 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-Dazs3pEa.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DpOXrabE.js → AchievementsPage-LWnxZvmw.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DiXyIoa0.js → AchievementsSection-BLqczmU9.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-CILTTkPF.js → BadgeDetailModal-B_n4aj2S.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CrBHWiRi.js → BotAnimated-KDWI_YcZ.js} +1 -1
  11. package/dashboard/dist/assets/{Card-DUItW3xI.js → Card-BYoxd4OC.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-C_t_PDlG.js → CloudCheckoutPage-BR2_B03P.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-pvJUrhya.js → CloudPage-DL5RkJKK.js} +1 -1
  14. package/dashboard/dist/assets/{CloudUsageExport-DQbDwiyk.js → CloudUsageExport-C_DqZKo1.js} +1 -1
  15. package/dashboard/dist/assets/{CommandPalette-BvHilrbV.js → CommandPalette-DkCaIQI4.js} +1 -1
  16. package/dashboard/dist/assets/{CommunityStatsModal-CizxEYv-.js → CommunityStatsModal-pX9OePi4.js} +1 -1
  17. package/dashboard/dist/assets/{ConfirmModal-h9nISVy7.js → ConfirmModal-tkgZwz4u.js} +1 -1
  18. package/dashboard/dist/assets/{Controls-BjdIonTX.js → Controls-MTUhGWy8.js} +1 -1
  19. package/dashboard/dist/assets/{DashboardPage-Bxryr4qp.js → DashboardPage-SOx-A7lH.js} +1 -1
  20. package/dashboard/dist/assets/{DateRangePopover-DpKh-0l8.js → DateRangePopover-B-BMOm3O.js} +1 -1
  21. package/dashboard/dist/assets/{DevicePage-DUJR9hJC.js → DevicePage-DSK9lm7v.js} +1 -1
  22. package/dashboard/dist/assets/{DialogClose-BmVAmN-o.js → DialogClose-uVUNu4rg.js} +1 -1
  23. package/dashboard/dist/assets/DialogDescription-DBqGGWNj.js +1 -0
  24. package/dashboard/dist/assets/{DialogTitle-BsaIbN8k.js → DialogTitle-B-VifiA3.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-BpSt2P_W.js → FadeIn-B6lrj4uo.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-Cc_VHB0R.js → HeaderGithubStar-Cx5zOhXa.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-WFd21iP9.js → HoverTooltip-DY1ENf69.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-C_mUPSRB.js → IpCheckPage-D6TEq64d.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-DVR8ClZO.js → LandingPage-OdZUfq--.js} +2 -2
  30. package/dashboard/dist/assets/LeaderboardPage-DCK5xcAt.js +2 -0
  31. package/dashboard/dist/assets/{LeaderboardProBadge-BjX4EFl2.js → LeaderboardProBadge-CBXJ1u_m.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-vbPMNn7W.js → LeaderboardProfileModal-Ccdzb7QK.js} +4 -4
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-CqClmuA-.js → LeaderboardProfilePage-CFXlZ0fL.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-5krjm-g9.js +9 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-B6nDke_u.js → LocalOnlyNotice-Rk1B8PaZ.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-C28QVjd7.js → LoginCard-DTTIm8vk.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-Bx1btNhH.js → LoginModal-BNcDdjfr.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-DVOGNGYI.js → LoginPage-CQgXP7Ee.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-nOKryTde.js → PetPage-Cw_kAPRs.js} +1 -1
  40. package/dashboard/dist/assets/{PopoverPopup-Ch6NLnu6.js → PopoverPopup-NYiGE8A2.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BRY2br-g.js → ProviderIcon-Bp7iFJ_m.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-ko1OQecQ.js → ResetPasswordPage-Cf9kAWDW.js} +1 -1
  43. package/dashboard/dist/assets/{Select-Q8mS3_od.js → Select-kIcvQFAo.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-C1id8GDj.js → SelectItemText-BpIsAg3b.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-Dzb2bDiM.js → SelfHostPage-Ch2vvSwI.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-ewFwz29J.js → ServiceStatusPage-hoYBvGbu.js} +1 -1
  47. package/dashboard/dist/assets/SessionsPage-BzRB6q-Z.js +3 -0
  48. package/dashboard/dist/assets/SettingsPage-DRl2uX-i.js +2 -0
  49. package/dashboard/dist/assets/{SkillsPage-CcG77tqU.js → SkillsPage-CkqThTZL.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D2HDrbQR.js → TokenGalaxy-eLG-UIZY.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-C_bF2ZwO.js → TooltipPopup-bnMJyilQ.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-BcyoN5ns.js → TrendMonitor-X7-aGzIv.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-BirK0U_N.js → WidgetsPage-CGznIzrW.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-thsXBuw6.js → WrappedPage-p31-uUoF.js} +1 -1
  55. package/dashboard/dist/assets/{__vite-browser-external-Bq8nj-JV.js → __vite-browser-external-2g_4s3ot.js} +12 -12
  56. package/dashboard/dist/assets/__vite-browser-external-LpFw4zXh.css +1 -0
  57. package/dashboard/dist/assets/agent-logos-2TVT3fIE.js +1 -0
  58. package/dashboard/dist/assets/{arrow-left-c_NOSfoq.js → arrow-left-DL0hAp_7.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-CP7drKCT.js → arrow-right-DFCpnV_9.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-DQ6QfCpo.js → arrow-up-right-DLvfE0Wu.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-D_pxFQvT.js → calendar-days-B0aDoKSG.js} +1 -1
  62. package/dashboard/dist/assets/{check-HAS31kTT.js → check-C_cfrxiz.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-BIMuk6oN.js → chevron-down-Ck8ml43e.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-COmDwrUT.js → cloud-checkout-PYnwNfdH.js} +1 -1
  65. package/dashboard/dist/assets/{composite-kLwqpqEk.js → composite-INW-ELWX.js} +1 -1
  66. package/dashboard/dist/assets/{download-9RgsZIiH.js → download-B7uokeIo.js} +1 -1
  67. package/dashboard/dist/assets/{external-link-C_gYjHGF.js → external-link-CI3crg4K.js} +1 -1
  68. package/dashboard/dist/assets/{flame-BSsCvgT8.js → flame-DCzvFKV1.js} +1 -1
  69. package/dashboard/dist/assets/{format-tokens-N22Wqv1X.js → format-tokens-CSTEYK2m.js} +1 -1
  70. package/dashboard/dist/assets/gift-dEHDFMc6.js +1 -0
  71. package/dashboard/dist/assets/{host-mode-JzrhHX7F.js → host-mode-r2SW9Mdv.js} +1 -1
  72. package/dashboard/dist/assets/{icons-pK99mIMv.js → icons-q-3zvEkh.js} +1 -1
  73. package/dashboard/dist/assets/{index-CmD84EJi.js → index-BV7qwUw4.js} +1 -1
  74. package/dashboard/dist/assets/{index-Dk43KNa6.js → index-BbrsgHf4.js} +1 -1
  75. package/dashboard/dist/assets/{index-BhviBroj.js → index-CyHYj9XI.js} +1 -1
  76. package/dashboard/dist/assets/{info-D-RLFnSK.js → info-BBdY9hV4.js} +1 -1
  77. package/dashboard/dist/assets/{link-2-BiKXW9mV.js → link-2-CLPePfXs.js} +1 -1
  78. package/dashboard/dist/assets/{loader-circle-KVUaEKV2.js → loader-circle-ETrjzfmn.js} +1 -1
  79. package/dashboard/dist/assets/{modifiers.esm-5NK8j3mT.js → modifiers.esm-DHqnp2ol.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-D1kdzrdb.js} +1 -1
  82. package/dashboard/dist/assets/{play-D0MaDCOx.js → play-Cxp-ronD.js} +1 -1
  83. package/dashboard/dist/assets/{plus-DTJVtoAJ.js → plus-4n94VzDK.js} +1 -1
  84. package/dashboard/dist/assets/{provider-display-YhtG0I9Y.js → provider-display-CRlbeO7t.js} +1 -1
  85. package/dashboard/dist/assets/{react-gIVxucOB.js → react-DlHjGQFN.js} +1 -1
  86. package/dashboard/dist/assets/{safePolygon-C_sPnJ1L.js → safePolygon-CVZ27a2_.js} +1 -1
  87. package/dashboard/dist/assets/{search-DdQRwFYV.js → search-BDPr56_h.js} +1 -1
  88. package/dashboard/dist/assets/{server-M0rmbGWy.js → server-Bm7fHvVa.js} +1 -1
  89. package/dashboard/dist/assets/{shield-check-9-sqQKdD.js → shield-check-DtT8ACzI.js} +1 -1
  90. package/dashboard/dist/assets/{skills-api-B0qBWnuK.js → skills-api-Dc1-2qu6.js} +1 -1
  91. package/dashboard/dist/assets/{sortable.esm-iqbm7x5D.js → sortable.esm-B-qN-Xk2.js} +1 -1
  92. package/dashboard/dist/assets/{store-DstuWd4a.js → store-gHeBU8dQ.js} +1 -1
  93. package/dashboard/dist/assets/{terminal-Dz0MxHNG.js → terminal-D4VZbvhg.js} +1 -1
  94. package/dashboard/dist/assets/{trash-DBMFOFVz.js → trash-BZtLAFQy.js} +1 -1
  95. package/dashboard/dist/assets/{use-community-stats-CacbAcUR.js → use-community-stats-BdqndrbB.js} +1 -1
  96. package/dashboard/dist/assets/{use-latest-request-guard-nBuQVZOR.js → use-latest-request-guard-DhGg1cDS.js} +1 -1
  97. package/dashboard/dist/assets/use-limits-display-prefs-D3HOcRFv.js +1 -0
  98. package/dashboard/dist/assets/{use-native-settings-EAqAFx2x.js → use-native-settings-BniKwA_n.js} +1 -1
  99. package/dashboard/dist/assets/{use-ordered-list-DGLjjU6Q.js → use-ordered-list-b09H7EHq.js} +1 -1
  100. package/dashboard/dist/assets/{use-pet-settings-C5MzWHE7.js → use-pet-settings-CPZdlq_e.js} +2 -2
  101. package/dashboard/dist/assets/use-reduced-motion-D1gcKJUo.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-C25eCuz_.js → use-transform-CIdxjmI8.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-eJlU65yY.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-t3PYQi1l.js → useCurrency-CM22CmAq.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-C59mj6kG.js → usePositioner-B3l2ulTw.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-C0xQsBGf.js → useTokenFormat-D7nzgxKw.js} +1 -1
  107. package/dashboard/dist/assets/{zap-CyH0tvKM.js → zap-Daxct1WV.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/commands/init.js +43 -1
  115. package/src/commands/status.js +27 -0
  116. package/src/commands/sync.js +54 -4
  117. package/src/commands/uninstall.js +19 -0
  118. package/src/lib/claude-config.js +47 -15
  119. package/src/lib/codex-rollout-parser.js +11 -3
  120. package/src/lib/command-runner.js +15 -1
  121. package/src/lib/cursor-config.js +2 -1
  122. package/src/lib/gemini-config.js +3 -2
  123. package/src/lib/local-api.js +13 -3
  124. package/src/lib/notify-command.js +38 -0
  125. package/src/lib/openclaw-session-plugin.js +47 -14
  126. package/src/lib/passive-mode.js +12 -0
  127. package/src/lib/pricing/curated-overrides.json +42 -5
  128. package/src/lib/pricing/matcher.js +14 -5
  129. package/src/lib/pricing/seed-snapshot.json +1 -1
  130. package/src/lib/rollout.js +150 -28
  131. package/src/lib/session-analytics.js +108 -9
  132. package/src/lib/usage-limits.js +183 -20
  133. package/src/lib/zcode-limits.js +218 -18
  134. package/src/lib/zcode-reset-credits.js +66 -0
  135. package/dashboard/dist/assets/DialogDescription-C0Cgq67g.js +0 -1
  136. package/dashboard/dist/assets/LeaderboardPage-D8ydzVpn.js +0 -2
  137. package/dashboard/dist/assets/LimitsPage-DaRggHge.js +0 -8
  138. package/dashboard/dist/assets/SessionsPage-C6Y8kbnM.js +0 -3
  139. package/dashboard/dist/assets/SettingsPage-nztCXEPs.js +0 -2
  140. package/dashboard/dist/assets/__vite-browser-external-BwmeEfv7.css +0 -1
  141. package/dashboard/dist/assets/agent-logos-CK6SzhI7.js +0 -1
  142. package/dashboard/dist/assets/module-WSx8viXK.js +0 -3
  143. package/dashboard/dist/assets/use-limits-display-prefs-fqg8hs6b.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-DK2oZ2uC.js +0 -1
  145. package/dashboard/dist/assets/use-usage-limits-BhqnEai3.js +0 -1
@@ -713,6 +713,8 @@ async function parseCodexRolloutFile(filePath, {
713
713
  };
714
714
  byModel.set(effectiveModel, row);
715
715
  }
716
+ const subsetKeys = Object.keys(row).filter((key) => /^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key));
717
+ const before = Object.fromEntries(subsetKeys.map((key) => [key, row[key]]));
716
718
  addInto(row, delta);
717
719
  row.usage_events += 1;
718
720
  if (attribution.selectedModel) {
@@ -758,6 +760,10 @@ async function parseCodexRolloutFile(filePath, {
758
760
  row.priority_long_context_reasoning_output_tokens += delta.reasoning_output_tokens;
759
761
  }
760
762
  }
763
+ return { ...delta, ...Object.fromEntries(subsetKeys.map((key) => [key, row[key] - before[key]])),
764
+ selected_models: attribution.selectedModel ? [attribution.selectedModel] : [],
765
+ reroute_reasons: attribution.rerouteReason ? [attribution.rerouteReason] : [],
766
+ model_attribution: attribution.rerouted ? "effective" : "selected" };
761
767
  }
762
768
 
763
769
  function ensureTool(name) {
@@ -927,7 +933,9 @@ async function parseCodexRolloutFile(filePath, {
927
933
  if (compactionResponseIds.has(record.responseId)) return;
928
934
  compactionResponseIds.add(record.responseId);
929
935
  if (!record.inRequestedRange) return;
930
- recordModelUsage(record.delta, record.rawUsage, record.attribution);
936
+ const pricedDelta = recordModelUsage(record.delta, record.rawUsage, record.attribution);
937
+ if (typeof onUsage === "function") onUsage({ timestamp: record.timestamp,
938
+ delta: pricedDelta || record.delta, rawUsage: record.rawUsage, model: record.attribution.model, origin: "compaction" });
931
939
  attributeTurn(record.delta);
932
940
  }
933
941
 
@@ -1098,8 +1106,8 @@ async function parseCodexRolloutFile(filePath, {
1098
1106
  attributeTurn(null);
1099
1107
  } else {
1100
1108
  if (seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
1101
- recordModelUsage(delta, lastUsage || rawDelta);
1102
- if (typeof onUsage === "function") onUsage({ timestamp: ts, delta, rawUsage: lastUsage || rawDelta, model: attributedModel });
1109
+ const pricedDelta = recordModelUsage(delta, lastUsage || rawDelta);
1110
+ if (typeof onUsage === "function") onUsage({ timestamp: ts, delta: pricedDelta || delta, rawUsage: lastUsage || rawDelta, model: attributedModel });
1103
1111
  attributeTurn(delta);
1104
1112
  }
1105
1113
  } else {
@@ -15,6 +15,7 @@ const fs = require("node:fs");
15
15
  const os = require("node:os");
16
16
  const path = require("node:path");
17
17
 
18
+ /** Run a command without a shell by default, optionally piping options.input to stdin. */
18
19
  function runCommand(commandRunner, command, args, options = {}) {
19
20
  const merged = {
20
21
  encoding: "utf8",
@@ -29,6 +30,7 @@ function runCommand(commandRunner, command, args, options = {}) {
29
30
  const {
30
31
  timeout,
31
32
  maxBuffer,
33
+ input,
32
34
  completeWhen,
33
35
  completionGraceMs = 250,
34
36
  killProcessGroup = false,
@@ -64,7 +66,7 @@ function runCommand(commandRunner, command, args, options = {}) {
64
66
  ...spawnOptions,
65
67
  detached: useProcessGroup || spawnOptions.detached,
66
68
  shell: useShell,
67
- stdio: ["ignore", "pipe", "pipe"],
69
+ stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
68
70
  });
69
71
  } catch (error) {
70
72
  resolve({ status: null, stdout: "", stderr: "", error });
@@ -168,6 +170,18 @@ function runCommand(commandRunner, command, args, options = {}) {
168
170
  if (Number.isFinite(timeout) && timeout > 0) {
169
171
  timer = setTimeout(() => stopChild({ timeoutExpired: true }), timeout);
170
172
  }
173
+ if (input !== undefined) {
174
+ child.stdin?.on("error", (error) => {
175
+ signalChild("SIGKILL");
176
+ settle({ error });
177
+ });
178
+ try {
179
+ child.stdin?.end(input);
180
+ } catch (error) {
181
+ signalChild("SIGKILL");
182
+ settle({ error });
183
+ }
184
+ }
171
185
  });
172
186
  }
173
187
 
@@ -15,7 +15,8 @@ function resolveCursorPaths({ home, platform = process.platform, env = process.e
15
15
  appDir = pathForPlatform.join(h, "Library", "Application Support", "Cursor");
16
16
  } else if (platform === "win32") {
17
17
  const appData =
18
- (typeof env.APPDATA === "string" && env.APPDATA.trim()) ||
18
+ ((!home || h === os.homedir() || env !== process.env) &&
19
+ typeof env.APPDATA === "string" && env.APPDATA.trim()) ||
19
20
  pathForPlatform.join(h, "AppData", "Roaming");
20
21
  appDir = pathForPlatform.join(appData, "Cursor");
21
22
  } else {
@@ -3,6 +3,7 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { ensureDir, readJson, writeJson } = require("./fs");
6
+ const { buildNotifyCommand } = require("./notify-command");
6
7
 
7
8
  const DEFAULT_EVENT = "SessionEnd";
8
9
  const DEFAULT_HOOK_NAME = "tokentracker";
@@ -99,9 +100,9 @@ async function isGeminiHookConfigured({
99
100
  return hasHook(entries, { hookCommand, hookName });
100
101
  }
101
102
 
102
- function buildGeminiHookCommand(notifyPath) {
103
+ function buildGeminiHookCommand(notifyPath, options) {
103
104
  const cmd = typeof notifyPath === "string" ? notifyPath : "";
104
- return `/usr/bin/env node ${quoteArg(cmd)} --source=gemini`;
105
+ return buildNotifyCommand(cmd, "gemini", options);
105
106
  }
106
107
 
107
108
  function buildHookEntry({ hookCommand, hookName, matcher }) {
@@ -1056,7 +1056,17 @@ function scanClaudeProjects(projectMap) {
1056
1056
  // ---------------------------------------------------------------------------
1057
1057
 
1058
1058
  function json(res, data, status) {
1059
- res.writeHead(status || 200, { "Content-Type": "application/json" });
1059
+ // Usage and cost payloads change on every sync, and cost is re-priced on
1060
+ // every read (pricing is applied per request, never stored in the queue) —
1061
+ // so a cached response can pin stale costs on screen indefinitely. WebView2
1062
+ // caches responses that carry no freshness directive, and the dashboard's
1063
+ // local fetch path omits `cache: "no-store"` (only the cloud path sets it).
1064
+ // Endpoints that care already set this header by hand; do it once here so
1065
+ // every JSON response is covered.
1066
+ res.writeHead(status || 200, {
1067
+ "Content-Type": "application/json",
1068
+ "Cache-Control": "no-store",
1069
+ });
1060
1070
  res.end(JSON.stringify(data));
1061
1071
  }
1062
1072
 
@@ -2610,7 +2620,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2610
2620
  const sessions = await buildSessionAnalytics({ force: refresh });
2611
2621
  const wantsCsv = url.searchParams.get("format") === "csv";
2612
2622
  const includeSessions = wantsCsv || ["1", "true"].includes(url.searchParams.get("include_sessions"));
2613
- const result = summarizeSessions(sessions, { from, to, includeSessions });
2623
+ const result = summarizeSessions(sessions, { from, to, includeSessions, ...getTimeZoneContext(url) });
2614
2624
  if (wantsCsv) {
2615
2625
  const content = sessionsToCsv(result.sessions);
2616
2626
  res.statusCode = 200;
@@ -2645,7 +2655,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2645
2655
  try {
2646
2656
  const { buildSessionAnalytics, listSessionsForBrowser } = require("./session-analytics");
2647
2657
  const sessions = await buildSessionAnalytics({ force: refresh });
2648
- const result = listSessionsForBrowser(sessions, { from, to, limit });
2658
+ const result = listSessionsForBrowser(sessions, { from, to, limit, ...getTimeZoneContext(url) });
2649
2659
  json(res, { from, to, ...result });
2650
2660
  } catch (error) {
2651
2661
  // Node fs errors embed the absolute path ("EACCES ... open '/Users/…'").
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ function powershellLiteral(value) {
4
+ return `'${String(value || "").replace(/'/g, "''")}'`;
5
+ }
6
+
7
+ function quotePosix(value) {
8
+ const text = String(value || "");
9
+ if (/^[A-Za-z0-9_\-./:@]+$/.test(text)) return text;
10
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')}"`;
11
+ }
12
+
13
+ function buildNotifyCommand(notifyPath, source, { platform = process.platform, execPath = process.execPath } = {}) {
14
+ if (!/^[a-z0-9-]+$/i.test(source)) throw new Error("Invalid notify source");
15
+ if (platform === "win32") {
16
+ return `& ${powershellLiteral(execPath)} ${powershellLiteral(notifyPath)} --source=${source}`;
17
+ }
18
+ return `/usr/bin/env node ${quotePosix(notifyPath)} --source=${source}`;
19
+ }
20
+
21
+ // Recognize only our complete notify invocation, never a user command merely
22
+ // containing notify.cjs or additional shell actions. Used to repair old hooks.
23
+ function notifyIdentity(command) {
24
+ if (typeof command !== "string") return null;
25
+ const tokens = command.match(/"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|[^\s]+/g) || [];
26
+ const decode = (token) => token.startsWith("'")
27
+ ? token.slice(1, -1).replace(/''/g, "'")
28
+ : token.startsWith('"') ? token.slice(1, -1).replace(/\\([\\"$`])/g, "$1") : token;
29
+ const args = tokens.map(decode);
30
+ if (args[0] === "&") args.shift();
31
+ if (args[0] === "/usr/bin/env" && args[1] === "node") args.shift();
32
+ if (args.length !== 3 || !/(?:^|[\\/])node(?:\.exe)?$/i.test(args[0])) return null;
33
+ if (!/(?:^|[\\/])notify\.cjs$/i.test(args[1]) || !/^--source=[a-z0-9-]+$/i.test(args[2])) return null;
34
+ const file = args[1].replace(/\\/g, "/");
35
+ return `${/^[a-z]:\//i.test(file) ? file.toLowerCase() : file}\u0000${args[2]}`;
36
+ }
37
+
38
+ module.exports = { buildNotifyCommand, notifyIdentity };
@@ -371,7 +371,7 @@ function buildSessionPluginIndex({ trackerDir, packageName = "tokentracker-cli",
371
371
  ` const agentId = normalize(ctx && ctx.agentId) || parseAgentId(sessionKey);\n` +
372
372
  ` if (!agentId) return;\n` +
373
373
  `\n` +
374
- ` const sessionInfo = resolveSessionInfo(agentId, sessionKey);\n` +
374
+ ` const sessionInfo = await resolveSessionInfo(agentId, sessionKey);\n` +
375
375
  ` const sessionId = normalize(sessionInfo && sessionInfo.sessionId);\n` +
376
376
  ` if (!sessionId) return;\n` +
377
377
  `\n` +
@@ -440,25 +440,58 @@ function buildSessionPluginIndex({ trackerDir, packageName = "tokentracker-cli",
440
440
  ` return out;\n` +
441
441
  `}\n` +
442
442
  `\n` +
443
- `function resolveSessionInfo(agentId, sessionKey) {\n` +
443
+ `async function resolveSessionInfo(agentId, sessionKey) {\n` +
444
444
  ` const key = normalize(sessionKey);\n` +
445
445
  ` if (!key) return null;\n` +
446
446
  ` const sessionsPath = path.join(openclawHome, 'agents', agentId, 'sessions', 'sessions.json');\n` +
447
- ` try {\n` +
448
- ` const raw = fs.readFileSync(sessionsPath, 'utf8');\n` +
449
- ` const parsed = JSON.parse(raw);\n` +
450
- ` if (!parsed || typeof parsed !== 'object') return null;\n` +
451
- ` const entry = parsed[key];\n` +
452
- ` if (!entry || typeof entry !== 'object') return null;\n` +
453
- ` return {\n` +
454
- ` sessionKey: key,\n` +
455
- ` sessionId: normalize(entry.sessionId),\n` +
456
- ` entry\n` +
457
- ` };\n` +
458
- ` } catch (_) {}\n` +
447
+ ` if (fs.existsSync(sessionsPath)) {\n` +
448
+ ` try {\n` +
449
+ ` const raw = fs.readFileSync(sessionsPath, 'utf8');\n` +
450
+ ` const parsed = JSON.parse(raw);\n` +
451
+ ` if (parsed && typeof parsed === 'object') {\n` +
452
+ ` const entry = parsed[key];\n` +
453
+ ` if (entry && typeof entry === 'object' && normalize(entry.sessionId)) {\n` +
454
+ ` return { sessionKey: key, sessionId: normalize(entry.sessionId), entry };\n` +
455
+ ` }\n` +
456
+ ` }\n` +
457
+ ` } catch (_) {}\n` +
458
+ ` }\n` +
459
+ ` // OpenClaw >= 2026.9.9 keeps session state in a per-agent SQLite\n` +
460
+ ` // database (~/.openclaw/agents/<id>/agent/openclaw-agent.sqlite);\n` +
461
+ ` // sessions.json is no longer written. Without this fallback the\n` +
462
+ ` // plugin would silently no-op on every agent_end for anyone on a\n` +
463
+ ` // current OpenClaw install (issue: openclaw migration to sqlite).\n` +
464
+ ` return resolveSessionInfoFromSqlite(agentId, key);\n` +
465
+ `}\n` +
466
+ `\n` +
467
+ `async function resolveSessionInfoFromSqlite(agentId, sessionKey) {\n` +
468
+ ` const sqliteModule = await importSqliteSync();\n` +
469
+ ` if (!sqliteModule) return null;\n` +
470
+ ` const { DatabaseSync } = sqliteModule;\n` +
471
+ ` const currentSessionQuery = "SELECT w.session_id FROM session_nodes n JOIN session_windows w ON w.session_id = n.current_session_id WHERE n.session_key = ? AND COALESCE(w.reason, '') != 'compaction'";\n` +
472
+ ` const sessionQuery = "SELECT session_id FROM session_windows WHERE session_key = ? AND COALESCE(reason, '') != 'compaction' ORDER BY updated_at DESC LIMIT 1";\n` +
473
+ ` const dbCandidates = [path.join(openclawHome, 'agents', agentId, 'agent', 'openclaw-agent.sqlite'), path.join(openclawHome, 'agents', agentId, 'openclaw-agent.sqlite')];\n` +
474
+ ` for (const dbPath of dbCandidates) {\n` +
475
+ ` if (!fs.existsSync(dbPath)) continue;\n` +
476
+ ` let db;\n` +
477
+ ` try { db = new DatabaseSync(dbPath, { readOnly: true }); } catch (_) { continue; }\n` +
478
+ ` try {\n` +
479
+ ` let row;\n` +
480
+ ` try { row = db.prepare(currentSessionQuery).get(sessionKey); } catch (_) {}\n` +
481
+ ` if (!row || !row.session_id) row = db.prepare(sessionQuery).get(sessionKey);\n` +
482
+ ` if (!row || !row.session_id) continue;\n` +
483
+ ` // The passive SQLite reader owns per-event accounting; never synthesize lifetime sums.\n` +
484
+ ` return { sessionKey, sessionId: row.session_id, entry: {} };\n` +
485
+ ` } catch (_) {}\n` +
486
+ ` finally { try { db.close(); } catch (_) {} }\n` +
487
+ ` }\n` +
459
488
  ` return null;\n` +
460
489
  `}\n` +
461
490
  `\n` +
491
+ `async function importSqliteSync() {\n` +
492
+ ` try { return await import('node:sqlite'); } catch (_) { return null; }\n` +
493
+ `}\n` +
494
+ `\n` +
462
495
  `function parseAgentId(sessionKey) {\n` +
463
496
  ` const s = normalize(sessionKey);\n` +
464
497
  ` if (!s || !s.startsWith('agent:')) return null;\n` +
@@ -28,6 +28,7 @@
28
28
 
29
29
  const fs = require("node:fs");
30
30
  const path = require("node:path");
31
+ const { expandHome } = require("./scan-roots");
31
32
 
32
33
  /**
33
34
  * @typedef {Object} PassiveProvider
@@ -171,6 +172,17 @@ function detectPassiveProviders({ home, hookStatus, env = process.env }) {
171
172
  settingsPath: path.join(home, ".workbuddy", "settings.json"),
172
173
  }));
173
174
 
175
+ // WorkBuddy AI — international build; data + hooks in ~/.workbuddy-ai
176
+ const workbuddyAiHome = expandHome(env.WORKBUDDY_AI_HOME, home) || path.join(home, ".workbuddy-ai");
177
+ out.push(buildEntry({
178
+ name: "workbuddy-ai",
179
+ hookExpected: true,
180
+ hookInstalled: Boolean(hookStatus?.["workbuddy-ai"]),
181
+ logsDir: workbuddyAiHome,
182
+ logsPredicate: (_full, name) => name === "projects" || name.endsWith(".jsonl"),
183
+ settingsPath: path.join(workbuddyAiHome, "settings.json"),
184
+ }));
185
+
174
186
  return out;
175
187
  }
176
188
 
@@ -3,9 +3,43 @@
3
3
  "note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
4
4
  "units": "usd_per_million_tokens",
5
5
  "deepseek_v4_pricing_note": "Official DeepSeek V4 pricing verified on 2026-08-21. Peak hours are 01:00-04:00 and 06:00-10:00 UTC; all other hours are off-peak at half price. Exact entries store peak rates. computeRowCost applies the time-of-use discount from each row's UTC hour_start.",
6
- "iflytek_maas_pricing_note": "iFlytek MaaS inference prices supplied for AStudio on 2026-08-31. AStudio homepage: https://agent.xfyun.cn/. Official pricing source: https://maas.xfyun.cn/modelSquare. Source values are RMB per million tokens, converted at 7.2 RMB/USD, then rounded to two decimal places for billing. Models without a dedicated cache-hit price use the normal input rate; cache writes also use the normal input rate. iFlytek MaaS prices are source-scoped and do not inherit DeepSeek time-of-use discounts."
6
+ "iflytek_maas_pricing_note": "iFlytek MaaS inference prices supplied for AStudio on 2026-08-31. AStudio homepage: https://agent.xfyun.cn/. Official pricing source: https://maas.xfyun.cn/modelSquare. Source values are RMB per million tokens, converted at 7.2 RMB/USD, then rounded to two decimal places for billing. Models without a dedicated cache-hit price use the normal input rate; cache writes also use the normal input rate. iFlytek MaaS prices are source-scoped and do not inherit DeepSeek time-of-use discounts. Spark-X2.5 entries (spark-x2.5, spark-x2.5-4b, spark-x2.5-1.7b) were read from the model square on 2026-10-08; spark-x2.5 is listed at a limited-time 30% rate (0.96 / 3.6 / 0.14 RMB input / output / cache-hit, list price 3.2 / 12 / 0.48), and the two small models are free."
7
7
  },
8
8
  "exact": {
9
+ "claude-opus-5-5": {
10
+ "input": 4,
11
+ "output": 20,
12
+ "cache_read": 0.2,
13
+ "cache_write": 5,
14
+ "note": "Official Anthropic pricing verified 2026-10-11: https://platform.claude.com/docs/en/about-claude/pricing"
15
+ },
16
+ "claude-opus-5.5": {
17
+ "input": 4,
18
+ "output": 20,
19
+ "cache_read": 0.2,
20
+ "cache_write": 5,
21
+ "note": "Official Anthropic pricing verified 2026-10-11: https://platform.claude.com/docs/en/about-claude/pricing"
22
+ },
23
+ "claude-sonnet-5-5": {
24
+ "input": 2,
25
+ "output": 10,
26
+ "cache_read": 0.1,
27
+ "cache_write": 2.5,
28
+ "note": "Official Anthropic pricing verified 2026-10-11: https://platform.claude.com/docs/en/about-claude/pricing"
29
+ },
30
+ "claude-sonnet-5.5": {
31
+ "input": 2,
32
+ "output": 10,
33
+ "cache_read": 0.1,
34
+ "cache_write": 2.5,
35
+ "note": "Official Anthropic pricing verified 2026-10-11: https://platform.claude.com/docs/en/about-claude/pricing"
36
+ },
37
+ "k3-256k": {
38
+ "input": 3,
39
+ "output": 15,
40
+ "cache_read": 0.3,
41
+ "note": "Bare alias emitted by Kimi Code (modelAlias \"kimi-code/k3\" -> \"k3\"). Same rates as kimi-k3."
42
+ },
9
43
  "cline-gateway-models": {
10
44
  "input": 0,
11
45
  "output": 0,
@@ -42,10 +76,10 @@
42
76
  "note": "Opus 4.8 not yet in LiteLLM. Pin to the standard Opus tier (same as 4.6/4.7); remove once LiteLLM carries it."
43
77
  },
44
78
  "claude-sonnet-5": {
45
- "input": 3,
46
- "output": 15,
47
- "cache_read": 0.3,
48
- "cache_write": 3.75,
79
+ "input": 2,
80
+ "output": 10,
81
+ "cache_read": 0.2,
82
+ "cache_write": 2.5,
49
83
  "note": "Sonnet 5. LiteLLM added this model on ~2026-06-30 with matching rates, but pin explicitly so pricing is correct immediately rather than depending on the 24h local pricing-cache refresh cycle."
50
84
  },
51
85
  "claude-haiku-4-5": {
@@ -482,6 +516,9 @@
482
516
  },
483
517
  "source_exact": {
484
518
  "acode": {
519
+ "spark-x2.5": { "input": 0.13, "output": 0.50, "cache_read": 0.02, "cache_write": 0.13 },
520
+ "spark-x2.5-4b": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
521
+ "spark-x2.5-1.7b": { "input": 0.00, "output": 0.00, "cache_read": 0.00, "cache_write": 0.00 },
485
522
  "xopglm53": { "input": 1.11, "output": 3.89, "cache_read": 0.28, "cache_write": 1.11 },
486
523
  "xopdeepseekv4pro0813": { "input": 1.25, "output": 3.75, "cache_read": 0.04, "cache_write": 1.25 },
487
524
  "xopdeepseekv4flash0731": { "input": 0.14, "output": 0.28, "cache_read": 0.03, "cache_write": 0.14 },
@@ -238,7 +238,13 @@ function lookupContainedExactCaseInsensitive(table, model) {
238
238
  const lower = model.toLowerCase();
239
239
  const keys = Object.keys(table).sort((a, b) => b.length - a.length);
240
240
  for (const key of keys) {
241
- if (lower.includes(key.toLowerCase())) return table[key];
241
+ const start = lower.indexOf(key.toLowerCase());
242
+ if (start < 0) continue;
243
+ // A model generation must not match a later numeric version (Opus 5.5
244
+ // is a separate SKU from Opus 5). Dated and provider suffixes still work.
245
+ const suffix = lower.slice(start + key.length);
246
+ if (/^[.\d]/.test(suffix)) continue;
247
+ return table[key];
242
248
  }
243
249
  return null;
244
250
  }
@@ -256,6 +262,9 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
256
262
  // 0. CURATED source exact. Source-specific prices apply only to their source,
257
263
  // preventing collisions with public prices for same-named models from other CLIs.
258
264
  const sourceKey = typeof source === "string" ? source.toLowerCase() : "";
265
+ if (sourceKey === "workbuddy-ai" && lower.trim() === "auto") {
266
+ return { hit: false, source: "miss", value: null };
267
+ }
259
268
  if (sourceKey === "cline" && lower.endsWith(":free")) {
260
269
  return {
261
270
  hit: true,
@@ -289,10 +298,6 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
289
298
  if (curatedDotExact) {
290
299
  return { hit: true, source: "curated:exact-dot", value: curatedDotExact };
291
300
  }
292
- const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
293
- if (curatedDotContainedExact) {
294
- return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };
295
- }
296
301
 
297
302
  // 2. LiteLLM exact
298
303
  if (litellm && litellm[lookupModel]) {
@@ -302,6 +307,10 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
302
307
  if (litellmDotExact) {
303
308
  return { hit: true, source: "litellm:exact-dot", value: litellmDotExact };
304
309
  }
310
+ const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
311
+ if (curatedDotContainedExact) {
312
+ return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };
313
+ }
305
314
 
306
315
  // 3. CURATED alias (literal mapping like "auto" -> "composer-1")
307
316
  if (curated.alias && curated.alias[lookupModel] && curated.exact[curated.alias[lookupModel]]) {