tokentracker-cli 0.87.7 → 0.87.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 (104) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +3 -2
  3. package/dashboard/dist/assets/{AchievementBadge-4SSFyzNE.js → AchievementBadge-D3_hERkQ.js} +1 -1
  4. package/dashboard/dist/assets/{AchievementsPage-4ASCXja2.js → AchievementsPage-DZGjT92g.js} +2 -2
  5. package/dashboard/dist/assets/{AchievementsSection-CyaMYMol.js → AchievementsSection-CEzq3U3A.js} +1 -1
  6. package/dashboard/dist/assets/{BadgeDetailModal-CUWTCEru.js → BadgeDetailModal-D3_B8E--.js} +1 -1
  7. package/dashboard/dist/assets/{Card-C79yXijL.js → Card-CMXM3-lp.js} +1 -1
  8. package/dashboard/dist/assets/{ClawdAnimated-Cw6J-x_d.js → ClawdAnimated-lQxUKRzw.js} +1 -1
  9. package/dashboard/dist/assets/{CommandPalette-C1RQ58De.js → CommandPalette-BCGdAeFB.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-CycaCGsd.js → CommunityStatsModal-BwE5h2y5.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-hPuA2aup.js → ConfirmModal-8uhS2ccj.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-DjunxsRS.js → Controls-CBEQTl2X.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-C5H5_EZN.js → DashboardPage-UE_3tGVy.js} +1 -1
  14. package/dashboard/dist/assets/{DevicePage-a1TT-0Bd.js → DevicePage-B06ybHJI.js} +1 -1
  15. package/dashboard/dist/assets/{DialogClose-CCRfnIbO.js → DialogClose-L_t-nrYm.js} +1 -1
  16. package/dashboard/dist/assets/DialogDescription-B7FFtR7A.js +1 -0
  17. package/dashboard/dist/assets/{DialogTitle-2Qa5pApU.js → DialogTitle-Bsft4VvL.js} +1 -1
  18. package/dashboard/dist/assets/{FadeIn-C3mpVy83.js → FadeIn-BNDdLqTu.js} +1 -1
  19. package/dashboard/dist/assets/{HeaderGithubStar-Dn_F7ByX.js → HeaderGithubStar-CvNaKDbl.js} +1 -1
  20. package/dashboard/dist/assets/{HoverTooltip-DaywO0Zz.js → HoverTooltip-D5j5IpNx.js} +1 -1
  21. package/dashboard/dist/assets/{IpCheckPage-oqGfnWb7.js → IpCheckPage-Csf8gMJW.js} +1 -1
  22. package/dashboard/dist/assets/{LandingPage-CX7dYsEX.js → LandingPage-BVSSwM0D.js} +2 -2
  23. package/dashboard/dist/assets/{LeaderboardAvatar-Bl-OMQ2x.js → LeaderboardAvatar-DXxSHqmP.js} +1 -1
  24. package/dashboard/dist/assets/{LeaderboardPage-yeWcgiVo.js → LeaderboardPage-DXj21BBR.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardProfileModal-CDmZtQ_m.js → LeaderboardProfileModal-DbEzZGg4.js} +3 -3
  26. package/dashboard/dist/assets/{LeaderboardProfilePage-BqJYxoN1.js → LeaderboardProfilePage-DoIm3jB_.js} +1 -1
  27. package/dashboard/dist/assets/{LimitsPage-D8NFgXbZ.js → LimitsPage-D_tpQrUM.js} +1 -1
  28. package/dashboard/dist/assets/{LocalOnlyNotice-DjdhDEiq.js → LocalOnlyNotice-DBi5iuPr.js} +1 -1
  29. package/dashboard/dist/assets/{LoginCard-jQXAmSQb.js → LoginCard-qDIJgUX1.js} +1 -1
  30. package/dashboard/dist/assets/{LoginModal-q4G6EN7b.js → LoginModal-D4b5p4u1.js} +1 -1
  31. package/dashboard/dist/assets/{LoginPage-BklWSXzs.js → LoginPage-CsSgItFA.js} +1 -1
  32. package/dashboard/dist/assets/{PetPage-DyWuMUTA.js → PetPage-B-wElg5q.js} +1 -1
  33. package/dashboard/dist/assets/{PopoverPopup-Dbxed_FL.js → PopoverPopup-9C_VarUQ.js} +1 -1
  34. package/dashboard/dist/assets/{ProviderIcon-ChYuSSfj.js → ProviderIcon-CciEdEeR.js} +1 -1
  35. package/dashboard/dist/assets/{ResetPasswordPage-DJww1dxo.js → ResetPasswordPage-lsI8qlzE.js} +1 -1
  36. package/dashboard/dist/assets/{SegmentedControl-Dr73_QVm.js → SegmentedControl-BGonMV45.js} +1 -1
  37. package/dashboard/dist/assets/{Select-BMCy_8oY.js → Select-DhRrGrDT.js} +1 -1
  38. package/dashboard/dist/assets/{SelectItemText-BRU7mlBI.js → SelectItemText-ClMYgZsa.js} +1 -1
  39. package/dashboard/dist/assets/{ServiceStatusPage-DDao-oTH.js → ServiceStatusPage-DuzttgL7.js} +1 -1
  40. package/dashboard/dist/assets/{SessionsPage-BjngkrsY.js → SessionsPage-gZbxx0f_.js} +1 -1
  41. package/dashboard/dist/assets/{SettingsPage-Dsftjg5j.js → SettingsPage-NhxTrVNW.js} +1 -1
  42. package/dashboard/dist/assets/{SkillsPage-igML0DOZ.js → SkillsPage-BeXojJqQ.js} +1 -1
  43. package/dashboard/dist/assets/{TokenGalaxy-BBETSYe7.js → TokenGalaxy-DM49zKzo.js} +1 -1
  44. package/dashboard/dist/assets/{ToolbarRootContext-CzEAxfqj.js → ToolbarRootContext-BfvoVNMW.js} +1 -1
  45. package/dashboard/dist/assets/{TrendMonitor-JV6lkw-s.js → TrendMonitor-Bu-dXcrV.js} +1 -1
  46. package/dashboard/dist/assets/{WidgetsPage-GpUmv0hI.js → WidgetsPage-D2Wo7seE.js} +1 -1
  47. package/dashboard/dist/assets/{WrappedPage-SP2r4wf1.js → WrappedPage-BXAwic4D.js} +1 -1
  48. package/dashboard/dist/assets/{__vite-browser-external-AQgdBxou.js → __vite-browser-external-BDEQk--l.js} +4 -3
  49. package/dashboard/dist/assets/{agent-logos-DxwSc05k.js → agent-logos-DMEEHjG7.js} +1 -1
  50. package/dashboard/dist/assets/{arrow-up-right-CDW5zn0Q.js → arrow-up-right-gNviKtY8.js} +1 -1
  51. package/dashboard/dist/assets/{check-CQfDkdqM.js → check-Cvg5pmYP.js} +1 -1
  52. package/dashboard/dist/assets/{chevron-right-DDFSY4w-.js → chevron-right-DxdaNTo8.js} +1 -1
  53. package/dashboard/dist/assets/{copy-hxnyMdBa.js → copy-BNIJL7bf.js} +1 -1
  54. package/dashboard/dist/assets/{download-DOUBKxcf.js → download-C9McJlPa.js} +1 -1
  55. package/dashboard/dist/assets/{external-link-CVipCZOa.js → external-link-C_IU6zDK.js} +1 -1
  56. package/dashboard/dist/assets/{flame-C-QU6mPf.js → flame-CII7X8Je.js} +1 -1
  57. package/dashboard/dist/assets/{format-tokens-DRX15id1.js → format-tokens-DuQbGpPt.js} +1 -1
  58. package/dashboard/dist/assets/{icons-ajxtn_0O.js → icons-B1YTqnxG.js} +1 -1
  59. package/dashboard/dist/assets/{index-DnVXRsqb.js → index-BRMxraNK.js} +1 -1
  60. package/dashboard/dist/assets/{index-OJC-yROZ.js → index-Dc9xsIu2.js} +1 -1
  61. package/dashboard/dist/assets/{index-Bq5VZMkS.js → index-QtcYir6O.js} +1 -1
  62. package/dashboard/dist/assets/{info-eITfPd17.js → info-CIgVqJoj.js} +1 -1
  63. package/dashboard/dist/assets/{limits-providers-TMq74bRy.js → limits-providers-B31NsGar.js} +1 -1
  64. package/dashboard/dist/assets/{link-2-D778MOmL.js → link-2-DTPXzHuB.js} +1 -1
  65. package/dashboard/dist/assets/{loader-circle-uN5rBNIF.js → loader-circle-CgQSyCz8.js} +1 -1
  66. package/dashboard/dist/assets/{maximize-2-C4FBbMpN.js → maximize-2-BL5J4aHZ.js} +1 -1
  67. package/dashboard/dist/assets/{modifiers.esm-DLeiQDTT.js → modifiers.esm-s_rzcEf2.js} +1 -1
  68. package/dashboard/dist/assets/{monitor-smartphone-C-obu2-u.js → monitor-smartphone-C9ei_xde.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-Bjl8tmYu.js → provider-display-ZgvoJi05.js} +2 -2
  70. package/dashboard/dist/assets/{react-Diah7uAT.js → react-DhfPVGOm.js} +1 -1
  71. package/dashboard/dist/assets/{search-B0bPOquo.js → search-Ba_MWO7q.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-6otf2Sdl.js → skills-api-BkmJmyg1.js} +1 -1
  73. package/dashboard/dist/assets/{sortable.esm-DcrJaOfF.js → sortable.esm-Bmd5t12E.js} +1 -1
  74. package/dashboard/dist/assets/{store-DOs4omJM.js → store-BxRQs9yc.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-CpMZjaGk.js → terminal-BzUwAs4g.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-BQYnn3zf.js → trash-2-CCprY7cu.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-CdL4zXXV.js → use-community-stats-TCdPJ_ir.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-DUh4f8f4.js → use-limits-display-prefs-CNF9luO6.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-DVs0Ik3O.js → use-ordered-list-BlTuSYJy.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-CfiR91Eg.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-CGggPI6Y.js → use-session-efficiency-pref-iHS0O_Fo.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-DMj91FEL.js → use-transform-CcVfL3mi.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-xJOXzuZG.js → use-usage-limits-Xdy-Y_1Z.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-BQRBx-KO.js → useCurrency-IXccXbmx.js} +1 -1
  85. package/dashboard/dist/assets/{useScrollLock-DuTPOBl8.js → useScrollLock-BEVewfZm.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-Dd95ph6d.js → useTokenFormat-Cihv3Ghl.js} +1 -1
  87. package/dashboard/dist/assets/{zap-DJMdRI6y.js → zap-zG2Fbb5_.js} +1 -1
  88. package/dashboard/dist/brand-logos/omp.svg +33 -0
  89. package/dashboard/dist/brand-logos/pi.svg +16 -0
  90. package/dashboard/dist/index.html +1 -1
  91. package/dashboard/dist/ip-check.html +1 -1
  92. package/dashboard/dist/leaderboard.html +1 -1
  93. package/dashboard/dist/share.html +1 -1
  94. package/package.json +1 -1
  95. package/src/commands/init.js +49 -8
  96. package/src/commands/status.js +12 -5
  97. package/src/commands/sync.js +245 -1
  98. package/src/commands/uninstall.js +36 -1
  99. package/src/lib/install-resolver.js +22 -6
  100. package/src/lib/omp-hook.js +589 -0
  101. package/src/lib/pricing/seed-snapshot.json +1 -1
  102. package/src/lib/rollout.js +272 -32
  103. package/dashboard/dist/assets/DialogDescription-C99d5K4r.js +0 -1
  104. package/dashboard/dist/assets/use-reduced-motion--_Yc7AWk.js +0 -1
@@ -84,6 +84,7 @@ const wsl = require("../lib/wsl-probe");
84
84
  const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
85
85
  const { resolveInstallPaths } = require("../lib/install-resolver");
86
86
  const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
87
+ const { probeOmpHookState } = require("../lib/omp-hook");
87
88
 
88
89
  function formatResolvedPaths(paths, filename) {
89
90
  const active = [];
@@ -359,10 +360,11 @@ async function cmdStatus(argv = []) {
359
360
  ? fssync.existsSync(path.join(workbuddyHome, "workbuddy.db"))
360
361
  : false;
361
362
 
362
- // oh-my-pi — passive scan only (no hooks).
363
+ // oh-my-pi — passive scan + optional notify extension.
363
364
  const ompAgentDir = resolveOmpAgentDir(process.env);
364
365
  const ompInstalled = Boolean(ompAgentDir) && fssync.existsSync(path.join(ompAgentDir, "sessions"));
365
366
  const ompFiles = ompInstalled ? resolveOmpSessionFiles(process.env) : [];
367
+ const ompHookState = await probeOmpHookState({ home, trackerDir, env: process.env });
366
368
 
367
369
  // pi (@mariozechner/pi-coding-agent) — passive scan only (no hooks).
368
370
  // Skip when its agent dir collides with omp's; sync would dedupe anyway.
@@ -771,8 +773,13 @@ async function cmdStatus(argv = []) {
771
773
  workbuddy: workbuddyInstalled
772
774
  ? { installed: true, files: workbuddyFiles.length }
773
775
  : { installed: false },
774
- omp: ompInstalled
775
- ? { installed: true, files: ompFiles.length }
776
+ omp: ompInstalled || ompHookState.ompPresent
777
+ ? {
778
+ installed: true,
779
+ files: ompFiles.length,
780
+ notify_extension: Boolean(ompHookState.configured),
781
+ notify_extension_path: ompHookState.extensionPath || null,
782
+ }
776
783
  : { installed: false },
777
784
  pi: piInstalled
778
785
  ? { installed: true, files: piFiles.length }
@@ -895,8 +902,8 @@ async function cmdStatus(argv = []) {
895
902
  workbuddyInstalled
896
903
  ? `- WorkBuddy hooks: ${workbuddyHookConfigured ? "set" : "unset"} (${workbuddyFiles.length} session jsonl file${workbuddyFiles.length !== 1 ? "s" : ""} found, SQLite DB ${workbuddyDbExists ? "found" : "not found"})`
897
904
  : null,
898
- ompInstalled
899
- ? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found)`
905
+ ompInstalled || ompHookState.ompPresent
906
+ ? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found${ompHookState.configured ? ", notify extension: yes" : ", notify extension: no"})`
900
907
  : null,
901
908
  piInstalled
902
909
  ? `- pi: passive reader (${piFiles.length} session jsonl file${piFiles.length !== 1 ? "s" : ""} found)`
@@ -83,6 +83,7 @@ const {
83
83
  listAntigravityTranscripts,
84
84
  parseAntigravityIncremental,
85
85
  resolveCodebuddyProjectFiles,
86
+ codebuddyJsonlHasUsage,
86
87
  parseCodebuddyIncremental,
87
88
  resolveWorkbuddyProjectFiles,
88
89
  parseWorkbuddyIncremental,
@@ -142,6 +143,7 @@ const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
142
143
  const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
143
144
  const CLAUDE_MEM_OBSERVER_REINCLUDE_KEY = "claudeMemObserverReinclude_2026_05_v3";
144
145
  const GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY = "grokAppendOnlyRepair_2026_05_v4";
146
+ const CODEBUDDY_LOG_JSONL_REPAIR_KEY = "codebuddyLogJsonlOverlapRepair_2026_08";
145
147
  const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
146
148
  // v1 had a cursor-format bug (wrote plain integer instead of {inode, offset,
147
149
  // updatedAt}), which made parseClaudeIncremental reread every jsonl from
@@ -589,7 +591,16 @@ async function cmdSync(argv, context = {}) {
589
591
  const wslCodexDir = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
590
592
  ? wsl.discoverWslHome(".codex")
591
593
  : null;
592
- const codexPaths = resolveInstallPaths({ nativeValue: codexNativeValue, wslValue: wslCodexDir });
594
+ // resolveInstallPaths stays the single authority for wsl-first /
595
+ // native-first / wsl-only / native-only / both selection; requireAnyChild
596
+ // makes it validate that a candidate actually holds sessions/ or
597
+ // archived_sessions/ so an empty WSL ~/.codex shell cannot shadow the
598
+ // native install (issue #codex-wsl-shadow).
599
+ const codexPaths = resolveInstallPaths({
600
+ nativeValue: codexNativeValue,
601
+ wslValue: wslCodexDir,
602
+ requireAnyChild: ["sessions", "archived_sessions"],
603
+ });
593
604
  if (codexPaths.native) {
594
605
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
595
606
  if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
@@ -1770,6 +1781,26 @@ async function cmdSync(argv, context = {}) {
1770
1781
  // Tencent's CodeBuddy CLI is a Claude Code clone; no hook system, so we
1771
1782
  // tail the per-session JSONL conversation logs incrementally on each sync.
1772
1783
  let codebuddyResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1784
+ if (isFullSourceScan && sourceAllowed("codebuddy")) {
1785
+ const repairCodebuddyFiles = mergeBothFileSources({
1786
+ resolveFiles: (env) => resolveCodebuddyProjectFiles({
1787
+ ...env,
1788
+ TOKENTRACKER_CODEBUDDY_LOG_FALLBACK: "1",
1789
+ }),
1790
+ env: process.env,
1791
+ });
1792
+ try {
1793
+ await repairCodebuddyLogJsonlOverlap({
1794
+ cursors,
1795
+ queuePath,
1796
+ queueStatePath,
1797
+ codebuddyFiles: repairCodebuddyFiles,
1798
+ env: process.env,
1799
+ });
1800
+ } catch (err) {
1801
+ warnProviderParseFailure("CodeBuddy log/JSONL repair", err, opts);
1802
+ }
1803
+ }
1773
1804
  const codebuddyFiles = sourceAllowed("codebuddy")
1774
1805
  ? mergeBothFileSources({ resolveFiles: resolveCodebuddyProjectFiles, env: process.env })
1775
1806
  : [];
@@ -2730,6 +2761,7 @@ module.exports = {
2730
2761
  acquireSyncLock,
2731
2762
  migrateCursorUnknownBuckets,
2732
2763
  migrateRolloutCumulativeDeltaBuckets,
2764
+ repairCodebuddyLogJsonlOverlap,
2733
2765
  repairCodexRescanInflation,
2734
2766
  repairCodexForkReplayInflation,
2735
2767
  repairCodexInterleavedUsageInflation,
@@ -2744,6 +2776,7 @@ module.exports = {
2744
2776
  recordCodexColdScanAudit,
2745
2777
  CURSOR_UNKNOWN_MIGRATION_KEY,
2746
2778
  ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
2779
+ CODEBUDDY_LOG_JSONL_REPAIR_KEY,
2747
2780
  CODEX_RESCAN_DEDUP_REPAIR_KEY,
2748
2781
  CODEX_FORK_REPLAY_REPAIR_KEY,
2749
2782
  CODEX_USAGE_LINEAGE_REPAIR_KEY,
@@ -3809,6 +3842,217 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
3809
3842
  cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
3810
3843
  }
3811
3844
 
3845
+ function codebuddyUsageRowKey(row) {
3846
+ if (!row || row.source !== "codebuddy") return null;
3847
+ const model = typeof row.model === "string" && row.model ? row.model : "unknown";
3848
+ const hourStart = typeof row.hour_start === "string" && row.hour_start ? row.hour_start : null;
3849
+ return hourStart ? bucketKey("codebuddy", model, hourStart) : null;
3850
+ }
3851
+
3852
+ function codebuddyZeroUsageRow(model, hourStart) {
3853
+ return {
3854
+ source: "codebuddy",
3855
+ model: model || "unknown",
3856
+ hour_start: hourStart,
3857
+ input_tokens: 0,
3858
+ cached_input_tokens: 0,
3859
+ cache_creation_input_tokens: 0,
3860
+ output_tokens: 0,
3861
+ reasoning_output_tokens: 0,
3862
+ total_tokens: 0,
3863
+ billable_total_tokens: 0,
3864
+ conversation_count: 0,
3865
+ };
3866
+ }
3867
+
3868
+ // Issue #403 historical repair. Before the JSONL usage fix, CodeBuddy could
3869
+ // ingest the same round-trip once from ~/.codebuddy/projects and once from the
3870
+ // IDE extension log. The forward resolver now disables logs whenever any JSONL
3871
+ // has usable rawUsage, but existing queues/hourly state need one guarded,
3872
+ // atomic rebuild so the old double-count is not left visible forever.
3873
+ async function repairCodebuddyLogJsonlOverlap({
3874
+ cursors,
3875
+ queuePath,
3876
+ queueStatePath,
3877
+ codebuddyFiles,
3878
+ env = process.env,
3879
+ } = {}) {
3880
+ if (!cursors || typeof cursors !== "object") return false;
3881
+ const migrations = (cursors.migrations ||= {});
3882
+ const prior = migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY];
3883
+ if (prior && !(typeof prior === "object" && prior.skipped)) return false;
3884
+
3885
+ const files = Array.isArray(codebuddyFiles) ? codebuddyFiles : [];
3886
+ const jsonlFiles = files.filter((filePath) => typeof filePath === "string" && filePath.endsWith(".jsonl"));
3887
+ const logFiles = files.filter((filePath) => typeof filePath === "string" && filePath.endsWith(".log"));
3888
+ const hasDetailedJsonl = jsonlFiles.some((filePath) => codebuddyJsonlHasUsage(filePath));
3889
+
3890
+ const hourly = cursors.hourly && typeof cursors.hourly === "object" ? cursors.hourly : {};
3891
+ const liveHourlyKeys = new Set(
3892
+ Object.keys(hourly.buckets || {}).filter((key) => key.startsWith("codebuddy|")),
3893
+ );
3894
+ let queueRaw = "";
3895
+ try {
3896
+ queueRaw = await fs.readFile(queuePath, "utf8");
3897
+ } catch (e) {
3898
+ if (e?.code !== "ENOENT") throw e;
3899
+ }
3900
+ const queueCodebuddyKeys = new Set();
3901
+ const keptQueueLines = [];
3902
+ for (const line of queueRaw.split("\n")) {
3903
+ if (!line.trim()) continue;
3904
+ let row;
3905
+ try {
3906
+ row = JSON.parse(line);
3907
+ } catch (_e) {
3908
+ keptQueueLines.push(line);
3909
+ continue;
3910
+ }
3911
+ const key = codebuddyUsageRowKey(row);
3912
+ if (!key) {
3913
+ keptQueueLines.push(line);
3914
+ continue;
3915
+ }
3916
+ queueCodebuddyKeys.add(key);
3917
+ }
3918
+ const liveKeys = new Set([...liveHourlyKeys, ...queueCodebuddyKeys]);
3919
+ if (liveKeys.size === 0) {
3920
+ migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
3921
+ status: "noop",
3922
+ appliedAt: new Date().toISOString(),
3923
+ reason: "no-codebuddy-history",
3924
+ };
3925
+ return false;
3926
+ }
3927
+
3928
+ // Keep this condition retryable when old CodeBuddy history exists but the
3929
+ // extension log is temporarily rotated/missing. A permanent no-op here
3930
+ // would close the repair window before a later full scan can rediscover it.
3931
+ if (jsonlFiles.length === 0 || logFiles.length === 0 || !hasDetailedJsonl) {
3932
+ migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
3933
+ skipped: true,
3934
+ reason: "no-mixed-detailed-sources",
3935
+ at: new Date().toISOString(),
3936
+ };
3937
+ return false;
3938
+ }
3939
+
3940
+ // A rebuild from the currently discoverable JSONL is safe only if every
3941
+ // file previously tailed by CodeBuddy is still available in this scan. If a
3942
+ // rotated/deleted log is missing, retain the user's history and retry later
3943
+ // rather than silently erasing an unreproducible bucket.
3944
+ const discovered = new Set(files);
3945
+ const priorFiles = cursors.codebuddy?.fileOffsets && typeof cursors.codebuddy.fileOffsets === "object"
3946
+ ? Object.keys(cursors.codebuddy.fileOffsets)
3947
+ : [];
3948
+ const missingPriorFile = priorFiles.find((filePath) => !discovered.has(filePath));
3949
+ if (missingPriorFile) {
3950
+ migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
3951
+ skipped: true,
3952
+ reason: "codebuddy_file_unreproducible",
3953
+ filePath: missingPriorFile,
3954
+ at: new Date().toISOString(),
3955
+ };
3956
+ return false;
3957
+ }
3958
+
3959
+ const tmpQueue = `${queuePath}.codebuddyrebuild.${process.pid}.${Date.now()}`;
3960
+ const tmpCursors = {
3961
+ version: 1,
3962
+ hourly: { buckets: {}, groupQueued: {} },
3963
+ codebuddy: {},
3964
+ };
3965
+ let rebuiltRows = [];
3966
+ try {
3967
+ await parseCodebuddyIncremental({
3968
+ projectFiles: jsonlFiles,
3969
+ cursors: tmpCursors,
3970
+ queuePath: tmpQueue,
3971
+ env,
3972
+ });
3973
+ let rebuiltRaw = "";
3974
+ try {
3975
+ rebuiltRaw = await fs.readFile(tmpQueue, "utf8");
3976
+ } catch (e) {
3977
+ if (e?.code !== "ENOENT") throw e;
3978
+ }
3979
+ rebuiltRows = rebuiltRaw.split("\n").filter((line) => line.trim());
3980
+ } catch (e) {
3981
+ console.error("[sync] CodeBuddy log/JSONL repair: rebuild failed, leaving history untouched:", e?.message || e);
3982
+ return false;
3983
+ } finally {
3984
+ await fs.rm(tmpQueue, { force: true }).catch(() => {});
3985
+ }
3986
+
3987
+ const rebuiltKeys = new Set();
3988
+ const validRebuiltRows = [];
3989
+ for (const line of rebuiltRows) {
3990
+ let row;
3991
+ try { row = JSON.parse(line); } catch (_e) { continue; }
3992
+ const key = codebuddyUsageRowKey(row);
3993
+ if (!key) continue;
3994
+ rebuiltKeys.add(key);
3995
+ validRebuiltRows.push(line);
3996
+ }
3997
+ if (rebuiltKeys.size === 0) {
3998
+ migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
3999
+ skipped: true,
4000
+ reason: "jsonl_rebuild_empty",
4001
+ at: new Date().toISOString(),
4002
+ };
4003
+ return false;
4004
+ }
4005
+
4006
+ const staleRetractions = [];
4007
+ for (const key of liveKeys) {
4008
+ if (rebuiltKeys.has(key)) continue;
4009
+ const [, model, ...hourParts] = key.split("|");
4010
+ staleRetractions.push(JSON.stringify(codebuddyZeroUsageRow(model, hourParts.join("|"))));
4011
+ }
4012
+
4013
+ const nextQueueLines = keptQueueLines.concat(staleRetractions, validRebuiltRows);
4014
+ await ensureDir(path.dirname(queuePath));
4015
+ const queueTmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
4016
+ await fs.writeFile(queueTmp, nextQueueLines.length ? `${nextQueueLines.join("\n")}\n` : "", "utf8");
4017
+ await fs.rename(queueTmp, queuePath);
4018
+
4019
+ const nextHourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
4020
+ nextHourly.buckets ||= {};
4021
+ nextHourly.groupQueued ||= {};
4022
+ for (const key of Object.keys(nextHourly.buckets)) {
4023
+ if (key.startsWith("codebuddy|")) delete nextHourly.buckets[key];
4024
+ }
4025
+ for (const key of Object.keys(nextHourly.groupQueued)) {
4026
+ if (key.startsWith("codebuddy|")) delete nextHourly.groupQueued[key];
4027
+ }
4028
+ for (const [key, bucket] of Object.entries(tmpCursors.hourly.buckets || {})) {
4029
+ if (key.startsWith("codebuddy|")) nextHourly.buckets[key] = bucket;
4030
+ }
4031
+ for (const [key, value] of Object.entries(tmpCursors.hourly.groupQueued || {})) {
4032
+ if (key.startsWith("codebuddy|")) nextHourly.groupQueued[key] = value;
4033
+ }
4034
+ cursors.codebuddy = tmpCursors.codebuddy || {};
4035
+
4036
+ let uploadState = {};
4037
+ try { uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8")); } catch (_e) {}
4038
+ uploadState.offset = 0;
4039
+ uploadState.updatedAt = new Date().toISOString();
4040
+ uploadState.note = "reset_after_codebuddy_log_jsonl_repair_2026_08";
4041
+ await ensureDir(path.dirname(queueStatePath));
4042
+ await fs.writeFile(queueStatePath, JSON.stringify(uploadState, null, 2) + "\n", "utf8");
4043
+
4044
+ migrations[CODEBUDDY_LOG_JSONL_REPAIR_KEY] = {
4045
+ status: "applied",
4046
+ appliedAt: new Date().toISOString(),
4047
+ jsonlFiles: jsonlFiles.length,
4048
+ logFiles: logFiles.length,
4049
+ previousBuckets: liveKeys.size,
4050
+ rebuiltBuckets: rebuiltKeys.size,
4051
+ retractedBuckets: staleRetractions.length,
4052
+ };
4053
+ return true;
4054
+ }
4055
+
3812
4056
  // One-time repair (#187): rebuild codex hourly buckets that the inode-keyed
3813
4057
  // re-scan double-counted before the codexHashes event-dedup landed, and push
3814
4058
  // the corrected values to the cloud. Runs BEFORE the codex parse in the same
@@ -24,6 +24,7 @@ const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/openc
24
24
  const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
25
25
  const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
26
26
  const { removeGrokHook } = require("../lib/grok-hook");
27
+ const { removeOmpHook } = require("../lib/omp-hook");
27
28
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
28
29
 
29
30
  async function cmdUninstall(argv) {
@@ -104,6 +105,7 @@ async function cmdUninstall(argv) {
104
105
  });
105
106
  const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
106
107
  const grokHookRemove = await removeGrokHook({ home, trackerDir, env: process.env });
108
+ const ompHookRemove = await removeOmpHook({ home, trackerDir, env: process.env });
107
109
 
108
110
  // Remove installed notify handler.
109
111
  await fs.unlink(notifyPath).catch(() => {});
@@ -195,6 +197,7 @@ async function cmdUninstall(argv) {
195
197
  grokHookRemove?.removed
196
198
  ? `- Grok Build hook removed: ${grokHookRemove.hookPath}`
197
199
  : "- Grok Build hook: no change",
200
+ formatOmpHookRemoveLine(ompHookRemove),
198
201
  opts.purge ? `- Purged: ${path.join(home, ".tokentracker")}` : "- Purge: skipped (use --purge)",
199
202
  ...(machineIdSeedKept
200
203
  ? [`- Kept: ${machineIdSeedPath} (cloud device identity — a reinstall reuses the same device; delete it to fully reset)`]
@@ -204,6 +207,38 @@ async function cmdUninstall(argv) {
204
207
  );
205
208
  }
206
209
 
210
+ function formatOmpHookRemoveLine(ompHookRemove) {
211
+ if (ompHookRemove?.removed) {
212
+ return `- oh-my-pi notify extension removed: ${ompHookRemove.extensionPath}`;
213
+ }
214
+ const reason = ompHookRemove?.skippedReason;
215
+ const residualPath = ompHookRemove?.stagedPath || ompHookRemove?.extensionPath;
216
+ const residual = residualPath
217
+ ? ` (left in place: ${residualPath})`
218
+ : "";
219
+ if (reason === "unmanaged") {
220
+ return `- oh-my-pi notify extension: skipped (unmanaged file)${residual}`;
221
+ }
222
+ if (reason === "unlink-failed") {
223
+ const detail = ompHookRemove?.error ? `: ${ompHookRemove.error}` : "";
224
+ return `- oh-my-pi notify extension: failed to remove${detail}${residual}`;
225
+ }
226
+ if (reason === "extension-read-failed") {
227
+ const detail = ompHookRemove?.error ? `: ${ompHookRemove.error}` : "";
228
+ return `- oh-my-pi notify extension: failed to read${detail}${residual}`;
229
+ }
230
+ if (reason === "identity-changed") {
231
+ return `- oh-my-pi notify extension: skipped (file changed during uninstall)${residual}`;
232
+ }
233
+ if (reason === "omp-agent-dir-unresolved") {
234
+ return "- oh-my-pi notify extension: skipped (omp agent dir unresolved)";
235
+ }
236
+ if (reason === "missing") {
237
+ return "- oh-my-pi notify extension: no change";
238
+ }
239
+ return "- oh-my-pi notify extension: no change";
240
+ }
241
+
207
242
  function parseArgs(argv) {
208
243
  const out = { purge: false };
209
244
  for (let i = 0; i < argv.length; i++) {
@@ -214,7 +249,7 @@ function parseArgs(argv) {
214
249
  return out;
215
250
  }
216
251
 
217
- module.exports = { cmdUninstall };
252
+ module.exports = { cmdUninstall, formatOmpHookRemoveLine };
218
253
 
219
254
  async function isFile(p) {
220
255
  try {
@@ -1,18 +1,29 @@
1
1
  const fssync = require("node:fs");
2
+ const path = require("node:path");
2
3
  const wsl = require("./wsl-probe");
3
4
 
4
- function resolveInstallPaths({ nativeValue, wslDir, wslValue } = {}, env = process.env, deps = {}) {
5
- if (process.platform !== "win32") {
5
+ function resolveInstallPaths({ nativeValue, wslDir, wslValue, requireAnyChild } = {}, env = process.env, deps = {}) {
6
+ const platform = deps.platform || process.platform;
7
+ if (platform !== "win32") {
6
8
  return { native: nativeValue ?? null, wsl: null };
7
9
  }
8
10
 
11
+ // requireAnyChild (opt-in): a candidate install must hold at least one of
12
+ // the named children (e.g. "sessions"/"archived_sessions") to qualify — an
13
+ // empty shell dir must not shadow a populated install. For wslDir discovery
14
+ // the check is folded into the existsSync probe so discoverWslHome skips an
15
+ // empty-shell distro and continues to the next one.
16
+ const populated = (p) => !requireAnyChild || hasAnyChild(p, requireAnyChild, deps.existsSync);
17
+ const wslExists = requireAnyChild
18
+ ? (p) => Boolean(pathExists(p, deps.existsSync)) && hasAnyChild(p, requireAnyChild, deps.existsSync)
19
+ : deps.existsSync;
9
20
  const wslCandidate = wslValue !== undefined
10
- ? (wsl.shouldProbeWsl(env) && pathExists(wslValue, deps.existsSync) ? wslValue : null)
11
- : (wslDir && wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(wslDir, { ...deps, env }) : null);
12
- const nativeCandidate = wsl.shouldProbeNative(env) && nativeValue
21
+ ? (wsl.shouldProbeWsl(env) && pathExists(wslValue, deps.existsSync) && populated(wslValue) ? wslValue : null)
22
+ : (wslDir && wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(wslDir, { ...deps, env, existsSync: wslExists }) : null);
23
+ const nativeCandidate = wsl.shouldProbeNative(env) && nativeValue && populated(nativeValue)
13
24
  ? pathExists(nativeValue, deps.existsSync) : null;
14
25
 
15
- return wsl.resolveAllWin32Paths({ nativeValue: nativeCandidate, wslValue: wslCandidate, env, platform: "win32" });
26
+ return wsl.resolveAllWin32Paths({ nativeValue: nativeCandidate, wslValue: wslCandidate, env, platform });
16
27
  }
17
28
 
18
29
  function pathExists(p, existsSync) {
@@ -20,6 +31,11 @@ function pathExists(p, existsSync) {
20
31
  try { return (existsSync || fssync.existsSync)(p) ? p : null; } catch (_e) { return null; }
21
32
  }
22
33
 
34
+ function hasAnyChild(p, children, existsSync) {
35
+ const ex = existsSync || fssync.existsSync;
36
+ return children.some((c) => { try { return ex(path.join(p, c)); } catch (_e) { return false; } });
37
+ }
38
+
23
39
  // Migrate a flat (single-install) cursor to { native, wsl } namespaces.
24
40
  // `activeKeys` names the namespaces seeded with a copy of the flat state; the
25
41
  // others start empty so their install's full history backfills on first parse.