tokentracker-cli 1.0.7 → 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-BI_HiL08.js → AchievementBadge-CKSLpTB6.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CbK_pV0C.js → AchievementsPage-r42Lu6ll.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DEQycrwd.js → AchievementsSection-1L_uoCV4.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-BdaHueq5.js → BadgeDetailModal-DlYT33qj.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-BATn4r97.js → BotAnimated-BJilDdvD.js} +1 -1
  11. package/dashboard/dist/assets/{Card-C3_Fzc5V.js → Card-DfMS7eJT.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BidgYaVn.js → ClawdAnimated-B2IKU3oX.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-D86_iwFa.js → CommandPalette-BQAnKMQp.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-CJuy4O88.js → CommunityStatsModal-DrYW6eRx.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-BaAc5rzt.js → ConfirmModal-DzIeh5dx.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-Brnf_QJ_.js → Controls-Ce-II3Yn.js} +1 -1
  17. package/dashboard/dist/assets/{DashboardPage-BmW622BX.js → DashboardPage-CeQYNbXo.js} +1 -1
  18. package/dashboard/dist/assets/{DevicePage-DDrrY5AJ.js → DevicePage-DmoCLkPn.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-BNcKZCV5.js → DialogClose-DhDGRv1X.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-1rzvf7wG.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-DL9AVJvk.js → DialogTitle-BI2-gsHD.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-BYwYqoVv.js → FadeIn-Bm7hEdb4.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-C71yS5fY.js → HeaderGithubStar-BDAWv2_t.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-nJQ-bK5U.js → HoverTooltip-Z8xawE-X.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CTwXZRZk.js → IpCheckPage-BjmCHK-1.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage--GOHgrB4.js → LandingPage-DPfKTaC7.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-B7SjpjnT.js → LeaderboardAvatar-Cd0_hasU.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BmExdKSM.js → LeaderboardPage-FbGCyz1h.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-C5cQqviO.js → LeaderboardProfileModal-CzL7x0ts.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-DJ-09rX6.js → LeaderboardProfilePage-CRU4_vJ_.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-CazL_F1h.js → LimitsPage-C8j3oSMF.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-K3aCR52T.js → LocalOnlyNotice-DMuYjLqc.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-_FbeoBO0.js → LoginCard-BTT8hq7T.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-CYXuWlcp.js → LoginModal-DFyh1GeQ.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-NgvQXPTx.js → LoginPage-Cylxxqn4.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-CQ1aHdDU.js → PetPage-CGgadvAb.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-BH948Enq.js → PopoverPopup-DiBUe7yt.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-CTBbzd0x.js → ProviderIcon-_jz2BcWs.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-GCsv0PKq.js → ResetPasswordPage-Fv4MB_xt.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-D-a1T6Ay.js → SegmentedControl-yi9ALUzr.js} +1 -1
  41. package/dashboard/dist/assets/{Select-BuBpeT1X.js → Select-CiF8kiQJ.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-BJl0lY5n.js → SelectItemText-Dbzv_uem.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-Xz-rDYxA.js → ServiceStatusPage-nIhZrGkz.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-BvJjRxbT.js → SessionsPage-BSyoUCDc.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-CDrrA4Z_.js → SettingsPage-Cl2eEX84.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-C3YDKdBr.js → SkillsPage-Cl8kb4jo.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-CwBN1Nw2.js → TokenGalaxy-Da8tynGB.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-2RKWzsAn.js → ToolbarRootContext-IQUwRnDp.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-CrCYxmL5.js → TrendMonitor-lpOCu-La.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-CdJhuvNK.js → WidgetsPage-Cmby6Cw0.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-xQN9f_Lg.js → WrappedPage-CE3GkR5a.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-CMMzxCk7.js → __vite-browser-external-dDN0mi9f.js} +3 -3
  53. package/dashboard/dist/assets/{agent-logos-fd9xH1K3.js → agent-logos-D9dYPUzL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-DWTEaioI.js → arrow-up-right-DwAAGP7p.js} +1 -1
  55. package/dashboard/dist/assets/{check-D7d6q9Tz.js → check-D1Lq-9ts.js} +1 -1
  56. package/dashboard/dist/assets/{composite-xFyWluZ9.js → composite-JEjFHOYa.js} +1 -1
  57. package/dashboard/dist/assets/{copy-BlQDmjZB.js → copy-Cgep9txD.js} +1 -1
  58. package/dashboard/dist/assets/{download-CCvNpDTW.js → download-BvCJfrsP.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-D3CFEoOU.js → external-link-BvrBuvDE.js} +1 -1
  60. package/dashboard/dist/assets/{flame-BVD3QAGl.js → flame-CpFTS5jY.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-BeU_fDKc.js → format-tokens-DMy_T00F.js} +1 -1
  62. package/dashboard/dist/assets/{icons-DT5tCQC8.js → icons-BF-Gt_W6.js} +1 -1
  63. package/dashboard/dist/assets/{index-CuRJmltu.js → index-BV_fWDL1.js} +1 -1
  64. package/dashboard/dist/assets/{index-7Rxzbfye.js → index-CG8txBGV.js} +1 -1
  65. package/dashboard/dist/assets/{index-CB5EF37x.js → index-D7gB7QU6.js} +1 -1
  66. package/dashboard/dist/assets/{info-Ct8mnVg8.js → info-DN-YTwZw.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-BkEYcjI7.js → link-2-C5QllJZa.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-Cwlqvkcz.js → loader-circle-Br6MWmXZ.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-Bfi31lVA.js → maximize-2-DWQUOE9I.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-DC5Ku6C4.js → modifiers.esm-BW3ZY-q5.js} +1 -1
  71. package/dashboard/dist/assets/{monitor-smartphone-CjsPxNBN.js → monitor-smartphone-C0lh9STU.js} +1 -1
  72. package/dashboard/dist/assets/{plus-CgM_CX0H.js → plus-DGzzR4Ta.js} +1 -1
  73. package/dashboard/dist/assets/{provider-display-TEnGljXd.js → provider-display-C6cpMzsX.js} +1 -1
  74. package/dashboard/dist/assets/{react-CwlX45ob.js → react-Bl3I3D1L.js} +1 -1
  75. package/dashboard/dist/assets/{search-CKtoGzmW.js → search-C9Xi6QaM.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-DuBly_gc.js → skills-api-BPqKd9uy.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-BNsp-WHM.js → sortable.esm-CDX2JVl9.js} +1 -1
  78. package/dashboard/dist/assets/{store-BTs6kZtj.js → store-BBp505Pu.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-DPMN8gR5.js → terminal-CS1tejfk.js} +1 -1
  80. package/dashboard/dist/assets/{trash-BDR38M5I.js → trash-CBCEVUxu.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-Deg7skO9.js → use-community-stats-W4PO2W2z.js} +1 -1
  82. package/dashboard/dist/assets/{use-latest-request-guard-Bs2kD7b1.js → use-latest-request-guard-DkL6VAD-.js} +1 -1
  83. package/dashboard/dist/assets/{use-limits-display-prefs-BgdguDzu.js → use-limits-display-prefs-BrBaoX2Q.js} +1 -1
  84. package/dashboard/dist/assets/{use-ordered-list-ezT84-W6.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-CTxl79WJ.js → use-session-efficiency-pref-8vzPyNum.js} +1 -1
  87. package/dashboard/dist/assets/{use-transform-BpIPybsm.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-e-C23rLv.js → useCurrency-_1mDCij1.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-D_LHEY77.js → useTokenFormat-DalbmT3S.js} +1 -1
  91. package/dashboard/dist/assets/{zap-CjrBTjSn.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 +497 -0
  107. package/src/lib/zcode-limits.js +147 -16
  108. package/dashboard/dist/assets/DialogDescription-CSvbgRdI.js +0 -1
  109. package/dashboard/dist/assets/use-reduced-motion-TNdjFH8O.js +0 -1
  110. package/dashboard/dist/assets/use-usage-limits-ts6E8fNZ.js +0 -1
@@ -4843,6 +4843,14 @@ function toNonNegativeInt(v) {
4843
4843
  return Math.floor(n);
4844
4844
  }
4845
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
+
4846
4854
  function firstPresentNonNegativeInt(values) {
4847
4855
  for (const value of values) {
4848
4856
  if (value !== undefined && value !== null) {
@@ -11686,6 +11694,487 @@ async function parseRoocodeIncremental({
11686
11694
  return { recordsProcessed, eventsAggregated, bucketsQueued };
11687
11695
  }
11688
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
+
11689
12178
  // ─────────────────────────────────────────────────────────────────────────────
11690
12179
  // Zed Agent (all model providers — hosted "zed.dev" and bring-your-own alike)
11691
12180
  //
@@ -23173,6 +23662,14 @@ module.exports = {
23173
23662
  readRoocodeTaskModel,
23174
23663
  normalizeRoocodeModel,
23175
23664
  parseRoocodeIncremental,
23665
+ resolveClineSessionsDir,
23666
+ resolveClineSessionsDirs,
23667
+ listClineSessionFiles,
23668
+ resolveClineSessionFiles,
23669
+ resolveClineSessionFilesWithStatus,
23670
+ readClineSessionModel,
23671
+ normalizeClineModel,
23672
+ parseClineIncremental,
23176
23673
  resolveZedDbPath,
23177
23674
  decodeZedThreadBlob,
23178
23675
  extractZedTotals,