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
@@ -122,7 +122,7 @@
122
122
  "price": "0",
123
123
  "priceCurrency": "USD"
124
124
  },
125
- "description": "Local-first AI coding usage tracker for 43 tools, with a desktop pet, four widgets, achievements, native macOS and Windows apps, accurate cross-provider deduplication, and no prompt collection."
125
+ "description": "Local-first AI coding usage tracker for 44 tools, with a desktop pet, four widgets, achievements, native macOS and Windows apps, accurate cross-provider deduplication, and no prompt collection."
126
126
  },
127
127
  {
128
128
  "@type": "WebSite",
@@ -205,7 +205,8 @@
205
205
  { "@type": "ListItem", "position": 40, "name": "Cline" },
206
206
  { "@type": "ListItem", "position": 41, "name": "MiniMax Code" },
207
207
  { "@type": "ListItem", "position": 42, "name": "Command Code" },
208
- { "@type": "ListItem", "position": 43, "name": "TRAE" }
208
+ { "@type": "ListItem", "position": 43, "name": "TRAE" },
209
+ { "@type": "ListItem", "position": 44, "name": "WorkBuddy AI" }
209
210
  ]
210
211
  },
211
212
  {
@@ -224,7 +225,7 @@
224
225
  "name": "Which AI coding CLIs does Token Tracker support?",
225
226
  "acceptedAnswer": {
226
227
  "@type": "Answer",
227
- "text": "Token Tracker supports 43 AI coding tools, including Claude Code, Codex CLI, AStudio, Cursor, Gemini CLI, Antigravity, Kiro, OpenCode, OpenClaw, Every Code, Hermes Agent, GitHub Copilot, Kimi Code, CodeBuddy, WorkBuddy, Grok Build, oh-my-pi, OmO, pi, Dots, Prime Agent, Craft Agents, Reasonix, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose, Droid, Mimo Code, ZCode, Qoder, AnythingLLM Desktop, Claude Science, DeepSeek Harness, TRAE Work CN, LM Studio, Unsloth Studio, Devin CLI, Cline, MiniMax Code, Command Code, and TRAE."
228
+ "text": "Token Tracker supports 44 AI coding tools, including Claude Code, Codex CLI, AStudio, Cursor, Gemini CLI, Antigravity, Kiro, OpenCode, OpenClaw, Every Code, Hermes Agent, GitHub Copilot, Kimi Code, CodeBuddy, WorkBuddy, WorkBuddy AI, Grok Build, oh-my-pi, OmO, pi, Dots, Prime Agent, Craft Agents, Reasonix, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose, Droid, Mimo Code, ZCode, Qoder, AnythingLLM Desktop, Claude Science, DeepSeek Harness, TRAE Work CN, LM Studio, Unsloth Studio, Devin CLI, Cline, MiniMax Code, Command Code, and TRAE."
228
229
  }
229
230
  },
230
231
  {
@@ -256,7 +257,7 @@
256
257
  "name": "How do I monitor AI token usage and cost across tools?",
257
258
  "acceptedAnswer": {
258
259
  "@type": "Answer",
259
- "text": "Install Token Tracker with npx tokentracker-cli. It is an AI token usage and cost tracker that monitors token consumption and spend across all 43 supported tools in one dashboard, with per-model pricing across 2,200+ models so you can see cost by model, project, and time window without manual tracking."
260
+ "text": "Install Token Tracker with npx tokentracker-cli. It is an AI token usage and cost tracker that monitors token consumption and spend across all 44 supported tools in one dashboard, with per-model pricing across 2,200+ models so you can see cost by model, project, and time window without manual tracking."
260
261
  }
261
262
  },
262
263
  {
@@ -272,15 +273,15 @@
272
273
  ]
273
274
  }
274
275
  </script>
275
- <script type="module" crossorigin src="/assets/__vite-browser-external-Bq8nj-JV.js"></script>
276
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-BwmeEfv7.css">
276
+ <script type="module" crossorigin src="/assets/__vite-browser-external-2g_4s3ot.js"></script>
277
+ <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-LpFw4zXh.css">
277
278
  </head>
278
279
  <body>
279
280
  <main class="aeo-seed-content" aria-label="Token Tracker AI-readable summary">
280
281
  <h1>Token Tracker: monitor AI token usage and cost with a local dashboard and desktop companion</h1>
281
282
  <p>
282
283
  Token Tracker is a free, open-source, local-first dashboard that automatically tracks and monitors token usage
283
- and cost across 43 AI coding tools — including Claude Code, OpenAI Codex, AStudio, Cursor, Gemini CLI, LM Studio, and Unsloth Studio. A desktop pet
284
+ and cost across 44 AI coding tools — including Claude Code, OpenAI Codex, AStudio, Cursor, Gemini CLI, LM Studio, and Unsloth Studio. A desktop pet
284
285
  reacts to real token activity, four desktop widgets
285
286
  keep usage visible, and achievements turn coding history into progress. Native macOS and Windows apps
286
287
  sit on top of accurate cross-provider deduplication. Token counts only — never prompts or conversations.
@@ -305,7 +306,7 @@
305
306
  <li><strong>Desktop pet</strong> — a pixel companion that reacts to real token burn, streaks, and rest.</li>
306
307
  <li><strong>Four desktop widgets</strong> — usage, activity heatmap, top models, and limits at a glance.</li>
307
308
  <li><strong>Achievements</strong> — unlock milestones from local and optional account-wide activity.</li>
308
- <li><strong>Unified usage dashboard</strong> — input, output, cached, and cache-creation tokens across 43 AI coding tools.</li>
309
+ <li><strong>Unified usage dashboard</strong> — input, output, cached, and cache-creation tokens across 44 AI coding tools.</li>
309
310
  <li><strong>Cost analysis</strong> — per-model pricing across 2,200+ models with an offline snapshot.</li>
310
311
  <li><strong>Usage limits view</strong> — live quota and subscription status for 17 providers, with last-good caching for local providers such as Antigravity and Qoder.</li>
311
312
  <li><strong>Service Status page</strong> — live operational and incident status from eight official provider status pages.</li>
@@ -316,11 +317,11 @@
316
317
  </ul>
317
318
  <p><em>Privacy by design</em>: token counts only, never prompts or conversation content. Local-first; cloud sync is optional and powers only the public leaderboard.</p>
318
319
 
319
- <h2>Supported AI coding tools (43)</h2>
320
+ <h2>Supported AI coding tools (44)</h2>
320
321
  <p>
321
322
  Use Token Tracker as a Claude Code token tracker, Codex token tracker, Cursor token tracker,
322
323
  Gemini token tracker, OpenCode token tracker, GitHub Copilot token tracker, or DeepSeek Harness token tracker — one dashboard
323
- covers all 43 tools below.
324
+ covers all 44 tools below.
324
325
  </p>
325
326
  <ul>
326
327
  <li><strong>Claude Code</strong> — Anthropic's official coding agent CLI; integrated via SessionEnd hook.</li>
@@ -338,6 +339,7 @@
338
339
  <li><strong>Kimi Code</strong> — passive <code>wire.jsonl</code> reader.</li>
339
340
  <li><strong>CodeBuddy (Tencent)</strong> — Claude-Code fork; SessionEnd hook in <code>~/.codebuddy/settings.json</code>.</li>
340
341
  <li><strong>WorkBuddy (Tencent)</strong> — SessionEnd hook and passive session reader.</li>
342
+ <li><strong>WorkBuddy AI</strong> — International WorkBuddy build with separate session tracking.</li>
341
343
  <li><strong>Grok Build</strong> — hook and passive cumulative usage reader.</li>
342
344
  <li><strong>oh-my-pi</strong> — passive JSONL session reader.</li>
343
345
  <li><strong>OmO</strong> — passive JSONL session reader, including nested subagent transcripts.</li>
@@ -396,7 +398,7 @@
396
398
  <p>Run <code>npx tokentracker-cli</code>. The CLI auto-detects your AI coding tools, installs hooks, and opens the dashboard.</p>
397
399
 
398
400
  <h3>Which AI coding tools does Token Tracker support?</h3>
399
- <p>Token Tracker supports 43 tools: Claude Code, Codex CLI, AStudio, Cursor, Gemini CLI, Antigravity, Kiro, OpenCode, OpenClaw, Every Code, Hermes Agent, GitHub Copilot, Kimi Code, CodeBuddy, WorkBuddy, Grok Build, oh-my-pi, OmO, pi, Dots, Prime Agent, Craft Agents, Reasonix, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose, Droid, Mimo Code, ZCode, Qoder, AnythingLLM Desktop, Claude Science, DeepSeek Harness, TRAE Work CN, LM Studio, Unsloth Studio, Devin CLI, Cline, MiniMax Code, Command Code, and TRAE.</p>
401
+ <p>Token Tracker supports 44 tools: Claude Code, Codex CLI, AStudio, Cursor, Gemini CLI, Antigravity, Kiro, OpenCode, OpenClaw, Every Code, Hermes Agent, GitHub Copilot, Kimi Code, CodeBuddy, WorkBuddy, WorkBuddy AI, Grok Build, oh-my-pi, OmO, pi, Dots, Prime Agent, Craft Agents, Reasonix, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose, Droid, Mimo Code, ZCode, Qoder, AnythingLLM Desktop, Claude Science, DeepSeek Harness, TRAE Work CN, LM Studio, Unsloth Studio, Devin CLI, Cline, MiniMax Code, Command Code, and TRAE.</p>
400
402
 
401
403
  <h3>Is Token Tracker free and open source?</h3>
402
404
  <p>Yes. Source on GitHub at <a href="https://github.com/xiufengsun/TokenTracker">github.com/xiufengsun/TokenTracker</a>; npm package <code>tokentracker-cli</code>.</p>
@@ -408,7 +410,7 @@
408
410
  <p>Input, output, cached, and cache-creation tokens per model, project, and time window. It also surfaces usage limits for 17 providers, including Claude, Codex, Cursor, Gemini, Antigravity, Qoder, Qoder CN, Command Code, and Devin.</p>
409
411
 
410
412
  <h3>How do I monitor AI token usage and cost across tools?</h3>
411
- <p>Token Tracker is an AI token usage and cost tracker: install it with <code>npx tokentracker-cli</code>, and it monitors token consumption and spend across all 43 tools in one dashboard, with per-model pricing across 2,200+ models so you can see cost by model, project, and time window without manual tracking.</p>
413
+ <p>Token Tracker is an AI token usage and cost tracker: install it with <code>npx tokentracker-cli</code>, and it monitors token consumption and spend across all 44 tools in one dashboard, with per-model pricing across 2,200+ models so you can see cost by model, project, and time window without manual tracking.</p>
412
414
 
413
415
  <h3>Does Token Tracker work offline?</h3>
414
416
  <p>Yes. Token Tracker is local-first. All parsing, aggregation, and dashboard rendering happen on your machine. Cloud sync is optional and powers only the public leaderboard.</p>
@@ -164,8 +164,8 @@
164
164
  ]
165
165
  }
166
166
  </script>
167
- <script type="module" crossorigin src="/assets/__vite-browser-external-Bq8nj-JV.js"></script>
168
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-BwmeEfv7.css">
167
+ <script type="module" crossorigin src="/assets/__vite-browser-external-2g_4s3ot.js"></script>
168
+ <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-LpFw4zXh.css">
169
169
  </head>
170
170
  <body>
171
171
  <main class="aeo-seed-content" aria-label="Claude IP Check AI-readable summary">
@@ -135,8 +135,8 @@
135
135
  ]
136
136
  }
137
137
  </script>
138
- <script type="module" crossorigin src="/assets/__vite-browser-external-Bq8nj-JV.js"></script>
139
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-BwmeEfv7.css">
138
+ <script type="module" crossorigin src="/assets/__vite-browser-external-2g_4s3ot.js"></script>
139
+ <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-LpFw4zXh.css">
140
140
  </head>
141
141
  <body>
142
142
  <main class="aeo-seed-content" aria-label="AI Coding Token Usage Leaderboard AI-readable summary">
@@ -1,7 +1,7 @@
1
1
  # Token Tracker LLMs Index
2
2
 
3
3
  Token Tracker is a free, open-source, local-first AI coding usage and cost
4
- tracker for 43 tools. Its distinctive desktop experience includes a pixel pet
4
+ tracker for 44 tools. Its distinctive desktop experience includes a pixel pet
5
5
  powered by real token activity, four desktop widgets, and achievements. The
6
6
  same data powers a cross-platform CLI, a native macOS menu bar app, a native
7
7
  Windows system tray app, and a dashboard. It records token counts and
@@ -23,7 +23,7 @@ timestamps, never prompts, responses, or source code.
23
23
  - [npm package](https://www.npmjs.com/package/tokentracker-cli)
24
24
  - [Latest release](https://github.com/xiufengsun/TokenTracker/releases/latest)
25
25
 
26
- ## Supported AI coding tools (43)
26
+ ## Supported AI coding tools (44)
27
27
  - Claude Code
28
28
  - Codex CLI
29
29
  - AStudio
@@ -75,8 +75,8 @@
75
75
  "description": "Shareable Token Tracker dashboard snapshot."
76
76
  }
77
77
  </script>
78
- <script type="module" crossorigin src="/assets/__vite-browser-external-Bq8nj-JV.js"></script>
79
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-BwmeEfv7.css">
78
+ <script type="module" crossorigin src="/assets/__vite-browser-external-2g_4s3ot.js"></script>
79
+ <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-LpFw4zXh.css">
80
80
  </head>
81
81
  <body>
82
82
  <main class="aeo-seed-content" aria-label="Token Tracker share page summary">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "1.3.4",
4
- "description": "Local-first AI coding token usage and cost tracker for 43 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
3
+ "version": "1.3.5",
4
+ "description": "Local-first AI coding token usage and cost tracker for 44 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tokentracker-cli": "bin/tracker.js",
@@ -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) {