tokentracker-cli 1.0.6 → 1.1.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 (110) hide show
  1. package/README.de.md +6 -5
  2. package/README.ja.md +6 -5
  3. package/README.ko.md +6 -5
  4. package/README.md +9 -7
  5. package/README.zh-CN.md +6 -5
  6. package/dashboard/dist/assets/{AchievementBadge-CaAqGeBX.js → AchievementBadge-CKSLpTB6.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BoiTFuxp.js → AchievementsPage-r42Lu6ll.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CYiF0QS1.js → AchievementsSection-1L_uoCV4.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-BH3jTMAq.js → BadgeDetailModal-DlYT33qj.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-BCn8ladQ.js → BotAnimated-BJilDdvD.js} +1 -1
  11. package/dashboard/dist/assets/{Card-Cf0-OFz3.js → Card-DfMS7eJT.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-B32inDYq.js → ClawdAnimated-B2IKU3oX.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-BOCXhSoJ.js → CommandPalette-BQAnKMQp.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-wj3ntE0k.js → CommunityStatsModal-DrYW6eRx.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-BtXy5Tpg.js → ConfirmModal-DzIeh5dx.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-C6R_k_Yo.js → Controls-Ce-II3Yn.js} +1 -1
  17. package/dashboard/dist/assets/{DashboardPage-3YAD-Ps-.js → DashboardPage-CeQYNbXo.js} +1 -1
  18. package/dashboard/dist/assets/{DevicePage-oQdvRuea.js → DevicePage-DmoCLkPn.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-DGg7gIgR.js → DialogClose-DhDGRv1X.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-1rzvf7wG.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-CRJVqO3Z.js → DialogTitle-BI2-gsHD.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-Bal-Jhbn.js → FadeIn-Bm7hEdb4.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-CoxP2fqd.js → HeaderGithubStar-BDAWv2_t.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-DAQKPLQl.js → HoverTooltip-Z8xawE-X.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-JbfQR0w5.js → IpCheckPage-BjmCHK-1.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-DjKmw_W1.js → LandingPage-DPfKTaC7.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-D3jdY9hy.js → LeaderboardAvatar-Cd0_hasU.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-fU-M_RcC.js → LeaderboardPage-FbGCyz1h.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-Db_AD0nx.js → LeaderboardProfileModal-CzL7x0ts.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CQ5kXnPL.js → LeaderboardProfilePage-CRU4_vJ_.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-DSVL32bb.js → LimitsPage-C8j3oSMF.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-LgNo6vft.js → LocalOnlyNotice-DMuYjLqc.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-BwEFa4pX.js → LoginCard-BTT8hq7T.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-rWVKapY3.js → LoginModal-DFyh1GeQ.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DEeIX36-.js → LoginPage-Cylxxqn4.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-81XOQqLW.js → PetPage-CGgadvAb.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-nU1lNHRX.js → PopoverPopup-DiBUe7yt.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-B-OZcL4W.js → ProviderIcon-_jz2BcWs.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-CV3LLQRl.js → ResetPasswordPage-Fv4MB_xt.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-DnXK5veD.js → SegmentedControl-yi9ALUzr.js} +1 -1
  41. package/dashboard/dist/assets/{Select-CyV3nxTO.js → Select-CiF8kiQJ.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-B9WQ2B5j.js → SelectItemText-Dbzv_uem.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-CjfvzEOn.js → ServiceStatusPage-nIhZrGkz.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-BXTr3S8x.js → SessionsPage-BSyoUCDc.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-C-O5617w.js → SettingsPage-Cl2eEX84.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-DSc5O7u7.js → SkillsPage-Cl8kb4jo.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-DIbAWJfY.js → TokenGalaxy-Da8tynGB.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-B7EwUsvt.js → ToolbarRootContext-IQUwRnDp.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-CeTPoZhn.js → TrendMonitor-lpOCu-La.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-DycR-Xy_.js → WidgetsPage-Cmby6Cw0.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-Buz1wzSM.js → WrappedPage-CE3GkR5a.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-_RSGXjh3.js → __vite-browser-external-dDN0mi9f.js} +3 -3
  53. package/dashboard/dist/assets/{agent-logos-B0aXIO3B.js → agent-logos-D9dYPUzL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right--SGPnId4.js → arrow-up-right-DwAAGP7p.js} +1 -1
  55. package/dashboard/dist/assets/{check-CJRaiH3e.js → check-D1Lq-9ts.js} +1 -1
  56. package/dashboard/dist/assets/{composite-Bwj_AQ49.js → composite-JEjFHOYa.js} +1 -1
  57. package/dashboard/dist/assets/{copy-DU0cBBCj.js → copy-Cgep9txD.js} +1 -1
  58. package/dashboard/dist/assets/{download-1G25F1-l.js → download-BvCJfrsP.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-BTawmHLx.js → external-link-BvrBuvDE.js} +1 -1
  60. package/dashboard/dist/assets/{flame-Ck7u_g0N.js → flame-CpFTS5jY.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-mvtbXN0_.js → format-tokens-DMy_T00F.js} +1 -1
  62. package/dashboard/dist/assets/{icons-C-vySzFI.js → icons-BF-Gt_W6.js} +1 -1
  63. package/dashboard/dist/assets/{index-Bn1BpzgP.js → index-BV_fWDL1.js} +1 -1
  64. package/dashboard/dist/assets/{index-Dl_4Ap2z.js → index-CG8txBGV.js} +1 -1
  65. package/dashboard/dist/assets/{index-oyq0VRzp.js → index-D7gB7QU6.js} +1 -1
  66. package/dashboard/dist/assets/{info-nLiOCn95.js → info-DN-YTwZw.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-DqpkQl5I.js → link-2-C5QllJZa.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-D9Uwv0lk.js → loader-circle-Br6MWmXZ.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-BZqFpymh.js → maximize-2-DWQUOE9I.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-D5idO4uG.js → modifiers.esm-BW3ZY-q5.js} +1 -1
  71. package/dashboard/dist/assets/{monitor-smartphone-wrYSytV5.js → monitor-smartphone-C0lh9STU.js} +1 -1
  72. package/dashboard/dist/assets/{plus-Buu86Rru.js → plus-DGzzR4Ta.js} +1 -1
  73. package/dashboard/dist/assets/{provider-display-DMDSBnrj.js → provider-display-C6cpMzsX.js} +1 -1
  74. package/dashboard/dist/assets/{react-Bj8RUcSV.js → react-Bl3I3D1L.js} +1 -1
  75. package/dashboard/dist/assets/{search-C8Vcia84.js → search-C9Xi6QaM.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-B9V7yAcx.js → skills-api-BPqKd9uy.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-BSct19df.js → sortable.esm-CDX2JVl9.js} +1 -1
  78. package/dashboard/dist/assets/{store-DUbjqECL.js → store-BBp505Pu.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-DVzgSayL.js → terminal-CS1tejfk.js} +1 -1
  80. package/dashboard/dist/assets/{trash-Bo7xY33E.js → trash-CBCEVUxu.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-BCBvwoBJ.js → use-community-stats-W4PO2W2z.js} +1 -1
  82. package/dashboard/dist/assets/{use-latest-request-guard-Drj2VWM_.js → use-latest-request-guard-DkL6VAD-.js} +1 -1
  83. package/dashboard/dist/assets/{use-limits-display-prefs-BGZ8e1eB.js → use-limits-display-prefs-BrBaoX2Q.js} +1 -1
  84. package/dashboard/dist/assets/{use-ordered-list-DskYb1p5.js → use-ordered-list-BH_LviiX.js} +1 -1
  85. package/dashboard/dist/assets/use-reduced-motion-CoktFwg_.js +1 -0
  86. package/dashboard/dist/assets/{use-session-efficiency-pref-BbpqWKhz.js → use-session-efficiency-pref-8vzPyNum.js} +1 -1
  87. package/dashboard/dist/assets/{use-transform-C4LDbETT.js → use-transform-ILmR4oVe.js} +1 -1
  88. package/dashboard/dist/assets/use-usage-limits-CeoHpc3u.js +1 -0
  89. package/dashboard/dist/assets/{useCurrency-fr7T9OGZ.js → useCurrency-_1mDCij1.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-DKAbaR7B.js → useTokenFormat-DalbmT3S.js} +1 -1
  91. package/dashboard/dist/assets/{zap-CqDAEt0Z.js → zap-DanLIcSw.js} +1 -1
  92. package/dashboard/dist/brand-logos/cline.png +0 -0
  93. package/dashboard/dist/index.html +13 -11
  94. package/dashboard/dist/ip-check.html +1 -1
  95. package/dashboard/dist/leaderboard.html +1 -1
  96. package/dashboard/dist/llms.txt +3 -2
  97. package/dashboard/dist/share.html +1 -1
  98. package/package.json +2 -2
  99. package/src/commands/init.js +25 -0
  100. package/src/commands/status.js +31 -0
  101. package/src/commands/sync.js +39 -0
  102. package/src/lib/pricing/curated-overrides.json +15 -0
  103. package/src/lib/pricing/index.js +6 -3
  104. package/src/lib/pricing/matcher.js +7 -0
  105. package/src/lib/pricing/seed-snapshot.json +1 -1
  106. package/src/lib/rollout.js +540 -0
  107. package/src/lib/zcode-limits.js +147 -16
  108. package/dashboard/dist/assets/DialogDescription-BS-3kTZ6.js +0 -1
  109. package/dashboard/dist/assets/use-reduced-motion-DE2WaiPY.js +0 -1
  110. package/dashboard/dist/assets/use-usage-limits-Cfx1w2nQ.js +0 -1
@@ -55,6 +55,13 @@ const CLAUDE_MEM_OBSERVER_PROJECT_REF =
55
55
  "https://local.tokentracker/claude-mem/observer-sessions";
56
56
  const PROJECT_ABSENT_CONTEXT_RESCAN_MS = 24 * 60 * 60 * 1000;
57
57
  const DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS = 2;
58
+ // A rollout keeps its creation date in its name, but Codex appends to a session
59
+ // for as long as it stays open, so a file dated Monday can still be growing on
60
+ // Thursday. Cold files younger than this are stat'ed every sync and re-read when
61
+ // they grew past the cursor (#592); older ones stay stat-free and rely on the
62
+ // daily cold-scan audit, which bounds the per-sync cost on machines with tens of
63
+ // thousands of rollouts.
64
+ const DEFAULT_CODEX_COLD_GROWTH_STAT_DAYS = 30;
58
65
  const FILE_METADATA_CONCURRENCY = 32;
59
66
 
60
67
  async function mapConcurrent(items, concurrency, mapper) {
@@ -701,6 +708,7 @@ async function filterColdCodexRolloutFiles({
701
708
  auditDue = false,
702
709
  nowMs = Date.now(),
703
710
  recentDays = DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS,
711
+ growthStatDays = DEFAULT_CODEX_COLD_GROWTH_STAT_DAYS,
704
712
  diagnostics = null,
705
713
  } = {}) {
706
714
  const files = Array.isArray(rolloutFiles) ? rolloutFiles : [];
@@ -785,6 +793,29 @@ async function filterColdCodexRolloutFiles({
785
793
  continue;
786
794
  }
787
795
 
796
+ // Neither check below notices an append: the day-level skip keys off the
797
+ // directory stat, which only changes when a file is added or removed, and
798
+ // the cursor offset records how far we read, not how big the file is. A
799
+ // still-open session whose name-date fell out of the active window was
800
+ // therefore skipped without ever being looked at, and its later days only
801
+ // surfaced at the daily audit (#592). Stat recent cold files and keep any
802
+ // that grew past the cursor; the parse resumes from the offset.
803
+ if (isRecentColdRollout(rolloutDate, { nowMs, growthStatDays })) {
804
+ await loadCodexCursorDirectory(filePath);
805
+ if (cursorStoreRestarted) break;
806
+ const readOffset = Number(cursors.files[filePath]?.offset);
807
+ if (Number.isFinite(readOffset) && readOffset > 0) {
808
+ const stat = await fs.stat(filePath).catch(() => null);
809
+ if (syncDiagnostics) {
810
+ syncDiagnostics.cold_growth_stats = Number(syncDiagnostics.cold_growth_stats || 0) + 1;
811
+ }
812
+ if (stat && stat.size > readOffset) {
813
+ out.push(entry);
814
+ continue;
815
+ }
816
+ }
817
+ }
818
+
788
819
  if (await canSkipCodexDirectory(filePath)) {
789
820
  skipped += 1;
790
821
  continue;
@@ -842,6 +873,18 @@ async function filterColdCodexRolloutFiles({
842
873
  return { rolloutFiles: out, skipped, restarted: false };
843
874
  }
844
875
 
876
+ // True when the rollout's name-date (local calendar day) is within
877
+ // `growthStatDays` of now. The date in the name is the session's creation day.
878
+ function isRecentColdRollout(rolloutDate, { nowMs = Date.now(), growthStatDays } = {}) {
879
+ const days = Number(growthStatDays);
880
+ if (!Number.isFinite(days) || days <= 0) return false;
881
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(rolloutDate || ""));
882
+ if (!match) return false;
883
+ const created = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getTime();
884
+ if (!Number.isFinite(created)) return false;
885
+ return nowMs - created <= days * 24 * 60 * 60 * 1000;
886
+ }
887
+
845
888
  function activeCodexRolloutDates(
846
889
  rolloutFiles,
847
890
  { nowMs = Date.now(), recentDays = DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS } = {},
@@ -4800,6 +4843,14 @@ function toNonNegativeInt(v) {
4800
4843
  return Math.floor(n);
4801
4844
  }
4802
4845
 
4846
+ // Cost values are fractional dollars, so unlike toNonNegativeInt this keeps
4847
+ // the decimals instead of flooring to whole units.
4848
+ function toNonNegativeNumber(v) {
4849
+ const n = Number(v);
4850
+ if (!Number.isFinite(n) || n < 0) return 0;
4851
+ return n;
4852
+ }
4853
+
4803
4854
  function firstPresentNonNegativeInt(values) {
4804
4855
  for (const value of values) {
4805
4856
  if (value !== undefined && value !== null) {
@@ -11643,6 +11694,487 @@ async function parseRoocodeIncremental({
11643
11694
  return { recordsProcessed, eventsAggregated, bucketsQueued };
11644
11695
  }
11645
11696
 
11697
+ // ─────────────────────────────────────────────────────────────────────────────
11698
+ // Cline (Cline CLI v3 / desktop app — ~/.cline)
11699
+ //
11700
+ // Cline outgrew its VS Code extension home. The standalone CLI and desktop app
11701
+ // keep sessions in Cline's own data dir instead of the extension's
11702
+ // `globalStorage/saoudrizwan.claude-dev/tasks/<id>/ui_messages.json` layout
11703
+ // that Roo Code and Kilo Code still fork and that we only read from IDE
11704
+ // globalStorage:
11705
+ //
11706
+ // <clineDir>/data/sessions/<session_id>/<session_id>.json metadata
11707
+ // <clineDir>/data/sessions/<session_id>/<session_id>.messages.json turns
11708
+ //
11709
+ // The messages file is `{ version, updated_at, agent, sessionId, origin,
11710
+ // messages[], system_prompt }`. Each assistant turn carries:
11711
+ //
11712
+ // ts: epoch ms
11713
+ // modelInfo: { id, provider, family? }
11714
+ // metrics: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
11715
+ // reasoningTokenCount?, cost? }
11716
+ //
11717
+ // TOKEN SEMANTICS — why no field can be copied 1:1. Cline fills `metrics` with
11718
+ // `usageDelta()`, which diffs AI SDK LanguageModelUsage totals, and those totals
11719
+ // are INCLUSIVE:
11720
+ //
11721
+ // inputTokens = noCache + cacheRead + cacheWrite
11722
+ // outputTokens = text + reasoning
11723
+ //
11724
+ // Cline's own legacy adapter spells out the input side —
11725
+ // `tokensIn: inputTokens - cacheRead - cacheWrite` — so storing inputTokens as
11726
+ // input_tokens while also storing cacheReadTokens as cached_input_tokens would
11727
+ // bill the cached prefix twice (the Codex/every-code inflation CLAUDE.md warns
11728
+ // about). Both cache buckets are subtracted here, and because reasoning sits
11729
+ // inside outputTokens it is reported as a SUBSET: `pricing/index.js` lists
11730
+ // `cline` in reasoningIncludedInOutput so it is never billed a second time.
11731
+ //
11732
+ // COUNTING MODEL. `metrics` is attached once, when the model call finishes
11733
+ // (`usageDelta(usageBeforeModel, this.state.usage)` runs after the call), so a
11734
+ // turn is either metrics-less — not counted, picked up by a later sync — or
11735
+ // final. We still keep last-emitted totals per message and emit the positive
11736
+ // difference: re-reading an unchanged file emits nothing, and if Cline ever
11737
+ // back-fills a larger total onto a message we already counted, only the
11738
+ // increase is added. The per-file mtime gate keeps the common re-read free.
11739
+ // Ledgers are kept per transcript so a deleted teammate file can be pruned
11740
+ // without affecting the rest of the session history.
11741
+ // ────────────────────────────────────────────────────────────────────────────
11742
+
11743
+ const CLINE_MESSAGES_SUFFIX = ".messages.json";
11744
+
11745
+ // Cline's own resolution chain, each step overridable ahead of it so a snapshot
11746
+ // can be pinned without touching the tool's environment:
11747
+ // CLINE_DIR -> <clineDir> (default ~/.cline)
11748
+ // CLINE_DATA_DIR -> <dataDir> (default <clineDir>/data)
11749
+ // CLINE_SESSION_DATA_DIR -> <sessionsDir> (default <dataDir>/sessions)
11750
+ function resolveClineSessionsDir(env = process.env, deps = {}) {
11751
+ const expand = deps.expandHomePath || expandHomePath;
11752
+ const nonEmpty = (value) => (typeof value === "string" && value.trim() ? value.trim() : null);
11753
+
11754
+ const sessionsOverride =
11755
+ nonEmpty(env.TOKENTRACKER_CLINE_SESSIONS_DIR) || nonEmpty(env.CLINE_SESSION_DATA_DIR);
11756
+ if (sessionsOverride) return expand(sessionsOverride, env);
11757
+
11758
+ const dataDir = nonEmpty(env.TOKENTRACKER_CLINE_DATA_DIR) || nonEmpty(env.CLINE_DATA_DIR);
11759
+ if (dataDir) return path.join(expand(dataDir, env), "sessions");
11760
+
11761
+ const home = env.HOME || require("node:os").homedir();
11762
+ const clineDir = nonEmpty(env.TOKENTRACKER_CLINE_HOME) || nonEmpty(env.CLINE_DIR);
11763
+ return path.join(clineDir ? expand(clineDir, env) : path.join(home, ".cline"), "data", "sessions");
11764
+ }
11765
+
11766
+ // Any explicit path override means the user pointed us at one install: skip the
11767
+ // WSL probe entirely rather than unioning in a distro copy they did not ask for.
11768
+ function clineSessionsDirIsOverridden(env = process.env) {
11769
+ return [
11770
+ env.TOKENTRACKER_CLINE_SESSIONS_DIR,
11771
+ env.TOKENTRACKER_CLINE_DATA_DIR,
11772
+ env.TOKENTRACKER_CLINE_HOME,
11773
+ env.CLINE_SESSION_DATA_DIR,
11774
+ env.CLINE_DATA_DIR,
11775
+ env.CLINE_DIR,
11776
+ ].some((value) => typeof value === "string" && value.trim());
11777
+ }
11778
+
11779
+ // Scan native Windows and WSL installs according to the selected WSL mode.
11780
+ function resolveClineSessionsDirs(env = process.env, deps = {}) {
11781
+ const platform = deps.platform || process.platform;
11782
+ const nativeDir = deps.nativeDir || resolveClineSessionsDir(env, deps);
11783
+ const single = (value) => (value ? [value] : []);
11784
+ if (clineSessionsDirIsOverridden(env) || platform !== "win32") return single(nativeDir);
11785
+
11786
+ const existsSync = deps.existsSync || fssync.existsSync;
11787
+ let nativeValue = null;
11788
+ try {
11789
+ if (nativeDir && existsSync(nativeDir)) nativeValue = nativeDir;
11790
+ } catch (_error) {
11791
+ // A probe failure just means we cannot vouch for the native install.
11792
+ }
11793
+
11794
+ const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
11795
+ const wslValue = wsl.shouldProbeWsl(env)
11796
+ ? discoverWslHome(".cline/data/sessions", { ...deps, env })
11797
+ : null;
11798
+ const resolved = wsl.resolveAllWin32Paths({ nativeValue, wslValue, env, platform });
11799
+ return [...new Set([resolved.native, resolved.wsl].filter(Boolean))];
11800
+ }
11801
+
11802
+
11803
+ function listClineSessionFiles(sessionsDir) {
11804
+ const result = scanClineSessionFiles(sessionsDir);
11805
+ if (result.error) throw result.error;
11806
+ return result.files;
11807
+ }
11808
+
11809
+ // A root is complete only when every directory read needed to enumerate it
11810
+ // succeeds. A missing root or session directory is an incomplete scan: it may
11811
+ // be a transient filesystem or WSL gap, so callers must retain its ledger.
11812
+ function scanClineSessionFiles(sessionsDir) {
11813
+ const out = [];
11814
+ if (typeof sessionsDir !== "string" || !sessionsDir) {
11815
+ return { files: out, complete: true, error: null };
11816
+ }
11817
+ let entries;
11818
+ try {
11819
+ entries = fssync.readdirSync(sessionsDir, { withFileTypes: true });
11820
+ } catch (error) {
11821
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
11822
+ return { files: out, complete: false, error: null };
11823
+ }
11824
+ return { files: out, complete: false, error };
11825
+ }
11826
+ let complete = true;
11827
+ for (const entry of entries) {
11828
+ if (!entry.isDirectory()) continue;
11829
+ const sessionDir = path.join(sessionsDir, entry.name);
11830
+ let artifacts;
11831
+ try {
11832
+ artifacts = fssync.readdirSync(sessionDir);
11833
+ } catch (error) {
11834
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
11835
+ complete = false;
11836
+ continue;
11837
+ }
11838
+ return { files: out, complete: false, error };
11839
+ }
11840
+ const transcripts = artifacts.filter((name) => name.endsWith(CLINE_MESSAGES_SUFFIX)).sort();
11841
+ if (transcripts.length === 0) continue;
11842
+ const metaName = `${entry.name}.json`;
11843
+ for (const messagesName of transcripts) {
11844
+ out.push({
11845
+ filePath: path.join(sessionDir, messagesName),
11846
+ sessionMetaPath: artifacts.includes(metaName) ? path.join(sessionDir, metaName) : null,
11847
+ sessionId: entry.name,
11848
+ });
11849
+ }
11850
+ }
11851
+ return { files: out, complete, error: null };
11852
+ }
11853
+
11854
+ // Every `<home>/data/sessions/*/<session>.messages.json` transcript across the
11855
+ // installs that own a Cline data dir.
11856
+ function resolveClineSessionFiles(env = process.env, deps = {}) {
11857
+ const result = resolveClineSessionFilesWithStatus(env, deps);
11858
+ if (result.errors.length > 0) throw result.errors[0].error;
11859
+ return result.files;
11860
+ }
11861
+
11862
+ function resolveClineSessionFilesWithStatus(env = process.env, deps = {}) {
11863
+ const out = [];
11864
+ const seen = new Set();
11865
+ const completedRoots = [];
11866
+ const errors = [];
11867
+ for (const sessionsDir of resolveClineSessionsDirs(env, deps)) {
11868
+ const result = scanClineSessionFiles(sessionsDir);
11869
+ for (const entry of result.files) {
11870
+ if (seen.has(entry.filePath)) continue;
11871
+ seen.add(entry.filePath);
11872
+ out.push(entry);
11873
+ }
11874
+ if (result.error) errors.push({ root: sessionsDir, error: result.error });
11875
+ else if (result.complete) completedRoots.push(sessionsDir);
11876
+ }
11877
+ out.sort((left, right) => left.filePath.localeCompare(right.filePath));
11878
+ return { files: out, completedRoots, errors };
11879
+ }
11880
+
11881
+ // The session sidecar names the model the session started on. It is only a
11882
+ // fallback: a turn's own `modelInfo.id` wins because Cline can switch models
11883
+ // mid-session. Imported sessions also retain the source transcript's usage;
11884
+ // the import timestamp lets us leave those already-counted turns to their
11885
+ // original provider parser.
11886
+ function readClineSessionMetadata(metaPath) {
11887
+ if (typeof metaPath !== "string" || !metaPath) return null;
11888
+ let parsed;
11889
+ try {
11890
+ parsed = JSON.parse(fssync.readFileSync(metaPath, "utf8"));
11891
+ } catch (_error) {
11892
+ return null;
11893
+ }
11894
+ const model = parsed && typeof parsed.model === "string" ? parsed.model.trim() : "";
11895
+ const importedAt = parsed?.metadata?.importedFrom?.importedAt;
11896
+ const importedAtMs = typeof importedAt === "string" ? Date.parse(importedAt) : NaN;
11897
+ return { model: model || null, importedAtMs: Number.isFinite(importedAtMs) ? importedAtMs : null };
11898
+ }
11899
+
11900
+ function readClineSessionModel(metaPath) {
11901
+ return readClineSessionMetadata(metaPath)?.model || null;
11902
+ }
11903
+
11904
+ function normalizeClineModel({ modelInfo, fallbackModel }) {
11905
+ const id = modelInfo && typeof modelInfo.id === "string" ? modelInfo.id.trim() : "";
11906
+ if (id) return id;
11907
+ const fallback = typeof fallbackModel === "string" ? fallbackModel.trim() : "";
11908
+ if (fallback) return fallback;
11909
+ // Mirrors Roo Code's `protocol:<x>`: surface the provider rather than a bare
11910
+ // "unknown" so the Model column does not imply a model id we never saw.
11911
+ const provider =
11912
+ modelInfo && typeof modelInfo.provider === "string"
11913
+ ? modelInfo.provider.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "")
11914
+ : "";
11915
+ return provider ? `provider:${provider}` : DEFAULT_MODEL;
11916
+ }
11917
+
11918
+ function clineMessageKey(message, index) {
11919
+ const id = message && typeof message.id === "string" ? message.id.trim() : "";
11920
+ // `id` is stable across in-place rewrites; ts is the fallback for a turn that
11921
+ // has not been assigned one.
11922
+ const timestamp = Number(message?.ts);
11923
+ return id || `ts:${Number.isFinite(timestamp) ? timestamp : 0}:${index}`;
11924
+ }
11925
+
11926
+ async function parseClineIncremental({
11927
+ sessionFiles,
11928
+ scanCompleteRoots,
11929
+ cursors,
11930
+ queuePath,
11931
+ onProgress,
11932
+ env,
11933
+ } = {}) {
11934
+ await ensureDir(path.dirname(queuePath));
11935
+ const clineState = cursors.cline && typeof cursors.cline === "object" ? { ...cursors.cline } : {};
11936
+ const legacyMessageTotals =
11937
+ clineState.messageTotals && typeof clineState.messageTotals === "object"
11938
+ ? { ...clineState.messageTotals }
11939
+ : {};
11940
+ const messageTotalsByFile =
11941
+ clineState.messageTotalsByFile && typeof clineState.messageTotalsByFile === "object"
11942
+ ? { ...clineState.messageTotalsByFile }
11943
+ : {};
11944
+ const fileOffsets =
11945
+ clineState.fileOffsets && typeof clineState.fileOffsets === "object"
11946
+ ? { ...clineState.fileOffsets }
11947
+ : {};
11948
+
11949
+ let files;
11950
+ let discoveredRoots = null;
11951
+ if (Array.isArray(sessionFiles)) {
11952
+ files = sessionFiles;
11953
+ } else {
11954
+ const scan = resolveClineSessionFilesWithStatus(env || process.env);
11955
+ files = scan.files;
11956
+ discoveredRoots = scan.completedRoots;
11957
+ }
11958
+ // Only files with old offsets were counted before teammate support. Migrate
11959
+ // them before the unchanged-file gate, including the old renamed-root fallback.
11960
+ const legacyFilesBySession = new Map();
11961
+ for (const filePath of Object.keys(fileOffsets)) {
11962
+ const sessionId = path.basename(path.dirname(filePath));
11963
+ legacyFilesBySession.set(sessionId, filePath);
11964
+ }
11965
+ for (const [legacyKey, totals] of Object.entries(legacyMessageTotals)) {
11966
+ const separator = legacyKey.indexOf(":");
11967
+ const filePath = legacyFilesBySession.get(legacyKey.slice(0, separator));
11968
+ if (!filePath || clineState.messageTotalsByFile?.[filePath]) continue;
11969
+ const ledger = messageTotalsByFile[filePath] ||= Object.create(null);
11970
+ ledger[legacyKey.slice(separator + 1)] = totals;
11971
+ }
11972
+ delete clineState.messageTotals;
11973
+
11974
+ const activeFilePaths = new Set(files.map((entry) => entry.filePath));
11975
+ const completedRoots = Array.isArray(scanCompleteRoots)
11976
+ ? new Set(scanCompleteRoots)
11977
+ : discoveredRoots
11978
+ ? new Set(discoveredRoots)
11979
+ : null;
11980
+ for (const filePath of new Set([...Object.keys(fileOffsets), ...Object.keys(messageTotalsByFile)])) {
11981
+ if (activeFilePaths.has(filePath)) continue;
11982
+ if (
11983
+ completedRoots &&
11984
+ !completedRoots.has(path.dirname(path.dirname(filePath)))
11985
+ ) {
11986
+ continue;
11987
+ }
11988
+ try {
11989
+ // A failed directory scan must not discard dedup state for existing files.
11990
+ fssync.statSync(filePath);
11991
+ } catch (error) {
11992
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") continue;
11993
+ delete fileOffsets[filePath];
11994
+ delete messageTotalsByFile[filePath];
11995
+ }
11996
+ }
11997
+
11998
+ if (files.length === 0) {
11999
+ cursors.cline = {
12000
+ ...clineState,
12001
+ messageTotalsByFile,
12002
+ fileOffsets,
12003
+ updatedAt: new Date().toISOString(),
12004
+ };
12005
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
12006
+ }
12007
+
12008
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
12009
+ // Enqueue also updates sibling buckets' alignment and queued keys. Isolate
12010
+ // Cline buckets until the append succeeds, keeping other sources untouched.
12011
+ for (const [key, bucket] of Object.entries(hourlyState.buckets)) {
12012
+ if (parseBucketKey(key).source === "cline" && bucket) {
12013
+ hourlyState.buckets[key] = { ...bucket, totals: { ...bucket.totals } };
12014
+ }
12015
+ }
12016
+ hourlyState.groupQueued = { ...hourlyState.groupQueued };
12017
+ const touchedBuckets = new Set();
12018
+ const cb = typeof onProgress === "function" ? onProgress : null;
12019
+ let recordsProcessed = 0;
12020
+ let eventsAggregated = 0;
12021
+
12022
+ for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
12023
+ const entry = files[fileIdx];
12024
+ const { filePath } = entry;
12025
+ try {
12026
+ let stat;
12027
+ let raw;
12028
+ let fd;
12029
+ try {
12030
+ // Check and read the same open file even if Cline replaces its path.
12031
+ fd = fssync.openSync(filePath, "r");
12032
+ stat = fssync.fstatSync(fd, { bigint: true });
12033
+ if (!stat.isFile()) continue;
12034
+ const prevEntry = fileOffsets[filePath];
12035
+ if (
12036
+ prevEntry &&
12037
+ prevEntry.size === stat.size.toString() &&
12038
+ prevEntry.mtimeNs === stat.mtimeNs.toString() &&
12039
+ prevEntry.dev === stat.dev.toString() &&
12040
+ prevEntry.ino === stat.ino.toString()
12041
+ ) {
12042
+ continue;
12043
+ }
12044
+ raw = fssync.readFileSync(fd, "utf8");
12045
+ } catch (_error) {
12046
+ continue;
12047
+ } finally {
12048
+ if (fd !== undefined) fssync.closeSync(fd);
12049
+ }
12050
+ let data;
12051
+ try {
12052
+ data = JSON.parse(raw);
12053
+ } catch (_error) {
12054
+ continue;
12055
+ }
12056
+ const messages = Array.isArray(data)
12057
+ ? data
12058
+ : Array.isArray(data?.messages)
12059
+ ? data.messages
12060
+ : null;
12061
+ if (!messages) continue;
12062
+
12063
+ const messageTotals = Object.assign(Object.create(null), messageTotalsByFile[filePath]);
12064
+ messageTotalsByFile[filePath] = messageTotals;
12065
+ const sessionMetadata = readClineSessionMetadata(entry.sessionMetaPath);
12066
+ const fallbackModel = sessionMetadata?.model || null;
12067
+ const importedAtMs = sessionMetadata?.importedAtMs ?? null;
12068
+
12069
+ for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
12070
+ const msg = messages[msgIdx];
12071
+ if (!msg || typeof msg !== "object") continue;
12072
+ if (msg.role !== "assistant") continue;
12073
+ const metrics = msg.metrics;
12074
+ if (!metrics || typeof metrics !== "object") continue;
12075
+
12076
+ const ts = Number(msg.ts);
12077
+ if (!Number.isFinite(ts) || ts <= 0) continue;
12078
+ if (importedAtMs !== null && ts <= importedAtMs) continue;
12079
+
12080
+ // Cline's `inputTokens` already contains both cache buckets, so only the
12081
+ // non-cached remainder is billable input. See the header comment.
12082
+ const cacheRead = toNonNegativeInt(metrics.cacheReadTokens);
12083
+ const cacheWrite = toNonNegativeInt(metrics.cacheWriteTokens);
12084
+ const inclusiveInput = toNonNegativeInt(metrics.inputTokens);
12085
+ const inputTokens = Math.max(0, inclusiveInput - cacheRead - cacheWrite);
12086
+ // Reasoning is a subset of outputTokens; it is reported, never added to
12087
+ // total_tokens and never billed on top of output.
12088
+ const outputTokens = toNonNegativeInt(metrics.outputTokens);
12089
+ const reasoningTokens = toNonNegativeInt(metrics.reasoningTokenCount);
12090
+ const cost = toNonNegativeNumber(metrics.cost);
12091
+ const totalTokens = inputTokens + cacheRead + cacheWrite + outputTokens;
12092
+
12093
+ recordsProcessed++;
12094
+
12095
+ const key = clineMessageKey(msg, msgIdx);
12096
+ const timestampKey = `ts:${Number.isFinite(ts) ? ts : 0}:${msgIdx}`;
12097
+ const legacyTimestampKey = `ts:${Number.isFinite(ts) ? ts : 0}`;
12098
+ const previous =
12099
+ messageTotals[key] ?? messageTotals[timestampKey] ?? messageTotals[legacyTimestampKey];
12100
+ if (previous !== undefined) {
12101
+ if (messageTotals[key] === undefined) messageTotals[key] = previous;
12102
+ if (key !== timestampKey) delete messageTotals[timestampKey];
12103
+ if (key !== legacyTimestampKey) delete messageTotals[legacyTimestampKey];
12104
+ }
12105
+ // A turn with no usage yet is left unrecorded so a later sync counts it
12106
+ // in full rather than latching the placeholder.
12107
+ if (totalTokens === 0 && reasoningTokens === 0 && cost === 0) continue;
12108
+
12109
+ const deltaInput = Math.max(0, inputTokens - (Number(previous?.input) || 0));
12110
+ const deltaCached = Math.max(0, cacheRead - (Number(previous?.cached_input) || 0));
12111
+ const deltaCreation = Math.max(0, cacheWrite - (Number(previous?.cache_creation) || 0));
12112
+ const deltaOutput = Math.max(0, outputTokens - (Number(previous?.output) || 0));
12113
+ const deltaReasoning = Math.max(0, reasoningTokens - (Number(previous?.reasoning) || 0));
12114
+ const deltaCost = Math.max(0, cost - (Number(previous?.cost) || 0));
12115
+ const deltaTotal = deltaInput + deltaCached + deltaCreation + deltaOutput;
12116
+ if (deltaTotal === 0 && deltaReasoning === 0 && deltaCost === 0) continue;
12117
+
12118
+ const bucketStart = toUtcHalfHourStart(new Date(ts).toISOString());
12119
+ if (!bucketStart) continue;
12120
+
12121
+ const model = normalizeClineModel({ modelInfo: msg.modelInfo, fallbackModel });
12122
+ const bucket = getHourlyBucket(hourlyState, "cline", model, bucketStart);
12123
+ addTotals(bucket.totals, {
12124
+ input_tokens: deltaInput,
12125
+ cached_input_tokens: deltaCached,
12126
+ cache_creation_input_tokens: deltaCreation,
12127
+ output_tokens: deltaOutput,
12128
+ reasoning_output_tokens: deltaReasoning,
12129
+ total_tokens: deltaTotal,
12130
+ total_cost_usd: deltaCost,
12131
+ conversation_count: previous ? 0 : 1,
12132
+ });
12133
+ touchedBuckets.add(bucketKey("cline", model, bucketStart));
12134
+
12135
+ messageTotals[key] = {
12136
+ input: inputTokens,
12137
+ cached_input: cacheRead,
12138
+ cache_creation: cacheWrite,
12139
+ output: outputTokens,
12140
+ reasoning: reasoningTokens,
12141
+ cost,
12142
+ };
12143
+ eventsAggregated++;
12144
+ }
12145
+
12146
+ // Strings retain large file IDs and nanosecond times through cursor JSON.
12147
+ fileOffsets[filePath] = {
12148
+ size: stat.size.toString(),
12149
+ mtimeNs: stat.mtimeNs.toString(),
12150
+ dev: stat.dev.toString(),
12151
+ ino: stat.ino.toString(),
12152
+ };
12153
+ } finally {
12154
+ // One tick per discovered transcript — including ones skipped as
12155
+ // unchanged or dropped as unreadable — so the sync progress bar always
12156
+ // reaches its total instead of stalling on the first skip.
12157
+ if (cb) {
12158
+ cb({
12159
+ index: fileIdx + 1,
12160
+ total: files.length,
12161
+ recordsProcessed,
12162
+ eventsAggregated,
12163
+ bucketsQueued: touchedBuckets.size,
12164
+ });
12165
+ }
12166
+ }
12167
+ }
12168
+
12169
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
12170
+ const updatedAt = new Date().toISOString();
12171
+ hourlyState.updatedAt = updatedAt;
12172
+ cursors.hourly = hourlyState;
12173
+ cursors.cline = { ...clineState, messageTotalsByFile, fileOffsets, updatedAt };
12174
+
12175
+ return { recordsProcessed, eventsAggregated, bucketsQueued };
12176
+ }
12177
+
11646
12178
  // ─────────────────────────────────────────────────────────────────────────────
11647
12179
  // Zed Agent (all model providers — hosted "zed.dev" and bring-your-own alike)
11648
12180
  //
@@ -23130,6 +23662,14 @@ module.exports = {
23130
23662
  readRoocodeTaskModel,
23131
23663
  normalizeRoocodeModel,
23132
23664
  parseRoocodeIncremental,
23665
+ resolveClineSessionsDir,
23666
+ resolveClineSessionsDirs,
23667
+ listClineSessionFiles,
23668
+ resolveClineSessionFiles,
23669
+ resolveClineSessionFilesWithStatus,
23670
+ readClineSessionModel,
23671
+ normalizeClineModel,
23672
+ parseClineIncremental,
23133
23673
  resolveZedDbPath,
23134
23674
  decodeZedThreadBlob,
23135
23675
  extractZedTotals,