tokentracker-cli 0.79.7 → 0.79.9

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 (80) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-BgLKRfg6.js → AchievementBadge-B1WOPlu0.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DAZPKi69.js → AchievementsPage-9kHEQicS.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-Bpck20e3.js → AchievementsSection-Dw05PYPr.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-Di0rpmTQ.js → BadgeDetailModal-DaWgiijJ.js} +1 -1
  5. package/dashboard/dist/assets/{Card-CiVg1n9y.js → Card-DYhrLLM4.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-Dhw6SxU-.js → ClawdAnimated-BLy2LUKz.js} +1 -1
  7. package/dashboard/dist/assets/CommunityStatsModal-CWYqfht3.js +1 -0
  8. package/dashboard/dist/assets/{ConfirmModal-DD1Ri-dh.js → ConfirmModal-DcE2C24Y.js} +1 -1
  9. package/dashboard/dist/assets/{Controls-BQxSK041.js → Controls-CI7qw0-b.js} +1 -1
  10. package/dashboard/dist/assets/DashboardPage-CAlgaBa7.js +19 -0
  11. package/dashboard/dist/assets/{DevicePage-BgByRtlZ.js → DevicePage-DinOyaeN.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-DigAXf6w.js → DialogClose-CjfSDrLo.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DWs-KIUA.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-Du1uOZMr.js → DialogTitle-f7gNmCca.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-Da6jgqZ_.js → FadeIn-D_5w0Qh0.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-Cqqy7vB7.js → HeaderGithubStar-DTZyNKyL.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-DkTAw_dO.js → IpCheckPage-D8CViF98.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-Dvb9s-kJ.js → LandingPage-CqN5KEjV.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-Ch7SLsZ_.js → LeaderboardAvatar-CwUVO4wn.js} +1 -1
  20. package/dashboard/dist/assets/LeaderboardPage-B12HJ9LA.js +2 -0
  21. package/dashboard/dist/assets/LeaderboardProfileModal-DFzPpsnV.js +73 -0
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-DRautoWL.js → LeaderboardProfilePage-Z0ZYpNFd.js} +2 -2
  23. package/dashboard/dist/assets/{LimitsPage-Bwc3XCCg.js → LimitsPage-BpwioIa-.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-DftNXQsE.js → LocalOnlyNotice-Covi6zdt.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CffpNmm-.js → LoginPage-CRqbhwCx.js} +1 -1
  26. package/dashboard/dist/assets/{PetPage-C_lq7mMA.js → PetPage-BY_yObVj.js} +1 -1
  27. package/dashboard/dist/assets/{PopoverPopup-B7L6V9OS.js → PopoverPopup-B2dE3Ary.js} +1 -1
  28. package/dashboard/dist/assets/{ResetPasswordPage-P-VatRkZ.js → ResetPasswordPage-C8IEJbVc.js} +1 -1
  29. package/dashboard/dist/assets/{Select-NpNBO_zW.js → Select-BmRLYSqA.js} +1 -1
  30. package/dashboard/dist/assets/{SelectItemText-DEqB9ndy.js → SelectItemText-FC46bk8b.js} +1 -1
  31. package/dashboard/dist/assets/SettingsPage-DipzOQVg.js +1 -0
  32. package/dashboard/dist/assets/{SkillsPage-Dy_sojV5.js → SkillsPage-CziypUTf.js} +1 -1
  33. package/dashboard/dist/assets/{WidgetsPage-CzbeMPoq.js → WidgetsPage-CJegJhVR.js} +1 -1
  34. package/dashboard/dist/assets/{WrappedPage-C9sMo5ha.js → WrappedPage-DcMiKvzf.js} +1 -1
  35. package/dashboard/dist/assets/{agent-logos-D95HrgpM.js → agent-logos-S7bF1LwW.js} +1 -1
  36. package/dashboard/dist/assets/{arrow-up-right-jaA_F8VA.js → arrow-up-right-D0yeL56y.js} +1 -1
  37. package/dashboard/dist/assets/check-Dn9BLYEf.js +1 -0
  38. package/dashboard/dist/assets/{copy-BUsG8Dxq.js → copy-D-z9ZtIu.js} +1 -1
  39. package/dashboard/dist/assets/{download-B89jns_I.js → download-Dwxnd3QG.js} +1 -1
  40. package/dashboard/dist/assets/{flame-DYJfKTkq.js → flame-Bdqk9A75.js} +1 -1
  41. package/dashboard/dist/assets/{format-tokens-Ck4iJNuB.js → format-tokens-DNVcCRwm.js} +1 -1
  42. package/dashboard/dist/assets/{info-DgaOnN42.js → info-CMLT6oUI.js} +1 -1
  43. package/dashboard/dist/assets/{link-2-l6SkcxP3.js → link-2-BmiHu726.js} +1 -1
  44. package/dashboard/dist/assets/{loader-circle-BTZ18vzb.js → loader-circle-CXVUQtzm.js} +1 -1
  45. package/dashboard/dist/assets/main-B9m79io-.js +1249 -0
  46. package/dashboard/dist/assets/provider-display-BoAesdke.js +42 -0
  47. package/dashboard/dist/assets/{trash-2-DDKBUuG2.js → trash-2-C9Rh2DAL.js} +1 -1
  48. package/dashboard/dist/assets/{use-community-stats-KM2l89gG.js → use-community-stats-aZwYxrXR.js} +1 -1
  49. package/dashboard/dist/assets/{use-limits-display-prefs-1RM6rjBg.js → use-limits-display-prefs-tJKR83RT.js} +1 -1
  50. package/dashboard/dist/assets/use-native-settings-PjfJPK5v.js +1 -0
  51. package/dashboard/dist/assets/{use-ordered-list-B3ExZ03c.js → use-ordered-list-D1QFmZUY.js} +1 -1
  52. package/dashboard/dist/assets/{use-quality-per-dollar-pref-oapCFOoT.js → use-quality-per-dollar-pref-4N3Su4wC.js} +1 -1
  53. package/dashboard/dist/assets/{use-transform-Cd2VlHK0.js → use-transform-D9nurInL.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-BMwpdXJP.js → use-usage-limits-DapTpi_6.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-BA2GiiJp.js → useCurrency-B0DH9ZZA.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-CBHwgGF7.js → useScrollLock-DijV6DYS.js} +1 -1
  57. package/dashboard/dist/assets/useTokenFormat-nxdG6gi9.js +1 -0
  58. package/dashboard/dist/assets/{zap-C3aLlz8b.js → zap-BVfhR3Ck.js} +1 -1
  59. package/dashboard/dist/index.html +1 -1
  60. package/dashboard/dist/share.html +1 -1
  61. package/package.json +1 -1
  62. package/src/commands/init.js +78 -12
  63. package/src/commands/serve.js +65 -4
  64. package/src/commands/status.js +2 -1
  65. package/src/commands/sync.js +10 -1
  66. package/src/commands/uninstall.js +10 -2
  67. package/src/lib/claude-config.js +69 -28
  68. package/src/lib/diagnostics.js +2 -2
  69. package/src/lib/local-api.js +8 -1
  70. package/src/lib/pricing/seed-snapshot.json +1 -1
  71. package/dashboard/dist/assets/CommunityStatsModal-DosBW4lR.js +0 -1
  72. package/dashboard/dist/assets/DashboardPage-B7F7CLG5.js +0 -19
  73. package/dashboard/dist/assets/DialogDescription-CS0HEX8l.js +0 -1
  74. package/dashboard/dist/assets/LeaderboardPage-BbPfFFuF.js +0 -2
  75. package/dashboard/dist/assets/LeaderboardProfileModal-PFZaQLMG.js +0 -73
  76. package/dashboard/dist/assets/SettingsPage-uCZ0OdF3.js +0 -1
  77. package/dashboard/dist/assets/check-ShRr2Grp.js +0 -1
  78. package/dashboard/dist/assets/main-BVPlyJJu.js +0 -1245
  79. package/dashboard/dist/assets/provider-display-DuQlae7O.js +0 -42
  80. package/dashboard/dist/assets/use-native-settings-S7AyC9Ou.js +0 -1
@@ -22,6 +22,7 @@ const WSL_DEFAULT_PORT = 7681;
22
22
  const DEFAULT_MAX_PORT_ATTEMPTS = 20;
23
23
  const NPM_PACKAGE_NAME = "tokentracker-cli";
24
24
  const LOCAL_BIND_HOST = "127.0.0.1";
25
+ const NATIVE_BACKGROUND_SYNC_INTERVAL_MS = 60_000;
25
26
  const STATIC_ASSET_EXTENSIONS = new Set([
26
27
  ".css",
27
28
  ".gif",
@@ -71,11 +72,13 @@ async function cmdServe(argv) {
71
72
  }
72
73
 
73
74
  try {
74
- const { installLocalTrackerApp, repairCodexNotifyIntegration } = require("./init");
75
+ const { installLocalTrackerApp, repairRuntimeIntegrations } = require("./init");
75
76
  await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
76
- const repairResult = await repairCodexNotifyIntegration({ trackerDir, binDir, safeMode: true });
77
- if (repairResult?.skippedReason && repairResult.skippedReason !== "config-missing") {
78
- process.stdout.write(`Codex notify repair skipped: ${repairResult.skippedReason}\n`);
77
+ const repairResult = await repairRuntimeIntegrations({ trackerDir, binDir, safeMode: true });
78
+ for (const warning of repairResult?.warnings || []) {
79
+ process.stdout.write(
80
+ `Runtime integration repair warning (${warning.integration}): ${warning.error}\n`,
81
+ );
79
82
  }
80
83
  } catch (e) {
81
84
  process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
@@ -217,6 +220,18 @@ async function cmdServe(argv) {
217
220
 
218
221
  await maybeShowStarCta({ trackerDir });
219
222
 
223
+ // Native desktop apps keep this server alive even when the dashboard window
224
+ // is closed. Provider hooks are the fast path, but they can be removed by a
225
+ // tool update or skipped by a provider. Scan every local source once a
226
+ // minute as a bounded fallback so the queue cannot remain stale indefinitely.
227
+ // `--no-sync` still skips the blocking startup sync; this periodic pass is
228
+ // native-shell-only and uses the lightweight local mode (no cloud upload,
229
+ // Cursor network request, or deep Codex archive scan).
230
+ const nativeBackgroundSync = startNativeBackgroundSync({
231
+ onError: (e) =>
232
+ process.stdout.write(`Background local sync warning: ${e?.message || e}\n`),
233
+ });
234
+
220
235
  // Anonymous daily heartbeat (see src/lib/telemetry.js for the privacy
221
236
  // contract). Fire-and-forget at startup, then re-checked every 6 hours so
222
237
  // long-lived embedded-app servers still count on later days; the shared
@@ -238,6 +253,7 @@ async function cmdServe(argv) {
238
253
  // 5. Graceful shutdown
239
254
  const shutdown = () => {
240
255
  process.stdout.write("\nShutting down...\n");
256
+ nativeBackgroundSync?.stop();
241
257
  server.close(() => process.exit(0));
242
258
  setTimeout(() => process.exit(0), 3000);
243
259
  };
@@ -248,6 +264,49 @@ async function cmdServe(argv) {
248
264
  await new Promise(() => {});
249
265
  }
250
266
 
267
+ function startNativeBackgroundSync({
268
+ appShell = process.env.TOKENTRACKER_APP_SHELL,
269
+ intervalMs = NATIVE_BACKGROUND_SYNC_INTERVAL_MS,
270
+ runSync,
271
+ setIntervalFn = setInterval,
272
+ clearIntervalFn = clearInterval,
273
+ onError = () => {},
274
+ } = {}) {
275
+ const normalizedShell = String(appShell || "").trim().toLowerCase();
276
+ if (normalizedShell !== "macos" && normalizedShell !== "windows") return null;
277
+
278
+ const sync = runSync || (async (args) => {
279
+ const { cmdSync } = require("./sync");
280
+ await cmdSync(args);
281
+ });
282
+ let inFlight = null;
283
+ const run = () => {
284
+ if (inFlight) return inFlight;
285
+ const pending = Promise.resolve()
286
+ .then(() => sync(["--auto", "--background", "--all-local-sources"]))
287
+ .catch((error) => {
288
+ try { onError(error); } catch (_e) {}
289
+ })
290
+ .finally(() => {
291
+ if (inFlight === pending) inFlight = null;
292
+ });
293
+ inFlight = pending;
294
+ return pending;
295
+ };
296
+
297
+ const timer = setIntervalFn(() => {
298
+ void run();
299
+ }, intervalMs);
300
+ timer?.unref?.();
301
+
302
+ return {
303
+ run,
304
+ stop() {
305
+ clearIntervalFn(timer);
306
+ },
307
+ };
308
+ }
309
+
251
310
  function findPidOnPort(port) {
252
311
  try {
253
312
  const out = cp.execFileSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf8", timeout: 5000 });
@@ -455,4 +514,6 @@ module.exports = {
455
514
  isRunningUnderWsl,
456
515
  resolveDefaultPort,
457
516
  shouldServeSpaFallback,
517
+ startNativeBackgroundSync,
518
+ NATIVE_BACKGROUND_SYNC_INTERVAL_MS,
458
519
  };
@@ -8,6 +8,7 @@ const { readJson } = require("../lib/fs");
8
8
  const { readCodexNotify, readEveryCodeNotify } = require("../lib/codex-config");
9
9
  const {
10
10
  isClaudeHookConfigured,
11
+ areClaudeUsageHooksConfigured,
11
12
  buildClaudeHookCommand,
12
13
  buildHookCommand,
13
14
  } = require("../lib/claude-config");
@@ -155,7 +156,7 @@ async function cmdStatus(argv = []) {
155
156
  const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
156
157
  const everyCodeConfigured =
157
158
  Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
158
- const claudeHookConfigured = await isClaudeHookConfigured({
159
+ const claudeHookConfigured = await areClaudeUsageHooksConfigured({
159
160
  settingsPath: claudeSettingsPath,
160
161
  hookCommand: claudeHookCommand,
161
162
  });
@@ -330,11 +330,18 @@ async function cmdSync(argv) {
330
330
 
331
331
  const autoSourceScope = resolveAutoSourceScope(opts);
332
332
  const isBackgroundLightweightSync = opts.auto && opts.background && !opts.drain;
333
+ const isBackgroundAllLocalSync = isBackgroundLightweightSync && opts.allLocalSources;
333
334
  const isFullSourceScan = !autoSourceScope && !isBackgroundLightweightSync;
334
335
  const sourceAllowed = (...sources) => {
335
336
  if (isBackgroundLightweightSync) {
336
337
  if (autoSourceScope) {
337
- return BACKGROUND_AUTO_SYNC_SOURCES.has(autoSourceScope) && sources.includes(autoSourceScope);
338
+ return (
339
+ (isBackgroundAllLocalSync || BACKGROUND_AUTO_SYNC_SOURCES.has(autoSourceScope)) &&
340
+ sources.includes(autoSourceScope)
341
+ );
342
+ }
343
+ if (isBackgroundAllLocalSync) {
344
+ return sources.some((source) => AUTO_SYNC_SOURCES.has(source));
338
345
  }
339
346
  return sources.some((source) => BACKGROUND_AUTO_SYNC_SOURCES.has(source));
340
347
  }
@@ -1998,6 +2005,7 @@ function parseArgs(argv) {
1998
2005
  source: null,
1999
2006
  drain: false,
2000
2007
  background: false,
2008
+ allLocalSources: false,
2001
2009
  repairGrok: false,
2002
2010
  };
2003
2011
  for (let i = 0; i < argv.length; i++) {
@@ -2013,6 +2021,7 @@ function parseArgs(argv) {
2013
2021
  else if (a.startsWith("--source=")) out.source = normalizeSyncSource(a.slice("--source=".length));
2014
2022
  else if (a === "--drain") out.drain = true;
2015
2023
  else if (a === "--background" || a === "--lightweight") out.background = true;
2024
+ else if (a === "--all-local-sources") out.allLocalSources = true;
2016
2025
  else if (a === "--repair-grok") out.repairGrok = true;
2017
2026
  else throw new Error(`Unknown option: ${a}`);
2018
2027
  }
@@ -3,7 +3,12 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { restoreCodexNotify, restoreEveryCodeNotify } = require("../lib/codex-config");
6
- const { removeClaudeHook, buildClaudeHookCommand, buildHookCommand } = require("../lib/claude-config");
6
+ const {
7
+ removeClaudeHook,
8
+ removeClaudeUsageHooks,
9
+ buildClaudeHookCommand,
10
+ buildHookCommand,
11
+ } = require("../lib/claude-config");
7
12
  const {
8
13
  resolveGeminiConfigDir,
9
14
  resolveGeminiSettingsPath,
@@ -64,7 +69,10 @@ async function cmdUninstall(argv) {
64
69
  })
65
70
  : { restored: false, skippedReason: "config-missing" };
66
71
  const claudeRemove = claudeConfigExists
67
- ? await removeClaudeHook({ settingsPath: claudeSettingsPath, hookCommand: claudeHookCommand })
72
+ ? await removeClaudeUsageHooks({
73
+ settingsPath: claudeSettingsPath,
74
+ hookCommand: claudeHookCommand,
75
+ })
68
76
  : { removed: false, skippedReason: "config-missing" };
69
77
  const codebuddyRemove = codebuddyConfigExists
70
78
  ? await removeClaudeHook({
@@ -4,56 +4,74 @@ const path = require("node:path");
4
4
  const { ensureDir, readJson, writeJson } = require("./fs");
5
5
 
6
6
  const DEFAULT_EVENT = "SessionEnd";
7
+ const CLAUDE_USAGE_EVENTS = ["Stop", DEFAULT_EVENT];
7
8
 
8
9
  async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
10
+ return upsertClaudeHooks({ settingsPath, hookCommand, events: [event] });
11
+ }
12
+
13
+ async function upsertClaudeUsageHooks({ settingsPath, hookCommand }) {
14
+ return upsertClaudeHooks({ settingsPath, hookCommand, events: CLAUDE_USAGE_EVENTS });
15
+ }
16
+
17
+ async function upsertClaudeHooks({ settingsPath, hookCommand, events }) {
9
18
  const existing = await readJson(settingsPath);
10
19
  const settings = normalizeSettings(existing);
11
20
  const hooks = normalizeHooks(settings.hooks);
12
- const entries = normalizeEntries(hooks[event]);
13
-
14
- const normalized = normalizeEntriesForCommand(entries, hookCommand);
15
- if (normalized.changed) {
16
- const nextHooks = { ...hooks, [event]: normalized.entries };
17
- const nextSettings = { ...settings, hooks: nextHooks };
18
- const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
19
- return { changed: true, backupPath };
20
- }
21
+ const nextHooks = { ...hooks };
22
+ let changed = false;
21
23
 
22
- if (hasHook(entries, hookCommand)) {
23
- return { changed: false, backupPath: null };
24
+ for (const event of normalizeEventList(events)) {
25
+ const entries = normalizeEntries(hooks[event]);
26
+ const normalized = normalizeEntriesForCommand(entries, hookCommand);
27
+ let nextEntries = normalized.entries;
28
+ if (!hasHook(nextEntries, hookCommand)) {
29
+ nextEntries = nextEntries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
30
+ changed = true;
31
+ }
32
+ if (normalized.changed) changed = true;
33
+ if (normalized.changed || nextEntries.length !== entries.length) {
34
+ nextHooks[event] = nextEntries;
35
+ }
24
36
  }
25
37
 
26
- const nextEntries = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
27
- const nextHooks = { ...hooks, [event]: nextEntries };
28
- const nextSettings = { ...settings, hooks: nextHooks };
38
+ if (!changed) return { changed: false, backupPath: null };
29
39
 
40
+ const nextSettings = { ...settings, hooks: nextHooks };
30
41
  const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
31
42
  return { changed: true, backupPath };
32
43
  }
33
44
 
34
45
  async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
46
+ return removeClaudeHooks({ settingsPath, hookCommand, events: [event] });
47
+ }
48
+
49
+ async function removeClaudeUsageHooks({ settingsPath, hookCommand }) {
50
+ return removeClaudeHooks({ settingsPath, hookCommand, events: CLAUDE_USAGE_EVENTS });
51
+ }
52
+
53
+ async function removeClaudeHooks({ settingsPath, hookCommand, events }) {
35
54
  const existing = await readJson(settingsPath);
36
55
  if (!existing) return { removed: false, skippedReason: "settings-missing" };
37
56
 
38
57
  const settings = normalizeSettings(existing);
39
58
  const hooks = normalizeHooks(settings.hooks);
40
- const entries = normalizeEntries(hooks[event]);
41
- if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
42
-
59
+ const nextHooks = { ...hooks };
43
60
  let removed = false;
44
- const nextEntries = [];
45
- for (const entry of entries) {
46
- const res = stripHookFromEntry(entry, hookCommand);
47
- if (res.removed) removed = true;
48
- if (res.entry) nextEntries.push(res.entry);
61
+ for (const event of normalizeEventList(events)) {
62
+ const entries = normalizeEntries(hooks[event]);
63
+ const nextEntries = [];
64
+ for (const entry of entries) {
65
+ const res = stripHookFromEntry(entry, hookCommand);
66
+ if (res.removed) removed = true;
67
+ if (res.entry) nextEntries.push(res.entry);
68
+ }
69
+ if (nextEntries.length > 0) nextHooks[event] = nextEntries;
70
+ else delete nextHooks[event];
49
71
  }
50
72
 
51
73
  if (!removed) return { removed: false, skippedReason: "hook-missing" };
52
74
 
53
- const nextHooks = { ...hooks };
54
- if (nextEntries.length > 0) nextHooks[event] = nextEntries;
55
- else delete nextHooks[event];
56
-
57
75
  const nextSettings = { ...settings };
58
76
  if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
59
77
  else delete nextSettings.hooks;
@@ -63,12 +81,26 @@ async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
63
81
  }
64
82
 
65
83
  async function isClaudeHookConfigured({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
84
+ return areClaudeHooksConfigured({ settingsPath, hookCommand, events: [event] });
85
+ }
86
+
87
+ async function areClaudeUsageHooksConfigured({ settingsPath, hookCommand }) {
88
+ return areClaudeHooksConfigured({
89
+ settingsPath,
90
+ hookCommand,
91
+ events: CLAUDE_USAGE_EVENTS,
92
+ });
93
+ }
94
+
95
+ async function areClaudeHooksConfigured({ settingsPath, hookCommand, events }) {
66
96
  const settings = await readJson(settingsPath);
67
97
  if (!settings || typeof settings !== "object") return false;
68
98
  const hooks = settings.hooks;
69
99
  if (!hooks || typeof hooks !== "object") return false;
70
- const entries = normalizeEntries(hooks[event]);
71
- return hasHook(entries, hookCommand);
100
+ return normalizeEventList(events).every((event) => {
101
+ const entries = normalizeEntries(hooks[event]);
102
+ return hasHook(entries, hookCommand);
103
+ });
72
104
  }
73
105
 
74
106
  // Generic Session-hook command builder. CodeBuddy CLI is a Claude-Code fork
@@ -96,6 +128,11 @@ function normalizeEntries(raw) {
96
128
  return Array.isArray(raw) ? raw.slice() : [];
97
129
  }
98
130
 
131
+ function normalizeEventList(events) {
132
+ const values = Array.isArray(events) ? events : [];
133
+ return Array.from(new Set(values.filter((event) => typeof event === "string" && event)));
134
+ }
135
+
99
136
  function normalizeCommand(cmd) {
100
137
  if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
101
138
  if (typeof cmd === "string") return cmd.trim();
@@ -193,9 +230,13 @@ async function writeClaudeSettings({ settingsPath, settings }) {
193
230
  }
194
231
 
195
232
  module.exports = {
233
+ CLAUDE_USAGE_EVENTS,
196
234
  upsertClaudeHook,
235
+ upsertClaudeUsageHooks,
197
236
  removeClaudeHook,
237
+ removeClaudeUsageHooks,
198
238
  isClaudeHookConfigured,
239
+ areClaudeUsageHooksConfigured,
199
240
  buildClaudeHookCommand,
200
241
  buildHookCommand,
201
242
  // Aliases for callers that want a name unbiased toward Claude (the schema
@@ -4,7 +4,7 @@ const fs = require("node:fs/promises");
4
4
 
5
5
  const { readJson } = require("./fs");
6
6
  const { readCodexNotify, readEveryCodeNotify } = require("./codex-config");
7
- const { isClaudeHookConfigured, buildClaudeHookCommand } = require("./claude-config");
7
+ const { areClaudeUsageHooksConfigured, buildClaudeHookCommand } = require("./claude-config");
8
8
  const {
9
9
  resolveGeminiConfigDir,
10
10
  resolveGeminiSettingsPath,
@@ -81,7 +81,7 @@ async function collectTrackerDiagnostics({
81
81
  ? everyCodeNotifyRaw.map((v) => redactValue(v, home))
82
82
  : null;
83
83
  const claudeHookCommand = buildClaudeHookCommand(path.join(binDir, "notify.cjs"));
84
- const claudeHookConfigured = await isClaudeHookConfigured({
84
+ const claudeHookConfigured = await areClaudeUsageHooksConfigured({
85
85
  settingsPath: claudeConfigPath,
86
86
  hookCommand: claudeHookCommand,
87
87
  });
@@ -685,6 +685,7 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
685
685
  const args = [TRACKER_BIN, "sync"];
686
686
  if (opts.auto === true) args.push("--auto");
687
687
  if (opts.background === true) args.push("--background");
688
+ if (opts.allLocalSources === true) args.push("--all-local-sources");
688
689
  if (opts.drain === true) args.push("--drain");
689
690
  const child = spawn(process.execPath, args, {
690
691
  env: { ...process.env, ...extraEnv },
@@ -1693,6 +1694,7 @@ function createLocalApiHandler({ queuePath }) {
1693
1694
  const auto = body.auto === true && !drain;
1694
1695
  const background =
1695
1696
  auto && (body.background === true || body.lightweight === true);
1697
+ const allLocalSources = background && body.allLocalSources === true;
1696
1698
  if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
1697
1699
  extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
1698
1700
  }
@@ -1724,7 +1726,12 @@ function createLocalApiHandler({ queuePath }) {
1724
1726
  extraEnv.TOKENTRACKER_DEVICE_TOKEN = issuedToken;
1725
1727
  }
1726
1728
  }
1727
- const result = await runSyncCommand(extraEnv, { drain, auto, background });
1729
+ const result = await runSyncCommand(extraEnv, {
1730
+ drain,
1731
+ auto,
1732
+ background,
1733
+ allLocalSources,
1734
+ });
1728
1735
  try {
1729
1736
  const { resetUsageLimitsCache } = require("./usage-limits");
1730
1737
  resetUsageLimitsCache();