tokentracker-cli 0.79.8 → 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 (74) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ctu3c_8t.js → AchievementBadge-B1WOPlu0.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-B3kBTHDB.js → AchievementsPage-9kHEQicS.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CAnTXGpC.js → AchievementsSection-Dw05PYPr.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-oTUXEQk9.js → BadgeDetailModal-DaWgiijJ.js} +1 -1
  5. package/dashboard/dist/assets/{Card-BHt5f5cD.js → Card-DYhrLLM4.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-ChxXY3l0.js → ClawdAnimated-BLy2LUKz.js} +1 -1
  7. package/dashboard/dist/assets/{CommunityStatsModal-CVLlItP1.js → CommunityStatsModal-CWYqfht3.js} +1 -1
  8. package/dashboard/dist/assets/{ConfirmModal-ClrILpb3.js → ConfirmModal-DcE2C24Y.js} +1 -1
  9. package/dashboard/dist/assets/{Controls-D_XBBpIh.js → Controls-CI7qw0-b.js} +1 -1
  10. package/dashboard/dist/assets/DashboardPage-CAlgaBa7.js +19 -0
  11. package/dashboard/dist/assets/{DevicePage-J7DW7yeX.js → DevicePage-DinOyaeN.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-B1r9gzUF.js → DialogClose-CjfSDrLo.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DWs-KIUA.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-BL2puDA7.js → DialogTitle-f7gNmCca.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-Cd-YT4oT.js → FadeIn-D_5w0Qh0.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BAtl4a-K.js → HeaderGithubStar-DTZyNKyL.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-BXK_cYN2.js → IpCheckPage-D8CViF98.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-C_16AbpF.js → LandingPage-CqN5KEjV.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-B0vUuCYU.js → LeaderboardAvatar-CwUVO4wn.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-Duhdaq09.js → LeaderboardPage-B12HJ9LA.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-CFFyHRTB.js → LeaderboardProfileModal-DFzPpsnV.js} +3 -3
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-CjTelV-4.js → LeaderboardProfilePage-Z0ZYpNFd.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-DIcXRgSv.js → LimitsPage-BpwioIa-.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-DC7wXWds.js → LocalOnlyNotice-Covi6zdt.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage--8hYT3aq.js → LoginPage-CRqbhwCx.js} +1 -1
  26. package/dashboard/dist/assets/{PetPage-ByWUW7-V.js → PetPage-BY_yObVj.js} +1 -1
  27. package/dashboard/dist/assets/{PopoverPopup-g_v1fmhH.js → PopoverPopup-B2dE3Ary.js} +1 -1
  28. package/dashboard/dist/assets/{ResetPasswordPage-BfunstIi.js → ResetPasswordPage-C8IEJbVc.js} +1 -1
  29. package/dashboard/dist/assets/{Select-C4ShPt1t.js → Select-BmRLYSqA.js} +1 -1
  30. package/dashboard/dist/assets/{SelectItemText-gw_TdvY6.js → SelectItemText-FC46bk8b.js} +1 -1
  31. package/dashboard/dist/assets/{SettingsPage-BFCFrDuA.js → SettingsPage-DipzOQVg.js} +1 -1
  32. package/dashboard/dist/assets/{SkillsPage-BNQDySeK.js → SkillsPage-CziypUTf.js} +1 -1
  33. package/dashboard/dist/assets/{WidgetsPage-B3bHP3Vk.js → WidgetsPage-CJegJhVR.js} +1 -1
  34. package/dashboard/dist/assets/{WrappedPage-6amxWnWT.js → WrappedPage-DcMiKvzf.js} +1 -1
  35. package/dashboard/dist/assets/{agent-logos-DPIQzYms.js → agent-logos-S7bF1LwW.js} +1 -1
  36. package/dashboard/dist/assets/{arrow-up-right-CKcE2mif.js → arrow-up-right-D0yeL56y.js} +1 -1
  37. package/dashboard/dist/assets/check-Dn9BLYEf.js +1 -0
  38. package/dashboard/dist/assets/{copy-vi-V2M_I.js → copy-D-z9ZtIu.js} +1 -1
  39. package/dashboard/dist/assets/{download-BH0Spg15.js → download-Dwxnd3QG.js} +1 -1
  40. package/dashboard/dist/assets/{flame-C3jUAnqA.js → flame-Bdqk9A75.js} +1 -1
  41. package/dashboard/dist/assets/{format-tokens-tnfJT09G.js → format-tokens-DNVcCRwm.js} +1 -1
  42. package/dashboard/dist/assets/{info-CGCZ5JvP.js → info-CMLT6oUI.js} +1 -1
  43. package/dashboard/dist/assets/{link-2-CXULWwZY.js → link-2-BmiHu726.js} +1 -1
  44. package/dashboard/dist/assets/{loader-circle-B324hSa3.js → loader-circle-CXVUQtzm.js} +1 -1
  45. package/dashboard/dist/assets/{main-vGUuyRq-.js → main-B9m79io-.js} +20 -20
  46. package/dashboard/dist/assets/{provider-display-CeoAhoCb.js → provider-display-BoAesdke.js} +1 -1
  47. package/dashboard/dist/assets/{trash-2-L9vxbDeP.js → trash-2-C9Rh2DAL.js} +1 -1
  48. package/dashboard/dist/assets/{use-community-stats-DLE30Lh0.js → use-community-stats-aZwYxrXR.js} +1 -1
  49. package/dashboard/dist/assets/{use-limits-display-prefs-BxN--PEW.js → use-limits-display-prefs-tJKR83RT.js} +1 -1
  50. package/dashboard/dist/assets/{use-native-settings-Dn5gWQkJ.js → use-native-settings-PjfJPK5v.js} +1 -1
  51. package/dashboard/dist/assets/{use-ordered-list-DYoz2NJ9.js → use-ordered-list-D1QFmZUY.js} +1 -1
  52. package/dashboard/dist/assets/{use-quality-per-dollar-pref-JXorBQsT.js → use-quality-per-dollar-pref-4N3Su4wC.js} +1 -1
  53. package/dashboard/dist/assets/{use-transform-BTTF5vPC.js → use-transform-D9nurInL.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-BUemSWQ9.js → use-usage-limits-DapTpi_6.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-Cxv4RR3U.js → useCurrency-B0DH9ZZA.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-DTPnHKfF.js → useScrollLock-DijV6DYS.js} +1 -1
  57. package/dashboard/dist/assets/useTokenFormat-nxdG6gi9.js +1 -0
  58. package/dashboard/dist/assets/{zap-CG8zuY9S.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/DashboardPage-CizTJox2.js +0 -19
  72. package/dashboard/dist/assets/DialogDescription-Bd9-kaCS.js +0 -1
  73. package/dashboard/dist/assets/check-B5V4_ezt.js +0 -1
  74. package/dashboard/dist/assets/useTokenFormat-DMAb2ec3.js +0 -1
@@ -21,9 +21,11 @@ const {
21
21
  } = require("../lib/codex-config");
22
22
  const {
23
23
  upsertClaudeHook,
24
+ upsertClaudeUsageHooks,
24
25
  buildClaudeHookCommand,
25
26
  buildHookCommand,
26
27
  isClaudeHookConfigured,
28
+ areClaudeUsageHooksConfigured,
27
29
  } = require("../lib/claude-config");
28
30
  const {
29
31
  resolveGeminiConfigDir,
@@ -418,6 +420,78 @@ async function repairCodexNotifyIntegration({ home = os.homedir(), trackerDir, b
418
420
  return { ...result, skippedReason: null, notifyPath };
419
421
  }
420
422
 
423
+ async function repairRuntimeIntegrations({
424
+ home = os.homedir(),
425
+ trackerDir,
426
+ binDir,
427
+ safeMode = true,
428
+ } = {}) {
429
+ const paths = trackerDir && binDir ? { trackerDir, binDir } : await resolveTrackerPaths({ home });
430
+ const resolvedTrackerDir = trackerDir || paths.trackerDir;
431
+ const resolvedBinDir = binDir || paths.binDir;
432
+ const notifyPath = await writeNotifyHandler({
433
+ trackerDir: resolvedTrackerDir,
434
+ binDir: resolvedBinDir,
435
+ });
436
+ const context = buildIntegrationTargets({
437
+ home,
438
+ trackerDir: resolvedTrackerDir,
439
+ notifyPath,
440
+ });
441
+ const integrations = {};
442
+ const warnings = [];
443
+
444
+ const attempt = async (key, work) => {
445
+ try {
446
+ integrations[key] = await work();
447
+ } catch (error) {
448
+ integrations[key] = { changed: false, skippedReason: "repair-failed" };
449
+ warnings.push({ integration: key, error: error?.message || String(error) });
450
+ }
451
+ };
452
+
453
+ await attempt("codex", () => repairCodexNotifyIntegration({
454
+ home,
455
+ trackerDir: resolvedTrackerDir,
456
+ binDir: resolvedBinDir,
457
+ safeMode,
458
+ }));
459
+
460
+ const hookRepairs = [
461
+ ["claude", context.claudeDir, () => upsertClaudeUsageHooks({
462
+ settingsPath: context.claudeSettingsPath,
463
+ hookCommand: context.claudeHookCommand,
464
+ })],
465
+ ["gemini", context.geminiConfigDir, () => upsertGeminiHook({
466
+ settingsPath: context.geminiSettingsPath,
467
+ hookCommand: context.geminiHookCommand,
468
+ })],
469
+ ["codebuddy", context.codebuddyDir, () => upsertClaudeHook({
470
+ settingsPath: context.codebuddySettingsPath,
471
+ hookCommand: context.codebuddyHookCommand,
472
+ })],
473
+ ["workbuddy", context.workbuddyDir, () => upsertClaudeHook({
474
+ settingsPath: context.workbuddySettingsPath,
475
+ hookCommand: context.workbuddyHookCommand,
476
+ })],
477
+ ];
478
+ for (const [key, configDir, repair] of hookRepairs) {
479
+ if (await isDir(configDir)) await attempt(key, repair);
480
+ else integrations[key] = { changed: false, skippedReason: "config-missing" };
481
+ }
482
+
483
+ if (await isDir(context.opencodeConfigDir)) {
484
+ await attempt("opencode", () => upsertOpencodePlugin({
485
+ configDir: context.opencodeConfigDir,
486
+ notifyPath,
487
+ }));
488
+ } else {
489
+ integrations.opencode = { changed: false, skippedReason: "config-missing" };
490
+ }
491
+
492
+ return { notifyPath, integrations, warnings };
493
+ }
494
+
421
495
  function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
422
496
  const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
423
497
  const codexConfigPath = path.join(codexHome, "config.toml");
@@ -495,7 +569,7 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
495
569
 
496
570
  const claudeDirExists = await isDir(context.claudeDir);
497
571
  if (claudeDirExists) {
498
- await upsertClaudeHook({
572
+ await upsertClaudeUsageHooks({
499
573
  settingsPath: context.claudeSettingsPath,
500
574
  hookCommand: context.claudeHookCommand,
501
575
  });
@@ -787,7 +861,7 @@ async function previewIntegrations({ context }) {
787
861
 
788
862
  const claudeDirExists = await isDir(context.claudeDir);
789
863
  if (claudeDirExists) {
790
- const configured = await isClaudeHookConfigured({
864
+ const configured = await areClaudeUsageHooksConfigured({
791
865
  settingsPath: context.claudeSettingsPath,
792
866
  hookCommand: context.claudeHookCommand,
793
867
  });
@@ -1071,7 +1145,6 @@ const codexOriginalPath = ${JSON.stringify(originalPath)};
1071
1145
  const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, "code_notify_original.json"))};
1072
1146
  const trackerBinPath = ${JSON.stringify(trackerBinPath)};
1073
1147
  const depsMarkerPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
1074
- const configPath = path.join(trackerDir, 'config.json');
1075
1148
  const fallbackPkg = ${JSON.stringify(fallbackPkg)};
1076
1149
  const selfPath = path.resolve(__filename);
1077
1150
  const home = os.homedir();
@@ -1109,18 +1182,10 @@ if (debugEnabled) {
1109
1182
  // Throttle spawn: at most once per 20 seconds.
1110
1183
  try {
1111
1184
  const throttlePath = path.join(trackerDir, 'sync.throttle');
1112
- let deviceToken = process.env.TOKENTRACKER_DEVICE_TOKEN || null;
1113
- if (!deviceToken) {
1114
- try {
1115
- const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1116
- if (cfg && typeof cfg.deviceToken === 'string') deviceToken = cfg.deviceToken;
1117
- } catch (_) {}
1118
- }
1119
- const canSync = Boolean(deviceToken && deviceToken.length > 0);
1120
1185
  const now = Date.now();
1121
1186
  let last = 0;
1122
1187
  try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
1123
- if (canSync && now - last > 20_000) {
1188
+ if (now - last > 20_000) {
1124
1189
  try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
1125
1190
  const hasLocalRuntime = fs.existsSync(trackerBinPath);
1126
1191
  const hasLocalDeps = fs.existsSync(depsMarkerPath);
@@ -1241,6 +1306,7 @@ module.exports = {
1241
1306
  buildNotifyHandler,
1242
1307
  installLocalTrackerApp,
1243
1308
  repairCodexNotifyIntegration,
1309
+ repairRuntimeIntegrations,
1244
1310
  };
1245
1311
 
1246
1312
  async function probeFile(p) {
@@ -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();