tokentracker-cli 1.3.3 → 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 (187) 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/THIRD_PARTY_NOTICES.md +92 -0
  7. package/dashboard/dist/THIRD_PARTY_NOTICES.txt +92 -0
  8. package/dashboard/dist/assets/AchievementBadge-Dazs3pEa.js +1 -0
  9. package/dashboard/dist/assets/AchievementsPage-LWnxZvmw.js +2 -0
  10. package/dashboard/dist/assets/{AchievementsSection-CDUUEfIi.js → AchievementsSection-BLqczmU9.js} +1 -1
  11. package/dashboard/dist/assets/BadgeDetailModal-B_n4aj2S.js +1 -0
  12. package/dashboard/dist/assets/{BotAnimated-Dx_vvTtC.js → BotAnimated-KDWI_YcZ.js} +1 -1
  13. package/dashboard/dist/assets/Card-BYoxd4OC.js +1 -0
  14. package/dashboard/dist/assets/CloudCheckoutPage-BR2_B03P.js +1 -0
  15. package/dashboard/dist/assets/CloudPage-DL5RkJKK.js +1 -0
  16. package/dashboard/dist/assets/CloudUsageExport-C_DqZKo1.js +4 -0
  17. package/dashboard/dist/assets/CommandPalette-DkCaIQI4.js +1 -0
  18. package/dashboard/dist/assets/CommunityStatsModal-pX9OePi4.js +1 -0
  19. package/dashboard/dist/assets/{ConfirmModal-BQ-xB1S_.js → ConfirmModal-tkgZwz4u.js} +1 -1
  20. package/dashboard/dist/assets/Controls-MTUhGWy8.js +1 -0
  21. package/dashboard/dist/assets/DashboardPage-SOx-A7lH.js +1 -0
  22. package/dashboard/dist/assets/DateRangePopover-B-BMOm3O.js +1 -0
  23. package/dashboard/dist/assets/{DevicePage-Dg6XkIaY.js → DevicePage-DSK9lm7v.js} +1 -1
  24. package/dashboard/dist/assets/DialogClose-uVUNu4rg.js +1 -0
  25. package/dashboard/dist/assets/DialogDescription-DBqGGWNj.js +1 -0
  26. package/dashboard/dist/assets/DialogTitle-B-VifiA3.js +1 -0
  27. package/dashboard/dist/assets/{FadeIn-ChVf45WW.js → FadeIn-B6lrj4uo.js} +1 -1
  28. package/dashboard/dist/assets/{HeaderGithubStar-B740_G7a.js → HeaderGithubStar-Cx5zOhXa.js} +1 -1
  29. package/dashboard/dist/assets/{HoverTooltip-BzP5FudR.js → HoverTooltip-DY1ENf69.js} +1 -1
  30. package/dashboard/dist/assets/{IpCheckPage-BuZH3F14.js → IpCheckPage-D6TEq64d.js} +1 -1
  31. package/dashboard/dist/assets/LandingPage-OdZUfq--.js +2 -0
  32. package/dashboard/dist/assets/LeaderboardPage-DCK5xcAt.js +2 -0
  33. package/dashboard/dist/assets/LeaderboardProBadge-CBXJ1u_m.js +1 -0
  34. package/dashboard/dist/assets/{LeaderboardProfileModal-OUZvO6u-.js → LeaderboardProfileModal-Ccdzb7QK.js} +4 -4
  35. package/dashboard/dist/assets/{LeaderboardProfilePage-bHI1oTEK.js → LeaderboardProfilePage-CFXlZ0fL.js} +2 -2
  36. package/dashboard/dist/assets/LimitsPage-5krjm-g9.js +9 -0
  37. package/dashboard/dist/assets/{LocalOnlyNotice-BaEq6qN7.js → LocalOnlyNotice-Rk1B8PaZ.js} +1 -1
  38. package/dashboard/dist/assets/LoginCard-DTTIm8vk.js +1 -0
  39. package/dashboard/dist/assets/LoginModal-BNcDdjfr.js +1 -0
  40. package/dashboard/dist/assets/LoginPage-CQgXP7Ee.js +1 -0
  41. package/dashboard/dist/assets/{PetPage-BPBsUHyL.js → PetPage-Cw_kAPRs.js} +1 -1
  42. package/dashboard/dist/assets/PopoverPopup-NYiGE8A2.js +1 -0
  43. package/dashboard/dist/assets/{ProviderIcon-BshO2uER.js → ProviderIcon-Bp7iFJ_m.js} +1 -1
  44. package/dashboard/dist/assets/{ResetPasswordPage-RTFTD8Dr.js → ResetPasswordPage-Cf9kAWDW.js} +1 -1
  45. package/dashboard/dist/assets/Select-kIcvQFAo.js +1 -0
  46. package/dashboard/dist/assets/SelectItemText-BpIsAg3b.js +1 -0
  47. package/dashboard/dist/assets/{SelfHostPage-BlwTt8XM.js → SelfHostPage-Ch2vvSwI.js} +1 -1
  48. package/dashboard/dist/assets/{ServiceStatusPage-BkIcJpYK.js → ServiceStatusPage-hoYBvGbu.js} +1 -1
  49. package/dashboard/dist/assets/SessionsPage-BzRB6q-Z.js +3 -0
  50. package/dashboard/dist/assets/SessionsPage-GV2fuNmd.css +1 -0
  51. package/dashboard/dist/assets/SettingsPage-DRl2uX-i.js +2 -0
  52. package/dashboard/dist/assets/SkillsPage-CkqThTZL.js +1 -0
  53. package/dashboard/dist/assets/{TokenGalaxy-DiXUpXKF.js → TokenGalaxy-eLG-UIZY.js} +1 -1
  54. package/dashboard/dist/assets/TooltipPopup-bnMJyilQ.js +1 -0
  55. package/dashboard/dist/assets/TrendMonitor-X7-aGzIv.js +19 -0
  56. package/dashboard/dist/assets/WidgetsPage-CGznIzrW.js +1 -0
  57. package/dashboard/dist/assets/{WrappedPage-ClEo-RmR.js → WrappedPage-p31-uUoF.js} +1 -1
  58. package/dashboard/dist/assets/__vite-browser-external-2g_4s3ot.js +45 -0
  59. package/dashboard/dist/assets/__vite-browser-external-LpFw4zXh.css +1 -0
  60. package/dashboard/dist/assets/agent-logos-2TVT3fIE.js +1 -0
  61. package/dashboard/dist/assets/arrow-left-DL0hAp_7.js +1 -0
  62. package/dashboard/dist/assets/{arrow-right-CwBz5dwb.js → arrow-right-DFCpnV_9.js} +1 -1
  63. package/dashboard/dist/assets/{arrow-up-right-jPmRN562.js → arrow-up-right-DLvfE0Wu.js} +1 -1
  64. package/dashboard/dist/assets/calendar-days-B0aDoKSG.js +1 -0
  65. package/dashboard/dist/assets/{check-CKwHc4Hv.js → check-C_cfrxiz.js} +1 -1
  66. package/dashboard/dist/assets/chevron-down-Ck8ml43e.js +1 -0
  67. package/dashboard/dist/assets/cloud-checkout-PYnwNfdH.js +1 -0
  68. package/dashboard/dist/assets/composite-INW-ELWX.js +1 -0
  69. package/dashboard/dist/assets/download-B7uokeIo.js +1 -0
  70. package/dashboard/dist/assets/{external-link-B5yXxdPy.js → external-link-CI3crg4K.js} +1 -1
  71. package/dashboard/dist/assets/{flame-CKjAluj_.js → flame-DCzvFKV1.js} +1 -1
  72. package/dashboard/dist/assets/{format-tokens-mk49THtv.js → format-tokens-CSTEYK2m.js} +1 -1
  73. package/dashboard/dist/assets/gift-dEHDFMc6.js +1 -0
  74. package/dashboard/dist/assets/{host-mode-efKlSYjX.js → host-mode-r2SW9Mdv.js} +1 -1
  75. package/dashboard/dist/assets/{icons-ZB1ZthjH.js → icons-q-3zvEkh.js} +1 -1
  76. package/dashboard/dist/assets/{index-BImBG3eS.js → index-BV7qwUw4.js} +1 -1
  77. package/dashboard/dist/assets/{index-BgdM7kBl.js → index-BbrsgHf4.js} +1 -1
  78. package/dashboard/dist/assets/{index-DweUMAUD.js → index-CyHYj9XI.js} +1 -1
  79. package/dashboard/dist/assets/info-BBdY9hV4.js +1 -0
  80. package/dashboard/dist/assets/{link-2-Dn2soiyd.js → link-2-CLPePfXs.js} +1 -1
  81. package/dashboard/dist/assets/{loader-circle-CNT630zn.js → loader-circle-ETrjzfmn.js} +1 -1
  82. package/dashboard/dist/assets/{modifiers.esm-WPx0bc2K.js → modifiers.esm-DHqnp2ol.js} +1 -1
  83. package/dashboard/dist/assets/module-BNnaKM__.js +3 -0
  84. package/dashboard/dist/assets/monitor-smartphone-D1kdzrdb.js +1 -0
  85. package/dashboard/dist/assets/play-Cxp-ronD.js +1 -0
  86. package/dashboard/dist/assets/plus-4n94VzDK.js +1 -0
  87. package/dashboard/dist/assets/provider-display-CRlbeO7t.js +42 -0
  88. package/dashboard/dist/assets/{react-B7SkZTgg.js → react-DlHjGQFN.js} +1 -1
  89. package/dashboard/dist/assets/safePolygon-CVZ27a2_.js +1 -0
  90. package/dashboard/dist/assets/{search-Ol3OZBm4.js → search-BDPr56_h.js} +1 -1
  91. package/dashboard/dist/assets/{server-DFCbznC7.js → server-Bm7fHvVa.js} +1 -1
  92. package/dashboard/dist/assets/{shield-check-YcmLwVZ6.js → shield-check-DtT8ACzI.js} +1 -1
  93. package/dashboard/dist/assets/{skills-api-C-9vK884.js → skills-api-Dc1-2qu6.js} +1 -1
  94. package/dashboard/dist/assets/{sortable.esm-CKC9NuR-.js → sortable.esm-B-qN-Xk2.js} +1 -1
  95. package/dashboard/dist/assets/store-gHeBU8dQ.js +1 -0
  96. package/dashboard/dist/assets/{terminal-B3_fCLFs.js → terminal-D4VZbvhg.js} +1 -1
  97. package/dashboard/dist/assets/{trash-CZGfgbFa.js → trash-BZtLAFQy.js} +1 -1
  98. package/dashboard/dist/assets/{use-community-stats-CW-qkYn7.js → use-community-stats-BdqndrbB.js} +1 -1
  99. package/dashboard/dist/assets/{use-latest-request-guard-DHv7vfFE.js → use-latest-request-guard-DhGg1cDS.js} +1 -1
  100. package/dashboard/dist/assets/use-limits-display-prefs-D3HOcRFv.js +1 -0
  101. package/dashboard/dist/assets/{use-native-settings-CWiLqfZh.js → use-native-settings-BniKwA_n.js} +1 -1
  102. package/dashboard/dist/assets/{use-ordered-list-Df8pw-WJ.js → use-ordered-list-b09H7EHq.js} +1 -1
  103. package/dashboard/dist/assets/use-pet-settings-CPZdlq_e.js +2 -0
  104. package/dashboard/dist/assets/use-reduced-motion-D1gcKJUo.js +1 -0
  105. package/dashboard/dist/assets/{use-transform-BHwEAfmO.js → use-transform-CIdxjmI8.js} +1 -1
  106. package/dashboard/dist/assets/use-usage-limits-eJlU65yY.js +1 -0
  107. package/dashboard/dist/assets/{useCurrency-pwClnCZ5.js → useCurrency-CM22CmAq.js} +1 -1
  108. package/dashboard/dist/assets/usePositioner-B3l2ulTw.js +1 -0
  109. package/dashboard/dist/assets/{useTokenFormat-BMvVAe6V.js → useTokenFormat-D7nzgxKw.js} +1 -1
  110. package/dashboard/dist/assets/zap-Daxct1WV.js +1 -0
  111. package/dashboard/dist/index.html +14 -12
  112. package/dashboard/dist/ip-check.html +2 -2
  113. package/dashboard/dist/leaderboard.html +2 -2
  114. package/dashboard/dist/llms.txt +2 -2
  115. package/dashboard/dist/share.html +2 -2
  116. package/package.json +3 -2
  117. package/src/commands/init.js +43 -1
  118. package/src/commands/status.js +27 -0
  119. package/src/commands/sync.js +54 -4
  120. package/src/commands/uninstall.js +19 -0
  121. package/src/lib/claude-config.js +47 -15
  122. package/src/lib/codex-rollout-parser.js +11 -3
  123. package/src/lib/command-runner.js +15 -1
  124. package/src/lib/cursor-config.js +2 -1
  125. package/src/lib/gemini-config.js +3 -2
  126. package/src/lib/local-api.js +13 -3
  127. package/src/lib/notify-command.js +38 -0
  128. package/src/lib/openclaw-session-plugin.js +47 -14
  129. package/src/lib/passive-mode.js +12 -0
  130. package/src/lib/pricing/curated-overrides.json +42 -5
  131. package/src/lib/pricing/matcher.js +14 -5
  132. package/src/lib/pricing/seed-snapshot.json +1 -1
  133. package/src/lib/rollout.js +150 -28
  134. package/src/lib/session-analytics.js +108 -9
  135. package/src/lib/usage-limits.js +183 -20
  136. package/src/lib/zcode-limits.js +218 -18
  137. package/src/lib/zcode-reset-credits.js +66 -0
  138. package/dashboard/dist/assets/AchievementBadge-BaqdyZaV.js +0 -1
  139. package/dashboard/dist/assets/AchievementsPage-B4Ju0guv.js +0 -2
  140. package/dashboard/dist/assets/BadgeDetailModal-B-XMJAvL.js +0 -1
  141. package/dashboard/dist/assets/Card-D-5mABVt.js +0 -1
  142. package/dashboard/dist/assets/CloudCheckoutPage-BELoh56h.js +0 -1
  143. package/dashboard/dist/assets/CloudPage-CfBruNxe.js +0 -1
  144. package/dashboard/dist/assets/CloudUsageExport-BoFU7xCT.js +0 -4
  145. package/dashboard/dist/assets/CommandPalette-BmmKZWn-.js +0 -1
  146. package/dashboard/dist/assets/CommunityStatsModal-BF77zVhN.js +0 -1
  147. package/dashboard/dist/assets/Controls-BnTeX7T4.js +0 -1
  148. package/dashboard/dist/assets/DashboardPage-BWulHa5J.js +0 -1
  149. package/dashboard/dist/assets/DialogClose-DAyilsH-.js +0 -1
  150. package/dashboard/dist/assets/DialogDescription-j4XnRy6y.js +0 -1
  151. package/dashboard/dist/assets/DialogTitle-B1iNGD2S.js +0 -1
  152. package/dashboard/dist/assets/LandingPage-Dtgq_caz.js +0 -2
  153. package/dashboard/dist/assets/LeaderboardPage-Di1kEg2O.js +0 -2
  154. package/dashboard/dist/assets/LeaderboardProBadge-h8FfR0au.js +0 -1
  155. package/dashboard/dist/assets/LimitsPage-DKojB73_.js +0 -8
  156. package/dashboard/dist/assets/LoginCard-Dn5Xy4If.js +0 -1
  157. package/dashboard/dist/assets/LoginModal-C5h-5GOA.js +0 -1
  158. package/dashboard/dist/assets/LoginPage-BpX8Tgjj.js +0 -1
  159. package/dashboard/dist/assets/PopoverPopup-CVTiEVHs.js +0 -1
  160. package/dashboard/dist/assets/Select-CtTceC4T.js +0 -1
  161. package/dashboard/dist/assets/SelectItemText-C7xi8iGp.js +0 -1
  162. package/dashboard/dist/assets/SessionsPage-BngTWN7Q.css +0 -1
  163. package/dashboard/dist/assets/SessionsPage-okHobk_L.js +0 -3
  164. package/dashboard/dist/assets/SettingsPage-C-cc6wcF.js +0 -2
  165. package/dashboard/dist/assets/SkillsPage-D2LlKKJN.js +0 -1
  166. package/dashboard/dist/assets/TrendMonitor-C88S-5aa.js +0 -19
  167. package/dashboard/dist/assets/WidgetsPage-WsEFPNwp.js +0 -1
  168. package/dashboard/dist/assets/__vite-browser-external-DFRX9dp-.css +0 -1
  169. package/dashboard/dist/assets/__vite-browser-external-DUJeOWgd.js +0 -45
  170. package/dashboard/dist/assets/agent-logos-BM9jlReI.js +0 -1
  171. package/dashboard/dist/assets/arrow-left-BEM-HVmC.js +0 -1
  172. package/dashboard/dist/assets/cloud-checkout-nYNHDFoR.js +0 -1
  173. package/dashboard/dist/assets/composite-B4Z3F65o.js +0 -1
  174. package/dashboard/dist/assets/download-CJUnX1Bs.js +0 -1
  175. package/dashboard/dist/assets/info-CyDY7NYM.js +0 -1
  176. package/dashboard/dist/assets/module-WSx8viXK.js +0 -3
  177. package/dashboard/dist/assets/monitor-smartphone-C-qC0RAt.js +0 -1
  178. package/dashboard/dist/assets/plus-Cx82jb7o.js +0 -1
  179. package/dashboard/dist/assets/provider-display-CodMmiRb.js +0 -42
  180. package/dashboard/dist/assets/safePolygon-CKpREa4K.js +0 -1
  181. package/dashboard/dist/assets/store-C9yWYHfg.js +0 -1
  182. package/dashboard/dist/assets/use-limits-display-prefs-NZ-AAYD5.js +0 -1
  183. package/dashboard/dist/assets/use-pet-settings-Bg2CDprr.js +0 -2
  184. package/dashboard/dist/assets/use-reduced-motion-iDZEUyua.js +0 -1
  185. package/dashboard/dist/assets/use-usage-limits-CSxFFn3_.js +0 -1
  186. package/dashboard/dist/assets/usePositioner-B0j1orGj.js +0 -1
  187. package/dashboard/dist/assets/zap-DvX2aYx5.js +0 -1
@@ -13,6 +13,7 @@ const {
13
13
  chmod600IfPossible,
14
14
  } = require("../lib/fs");
15
15
  const { resolveMimoNativeDbPath } = require("../lib/install-resolver");
16
+ const { expandHome } = require("../lib/scan-roots");
16
17
  const { prompt, promptHidden } = require("../lib/prompt");
17
18
  const {
18
19
  upsertCodexNotify,
@@ -130,6 +131,7 @@ const SUPPORTED_PROVIDERS = [
130
131
  "Kimi Code",
131
132
  "CodeBuddy",
132
133
  "WorkBuddy",
134
+ "WorkBuddy AI",
133
135
  "Grok Build",
134
136
  "oh-my-pi",
135
137
  "OmO",
@@ -578,6 +580,10 @@ async function repairRuntimeIntegrations({
578
580
  settingsPath: context.workbuddySettingsPath,
579
581
  hookCommand: context.workbuddyHookCommand,
580
582
  })],
583
+ ["workbuddy-ai", context.workbuddyAiDir, () => upsertClaudeHook({
584
+ settingsPath: context.workbuddyAiSettingsPath,
585
+ hookCommand: context.workbuddyAiHookCommand,
586
+ })],
581
587
  ];
582
588
  for (const [key, configDir, repair] of hookRepairs) {
583
589
  if (await isDir(configDir)) await attempt(key, repair);
@@ -622,6 +628,12 @@ function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
622
628
  const workbuddyDir = process.env.WORKBUDDY_HOME || path.join(home, ".workbuddy");
623
629
  const workbuddySettingsPath = path.join(workbuddyDir, "settings.json");
624
630
  const workbuddyHookCommand = buildHookCommand(notifyPath, "workbuddy");
631
+ // WorkBuddy AI is the international build of the very same CLI. It keeps its
632
+ // data in a sibling home (~/.workbuddy-ai), so it needs its own hook install
633
+ // and its own --source token to be attributed separately from the CN build.
634
+ const workbuddyAiDir = expandHome(process.env.WORKBUDDY_AI_HOME, home) || path.join(home, ".workbuddy-ai");
635
+ const workbuddyAiSettingsPath = path.join(workbuddyAiDir, "settings.json");
636
+ const workbuddyAiHookCommand = buildHookCommand(notifyPath, "workbuddy-ai");
625
637
  const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
626
638
  const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
627
639
  const geminiHookCommand = buildGeminiHookCommand(notifyPath);
@@ -647,6 +659,9 @@ function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
647
659
  workbuddyDir,
648
660
  workbuddySettingsPath,
649
661
  workbuddyHookCommand,
662
+ workbuddyAiDir,
663
+ workbuddyAiSettingsPath,
664
+ workbuddyAiHookCommand,
650
665
  geminiConfigDir,
651
666
  geminiSettingsPath,
652
667
  geminiHookCommand,
@@ -1034,6 +1049,18 @@ async function applyIntegrationSetup({
1034
1049
  summary.push({ label: "WorkBuddy", status: "skipped", detail: "Config not found" });
1035
1050
  }
1036
1051
 
1052
+ // WorkBuddy AI: international build, same hook schema, sibling home.
1053
+ const workbuddyAiDirExists = await isDir(context.workbuddyAiDir);
1054
+ if (workbuddyAiDirExists) {
1055
+ await upsertClaudeHook({
1056
+ settingsPath: context.workbuddyAiSettingsPath,
1057
+ hookCommand: context.workbuddyAiHookCommand,
1058
+ });
1059
+ summary.push({ label: "WorkBuddy AI", status: "installed", detail: "Hooks installed" });
1060
+ } else {
1061
+ summary.push({ label: "WorkBuddy AI", status: "skipped", detail: "Config not found" });
1062
+ }
1063
+
1037
1064
  const openclawBefore = await probeOpenclawSessionPluginState({
1038
1065
  home,
1039
1066
  trackerDir,
@@ -1187,6 +1214,21 @@ async function previewIntegrations({ context }) {
1187
1214
  summary.push({ label: "WorkBuddy", status: "skipped", detail: "Config not found" });
1188
1215
  }
1189
1216
 
1217
+ const workbuddyAiDirExists = await isDir(context.workbuddyAiDir);
1218
+ if (workbuddyAiDirExists) {
1219
+ const configured = await isClaudeHookConfigured({
1220
+ settingsPath: context.workbuddyAiSettingsPath,
1221
+ hookCommand: context.workbuddyAiHookCommand,
1222
+ });
1223
+ summary.push({
1224
+ label: "WorkBuddy AI",
1225
+ status: "installed",
1226
+ detail: configured ? "Hooks already installed" : "Will install hooks",
1227
+ });
1228
+ } else {
1229
+ summary.push({ label: "WorkBuddy AI", status: "skipped", detail: "Config not found" });
1230
+ }
1231
+
1190
1232
  const geminiConfigExists = await isDir(context.geminiConfigDir);
1191
1233
  if (geminiConfigExists) {
1192
1234
  const configured = await isGeminiHookConfigured({
@@ -1491,7 +1533,7 @@ try {
1491
1533
  ? codeOriginalPath
1492
1534
  : source === 'acode'
1493
1535
  ? acodeOriginalPath
1494
- : source === 'claude' || source === 'opencode' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy'
1536
+ : source === 'claude' || source === 'opencode' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'workbuddy-ai'
1495
1537
  ? null
1496
1538
  : codexOriginalPath;
1497
1539
  if (originalPath) {
@@ -221,6 +221,11 @@ async function cmdStatus(argv = []) {
221
221
  resolveWorkbuddyHome(process.env) || path.join(home, ".workbuddy"),
222
222
  "settings.json",
223
223
  );
224
+ // WorkBuddy AI — international build, sibling home (~/.workbuddy-ai).
225
+ const workbuddyAiSettingsPath = path.join(
226
+ resolveWorkbuddyHome(process.env, "workbuddy-ai") || path.join(home, ".workbuddy-ai"),
227
+ "settings.json",
228
+ );
224
229
  const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
225
230
  const geminiSettingsPath = resolveGeminiSettingsPath({
226
231
  configDir: geminiConfigDir,
@@ -235,6 +240,7 @@ async function cmdStatus(argv = []) {
235
240
  const claudeHookCommand = buildClaudeHookCommand(notifyPath);
236
241
  const codebuddyHookCommand = buildHookCommand(notifyPath, "codebuddy");
237
242
  const workbuddyHookCommand = buildHookCommand(notifyPath, "workbuddy");
243
+ const workbuddyAiHookCommand = buildHookCommand(notifyPath, "workbuddy-ai");
238
244
  const geminiHookCommand = buildGeminiHookCommand(notifyPath);
239
245
 
240
246
  const config = await readJson(configPath);
@@ -308,6 +314,10 @@ async function cmdStatus(argv = []) {
308
314
  settingsPath: workbuddySettingsPath,
309
315
  hookCommand: workbuddyHookCommand,
310
316
  });
317
+ const workbuddyAiHookConfigured = await isClaudeHookConfigured({
318
+ settingsPath: workbuddyAiSettingsPath,
319
+ hookCommand: workbuddyAiHookCommand,
320
+ });
311
321
  const geminiHookConfigured = await isGeminiHookConfigured({
312
322
  settingsPath: geminiSettingsPath,
313
323
  hookCommand: geminiHookCommand,
@@ -493,6 +503,15 @@ async function cmdStatus(argv = []) {
493
503
  ? fssync.existsSync(path.join(workbuddyHome, "workbuddy.db"))
494
504
  : false;
495
505
 
506
+ const workbuddyAiHome = resolveWorkbuddyHome(process.env, "workbuddy-ai");
507
+ const workbuddyAiInstalled = Boolean(workbuddyAiHome && fssync.existsSync(workbuddyAiHome));
508
+ const workbuddyAiFiles = workbuddyAiInstalled
509
+ ? resolveWorkbuddyProjectFiles(process.env, "workbuddy-ai")
510
+ : [];
511
+ const workbuddyAiDbExists = workbuddyAiInstalled
512
+ ? fssync.existsSync(path.join(workbuddyAiHome, "workbuddy.db"))
513
+ : false;
514
+
496
515
  // oh-my-pi — passive scan + optional notify extension.
497
516
  const ompAgentDir = resolveOmpAgentDir(process.env);
498
517
  const ompInstalled = Boolean(ompAgentDir) && fssync.existsSync(path.join(ompAgentDir, "sessions"));
@@ -1003,6 +1022,7 @@ async function cmdStatus(argv = []) {
1003
1022
  openclaw_session_plugin: Boolean(openclawSessionPluginState?.configured),
1004
1023
  codebuddy: Boolean(codebuddyHookConfigured),
1005
1024
  workbuddy: Boolean(workbuddyHookConfigured),
1025
+ "workbuddy-ai": Boolean(workbuddyAiHookConfigured),
1006
1026
  grok: Boolean(grokHookState?.configured),
1007
1027
  },
1008
1028
  });
@@ -1046,6 +1066,7 @@ async function cmdStatus(argv = []) {
1046
1066
  openclaw_legacy: Boolean(openclawHookState?.configured),
1047
1067
  codebuddy: codebuddyInstalled ? Boolean(codebuddyHookConfigured) : null,
1048
1068
  workbuddy: workbuddyInstalled ? Boolean(workbuddyHookConfigured) : null,
1069
+ "workbuddy-ai": workbuddyAiInstalled ? Boolean(workbuddyAiHookConfigured) : null,
1049
1070
  grok: grokInstalled ? Boolean(grokHookState?.configured) : null,
1050
1071
  },
1051
1072
  providers: {
@@ -1077,6 +1098,9 @@ async function cmdStatus(argv = []) {
1077
1098
  workbuddy: workbuddyInstalled
1078
1099
  ? { installed: true, files: workbuddyFiles.length }
1079
1100
  : { installed: false },
1101
+ workbuddy_ai: workbuddyAiInstalled
1102
+ ? { installed: true, files: workbuddyAiFiles.length }
1103
+ : { installed: false },
1080
1104
  omp: ompInstalled || ompHookState.ompPresent
1081
1105
  ? {
1082
1106
  installed: true,
@@ -1275,6 +1299,9 @@ async function cmdStatus(argv = []) {
1275
1299
  workbuddyInstalled
1276
1300
  ? `- WorkBuddy hooks: ${workbuddyHookConfigured ? "set" : "unset"} (${workbuddyFiles.length} session jsonl file${workbuddyFiles.length !== 1 ? "s" : ""} found, SQLite DB ${workbuddyDbExists ? "found" : "not found"})`
1277
1301
  : null,
1302
+ workbuddyAiInstalled
1303
+ ? `- WorkBuddy AI hooks: ${workbuddyAiHookConfigured ? "set" : "unset"} (${workbuddyAiFiles.length} session jsonl file${workbuddyAiFiles.length !== 1 ? "s" : ""} found, SQLite DB ${workbuddyAiDbExists ? "found" : "not found"})`
1304
+ : null,
1278
1305
  ompInstalled || ompHookState.ompPresent
1279
1306
  ? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found${ompHookState.configured ? ", notify extension: yes" : ", notify extension: no"})`
1280
1307
  : null,
@@ -354,6 +354,7 @@ const AUTO_SYNC_SOURCES = new Set([
354
354
  "trae",
355
355
  "unsloth",
356
356
  "workbuddy",
357
+ "workbuddy-ai",
357
358
  "zcode",
358
359
  "zed",
359
360
  ]);
@@ -984,8 +985,9 @@ async function cmdSync(argv, context = {}) {
984
985
  // Plugin-triggered sync points at one specific session file; a normal full
985
986
  // sync also passively scans every on-disk OpenClaw transcript so usage is
986
987
  // captured even when the session plugin never fires (issue #264). The scan
987
- // is gated to full syncs so a scoped `--from-openclaw` hook still only
988
- // touches its own file and the 5-minute background tick stays cheap. The
988
+ // is gated to full syncs except for gateway lifecycle signals. A scoped
989
+ // agent_end reads only its transcript or its session in the agent database,
990
+ // and the ordinary background tick stays cheap. The
989
991
  // event-identity dedup makes the plugin and passive paths idempotent, so
990
992
  // overlap on the same file is safe.
991
993
  //
@@ -999,12 +1001,22 @@ async function cmdSync(argv, context = {}) {
999
1001
  path: openclawSignal.sessionFile,
1000
1002
  source: "openclaw",
1001
1003
  });
1004
+ for (const dbPath of [
1005
+ path.join(openclawSignal.openclawHome, "agents", openclawSignal.agentId, "agent", "openclaw-agent.sqlite"),
1006
+ path.join(openclawSignal.openclawHome, "agents", openclawSignal.agentId, "openclaw-agent.sqlite"),
1007
+ ]) {
1008
+ if (!(await fs.stat(dbPath).catch(() => null))?.isFile()) continue;
1009
+ openclawSessionFiles.set(openclawCursorKey(dbPath), {
1010
+ path: dbPath, source: "openclaw", sessionId: openclawSignal.sessionId,
1011
+ });
1012
+ break;
1013
+ }
1002
1014
  }
1003
- if (isFullSourceScan && sourceAllowed("openclaw")) {
1015
+ if ((isFullSourceScan || (opts.fromOpenclaw && !openclawSignal)) && sourceAllowed("openclaw")) {
1004
1016
  try {
1005
1017
  for (const f of await resolveOpenclawSessionFiles(process.env)) {
1006
1018
  const key = openclawCursorKey(f);
1007
- if (!openclawSessionFiles.has(key)) {
1019
+ if (!openclawSessionFiles.has(key) || f.endsWith(".sqlite")) {
1008
1020
  openclawSessionFiles.set(key, { path: f, source: "openclaw" });
1009
1021
  }
1010
1022
  }
@@ -2597,6 +2609,42 @@ async function cmdSync(argv, context = {}) {
2597
2609
  }
2598
2610
  }
2599
2611
 
2612
+ // ── WorkBuddy AI — international build (~/.workbuddy-ai/projects/**/*.jsonl) ──
2613
+ // Same Claude-fork family and byte-identical JSONL as the CN build above;
2614
+ // only the home directory differs, so it reuses the WorkBuddy parser and
2615
+ // gets its own source key (and cursor namespace) so the two builds stay
2616
+ // separable instead of merging into one provider row.
2617
+ let workbuddyAiResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2618
+ const workbuddyAiFiles = sourceAllowed("workbuddy-ai")
2619
+ ? mergeBothFileSources({
2620
+ resolveFiles: (resolveEnv) => resolveWorkbuddyProjectFiles(resolveEnv, "workbuddy-ai"),
2621
+ env: process.env,
2622
+ })
2623
+ : [];
2624
+ if (sourceAllowed("workbuddy-ai")) {
2625
+ if (progress?.enabled) {
2626
+ progress.start(`Parsing WorkBuddy AI ${renderBar(0)} | buckets 0`);
2627
+ }
2628
+ try {
2629
+ workbuddyAiResult = await parseWorkbuddyIncremental({
2630
+ projectFiles: workbuddyAiFiles,
2631
+ cursors,
2632
+ queuePath,
2633
+ env: process.env,
2634
+ source: "workbuddy-ai",
2635
+ onProgress: (p) => {
2636
+ if (!progress?.enabled) return;
2637
+ const pct = p.total > 0 ? p.index / p.total : 1;
2638
+ progress.update(
2639
+ `Parsing WorkBuddy AI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
2640
+ );
2641
+ },
2642
+ });
2643
+ } catch (err) {
2644
+ warnProviderParseFailure("WorkBuddy AI", err, opts);
2645
+ }
2646
+ }
2647
+
2600
2648
  // ── oh-my-pi (passive ~/.omp/agent/sessions/**/*.jsonl reader) ──
2601
2649
  // Task-subagent transcripts (nested below the cwd level) are scanned too,
2602
2650
  // so their usage counts toward the omp totals.
@@ -3256,6 +3304,7 @@ async function cmdSync(argv, context = {}) {
3256
3304
  kimiCodeResult.recordsProcessed +
3257
3305
  codebuddyResult.recordsProcessed +
3258
3306
  workbuddyResult.recordsProcessed +
3307
+ workbuddyAiResult.recordsProcessed +
3259
3308
  ompResult.recordsProcessed +
3260
3309
  omoResult.recordsProcessed +
3261
3310
  piResult.recordsProcessed +
@@ -3300,6 +3349,7 @@ async function cmdSync(argv, context = {}) {
3300
3349
  kimiCodeResult.bucketsQueued +
3301
3350
  codebuddyResult.bucketsQueued +
3302
3351
  workbuddyResult.bucketsQueued +
3352
+ workbuddyAiResult.bucketsQueued +
3303
3353
  ompResult.bucketsQueued +
3304
3354
  omoResult.bucketsQueued +
3305
3355
  piResult.bucketsQueued +
@@ -1,6 +1,7 @@
1
1
  const os = require("node:os");
2
2
  const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
+ const { expandHome } = require("../lib/scan-roots");
4
5
 
5
6
  const {
6
7
  restoreCodexNotify,
@@ -44,6 +45,9 @@ async function cmdUninstall(argv) {
44
45
  const codebuddySettingsPath = path.join(codebuddyDir, "settings.json");
45
46
  const workbuddyDir = process.env.WORKBUDDY_HOME || path.join(home, ".workbuddy");
46
47
  const workbuddySettingsPath = path.join(workbuddyDir, "settings.json");
48
+ // WorkBuddy AI — international build, sibling home (~/.workbuddy-ai).
49
+ const workbuddyAiDir = expandHome(process.env.WORKBUDDY_AI_HOME, home) || path.join(home, ".workbuddy-ai");
50
+ const workbuddyAiSettingsPath = path.join(workbuddyAiDir, "settings.json");
47
51
  const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
48
52
  const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
49
53
  const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
@@ -57,6 +61,7 @@ async function cmdUninstall(argv) {
57
61
  const claudeHookCommand = buildClaudeHookCommand(notifyPath);
58
62
  const codebuddyHookCommand = buildHookCommand(notifyPath, "codebuddy");
59
63
  const workbuddyHookCommand = buildHookCommand(notifyPath, "workbuddy");
64
+ const workbuddyAiHookCommand = buildHookCommand(notifyPath, "workbuddy-ai");
60
65
  const geminiHookCommand = buildGeminiHookCommand(notifyPath);
61
66
 
62
67
  const codexConfigExists = await isFile(codexConfigPath);
@@ -65,6 +70,7 @@ async function cmdUninstall(argv) {
65
70
  const claudeConfigExists = await isFile(claudeSettingsPath);
66
71
  const codebuddyConfigExists = await isFile(codebuddySettingsPath);
67
72
  const workbuddyConfigExists = await isFile(workbuddySettingsPath);
73
+ const workbuddyAiConfigExists = await isFile(workbuddyAiSettingsPath);
68
74
  const geminiConfigExists = await isDir(geminiConfigDir);
69
75
  const opencodeConfigExists = await isDir(opencodeConfigDir);
70
76
  const codexRestore = codexConfigExists
@@ -106,6 +112,12 @@ async function cmdUninstall(argv) {
106
112
  hookCommand: workbuddyHookCommand,
107
113
  })
108
114
  : { removed: false, skippedReason: "config-missing" };
115
+ const workbuddyAiRemove = workbuddyAiConfigExists
116
+ ? await removeClaudeHook({
117
+ settingsPath: workbuddyAiSettingsPath,
118
+ hookCommand: workbuddyAiHookCommand,
119
+ })
120
+ : { removed: false, skippedReason: "config-missing" };
109
121
  const geminiRemove = geminiConfigExists
110
122
  ? await removeGeminiHook({ settingsPath: geminiSettingsPath, hookCommand: geminiHookCommand })
111
123
  : { removed: false, skippedReason: "config-missing" };
@@ -191,6 +203,13 @@ async function cmdUninstall(argv) {
191
203
  ? "- WorkBuddy hooks: no change"
192
204
  : "- WorkBuddy hooks: skipped"
193
205
  : "- WorkBuddy hooks: skipped (settings.json not found)",
206
+ workbuddyAiConfigExists
207
+ ? workbuddyAiRemove?.removed
208
+ ? `- WorkBuddy AI hooks removed: ${workbuddyAiSettingsPath}`
209
+ : workbuddyAiRemove?.skippedReason === "hook-missing"
210
+ ? "- WorkBuddy AI hooks: no change"
211
+ : "- WorkBuddy AI hooks: skipped"
212
+ : "- WorkBuddy AI hooks: skipped (settings.json not found)",
194
213
  geminiConfigExists
195
214
  ? geminiRemove?.removed
196
215
  ? `- Gemini hooks removed: ${geminiSettingsPath}`
@@ -2,6 +2,7 @@ const fs = require("node:fs/promises");
2
2
  const path = require("node:path");
3
3
 
4
4
  const { ensureDir, readJson, writeJson } = require("./fs");
5
+ const { buildNotifyCommand, notifyIdentity } = require("./notify-command");
5
6
 
6
7
  const DEFAULT_EVENT = "SessionEnd";
7
8
  const CLAUDE_USAGE_EVENTS = ["Stop", DEFAULT_EVENT];
@@ -23,10 +24,10 @@ async function upsertClaudeHooks({ settingsPath, hookCommand, events }) {
23
24
 
24
25
  for (const event of normalizeEventList(events)) {
25
26
  const entries = normalizeEntries(hooks[event]);
26
- const normalized = normalizeEntriesForCommand(entries, hookCommand);
27
+ const normalized = normalizeEntriesForCommand(entries, hookCommand, event);
27
28
  let nextEntries = normalized.entries;
28
29
  if (!hasHook(nextEntries, hookCommand)) {
29
- nextEntries = nextEntries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
30
+ nextEntries = nextEntries.concat([{ hooks: [{ type: "command", command: hookCommand, ...hookShell(hookCommand) }] }]);
30
31
  changed = true;
31
32
  }
32
33
  if (normalized.changed) changed = true;
@@ -106,14 +107,14 @@ async function areClaudeHooksConfigured({ settingsPath, hookCommand, events }) {
106
107
  // Generic Session-hook command builder. CodeBuddy CLI is a Claude-Code fork
107
108
  // and uses the exact same settings.json hook schema, so this function works
108
109
  // for any source that accepts the `node notify.cjs --source=<name>` contract.
109
- function buildHookCommand(notifyPath, source) {
110
+ function buildHookCommand(notifyPath, source, options) {
110
111
  const cmd = typeof notifyPath === "string" ? notifyPath : "";
111
112
  const src = typeof source === "string" && source ? source : "claude";
112
- return `/usr/bin/env node ${quoteArg(cmd)} --source=${src}`;
113
+ return buildNotifyCommand(cmd, src, options);
113
114
  }
114
115
 
115
- function buildClaudeHookCommand(notifyPath) {
116
- return buildHookCommand(notifyPath, "claude");
116
+ function buildClaudeHookCommand(notifyPath, options) {
117
+ return buildHookCommand(notifyPath, "claude", options);
117
118
  }
118
119
 
119
120
  function normalizeSettings(raw) {
@@ -140,12 +141,15 @@ function normalizeCommand(cmd) {
140
141
  }
141
142
 
142
143
  function hasHook(entries, hookCommand) {
144
+ const shell = hookShell(hookCommand).shell;
145
+ const configured = (hook) => normalizeCommand(hook?.command) === normalizeCommand(hookCommand) &&
146
+ (!shell || (hook.shell === shell && !Object.hasOwn(hook, "args")));
143
147
  for (const entry of entries) {
144
148
  if (!entry || typeof entry !== "object") continue;
145
- if (entry.command && commandsEqual(entry.command, hookCommand)) return true;
149
+ if (entry.command && configured(entry)) return true;
146
150
  const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
147
151
  for (const hook of hooks) {
148
- if (hook && commandsEqual(hook.command, hookCommand)) return true;
152
+ if (hook && configured(hook)) return true;
149
153
  }
150
154
  }
151
155
  return false;
@@ -169,14 +173,25 @@ function stripHookFromEntry(entry, hookCommand) {
169
173
  return { entry: { ...entry, hooks: nextHooks }, removed: true };
170
174
  }
171
175
 
172
- function normalizeEntriesForCommand(entries, hookCommand) {
176
+ function hookShell(command) {
177
+ return typeof command === "string" && command.trimStart().startsWith("& '")
178
+ ? { shell: "powershell" } : {};
179
+ }
180
+
181
+ function normalizeEntriesForCommand(entries, hookCommand, event) {
182
+ const shell = hookShell(hookCommand);
183
+ const repair = (hook) => {
184
+ const next = { ...hook, type: "command", command: hookCommand, ...shell };
185
+ if (shell.shell) delete next.args;
186
+ return next;
187
+ };
173
188
  let changed = false;
174
189
  const nextEntries = entries.map((entry) => {
175
190
  if (!entry || typeof entry !== "object") return entry;
176
191
  if (entry.command && commandsEqual(entry.command, hookCommand)) {
177
- if (entry.type !== "command") {
192
+ if (entry.type !== "command" || entry.command !== hookCommand || (shell.shell && (entry.shell !== shell.shell || Object.hasOwn(entry, "args")))) {
178
193
  changed = true;
179
- return { ...entry, type: "command" };
194
+ return repair(entry);
180
195
  }
181
196
  return entry;
182
197
  }
@@ -184,9 +199,9 @@ function normalizeEntriesForCommand(entries, hookCommand) {
184
199
  let hooksChanged = false;
185
200
  const nextHooks = entry.hooks.map((hook) => {
186
201
  if (hook && commandsEqual(hook.command, hookCommand)) {
187
- if (hook.type !== "command") {
202
+ if (hook.type !== "command" || hook.command !== hookCommand || (shell.shell && (hook.shell !== shell.shell || Object.hasOwn(hook, "args")))) {
188
203
  hooksChanged = true;
189
- return { ...hook, type: "command" };
204
+ return repair(hook);
190
205
  }
191
206
  }
192
207
  return hook;
@@ -195,13 +210,30 @@ function normalizeEntriesForCommand(entries, hookCommand) {
195
210
  changed = true;
196
211
  return { ...entry, hooks: nextHooks };
197
212
  });
198
- return { entries: nextEntries, changed };
213
+ const seen = new Set();
214
+ const deduplicated = nextEntries.map((entry) => {
215
+ const matcher = entry?.matcher;
216
+ const scope = JSON.stringify(event === "Stop" || matcher == null || ["", "*", ".*"].includes(matcher) ? null : matcher);
217
+ const keep = (hook) => {
218
+ if (!commandsEqual(hook?.command, hookCommand)) return true;
219
+ if (seen.has(scope)) { changed = true; return false; }
220
+ seen.add(scope);
221
+ return true;
222
+ };
223
+ if (entry?.command) return keep(entry) ? entry : null;
224
+ if (!Array.isArray(entry?.hooks)) return entry;
225
+ const hooks = entry.hooks.filter(keep);
226
+ if (!hooks.length) return null;
227
+ return hooks.length === entry.hooks.length ? entry : { ...entry, hooks };
228
+ }).filter(Boolean);
229
+ return { entries: deduplicated, changed };
199
230
  }
200
231
 
201
232
  function commandsEqual(a, b) {
202
233
  const left = normalizeCommand(a);
203
234
  const right = normalizeCommand(b);
204
- return Boolean(left && right && left === right);
235
+ return Boolean(left && right && (left === right ||
236
+ (notifyIdentity(left) && notifyIdentity(left) === notifyIdentity(right))));
205
237
  }
206
238
 
207
239
  function quoteArg(value) {
@@ -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/…'").