tokentracker-cli 0.84.9 → 0.86.0

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 (109) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +8 -7
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-BgBFpxmU.js → AchievementBadge-BgbH_mX4.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DHPD3Fnu.js → AchievementsPage-CQqkh1JS.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DouWVD-3.js → AchievementsSection-Dmkxpn0s.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Df9eQiA3.js → BadgeDetailModal-7OQf37lk.js} +1 -1
  10. package/dashboard/dist/assets/{Card-CQ9wBnz6.js → Card-1PmWZRTK.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-XIO-I-5v.js → ClawdAnimated-CMJFagpX.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-Cy1cJ91B.js → CommandPalette-D4OrwF1w.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-CwKG4LQM.js → CommunityStatsModal-BDC9EcwZ.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-BNcWqick.js → ConfirmModal-22DL5C_9.js} +1 -1
  15. package/dashboard/dist/assets/DashboardPage-CEXWzDtt.js +1 -0
  16. package/dashboard/dist/assets/{DevicePage-pJ2jNkZV.js → DevicePage-8uhHB7kX.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-C2SOtkJN.js → DialogClose-BcJx6VK-.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-D6zxtOM5.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-CGSbB8G7.js → DialogTitle-gRzunAqv.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CZSiw7me.js → FadeIn-4rlPdBVe.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-ZmQhX_kR.js → HeaderGithubStar-CR003aO3.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-D_YdG369.js → HoverTooltip-Dgz-dM8Q.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-du886dF0.js → IpCheckPage-B4PJRJP_.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-BuAak8lU.js → LandingPage-3JQe-jOZ.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardAvatar-nU0ARYnN.js → LeaderboardAvatar-CL9LxiEf.js} +1 -1
  26. package/dashboard/dist/assets/{LeaderboardPage-CngwCvC7.js → LeaderboardPage-DhIzM2Z2.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-CXZJ0e-T.js → LeaderboardProfileModal-bbf5uJR5.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CZve6s6b.js → LeaderboardProfilePage-CzuPxscw.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-CWvoJPk8.js → LimitsPage-B59rb2gg.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DyUAAyyE.js → LocalOnlyNotice-BkK-6n1R.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-eEiRfbdv.js → LoginCard-BFGCzEjE.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-C16WdIQm.js → LoginModal-yhkyscFn.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-B2WqpE1t.js → LoginPage-DwU5s0F7.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DtQWrGmI.js → PetPage-BPscYR6z.js} +1 -1
  35. package/dashboard/dist/assets/{PopoverPopup-CRhqtFLB.js → PopoverPopup-E7PyGTTB.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-DWNH_wYu.js → ProviderIcon-CvmnzwKU.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DzCczcWZ.js → ResetPasswordPage-DD6qX616.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-CV9ccq6R.js → SegmentedControl-DjGBL_l1.js} +1 -1
  39. package/dashboard/dist/assets/{Select-BrogNNk0.js → Select-B2MGmsi2.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-Bpu3swt0.js → SelectItemText-BaTwA_ZF.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-D_FIrMJP.js → ServiceStatusPage-0K0y8A6m.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-C8TvTy1o.js → SessionsPage-CxrtRm3B.js} +2 -2
  43. package/dashboard/dist/assets/{SettingsPage-Bd-tzJAR.js → SettingsPage-CyvSWvDP.js} +1 -1
  44. package/dashboard/dist/assets/{SkillsPage-CKGU9wQ4.js → SkillsPage-BVOU8t3t.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DhHW6L-U.js → TokenGalaxy-w7bwGloC.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-DQo-4pPB.js → ToolbarRootContext-BAzpDZej.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-Ejl8d45g.js → TrendMonitor-DyT7Q4xp.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-CY7f4wCD.js → WidgetsPage-eDDtopQT.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-B4-B2v-v.js → WrappedPage-CKkbdWHF.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-BNUFXCfp.js → __vite-browser-external-SU8sCTLB.js} +23 -16
  51. package/dashboard/dist/assets/{agent-logos-BRzutmeR.js → agent-logos-bkh9XZJd.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-up-right-BcrbPk8c.js → arrow-up-right-D16zCIw8.js} +1 -1
  53. package/dashboard/dist/assets/{check-BMxeFPfQ.js → check-BFL17JWQ.js} +1 -1
  54. package/dashboard/dist/assets/{chevron-right-DOstgiRf.js → chevron-right-CUJgrtkp.js} +1 -1
  55. package/dashboard/dist/assets/{copy-BsKO524t.js → copy-CRQiCjgs.js} +1 -1
  56. package/dashboard/dist/assets/{download-dadqxej7.js → download-Cd12nDrQ.js} +1 -1
  57. package/dashboard/dist/assets/{external-link-CTu3BMeU.js → external-link-CvAfnsG-.js} +1 -1
  58. package/dashboard/dist/assets/{flame-B_IvWcMn.js → flame-CCi681Yl.js} +1 -1
  59. package/dashboard/dist/assets/{format-tokens-DIT6hLSb.js → format-tokens-BAVt8uTZ.js} +1 -1
  60. package/dashboard/dist/assets/{icons-mWku76bu.js → icons-CbKC96gR.js} +1 -1
  61. package/dashboard/dist/assets/{index-D5-w-aGy.js → index--TDvaL1y.js} +1 -1
  62. package/dashboard/dist/assets/{index-D-gQV-hF.js → index-B5b6PCh5.js} +1 -1
  63. package/dashboard/dist/assets/{index-CboN9E82.js → index-B9LWn-mo.js} +1 -1
  64. package/dashboard/dist/assets/{info-x6l352SW.js → info-BPFpD--M.js} +1 -1
  65. package/dashboard/dist/assets/{limits-providers-Bo12VlLJ.js → limits-providers-Cyedt-WJ.js} +1 -1
  66. package/dashboard/dist/assets/{link-2-eo_xPxH1.js → link-2-BkEPPdfC.js} +1 -1
  67. package/dashboard/dist/assets/{loader-circle-C4n1iGBv.js → loader-circle-CzASiylJ.js} +1 -1
  68. package/dashboard/dist/assets/{maximize-2-pR8xwsI_.js → maximize-2-CkwnHUJn.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-ErggQWu4.js → provider-display-BXBBedC-.js} +2 -2
  70. package/dashboard/dist/assets/{react-DYKg02Aq.js → react-BXSctDQj.js} +1 -1
  71. package/dashboard/dist/assets/{search-B_jbUwPS.js → search-_qGma9tM.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-DY5ruGmw.js → skills-api-DdmJjnJ9.js} +1 -1
  73. package/dashboard/dist/assets/{store-Cu8i1y26.js → store-owYCN530.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-D3j6X68s.js → terminal-Clzw-KEF.js} +1 -1
  75. package/dashboard/dist/assets/{trash-2-CFpwgJw5.js → trash-2-C5Giwp_E.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-C0fKq66G.js → use-community-stats-Dk8UjLEr.js} +1 -1
  77. package/dashboard/dist/assets/{use-limits-display-prefs-TOp4UAnI.js → use-limits-display-prefs-CArM595Q.js} +1 -1
  78. package/dashboard/dist/assets/{use-native-settings-BZie_7z1.js → use-native-settings-Cpd69kiF.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-Bq92YyUp.js → use-ordered-list--CzwhBTx.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-CcILnpU1.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-BcpytkES.js → use-session-efficiency-pref-DUXO8cCg.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-Dmmrne2z.js → use-transform-Cg3kuMj_.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-Cw0Z9Tew.js → use-usage-limits-DY9_mV28.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-DyyonINj.js → useCurrency-BIe86hMn.js} +1 -1
  85. package/dashboard/dist/assets/{useScrollLock-DvWw86lJ.js → useScrollLock-7-hejkNC.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-CBkvGfJ1.js → useTokenFormat-DEtLbRgQ.js} +1 -1
  87. package/dashboard/dist/assets/{zap-CnYA39Ug.js → zap-DUxY1d-O.js} +1 -1
  88. package/dashboard/dist/index.html +12 -10
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/llms.txt +3 -2
  92. package/dashboard/dist/share.html +1 -1
  93. package/package.json +2 -2
  94. package/src/commands/init.js +1 -0
  95. package/src/commands/status.js +129 -9
  96. package/src/commands/sync.js +265 -54
  97. package/src/lib/claude-categorizer.js +36 -9
  98. package/src/lib/diagnostics.js +86 -13
  99. package/src/lib/doctor.js +17 -0
  100. package/src/lib/grok-context-breakdown.js +794 -0
  101. package/src/lib/install-resolver.js +10 -2
  102. package/src/lib/local-api.js +21 -3
  103. package/src/lib/multi-install-parser.js +9 -1
  104. package/src/lib/pricing/seed-snapshot.json +1 -1
  105. package/src/lib/rollout.js +788 -44
  106. package/src/lib/session-analytics.js +332 -9
  107. package/dashboard/dist/assets/DashboardPage-Dns_1yXL.js +0 -1
  108. package/dashboard/dist/assets/DialogDescription-Bhfqqmll.js +0 -1
  109. package/dashboard/dist/assets/use-reduced-motion-ZiVeOkvb.js +0 -1
@@ -48,6 +48,9 @@ const {
48
48
  resolveKimiCodeWireFiles,
49
49
  resolveKiroCliDbPath,
50
50
  resolveKiroCliSessionFiles,
51
+ resolveKiroBasePath,
52
+ resolveKiroDbPath,
53
+ resolveKiroJsonlPath,
51
54
  resolveCodebuddyHome,
52
55
  resolveCodebuddyProjectFiles,
53
56
  resolveWorkbuddyHome,
@@ -63,6 +66,7 @@ const {
63
66
  resolveRoocodeTaskFiles,
64
67
  resolveZedDbPath,
65
68
  resolveQoderDbPaths,
69
+ resolveClaudeScienceDbPaths,
66
70
  resolveAnythingllmDbPath,
67
71
  resolveGooseDbPath,
68
72
  listDroidSettingsFiles,
@@ -235,10 +239,100 @@ async function cmdStatus(argv = []) {
235
239
  // event sessions. End-user dashboards show them merged under "Kiro"; this
236
240
  // status line surfaces which passive sources are actually present.
237
241
  const kiroCliDbPath = resolveKiroCliDbPath(process.env);
238
- const kiroCliDbExists = fssync.existsSync(kiroCliDbPath);
239
242
  const kiroCliSessionFiles = resolveKiroCliSessionFiles(process.env);
240
- const kiroCliInstalled =
241
- kiroCliDbExists || kiroCliSessionFiles.length > 0;
243
+ const kiroCliNativePresent =
244
+ fssync.existsSync(kiroCliDbPath) || kiroCliSessionFiles.length > 0;
245
+ // WSL install discovery mirrors sync: overrides pin a single install.
246
+ let kiroCliWsl = null; // { db, sessionFiles }
247
+ if (
248
+ process.platform === "win32" &&
249
+ !process.env.KIRO_CLI_DB_PATH && !process.env.KIRO_HOME &&
250
+ wsl.shouldProbeWsl(process.env)
251
+ ) {
252
+ const wslKiroHomeDir = wsl.discoverWslHome(".kiro");
253
+ const wslCliDataDir = wsl.discoverWslHome(".local/share/kiro-cli");
254
+ const wslHomeRoot = wslKiroHomeDir
255
+ ? path.dirname(wslKiroHomeDir)
256
+ : (wslCliDataDir ? path.dirname(path.dirname(path.dirname(wslCliDataDir))) : null);
257
+ if (wslHomeRoot) {
258
+ const wslDb = path.join(wslHomeRoot, ".local", "share", "kiro-cli", "data.sqlite3");
259
+ const wslFiles = resolveKiroCliSessionFiles({
260
+ ...process.env,
261
+ KIRO_CLI_DB_PATH: wslDb,
262
+ KIRO_HOME: path.join(wslHomeRoot, ".kiro"),
263
+ });
264
+ if (fssync.existsSync(wslDb) || wslFiles.length > 0) {
265
+ kiroCliWsl = { db: wslDb, sessionFiles: wslFiles };
266
+ }
267
+ }
268
+ }
269
+ const kiroCliPaths = process.platform === "win32"
270
+ ? wsl.resolveAllWin32Paths({
271
+ nativeValue: kiroCliNativePresent ? kiroCliDbPath : null,
272
+ wslValue: kiroCliWsl ? kiroCliWsl.db : null,
273
+ env: process.env,
274
+ platform: "win32",
275
+ })
276
+ : { native: kiroCliNativePresent ? kiroCliDbPath : null, wsl: null };
277
+ // Non-both modes park the picked value in the native slot — label by
278
+ // marker identity, not slot name. A session-files-only install has no DB
279
+ // on disk; show the sessions dir instead of a nonexistent DB path.
280
+ const kiroCliMarkers = [kiroCliPaths.native, kiroCliPaths.wsl].filter(Boolean);
281
+ const kiroCliActive = kiroCliMarkers.map((m) => {
282
+ if (kiroCliWsl && m === kiroCliWsl.db) {
283
+ const shown = fssync.existsSync(m) || kiroCliWsl.sessionFiles.length === 0
284
+ ? m
285
+ : path.dirname(kiroCliWsl.sessionFiles[0]);
286
+ return `WSL: ${shown}`;
287
+ }
288
+ const shown = fssync.existsSync(m) || kiroCliSessionFiles.length === 0
289
+ ? m
290
+ : path.dirname(kiroCliSessionFiles[0]);
291
+ return `native: ${shown}`;
292
+ });
293
+ const kiroCliFileCount =
294
+ (kiroCliMarkers.includes(kiroCliDbPath) ? kiroCliSessionFiles.length : 0) +
295
+ (kiroCliWsl && kiroCliMarkers.includes(kiroCliWsl.db) ? kiroCliWsl.sessionFiles.length : 0);
296
+ const kiroCliDbFound = kiroCliMarkers.some((m) => {
297
+ try { return fssync.existsSync(m); } catch (_e) { return false; }
298
+ });
299
+ const kiroCliInstalled = kiroCliMarkers.length > 0;
300
+
301
+ // Kiro IDE — passive scan of globalStorage dev_data (SQLite or JSONL).
302
+ const kiroIdeNativeBase = resolveKiroBasePath(process.env);
303
+ const wslKiroIdeBase = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
304
+ ? wsl.discoverWslHome(".config/Kiro/User/globalStorage/kiro.kiroagent")
305
+ : null;
306
+ const kiroIdePaths = resolveInstallPaths({ nativeValue: kiroIdeNativeBase, wslValue: wslKiroIdeBase });
307
+ const kiroIdeHasData = (base) => Boolean(base)
308
+ && (fssync.existsSync(resolveKiroDbPath(base)) || fssync.existsSync(resolveKiroJsonlPath(base)));
309
+ const kiroIdeActive = [kiroIdePaths.native, kiroIdePaths.wsl]
310
+ .filter((base) => kiroIdeHasData(base))
311
+ .map((base) => (wslKiroIdeBase && base === wslKiroIdeBase ? `WSL: ${base}` : `native: ${base}`));
312
+ const kiroIdeInstalled = kiroIdeActive.length > 0;
313
+
314
+ // Claude Code — dual-install aware projects scan (#307). Mirrors sync:
315
+ // installs are UNIONED (not single-picked by mode) so a WSL ~/.claude
316
+ // never hides the native one from view, and labels come from install
317
+ // identity rather than resolveAllWin32Paths' slot names.
318
+ const claudeCodeActive = [];
319
+ {
320
+ const claudeHomesStatus = [];
321
+ if (process.platform !== "win32" || wsl.shouldProbeNative(process.env)) {
322
+ claudeHomesStatus.push({ dir: path.join(home, ".claude"), label: "native" });
323
+ }
324
+ const wslClaudeHomeStatus = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
325
+ ? wsl.discoverWslHome(".claude")
326
+ : null;
327
+ if (wslClaudeHomeStatus) claudeHomesStatus.push({ dir: wslClaudeHomeStatus, label: "WSL" });
328
+ for (const { dir, label } of claudeHomesStatus) {
329
+ const projects = path.join(dir, "projects");
330
+ try {
331
+ if (fssync.existsSync(projects)) claudeCodeActive.push(`${label}: ${projects}`);
332
+ } catch (_e) {}
333
+ }
334
+ }
335
+ const claudeCodeInstalled = claudeCodeActive.length > 0;
242
336
 
243
337
  // AnythingLLM Desktop — per-message token metrics in workspace_chats.
244
338
  const anythingllmDbPath = resolveAnythingllmDbPath(process.env);
@@ -332,6 +426,14 @@ async function cmdStatus(argv = []) {
332
426
  const qoderInstalled = qoderActive.length > 0;
333
427
  const qoderDbPath = qoderActive.join(" | ");
334
428
 
429
+ // Claude Science — token usage lives on the `frames` table of operon-cli.db.
430
+ // Unlike the native/WSL pair other providers resolve to, this is an open-ended
431
+ // list: multi-org installs keep one DB per org (and on Windows they all sit
432
+ // inside WSL), so the resolver already returns only paths that exist.
433
+ const claudeScienceActive = resolveClaudeScienceDbPaths({ home, env: process.env });
434
+ const claudeScienceInstalled = claudeScienceActive.length > 0;
435
+ const claudeScienceDbPath = claudeScienceActive.join(" | ");
436
+
335
437
  // OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
336
438
  const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
337
439
  const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
@@ -650,13 +752,19 @@ async function cmdStatus(argv = []) {
650
752
  kiro_cli: kiroCliInstalled
651
753
  ? {
652
754
  installed: true,
653
- detail: kiroCliDbExists
654
- ? kiroCliDbPath
655
- : path.dirname(kiroCliSessionFiles[0]),
656
- database: kiroCliDbExists ? kiroCliDbPath : null,
657
- files: kiroCliSessionFiles.length,
755
+ detail: kiroCliActive.join(" | "),
756
+ database: kiroCliMarkers.find((m) => {
757
+ try { return fssync.existsSync(m); } catch (_e) { return false; }
758
+ }) || null,
759
+ files: kiroCliFileCount,
658
760
  }
659
761
  : { installed: false },
762
+ kiro_ide: kiroIdeInstalled
763
+ ? { installed: true, detail: kiroIdeActive.join(" | ") }
764
+ : { installed: false },
765
+ claude_code: claudeCodeInstalled
766
+ ? { installed: true, detail: claudeCodeActive.join(" | ") }
767
+ : { installed: false },
660
768
  codebuddy: codebuddyInstalled
661
769
  ? { installed: true, files: codebuddyFiles.length }
662
770
  : { installed: false },
@@ -687,6 +795,9 @@ async function cmdStatus(argv = []) {
687
795
  qoder: qoderInstalled
688
796
  ? { installed: true, detail: qoderDbPath }
689
797
  : { installed: false },
798
+ "claude-science": claudeScienceInstalled
799
+ ? { installed: true, detail: claudeScienceDbPath }
800
+ : { installed: false },
690
801
  kilocode: kilocodeInstalled
691
802
  ? { installed: true, files: kilocodeTaskFiles.length }
692
803
  : { installed: false },
@@ -761,6 +872,9 @@ async function cmdStatus(argv = []) {
761
872
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : "unset"}`,
762
873
  `- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : "unset"}`,
763
874
  `- Claude hooks: ${claudeHookConfigured ? "set" : "unset"}`,
875
+ claudeCodeInstalled
876
+ ? `- Claude Code: projects found (${claudeCodeActive.join(" | ")})`
877
+ : null,
764
878
  `- Gemini hooks: ${geminiHookConfigured ? "set" : "unset"}`,
765
879
  `- Opencode plugin: ${opencodePluginConfigured ? "set" : "unset"}`,
766
880
  `- OpenClaw session plugin: ${openclawSessionPluginState?.configured ? "set" : "unset"}`,
@@ -770,7 +884,10 @@ async function cmdStatus(argv = []) {
770
884
  ? `- Kimi Code: passive reader (${kimiWireFiles.length + kimiCodeWireFiles.length} wire.jsonl file${(kimiWireFiles.length + kimiCodeWireFiles.length) !== 1 ? "s" : ""} found, directories: ${kimiActive.join(" | ") || "none"})`
771
885
  : null,
772
886
  kiroCliInstalled
773
- ? `- Kiro CLI: passive reader (${kiroCliSessionFiles.length} session file${kiroCliSessionFiles.length !== 1 ? "s" : ""} found, SQLite ${kiroCliDbExists ? "found" : "not found"}; tokens approximated from char lengths and merged under 'kiro')`
887
+ ? `- Kiro CLI: passive reader (${kiroCliFileCount} session file${kiroCliFileCount !== 1 ? "s" : ""} found, SQLite ${kiroCliDbFound ? "found" : "not found"}, installs: ${kiroCliActive.join(" | ")}; tokens approximated from char lengths and merged under 'kiro')`
888
+ : null,
889
+ kiroIdeInstalled
890
+ ? `- Kiro IDE: passive reader (${kiroIdeActive.join(" | ")})`
774
891
  : null,
775
892
  codebuddyInstalled
776
893
  ? `- CodeBuddy hooks: ${codebuddyHookConfigured ? "set" : "unset"} (${codebuddyFiles.length} usage file${codebuddyFiles.length !== 1 ? "s" : ""} found)`
@@ -802,6 +919,9 @@ async function cmdStatus(argv = []) {
802
919
  qoderInstalled
803
920
  ? `- Qoder: passive reader (${qoderDbPath})`
804
921
  : null,
922
+ claudeScienceInstalled
923
+ ? `- Claude Science: passive reader (${claudeScienceDbPath})`
924
+ : null,
805
925
  opencodeInstalled
806
926
  ? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
807
927
  : null,
@@ -24,6 +24,7 @@ const {
24
24
  readQoderDbMessages,
25
25
  resolveKiroDbPath,
26
26
  resolveKiroJsonlPath,
27
+ resolveKiroBasePath,
27
28
  resolveHermesPath,
28
29
  resolveCopilotOtelPaths,
29
30
  normalizeCopilotDbPath,
@@ -39,12 +40,20 @@ const {
39
40
  parseOpencodeDbIncremental,
40
41
  parseQoderDbIncremental,
41
42
  parseOpenclawIncremental,
43
+ resolveOpenclawSessionFiles,
44
+ resolveOpenclawHome,
45
+ openclawCursorKey,
46
+ resolveClaudeScienceDbPaths,
47
+ readClaudeScienceFrames,
48
+ parseClaudeScienceIncremental,
42
49
  parseCursorApiIncremental,
43
50
  parseKiroIncremental,
44
51
  parseHermesIncremental,
45
52
  gooseInstallOwnsCursor,
46
53
  zedInstallOwnsCursor,
47
54
  hermesInstallOwnsCursor,
55
+ kiroInstallOwnsCursor,
56
+ kiroCliInstallOwnsCursor,
48
57
  copilotOtelCursorHasLegacyCliUsage,
49
58
  pruneCopilotUsageClaims,
50
59
  parseCopilotIncremental,
@@ -234,6 +243,7 @@ const AUTO_SYNC_SOURCES = new Set([
234
243
  "antigravity",
235
244
  "anythingllm",
236
245
  "claude",
246
+ "claude-science",
237
247
  "codebuddy",
238
248
  "codex",
239
249
  "copilot",
@@ -443,7 +453,25 @@ async function cmdSync(argv, context = {}) {
443
453
  }
444
454
  let grokHookSignalConsumed = false;
445
455
 
446
- const claudeProjectsDir = path.join(home, ".claude", "projects");
456
+ // Claude Code home dual-install aware (#307): a Windows host may carry
457
+ // a native ~/.claude plus a WSL install reachable over \\wsl$. Unlike the
458
+ // SQLite providers, Claude UNIONS every allowed install instead of taking
459
+ // resolveAllWin32Paths' single pick: under the default wsl-first mode a
460
+ // WSL ~/.claude would otherwise silently evict the native one from
461
+ // collection — and from the "disk is truth" ground-truth repair below,
462
+ // which would then erase the native history. The file-hash and
463
+ // message-hash dedup layers make the union safe. Native is listed first
464
+ // so the cross-environment file dedup keeps the native copy as primary.
465
+ const claudeNativeHome = path.join(home, ".claude");
466
+ const wslClaudeHome = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
467
+ ? wsl.discoverWslHome(".claude")
468
+ : null;
469
+ const claudeInstallHomes = [];
470
+ if (process.platform !== "win32" || wsl.shouldProbeNative(process.env)) {
471
+ claudeInstallHomes.push(claudeNativeHome);
472
+ }
473
+ if (wslClaudeHome) claudeInstallHomes.push(wslClaudeHome);
474
+ const claudeProjectsDirs = claudeInstallHomes.map((h) => path.join(h, "projects"));
447
475
  const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
448
476
  const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
449
477
  const mimoHome = process.env.MIMO_HOME || path.join(xdgDataHome, "mimocode");
@@ -452,7 +480,7 @@ async function cmdSync(argv, context = {}) {
452
480
  // OpenClaw session plugin integration: lifecycle hooks request an
453
481
  // OpenClaw-only auto sync so unrelated providers do not get walked.
454
482
  const openclawSignal = opts.fromOpenclaw
455
- ? resolveOpenclawSignal({ home, env: process.env })
483
+ ? resolveOpenclawSignal({ env: process.env })
456
484
  : null;
457
485
 
458
486
  const autoSourceScope = resolveAutoSourceScope(opts);
@@ -647,9 +675,38 @@ async function cmdSync(argv, context = {}) {
647
675
  codexCursorLoadRestarted = Boolean(loadResult?.restarted);
648
676
  }
649
677
 
650
- const openclawFiles = openclawSignal?.sessionFile
651
- ? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
652
- : [];
678
+ // Plugin-triggered sync points at one specific session file; a normal full
679
+ // sync also passively scans every on-disk OpenClaw transcript so usage is
680
+ // captured even when the session plugin never fires (issue #264). The scan
681
+ // is gated to full syncs so a scoped `--from-openclaw` hook still only
682
+ // touches its own file and the 5-minute background tick stays cheap. The
683
+ // event-identity dedup makes the plugin and passive paths idempotent, so
684
+ // overlap on the same file is safe.
685
+ //
686
+ // Keyed by openclawCursorKey, not the raw path: that is the same identity
687
+ // the parser assigns cursors by, so a plugin-supplied path and a scanned
688
+ // path that differ only in Windows casing collapse to one entry instead of
689
+ // being parsed twice.
690
+ const openclawSessionFiles = new Map();
691
+ if (openclawSignal?.sessionFile) {
692
+ openclawSessionFiles.set(openclawCursorKey(openclawSignal.sessionFile), {
693
+ path: openclawSignal.sessionFile,
694
+ source: "openclaw",
695
+ });
696
+ }
697
+ if (isFullSourceScan && sourceAllowed("openclaw")) {
698
+ try {
699
+ for (const f of await resolveOpenclawSessionFiles(process.env)) {
700
+ const key = openclawCursorKey(f);
701
+ if (!openclawSessionFiles.has(key)) {
702
+ openclawSessionFiles.set(key, { path: f, source: "openclaw" });
703
+ }
704
+ }
705
+ } catch (err) {
706
+ warnProviderParseFailure("OpenClaw", err, opts);
707
+ }
708
+ }
709
+ const openclawFiles = Array.from(openclawSessionFiles.values());
653
710
 
654
711
  if (progress?.enabled) {
655
712
  progress.start(
@@ -719,7 +776,7 @@ async function cmdSync(argv, context = {}) {
719
776
 
720
777
  let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
721
778
  if (sourceAllowed("openclaw") && openclawFiles.length > 0) {
722
- // Only runs when explicitly triggered by the OpenClaw session plugin.
779
+ // Parses plugin-triggered and/or passively discovered session files.
723
780
  try {
724
781
  openclawResult = await parseOpenclawIncremental({
725
782
  sessionFiles: openclawFiles,
@@ -751,7 +808,18 @@ async function cmdSync(argv, context = {}) {
751
808
  openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
752
809
  openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
753
810
 
754
- const claudeFiles = sourceAllowed("claude") ? await listClaudeProjectFiles(claudeProjectsDir) : [];
811
+ let claudeFiles = [];
812
+ if (sourceAllowed("claude")) {
813
+ const seenClaudeFiles = new Set();
814
+ for (const dir of claudeProjectsDirs) {
815
+ for (const f of await listClaudeProjectFiles(dir)) {
816
+ if (!seenClaudeFiles.has(f)) {
817
+ seenClaudeFiles.add(f);
818
+ claudeFiles.push(f);
819
+ }
820
+ }
821
+ }
822
+ }
755
823
  if (isFullSourceScan) {
756
824
  await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath, queueStatePath });
757
825
  await repairClaudeQueueFromGroundTruth({
@@ -760,6 +828,7 @@ async function cmdSync(argv, context = {}) {
760
828
  queueStatePath,
761
829
  projectQueuePath,
762
830
  projectQueueStatePath,
831
+ rootDirs: claudeProjectsDirs,
763
832
  });
764
833
  }
765
834
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -1022,6 +1091,45 @@ async function cmdSync(argv, context = {}) {
1022
1091
  }
1023
1092
  }
1024
1093
 
1094
+ // ── Claude Science (Anthropic's local research workbench, issue #246) ──
1095
+ // Per-frame token usage lives on the `frames` table of operon-cli.db.
1096
+ // Passive, incremental, subtract-on-change. There can be more than one DB:
1097
+ // multi-org installs keep one per org, and on Windows the app runs inside
1098
+ // WSL so the DB is on the distro home.
1099
+ let claudeScienceResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1100
+ if (sourceAllowed("claude-science")) {
1101
+ // Resolution is inside the error isolation too: it touches the filesystem
1102
+ // and (on Windows) probes WSL, and a throw here would abort cmdSync before
1103
+ // any provider's buckets are written, not just this one's.
1104
+ let claudeScienceDbPaths = [];
1105
+ try {
1106
+ claudeScienceDbPaths = resolveClaudeScienceDbPaths({ home, env: process.env });
1107
+ } catch (err) {
1108
+ warnProviderParseFailure("Claude Science", err, opts);
1109
+ }
1110
+ if (claudeScienceDbPaths.length > 0 && progress?.enabled) {
1111
+ progress.start(`Parsing Claude Science ${renderBar(0)} | buckets 0`);
1112
+ }
1113
+ for (const claudeScienceDbPath of claudeScienceDbPaths) {
1114
+ try {
1115
+ const dbRows = await readClaudeScienceFrames(claudeScienceDbPath);
1116
+ const parsed = await parseClaudeScienceIncremental({
1117
+ dbRows,
1118
+ cursors,
1119
+ queuePath,
1120
+ onProgress: makeProviderProgress("Claude Science"),
1121
+ });
1122
+ claudeScienceResult = mergeParseResult(claudeScienceResult, {
1123
+ recordsProcessed: parsed.recordsProcessed || 0,
1124
+ eventsAggregated: parsed.eventsAggregated || 0,
1125
+ bucketsQueued: parsed.bucketsQueued || 0,
1126
+ });
1127
+ } catch (err) {
1128
+ warnProviderParseFailure("Claude Science", err, opts);
1129
+ }
1130
+ }
1131
+ }
1132
+
1025
1133
  async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
1026
1134
  if (!dbPath || !fssync.existsSync(dbPath)) {
1027
1135
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -1344,30 +1452,44 @@ async function cmdSync(argv, context = {}) {
1344
1452
  }
1345
1453
  }
1346
1454
 
1347
- // ── Kiro (SQLite-based, with JSONL fallback) ──
1455
+ // ── Kiro (SQLite-based, with JSONL fallback; dual-install aware) ──
1348
1456
  let kiroResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1349
- const kiroDbPath = resolveKiroDbPath();
1350
- const kiroJsonlPath = resolveKiroJsonlPath();
1351
- if (sourceAllowed("kiro") && (fssync.existsSync(kiroDbPath) || fssync.existsSync(kiroJsonlPath))) {
1352
- if (progress?.enabled) {
1353
- progress.start(`Parsing Kiro ${renderBar(0)} | buckets 0`);
1354
- }
1355
- try {
1356
- kiroResult = await parseKiroIncremental({
1357
- dbPath: kiroDbPath,
1358
- jsonlPath: kiroJsonlPath,
1359
- cursors,
1360
- queuePath,
1361
- onProgress: (p) => {
1362
- if (!progress?.enabled) return;
1363
- const pct = p.total > 0 ? p.index / p.total : 1;
1364
- progress.update(
1365
- `Parsing Kiro ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} records | buckets ${formatNumber(p.bucketsQueued)}`,
1366
- );
1367
- },
1368
- });
1369
- } catch (err) {
1370
- warnProviderParseFailure("Kiro", err, opts);
1457
+ if (sourceAllowed("kiro")) {
1458
+ const kiroNativeBase = resolveKiroBasePath(process.env);
1459
+ const wslKiroBase = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
1460
+ ? wsl.discoverWslHome(".config/Kiro/User/globalStorage/kiro.kiroagent")
1461
+ : null;
1462
+ const kiroPaths = resolveInstallPaths({ nativeValue: kiroNativeBase, wslValue: wslKiroBase });
1463
+ // resolveInstallPaths only checks the base dir (and skips the check off
1464
+ // win32); keep the original per-install db/jsonl presence gate so empty
1465
+ // installs never spin up a parse or seed a cursor namespace.
1466
+ const kiroHasData = (base) => Boolean(base)
1467
+ && (fssync.existsSync(resolveKiroDbPath(base)) || fssync.existsSync(resolveKiroJsonlPath(base)));
1468
+ if (!kiroHasData(kiroPaths.native)) kiroPaths.native = null;
1469
+ if (!kiroHasData(kiroPaths.wsl)) kiroPaths.wsl = null;
1470
+ if (kiroPaths.native || kiroPaths.wsl) {
1471
+ if (progress?.enabled) {
1472
+ progress.start(`Parsing Kiro ${renderBar(0)} | buckets 0`);
1473
+ }
1474
+ try {
1475
+ kiroResult = await multiInstallParse({
1476
+ paths: kiroPaths,
1477
+ parserFn: parseKiroIncremental,
1478
+ providerName: "kiro",
1479
+ cursors,
1480
+ getParams: (base) => ({
1481
+ basePath: base,
1482
+ dbPath: resolveKiroDbPath(base),
1483
+ jsonlPath: resolveKiroJsonlPath(base),
1484
+ }),
1485
+ queuePath,
1486
+ onProgress: makeProviderProgress("Kiro"),
1487
+ detectInstall: (base, flatState) =>
1488
+ kiroInstallOwnsCursor(resolveKiroDbPath(base), flatState),
1489
+ });
1490
+ } catch (err) {
1491
+ warnProviderParseFailure("Kiro", err, opts);
1492
+ }
1371
1493
  }
1372
1494
  }
1373
1495
 
@@ -1454,28 +1576,79 @@ async function cmdSync(argv, context = {}) {
1454
1576
  // token counts (billing is credit-based on Bedrock); we approximate at
1455
1577
  // 4 chars/token from user prompt chars and assistant response chars.
1456
1578
  let kiroCliResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1457
- const kiroCliDb = resolveKiroCliDbPath(process.env);
1458
- const kiroCliSessionFiles = sourceAllowed("kiro") ? resolveKiroCliSessionFiles(process.env) : [];
1459
- if (sourceAllowed("kiro") && (fssync.existsSync(kiroCliDb) || kiroCliSessionFiles.length > 0)) {
1460
- if (progress?.enabled) {
1461
- progress.start(`Parsing Kiro CLI ${renderBar(0)} | buckets 0`);
1579
+ if (sourceAllowed("kiro")) {
1580
+ const kiroCliDb = resolveKiroCliDbPath(process.env);
1581
+ const kiroCliSessionFiles = resolveKiroCliSessionFiles(process.env);
1582
+ const nativeCliPresent = fssync.existsSync(kiroCliDb) || kiroCliSessionFiles.length > 0;
1583
+
1584
+ // Explicit overrides pin a single install — never mix them with WSL
1585
+ // auto-discovery (mirrors the Hermes TOKENTRACKER_HERMES_HOME branch).
1586
+ const kiroCliOverride = Boolean(process.env.KIRO_CLI_DB_PATH || process.env.KIRO_HOME);
1587
+ let wslKiroCliEnv = null;
1588
+ let wslKiroCliMarker = null;
1589
+ if (!kiroCliOverride && process.platform === "win32" && wsl.shouldProbeWsl(process.env)) {
1590
+ // A WSL install owns BOTH a data dir (~/.local/share/kiro-cli) and a
1591
+ // sessions home (~/.kiro). Derive both from whichever probe hits so
1592
+ // the per-install env never falls back to native paths.
1593
+ const wslKiroHomeDir = wsl.discoverWslHome(".kiro");
1594
+ const wslCliDataDir = wsl.discoverWslHome(".local/share/kiro-cli");
1595
+ const wslHomeRoot = wslKiroHomeDir
1596
+ ? path.dirname(wslKiroHomeDir)
1597
+ : (wslCliDataDir ? path.dirname(path.dirname(path.dirname(wslCliDataDir))) : null);
1598
+ if (wslHomeRoot) {
1599
+ const wslCliDb = path.join(wslHomeRoot, ".local", "share", "kiro-cli", "data.sqlite3");
1600
+ wslKiroCliEnv = {
1601
+ ...process.env,
1602
+ KIRO_CLI_DB_PATH: wslCliDb,
1603
+ KIRO_HOME: path.join(wslHomeRoot, ".kiro"),
1604
+ };
1605
+ const wslCliPresent = fssync.existsSync(wslCliDb)
1606
+ || resolveKiroCliSessionFiles(wslKiroCliEnv).length > 0;
1607
+ if (wslCliPresent) wslKiroCliMarker = wslCliDb;
1608
+ }
1462
1609
  }
1463
- try {
1464
- kiroCliResult = await parseKiroCliIncremental({
1465
- cursors,
1466
- queuePath,
1610
+
1611
+ // Paths here are install markers only — the parser resolves its DB and
1612
+ // session files from the per-install env (KIRO_CLI_DB_PATH/KIRO_HOME).
1613
+ const kiroCliPaths = process.platform === "win32"
1614
+ ? wsl.resolveAllWin32Paths({
1615
+ nativeValue: nativeCliPresent ? kiroCliDb : null,
1616
+ wslValue: wslKiroCliMarker,
1467
1617
  env: process.env,
1468
- onProgress: (p) => {
1469
- if (!progress?.enabled) return;
1470
- const pct = p.total > 0 ? p.index / p.total : 1;
1471
- progress.update(
1472
- `Parsing Kiro CLI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
1473
- );
1474
- },
1475
- });
1476
- } catch (err) {
1477
- if (!opts.auto) {
1478
- process.stderr.write(`Kiro CLI sync: ${err.message}\n`);
1618
+ platform: "win32",
1619
+ })
1620
+ : { native: nativeCliPresent ? kiroCliDb : null, wsl: null };
1621
+ const kiroCliEnvFor = (p) =>
1622
+ wslKiroCliEnv && p === wslKiroCliMarker ? wslKiroCliEnv : process.env;
1623
+ if (kiroCliPaths.native || kiroCliPaths.wsl) {
1624
+ if (progress?.enabled) {
1625
+ progress.start(`Parsing Kiro CLI ${renderBar(0)} | buckets 0`);
1626
+ }
1627
+ try {
1628
+ kiroCliResult = await multiInstallParse({
1629
+ paths: kiroCliPaths,
1630
+ parserFn: parseKiroCliIncremental,
1631
+ providerName: "kiroCli",
1632
+ cursors,
1633
+ // Per-install env MUST come from getParams: multiInstallParse
1634
+ // spreads shared params over it, so a top-level env would clobber
1635
+ // the per-install one.
1636
+ getParams: (p) => ({ env: kiroCliEnvFor(p) }),
1637
+ queuePath,
1638
+ onProgress: (p) => {
1639
+ if (!progress?.enabled) return;
1640
+ const pct = p.total > 0 ? p.index / p.total : 1;
1641
+ progress.update(
1642
+ `Parsing Kiro CLI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
1643
+ );
1644
+ },
1645
+ detectInstall: (p, flatState) =>
1646
+ kiroCliInstallOwnsCursor(kiroCliEnvFor(p).KIRO_CLI_DB_PATH || kiroCliDb, flatState),
1647
+ });
1648
+ } catch (err) {
1649
+ if (!opts.auto) {
1650
+ process.stderr.write(`Kiro CLI sync: ${err.message}\n`);
1651
+ }
1479
1652
  }
1480
1653
  }
1481
1654
  }
@@ -2117,6 +2290,7 @@ async function cmdSync(argv, context = {}) {
2117
2290
  antigravityResult.filesProcessed +
2118
2291
  opencodeResult.filesProcessed +
2119
2292
  qoderResult.recordsProcessed +
2293
+ claudeScienceResult.recordsProcessed +
2120
2294
  cursorResult.recordsProcessed +
2121
2295
  kiroResult.recordsProcessed +
2122
2296
  kiroCliResult.recordsProcessed +
@@ -2147,6 +2321,7 @@ async function cmdSync(argv, context = {}) {
2147
2321
  antigravityResult.bucketsQueued +
2148
2322
  opencodeResult.bucketsQueued +
2149
2323
  qoderResult.bucketsQueued +
2324
+ claudeScienceResult.bucketsQueued +
2150
2325
  cursorResult.bucketsQueued +
2151
2326
  kiroResult.bucketsQueued +
2152
2327
  kiroCliResult.bucketsQueued +
@@ -2488,15 +2663,18 @@ function normalizeString(value) {
2488
2663
  return trimmed.length > 0 ? trimmed : null;
2489
2664
  }
2490
2665
 
2491
- function resolveOpenclawSignal({ home, env } = {}) {
2666
+ function resolveOpenclawSignal({ env } = {}) {
2492
2667
  if (!env) return null;
2493
2668
 
2494
2669
  const agentId = normalizeString(env.TOKENTRACKER_OPENCLAW_AGENT_ID);
2495
2670
  const sessionId = normalizeString(env.TOKENTRACKER_OPENCLAW_PREV_SESSION_ID);
2496
2671
  if (!agentId || !sessionId) return null;
2497
2672
 
2498
- const openclawHome =
2499
- normalizeString(env.TOKENTRACKER_OPENCLAW_HOME) || path.join(home || os.homedir(), ".openclaw");
2673
+ // Resolve the OpenClaw home identically to the passive scanner so the
2674
+ // plugin-triggered file and passively discovered files share one path
2675
+ // spelling — otherwise the same transcript could get two cursors and be
2676
+ // counted twice (issue #264 review).
2677
+ const openclawHome = resolveOpenclawHome(env);
2500
2678
  const sessionFile = path.join(openclawHome, "agents", agentId, "sessions", `${sessionId}.jsonl`);
2501
2679
 
2502
2680
  const prevTotals = {
@@ -2529,6 +2707,16 @@ async function applyOpenclawTotalsFallback({
2529
2707
  return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2530
2708
  }
2531
2709
 
2710
+ // The passive/plugin transcript parse is authoritative: if this session's
2711
+ // transcript has ever yielded real per-event usage, synthesizing sessions.json
2712
+ // totals on top would count the same tokens twice (issue #264 review). Defer
2713
+ // to the real events and advance the fallback baseline so no stale delta lingers.
2714
+ const transcriptCursor =
2715
+ signal.sessionFile && cursors?.files
2716
+ ? cursors.files[openclawCursorKey(signal.sessionFile)]
2717
+ : null;
2718
+ const transcriptHasRealUsage = Boolean(transcriptCursor?.hasRealUsage);
2719
+
2532
2720
  const sessionKey = `${signal.agentId}:${signal.sessionId}`;
2533
2721
  const statePath = path.join(trackerDir, "openclaw.fallback.state.json");
2534
2722
  const fallbackFilePath = path.join(trackerDir, "openclaw.fallback.jsonl");
@@ -2537,6 +2725,22 @@ async function applyOpenclawTotalsFallback({
2537
2725
  const prev =
2538
2726
  sessions[sessionKey] && typeof sessions[sessionKey] === "object" ? sessions[sessionKey] : null;
2539
2727
 
2728
+ if (transcriptHasRealUsage) {
2729
+ sessions[sessionKey] = {
2730
+ totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
2731
+ inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
2732
+ outputTokens: normalizeNonNegativeInt(signal?.prevTotals?.outputTokens) || 0,
2733
+ model: normalizeString(signal?.prevTotals?.model) || "unknown",
2734
+ updatedAt: normalizeIsoOrEpoch(signal?.prevTotals?.updatedAt) || new Date().toISOString(),
2735
+ seenAt: new Date().toISOString(),
2736
+ coveredByEvents: true,
2737
+ };
2738
+ state.version = 1;
2739
+ state.sessions = sessions;
2740
+ await writeJson(statePath, state);
2741
+ return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2742
+ }
2743
+
2540
2744
  const current = {
2541
2745
  totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
2542
2746
  inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
@@ -4688,6 +4892,7 @@ async function repairClaudeQueueFromGroundTruth({
4688
4892
  queueStatePath = null,
4689
4893
  projectQueuePath = null,
4690
4894
  projectQueueStatePath = null,
4895
+ rootDirs = null,
4691
4896
  }) {
4692
4897
  if (!cursors || typeof cursors !== "object") return false;
4693
4898
  const migrations = (cursors.migrations ||= {});
@@ -4695,7 +4900,13 @@ async function repairClaudeQueueFromGroundTruth({
4695
4900
 
4696
4901
  let result;
4697
4902
  try {
4698
- result = await computeClaudeGroundTruthBuckets();
4903
+ // rootDirs must cover every install the incremental parser scans (native
4904
+ // AND WSL): this repair's semantics are "disk is truth" — it clears all
4905
+ // claude buckets and replaces claudeHashes wholesale, so scanning fewer
4906
+ // roots than sync would silently drop the missing install's history.
4907
+ result = await computeClaudeGroundTruthBuckets(
4908
+ Array.isArray(rootDirs) && rootDirs.length > 0 ? { rootDirs } : {},
4909
+ );
4699
4910
  } catch (e) {
4700
4911
  console.error("[sync] claude ground-truth repair: scan failed:", e?.message || e);
4701
4912
  return false;