tokentracker-cli 1.1.4 → 1.1.6

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 (105) hide show
  1. package/README.md +15 -0
  2. package/dashboard/dist/assets/{AchievementBadge-BGA-HT_B.js → AchievementBadge-CpK5rOMf.js} +1 -1
  3. package/dashboard/dist/assets/{AchievementsPage-CtRh5AjH.js → AchievementsPage-BtcQkHZR.js} +2 -2
  4. package/dashboard/dist/assets/{AchievementsSection-BarRcKaN.js → AchievementsSection-BT50hdFh.js} +1 -1
  5. package/dashboard/dist/assets/{BadgeDetailModal-Bg0BDAN2.js → BadgeDetailModal-DFtz7FxR.js} +1 -1
  6. package/dashboard/dist/assets/{BotAnimated-By9J3-dn.js → BotAnimated-DuZSWD8p.js} +1 -1
  7. package/dashboard/dist/assets/{Card-CIoJYi2j.js → Card-BzGCR7t6.js} +1 -1
  8. package/dashboard/dist/assets/{ClawdAnimated-Dkvc2xe8.js → ClawdAnimated-tpie3z4W.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-BssDtHRl.js → CommandPalette-DxL_wTUY.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-CQrgLlDb.js → CommunityStatsModal-5FYbcq_Q.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-C3hBPgYc.js → ConfirmModal-Dd1z1ofU.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-BSW6a1HS.js → Controls-DlImhUxL.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-Dhv7fejy.js → DashboardPage-DIPzwCc4.js} +1 -1
  14. package/dashboard/dist/assets/{DevicePage-D5eZn0wo.js → DevicePage-CtdxXb6m.js} +1 -1
  15. package/dashboard/dist/assets/{DialogClose-BltQpaJo.js → DialogClose-FTnu24dV.js} +1 -1
  16. package/dashboard/dist/assets/DialogDescription-CqQpH51z.js +1 -0
  17. package/dashboard/dist/assets/{DialogTitle-CVddcRPZ.js → DialogTitle-C8_lCmoh.js} +1 -1
  18. package/dashboard/dist/assets/{FadeIn-goPv73MT.js → FadeIn-Cw9s9Vyn.js} +1 -1
  19. package/dashboard/dist/assets/{HeaderGithubStar-BDBrrkzt.js → HeaderGithubStar-C8GHdUnS.js} +1 -1
  20. package/dashboard/dist/assets/{HoverTooltip-qyMezXOs.js → HoverTooltip-BFgXXFLw.js} +1 -1
  21. package/dashboard/dist/assets/{IpCheckPage-Ces8JIPb.js → IpCheckPage-C62rs4Sf.js} +1 -1
  22. package/dashboard/dist/assets/{LandingPage-B2TmLGno.js → LandingPage-qa3wKk7Y.js} +2 -2
  23. package/dashboard/dist/assets/{LeaderboardAvatar-CVVRoNua.js → LeaderboardAvatar-BkznbeVC.js} +1 -1
  24. package/dashboard/dist/assets/{LeaderboardPage-xtt-bOY8.js → LeaderboardPage-DTNaWWUP.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardProfileModal-BM4d_I_r.js → LeaderboardProfileModal-DkP4Wttv.js} +3 -3
  26. package/dashboard/dist/assets/{LeaderboardProfilePage-BWqhFLQX.js → LeaderboardProfilePage-C7K3PNq3.js} +1 -1
  27. package/dashboard/dist/assets/{LimitsPage-BZL4Pcl8.js → LimitsPage-CdQfWCxG.js} +1 -1
  28. package/dashboard/dist/assets/{LocalOnlyNotice-D4gnOEJj.js → LocalOnlyNotice-ClvY1_wG.js} +1 -1
  29. package/dashboard/dist/assets/{LoginCard-CB17FQ5w.js → LoginCard-B1kZLJZd.js} +1 -1
  30. package/dashboard/dist/assets/{LoginModal-BI-O1rH4.js → LoginModal-GiYCVrbT.js} +1 -1
  31. package/dashboard/dist/assets/{LoginPage-DnGzsvJ4.js → LoginPage-INlKWFTQ.js} +1 -1
  32. package/dashboard/dist/assets/{PetPage-NRnIKMaJ.js → PetPage-uq1reCKp.js} +1 -1
  33. package/dashboard/dist/assets/{PopoverPopup-DHoK6UJZ.js → PopoverPopup-Dc5Z66VI.js} +1 -1
  34. package/dashboard/dist/assets/{ProviderIcon-BTkdx9o2.js → ProviderIcon-Qh2N0LTI.js} +1 -1
  35. package/dashboard/dist/assets/{ResetPasswordPage-C6K0w318.js → ResetPasswordPage-BdlpShC8.js} +1 -1
  36. package/dashboard/dist/assets/{SegmentedControl-gEoWioBE.js → SegmentedControl-DCmwiJYf.js} +1 -1
  37. package/dashboard/dist/assets/{Select-CI-qtEYu.js → Select-BIMHJc2D.js} +1 -1
  38. package/dashboard/dist/assets/{SelectItemText-q-yhzzjw.js → SelectItemText-CMkxqxvY.js} +1 -1
  39. package/dashboard/dist/assets/{ServiceStatusPage-C4xD1CEe.js → ServiceStatusPage-C1ll8UfE.js} +1 -1
  40. package/dashboard/dist/assets/{SessionsPage-CS0Yun2z.js → SessionsPage-DVnwnAWa.js} +1 -1
  41. package/dashboard/dist/assets/{SettingsPage-GVY_ktz4.js → SettingsPage-CGs5NN_Z.js} +2 -2
  42. package/dashboard/dist/assets/{SkillsPage-COPRL95O.js → SkillsPage-5hrIvbe1.js} +1 -1
  43. package/dashboard/dist/assets/{TokenGalaxy-Czi0Ic48.js → TokenGalaxy-BfD0rv-M.js} +1 -1
  44. package/dashboard/dist/assets/{ToolbarRootContext-BZkTiBJB.js → ToolbarRootContext-BlMLA5Ez.js} +1 -1
  45. package/dashboard/dist/assets/{TrendMonitor-CnIsuxV-.js → TrendMonitor-DnJCeEQS.js} +1 -1
  46. package/dashboard/dist/assets/{WidgetsPage-OdX_Fsul.js → WidgetsPage-DsKnuAcr.js} +1 -1
  47. package/dashboard/dist/assets/{WrappedPage-h9N63-F8.js → WrappedPage-CXwtk-hY.js} +1 -1
  48. package/dashboard/dist/assets/{__vite-browser-external-BFNWaGsk.js → __vite-browser-external-cuNuS_b-.js} +2 -2
  49. package/dashboard/dist/assets/{agent-logos-BDiPxrST.js → agent-logos-DxLnGJgJ.js} +1 -1
  50. package/dashboard/dist/assets/{arrow-up-right-DtFQUeJG.js → arrow-up-right-Cj7u2wr7.js} +1 -1
  51. package/dashboard/dist/assets/{check-C35onaNL.js → check-DNFOvbbk.js} +1 -1
  52. package/dashboard/dist/assets/{composite-qxBzts0W.js → composite-DQR0hWa5.js} +1 -1
  53. package/dashboard/dist/assets/{copy-D_D71TLc.js → copy-D0SHZEpt.js} +1 -1
  54. package/dashboard/dist/assets/{download-DMQI2hQe.js → download-UlivGLVm.js} +1 -1
  55. package/dashboard/dist/assets/{external-link-CemM4dWf.js → external-link-rdZCNL_v.js} +1 -1
  56. package/dashboard/dist/assets/{flame-CIvk0SP5.js → flame-qfy_Ty_R.js} +1 -1
  57. package/dashboard/dist/assets/{format-tokens-D3JSvcFl.js → format-tokens-DU-AJn2b.js} +1 -1
  58. package/dashboard/dist/assets/{icons-Ccf5W0AL.js → icons-CZ7P_cMG.js} +1 -1
  59. package/dashboard/dist/assets/{index-Brd2evyz.js → index-CXnOAYdM.js} +1 -1
  60. package/dashboard/dist/assets/{index-C8Wri_HI.js → index-DDfiGiEo.js} +1 -1
  61. package/dashboard/dist/assets/{index-CFXmD_KX.js → index-DKnHC0bC.js} +1 -1
  62. package/dashboard/dist/assets/{info-CafrKEG8.js → info-BY3dZuIN.js} +1 -1
  63. package/dashboard/dist/assets/{link-2-Ds-9Nzlp.js → link-2-CSTcMOFn.js} +1 -1
  64. package/dashboard/dist/assets/{loader-circle-DA4I55-V.js → loader-circle-yIM6ih7L.js} +1 -1
  65. package/dashboard/dist/assets/{maximize-2-CjMPt5kU.js → maximize-2-C1P3TlYj.js} +1 -1
  66. package/dashboard/dist/assets/{modifiers.esm-DIxTS09h.js → modifiers.esm-BoQpSulH.js} +1 -1
  67. package/dashboard/dist/assets/{monitor-smartphone-IZpIOxn3.js → monitor-smartphone-DafIVSH1.js} +1 -1
  68. package/dashboard/dist/assets/{plus-vbLFNs70.js → plus-B4l58vrs.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-BAu_UIfV.js → provider-display-CN_xz_OA.js} +1 -1
  70. package/dashboard/dist/assets/{react-Djb4VGKb.js → react-B9dzohvC.js} +1 -1
  71. package/dashboard/dist/assets/{search-Ct6LY9Ke.js → search-B8ejosPU.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-BnGI2gEp.js → skills-api-BLC6ZUle.js} +1 -1
  73. package/dashboard/dist/assets/{sortable.esm-CjCGd6g5.js → sortable.esm-BLUZnPyk.js} +1 -1
  74. package/dashboard/dist/assets/{store-CBxkOqRe.js → store-3B91_nUE.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-Dlpdp5Az.js → terminal-DccsqdRD.js} +1 -1
  76. package/dashboard/dist/assets/{trash-pxUSKv66.js → trash-DfkcSCtq.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-tHaixRIw.js → use-community-stats-CuIZHVoQ.js} +1 -1
  78. package/dashboard/dist/assets/{use-latest-request-guard-Cs44WPfL.js → use-latest-request-guard-B_oV9ZfF.js} +1 -1
  79. package/dashboard/dist/assets/{use-limits-display-prefs-Dw56PXWy.js → use-limits-display-prefs-CXj8GHMY.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-DpLx8oLV.js → use-ordered-list-dAFHiuLi.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-UMqqLkcq.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-BN3FtBvr.js → use-session-efficiency-pref-B9Q45jnR.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-C4nTGgzb.js → use-transform-CWR6dZvr.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-Dmwdc1yo.js → use-usage-limits-BmwSEzJh.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-DSc3A_Ih.js → useCurrency-BjHTP-xy.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-BHjcMgm8.js → useTokenFormat-DMOiOFlN.js} +1 -1
  87. package/dashboard/dist/assets/{zap-BE8i_wu2.js → zap-v0h1PWbB.js} +1 -1
  88. package/dashboard/dist/index.html +1 -1
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/share.html +1 -1
  92. package/package.json +1 -1
  93. package/src/commands/serve.js +147 -17
  94. package/src/commands/status.js +57 -2
  95. package/src/commands/sync.js +90 -11
  96. package/src/lib/codex-context-breakdown.js +3 -1
  97. package/src/lib/diagnostics.js +20 -1
  98. package/src/lib/doctor.js +30 -0
  99. package/src/lib/local-api.js +6 -2
  100. package/src/lib/pricing/seed-snapshot.json +1 -1
  101. package/src/lib/scan-roots.js +277 -0
  102. package/src/lib/session-analytics.js +98 -16
  103. package/src/lib/usage-limits.js +27 -1
  104. package/dashboard/dist/assets/DialogDescription-Cof52Icj.js +0 -1
  105. package/dashboard/dist/assets/use-reduced-motion-DrlJ9X6p.js +0 -1
@@ -116,6 +116,13 @@ const {
116
116
  const wsl = require("../lib/wsl-probe");
117
117
  const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
118
118
  const { resolveInstallPaths, resolveZcodeNativeDbPath, resolveMimoNativeDbPath } = require("../lib/install-resolver");
119
+ const {
120
+ describeScanRootOrigin,
121
+ describeScanRootState,
122
+ hasAnyScanChild,
123
+ resolveEnvRoot,
124
+ resolveScanRoots,
125
+ } = require("../lib/scan-roots");
119
126
  const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
120
127
  const { probeOmpHookState } = require("../lib/omp-hook");
121
128
 
@@ -197,7 +204,9 @@ async function cmdStatus(argv = []) {
197
204
  const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
198
205
  const autoRetryPath = path.join(trackerDir, "auto.retry.json");
199
206
  const syncSkipPath = path.join(trackerDir, "sync.skip.json");
200
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
207
+ // Normalized like sync (a relative CODEX_HOME is anchored to home, not cwd)
208
+ // so status lists exactly the root sync walks.
209
+ const codexHome = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex");
201
210
  const codexConfigPath = path.join(codexHome, "config.toml");
202
211
  const acodeHome = process.env.TOKENTRACKER_ACODE_HOME || path.join(home, ".acode");
203
212
  const acodeConfigPath = path.join(acodeHome, "config.toml");
@@ -229,6 +238,33 @@ async function cmdStatus(argv = []) {
229
238
  const geminiHookCommand = buildGeminiHookCommand(notifyPath);
230
239
 
231
240
  const config = await readJson(configPath);
241
+ // Extra scan roots (#657): CODEX_HOME / CLAUDE_CONFIG_DIR of this process
242
+ // plus config.scanRoots, resolved exactly as sync does, so status lists every
243
+ // root sync walks — this is the output users paste when usage looks wrong.
244
+ const scanRoots = resolveScanRoots({
245
+ home,
246
+ env: process.env,
247
+ config,
248
+ base: { codex: [codexHome], claude: [path.join(home, ".claude")] },
249
+ });
250
+ const extraScanRoots = [];
251
+ for (const provider of ["codex", "claude"]) {
252
+ for (const entry of scanRoots[provider]) {
253
+ if (entry.origin === "native") continue;
254
+ extraScanRoots.push({
255
+ provider,
256
+ origin: describeScanRootOrigin(entry, provider),
257
+ path: entry.path,
258
+ exists: entry.exists,
259
+ error: entry.error || null,
260
+ });
261
+ }
262
+ }
263
+ const describeExtraScanRoot = (root) =>
264
+ `${root.provider} ${root.origin}: ${root.path}${describeScanRootState(root)}`;
265
+ const scanRootsLine = extraScanRoots.length > 0
266
+ ? `- Extra scan roots: ${extraScanRoots.map(describeExtraScanRoot).join(" | ")}`
267
+ : null;
232
268
  const { cursors } = await readCursorStateSummary({ trackerDir, cursorsPath });
233
269
  const codexRecordOnlyWarning = formatRecordOnlyWarning(countRecordOnlyFiles(cursors));
234
270
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
@@ -419,6 +455,10 @@ async function cmdStatus(argv = []) {
419
455
  ? wsl.discoverWslHome(".claude")
420
456
  : null;
421
457
  if (wslClaudeHomeStatus) claudeHomesStatus.push({ dir: wslClaudeHomeStatus, label: "WSL" });
458
+ for (const entry of scanRoots.claude) {
459
+ if (entry.origin === "native" || !entry.exists) continue;
460
+ claudeHomesStatus.push({ dir: entry.path, label: describeScanRootOrigin(entry, "claude") });
461
+ }
422
462
  for (const { dir, label } of claudeHomesStatus) {
423
463
  const projects = path.join(dir, "projects");
424
464
  try {
@@ -657,7 +697,7 @@ async function cmdStatus(argv = []) {
657
697
  // users are asked to paste when Codex usage looks wrong, so it must list
658
698
  // every root sync actually walks — and no empty shell sync would skip.
659
699
  const codexPaths = resolveInstallPaths({
660
- nativeValue: process.env.CODEX_HOME || path.join(home, ".codex"),
700
+ nativeValue: codexHome,
661
701
  wslDir: ".codex",
662
702
  requireAnyChild: ["sessions", "archived_sessions"],
663
703
  union: true,
@@ -665,6 +705,11 @@ async function cmdStatus(argv = []) {
665
705
  // Both children, matching requireAnyChild above: an install holding only
666
706
  // archived_sessions/ is counted by sync and must not read as "not detected".
667
707
  const codexActive = formatResolvedPaths(codexPaths, ["sessions", "archived_sessions"]);
708
+ for (const entry of scanRoots.codex) {
709
+ if (entry.origin === "native" || !entry.exists) continue;
710
+ if (!hasAnyScanChild(entry.path, ["sessions", "archived_sessions"])) continue;
711
+ codexActive.push(`${describeScanRootOrigin(entry, "codex")}: ${entry.path}`);
712
+ }
668
713
  const codexInstalledStatus = codexActive.length > 0;
669
714
 
670
715
  const acodePaths = resolveInstallPaths({
@@ -983,6 +1028,10 @@ async function cmdStatus(argv = []) {
983
1028
  last_upload_error: lastUploadError || null,
984
1029
  last_sync_skipped: syncSkip?.at ? syncSkip : null,
985
1030
  auto_retry: autoRetry || null,
1031
+ // Extra scan roots (#657): CODEX_HOME / CLAUDE_CONFIG_DIR of this process
1032
+ // plus config.scanRoots; `exists` false + `error` null means absent,
1033
+ // `error` set means present but unreadable.
1034
+ extra_scan_roots: extraScanRoots,
986
1035
  hooks: {
987
1036
  codex_notify: notifyConfigured,
988
1037
  acode_notify: acodeConfigured,
@@ -1198,6 +1247,7 @@ async function cmdStatus(argv = []) {
1198
1247
  lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
1199
1248
  syncSkipLine,
1200
1249
  autoRetryLine,
1250
+ scanRootsLine,
1201
1251
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : "unset"}`,
1202
1252
  `- AStudio notify: ${acodeConfigured ? JSON.stringify(acodeNotify) : "unset"}`,
1203
1253
  `- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : "unset"}`,
@@ -1525,6 +1575,11 @@ function renderLightTable(summary) {
1525
1575
  push(`Hook · ${name}`, state ? "set" : "unset");
1526
1576
  }
1527
1577
 
1578
+ for (const root of summary.extra_scan_roots || []) {
1579
+ const state = root.exists ? "" : (root.error ? ` (unreadable: ${root.error})` : " (missing)");
1580
+ push(`Scan root · ${root.provider}`, `${root.origin}: ${root.path}${state}`);
1581
+ }
1582
+
1528
1583
  for (const [name, info] of Object.entries(summary.providers || {})) {
1529
1584
  const detail = [];
1530
1585
  if (typeof info.installed === "boolean") detail.push(info.installed ? "installed" : "not installed");
@@ -172,6 +172,14 @@ const {
172
172
  openCursorStore,
173
173
  } = require("../lib/cursor-store");
174
174
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
175
+ const {
176
+ appendUniqueDirs,
177
+ extraScanRootPaths,
178
+ hasAnyScanChild,
179
+ resolveEnvRoot,
180
+ resolveScanRoots,
181
+ scanRootDirState,
182
+ } = require("../lib/scan-roots");
175
183
  const { resolveRuntimeConfig, isLegacyInsforgeBaseUrl } = require("../lib/runtime-config");
176
184
  const { extractTokenCount } = require("../lib/codex-rollout-parser");
177
185
  const {
@@ -601,7 +609,24 @@ async function cmdSync(argv, context = {}) {
601
609
  persistedAnonKey: config.anonKey,
602
610
  };
603
611
  }
604
- const codexCursorRoots = [process.env.CODEX_HOME || path.join(home, ".codex")];
612
+ // Scan roots (#657). Every producer — hook-fired sync, native background
613
+ // refresh, CLI — must derive the SAME root list, and the cursor store's
614
+ // codexRoots must come from that list: a rollout under a root the store
615
+ // does not know is filed in core.json instead of its per-day shard, so two
616
+ // producers with different roots never see each other's cursor and re-parse
617
+ // the file from byte 0 on every alternation (#639). CODEX_HOME keeps its
618
+ // existing meaning (replaces ~/.codex for this process) but is normalized
619
+ // once by resolveEnvRoot — a relative value is anchored to home, not cwd —
620
+ // and that single value feeds discovery AND the cursor store below.
621
+ // config.scanRoots adds roots for every process.
622
+ const codexNativeValue = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex");
623
+ const scanRoots = resolveScanRoots({
624
+ home,
625
+ env: process.env,
626
+ config,
627
+ base: { codex: [codexNativeValue], claude: [path.join(home, ".claude")] },
628
+ });
629
+ const codexCursorRoots = scanRoots.codex.map((entry) => entry.path);
605
630
  const cursorStore = await openCursorStore({
606
631
  trackerDir,
607
632
  cursorsPath,
@@ -643,7 +668,16 @@ async function cmdSync(argv, context = {}) {
643
668
  claudeInstallHomes.push(claudeNativeHome);
644
669
  }
645
670
  if (wslClaudeHome) claudeInstallHomes.push(wslClaudeHome);
646
- const claudeProjectsDirs = claudeInstallHomes.map((h) => path.join(h, "projects"));
671
+ // Extra Claude roots (#657): CLAUDE_CONFIG_DIR of the spawning process and
672
+ // config.scanRoots.claude, additive to the homes above so coverage does not
673
+ // depend on which process spawned this sync. Deduped by realpath at the
674
+ // projects/ level: two profiles may symlink one projects/ dir, and reading
675
+ // it under both spellings would double-parse every file, leaving
676
+ // correctness to the bounded claudeHashes layer.
677
+ const claudeProjectsDirs = appendUniqueDirs(
678
+ claudeInstallHomes.map((h) => path.join(h, "projects")),
679
+ extraScanRootPaths(scanRoots.claude).map((h) => path.join(h, "projects")),
680
+ );
647
681
  const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
648
682
  const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
649
683
 
@@ -700,7 +734,6 @@ async function cmdSync(argv, context = {}) {
700
734
 
701
735
  const sources = [];
702
736
  if (sourceAllowed("codex")) {
703
- const codexNativeValue = process.env.CODEX_HOME || path.join(home, ".codex");
704
737
  // resolveInstallPaths stays the single authority for wsl-first /
705
738
  // native-first / wsl-only / native-only / both selection; requireAnyChild
706
739
  // makes it validate that a candidate actually holds sessions/ or
@@ -740,6 +773,17 @@ async function cmdSync(argv, context = {}) {
740
773
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
741
774
  }
742
775
  }
776
+ // Extra Codex roots (#657) from config.scanRoots.codex. Same populated-
777
+ // root rule as requireAnyChild above so an empty shell dir is not walked;
778
+ // the day-inventory cache is keyed by day directory, so sharing it across
779
+ // roots is safe.
780
+ for (const extraRoot of extraScanRootPaths(scanRoots.codex)) {
781
+ if (!hasAnyScanChild(extraRoot, ["sessions", "archived_sessions"])) continue;
782
+ sources.push({ source: "codex", sessionsDir: path.join(extraRoot, "sessions"), inventoryCacheKey: "codexDayInventoryCache" });
783
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
784
+ sources.push({ source: "codex", sessionsDir: path.join(extraRoot, "archived_sessions"), deep: true });
785
+ }
786
+ }
743
787
  }
744
788
  if (sourceAllowed("acode")) {
745
789
  const acodeNativeValue =
@@ -1067,14 +1111,49 @@ async function cmdSync(argv, context = {}) {
1067
1111
  }
1068
1112
  if (isFullSourceScan) {
1069
1113
  await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath });
1070
- await repairClaudeQueueFromGroundTruth({
1071
- cursors,
1072
- queuePath,
1073
- queueStatePath,
1074
- projectQueuePath,
1075
- projectQueueStatePath,
1076
- rootDirs: claudeProjectsDirs,
1077
- });
1114
+ // The ground-truth repair rebuilds every Claude queue row from the roots
1115
+ // it is given. A configured root that is absent or unreadable right now
1116
+ // (unmounted volume, permissions) may still hold history that an earlier
1117
+ // scoped sync queued, so rebuilding without it would erase that history.
1118
+ // The same applies one level down: listClaudeProjectFiles turns a read
1119
+ // error on projects/ into an empty listing, so an unreadable projects/
1120
+ // would let the repair run against nothing and mark itself complete. An
1121
+ // ABSENT projects/ is fine for a NEW root (a profile with no sessions
1122
+ // yet) but not for one the cursor store shows has supplied files before:
1123
+ // then the directory vanished and rebuilding without it would erase its
1124
+ // history. Defer in both cases: the migration key stays unset and the
1125
+ // repair runs on a later full scan once the root is back. Ordinary
1126
+ // scanning still proceeds.
1127
+ const cursorFilePaths = Object.keys(cursors.files || {});
1128
+ const rootPreviouslySuppliedFiles = (rootPath) => {
1129
+ const prefix = path.join(rootPath, "projects") + path.sep;
1130
+ return cursorFilePaths.some((filePath) => filePath.startsWith(prefix));
1131
+ };
1132
+ const unavailableClaudeRoots = scanRoots.claude
1133
+ .filter((entry) => {
1134
+ if (entry.origin === "native") return false;
1135
+ if (!entry.exists) return true;
1136
+ const projectsState = scanRootDirState(path.join(entry.path, "projects"));
1137
+ if (projectsState.error !== null) return true;
1138
+ return !projectsState.exists && rootPreviouslySuppliedFiles(entry.path);
1139
+ })
1140
+ .map((entry) => entry.path);
1141
+ if (unavailableClaudeRoots.length > 0) {
1142
+ if (!opts.auto) {
1143
+ process.stderr.write(
1144
+ `Claude ground-truth repair deferred: configured scan root(s) unavailable: ${unavailableClaudeRoots.join(", ")}\n`,
1145
+ );
1146
+ }
1147
+ } else {
1148
+ await repairClaudeQueueFromGroundTruth({
1149
+ cursors,
1150
+ queuePath,
1151
+ queueStatePath,
1152
+ projectQueuePath,
1153
+ projectQueueStatePath,
1154
+ rootDirs: claudeProjectsDirs,
1155
+ });
1156
+ }
1078
1157
  }
1079
1158
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1080
1159
  if (claudeFiles.length > 0) {
@@ -15,6 +15,8 @@ const fs = require("node:fs");
15
15
  const os = require("node:os");
16
16
  const path = require("node:path");
17
17
 
18
+ const { resolveEnvRoot } = require("./scan-roots");
19
+
18
20
  const {
19
21
  emptyTotals,
20
22
  addInto,
@@ -396,7 +398,7 @@ async function computeCodexContextBreakdown({
396
398
  toKey = range?.to || null;
397
399
  }
398
400
 
399
- const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
401
+ const codexHome = resolveEnvRoot("codex", { env: process.env, home: os.homedir() }) || path.join(os.homedir(), ".codex");
400
402
  const roots = codexDir
401
403
  ? [codexDir]
402
404
  : [path.join(codexHome, "sessions"), path.join(codexHome, "archived_sessions")];
@@ -3,6 +3,7 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { readJson } = require("./fs");
6
+ const { resolveEnvRoot, resolveScanRoots } = require("./scan-roots");
6
7
  const { readCursorStateSummary } = require("./cursor-store");
7
8
  const { readCodexNotify, readEveryCodeNotify } = require("./codex-config");
8
9
  const { areClaudeUsageHooksConfigured, buildClaudeHookCommand } = require("./claude-config");
@@ -66,7 +67,7 @@ function resolveKiroCliDbPathInline(env, home) {
66
67
 
67
68
  async function collectTrackerDiagnostics({
68
69
  home = os.homedir(),
69
- codexHome = process.env.CODEX_HOME || path.join(home, ".codex"),
70
+ codexHome = resolveEnvRoot("codex", { env: process.env, home }) || path.join(home, ".codex"),
70
71
  codeHome = process.env.CODE_HOME || path.join(home, ".code"),
71
72
  } = {}) {
72
73
  const { trackerDir, binDir } = await resolveTrackerPaths({ home });
@@ -92,6 +93,20 @@ async function collectTrackerDiagnostics({
92
93
  path.join(home, ".grok");
93
94
 
94
95
  const config = await readJson(configPath);
96
+ // Extra scan roots (#657), resolved as sync does. Paths are home-redacted
97
+ // like every other entry in `paths`.
98
+ const scanRoots = resolveScanRoots({
99
+ home,
100
+ env: process.env,
101
+ config,
102
+ base: { codex: [codexHome], claude: [path.join(home, ".claude")] },
103
+ });
104
+ const describeScanRoots = (entries) => entries.map((entry) => ({
105
+ path: redactValue(entry.path, home),
106
+ origin: entry.origin,
107
+ exists: entry.exists,
108
+ error: entry.error || null,
109
+ }));
95
110
  const cursorSummary = await readCursorStateSummary({ trackerDir, cursorsPath });
96
111
  const cursors = cursorSummary.cursors;
97
112
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
@@ -217,6 +232,10 @@ async function collectTrackerDiagnostics({
217
232
  merge_policy:
218
233
  "Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost, heatmap, and leaderboard aggregations merge transparently. Use this block to distinguish sub-path contributions.",
219
234
  },
235
+ scan_roots: {
236
+ codex: describeScanRoots(scanRoots.codex),
237
+ claude: describeScanRoots(scanRoots.claude),
238
+ },
220
239
  config: {
221
240
  base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
222
241
  device_token: config?.deviceToken ? "set" : "unset",
package/src/lib/doctor.js CHANGED
@@ -337,6 +337,36 @@ function buildDiagnosticsChecks(diagnostics) {
337
337
  });
338
338
  }
339
339
 
340
+ // Extra scan roots (#657): surface CODEX_HOME / CLAUDE_CONFIG_DIR / config
341
+ // roots in the human-readable report and warn when a configured root is not
342
+ // on disk — a silently missing root is the difference between "no usage"
343
+ // and "not scanned". Installs without extras get no additional line.
344
+ const scanRoots = diagnostics?.scan_roots;
345
+ if (scanRoots && typeof scanRoots === "object") {
346
+ const extras = [];
347
+ for (const provider of ["codex", "claude"]) {
348
+ for (const entry of Array.isArray(scanRoots[provider]) ? scanRoots[provider] : []) {
349
+ if (!entry || entry.origin === "native") continue;
350
+ extras.push({ provider, ...entry });
351
+ }
352
+ }
353
+ if (extras.length > 0) {
354
+ const missing = extras.filter((entry) => !entry.exists);
355
+ const describe = (list) => list
356
+ .map((entry) => `${entry.provider} ${entry.path}${entry.error ? ` (unreadable: ${entry.error})` : ""}`)
357
+ .join(", ");
358
+ checks.push({
359
+ id: "scan_roots.extra",
360
+ status: missing.length > 0 ? "warn" : "ok",
361
+ detail: missing.length > 0
362
+ ? `${missing.length} of ${extras.length} extra scan root(s) unavailable: ${describe(missing)}`
363
+ : `${extras.length} extra scan root(s) present: ${describe(extras)}`,
364
+ critical: false,
365
+ meta: { roots: extras },
366
+ });
367
+ }
368
+ }
369
+
340
370
  return checks;
341
371
  }
342
372
 
@@ -1131,8 +1131,9 @@ function buildProxyHeaders(headers) {
1131
1131
  // Main handler factory
1132
1132
  // ---------------------------------------------------------------------------
1133
1133
 
1134
- function createLocalApiHandler({ queuePath }) {
1134
+ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1135
1135
  const qp = queuePath || resolveQueuePath();
1136
+ const normalizedServerVersion = typeof serverVersion === "string" ? serverVersion.trim() : "";
1136
1137
 
1137
1138
  // Server-side cookie relay: captures auth cookies from InsForge cloud responses
1138
1139
  // so that both browser and WKWebView share the same login session via the proxy.
@@ -1581,7 +1582,10 @@ function createLocalApiHandler({ queuePath }) {
1581
1582
  "Content-Type": "application/json",
1582
1583
  "Cache-Control": "no-store",
1583
1584
  });
1584
- res.end(JSON.stringify({ token: localAuthToken }));
1585
+ res.end(JSON.stringify({
1586
+ token: localAuthToken,
1587
+ ...(normalizedServerVersion ? { serverVersion: normalizedServerVersion } : {}),
1588
+ }));
1585
1589
  return true;
1586
1590
  }
1587
1591