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.
- package/README.de.md +6 -5
- package/README.ja.md +6 -5
- package/README.ko.md +6 -5
- package/README.md +9 -7
- package/README.zh-CN.md +6 -5
- package/dashboard/dist/assets/{AchievementBadge-CaAqGeBX.js → AchievementBadge-CKSLpTB6.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BoiTFuxp.js → AchievementsPage-r42Lu6ll.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CYiF0QS1.js → AchievementsSection-1L_uoCV4.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BH3jTMAq.js → BadgeDetailModal-DlYT33qj.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-BCn8ladQ.js → BotAnimated-BJilDdvD.js} +1 -1
- package/dashboard/dist/assets/{Card-Cf0-OFz3.js → Card-DfMS7eJT.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-B32inDYq.js → ClawdAnimated-B2IKU3oX.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BOCXhSoJ.js → CommandPalette-BQAnKMQp.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-wj3ntE0k.js → CommunityStatsModal-DrYW6eRx.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BtXy5Tpg.js → ConfirmModal-DzIeh5dx.js} +1 -1
- package/dashboard/dist/assets/{Controls-C6R_k_Yo.js → Controls-Ce-II3Yn.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-3YAD-Ps-.js → DashboardPage-CeQYNbXo.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-oQdvRuea.js → DevicePage-DmoCLkPn.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DGg7gIgR.js → DialogClose-DhDGRv1X.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-1rzvf7wG.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CRJVqO3Z.js → DialogTitle-BI2-gsHD.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Bal-Jhbn.js → FadeIn-Bm7hEdb4.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CoxP2fqd.js → HeaderGithubStar-BDAWv2_t.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DAQKPLQl.js → HoverTooltip-Z8xawE-X.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-JbfQR0w5.js → IpCheckPage-BjmCHK-1.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DjKmw_W1.js → LandingPage-DPfKTaC7.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-D3jdY9hy.js → LeaderboardAvatar-Cd0_hasU.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-fU-M_RcC.js → LeaderboardPage-FbGCyz1h.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Db_AD0nx.js → LeaderboardProfileModal-CzL7x0ts.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CQ5kXnPL.js → LeaderboardProfilePage-CRU4_vJ_.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DSVL32bb.js → LimitsPage-C8j3oSMF.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-LgNo6vft.js → LocalOnlyNotice-DMuYjLqc.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BwEFa4pX.js → LoginCard-BTT8hq7T.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-rWVKapY3.js → LoginModal-DFyh1GeQ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DEeIX36-.js → LoginPage-Cylxxqn4.js} +1 -1
- package/dashboard/dist/assets/{PetPage-81XOQqLW.js → PetPage-CGgadvAb.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-nU1lNHRX.js → PopoverPopup-DiBUe7yt.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-B-OZcL4W.js → ProviderIcon-_jz2BcWs.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CV3LLQRl.js → ResetPasswordPage-Fv4MB_xt.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DnXK5veD.js → SegmentedControl-yi9ALUzr.js} +1 -1
- package/dashboard/dist/assets/{Select-CyV3nxTO.js → Select-CiF8kiQJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-B9WQ2B5j.js → SelectItemText-Dbzv_uem.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CjfvzEOn.js → ServiceStatusPage-nIhZrGkz.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BXTr3S8x.js → SessionsPage-BSyoUCDc.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-C-O5617w.js → SettingsPage-Cl2eEX84.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-DSc5O7u7.js → SkillsPage-Cl8kb4jo.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DIbAWJfY.js → TokenGalaxy-Da8tynGB.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-B7EwUsvt.js → ToolbarRootContext-IQUwRnDp.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CeTPoZhn.js → TrendMonitor-lpOCu-La.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DycR-Xy_.js → WidgetsPage-Cmby6Cw0.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Buz1wzSM.js → WrappedPage-CE3GkR5a.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-_RSGXjh3.js → __vite-browser-external-dDN0mi9f.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-B0aXIO3B.js → agent-logos-D9dYPUzL.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right--SGPnId4.js → arrow-up-right-DwAAGP7p.js} +1 -1
- package/dashboard/dist/assets/{check-CJRaiH3e.js → check-D1Lq-9ts.js} +1 -1
- package/dashboard/dist/assets/{composite-Bwj_AQ49.js → composite-JEjFHOYa.js} +1 -1
- package/dashboard/dist/assets/{copy-DU0cBBCj.js → copy-Cgep9txD.js} +1 -1
- package/dashboard/dist/assets/{download-1G25F1-l.js → download-BvCJfrsP.js} +1 -1
- package/dashboard/dist/assets/{external-link-BTawmHLx.js → external-link-BvrBuvDE.js} +1 -1
- package/dashboard/dist/assets/{flame-Ck7u_g0N.js → flame-CpFTS5jY.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-mvtbXN0_.js → format-tokens-DMy_T00F.js} +1 -1
- package/dashboard/dist/assets/{icons-C-vySzFI.js → icons-BF-Gt_W6.js} +1 -1
- package/dashboard/dist/assets/{index-Bn1BpzgP.js → index-BV_fWDL1.js} +1 -1
- package/dashboard/dist/assets/{index-Dl_4Ap2z.js → index-CG8txBGV.js} +1 -1
- package/dashboard/dist/assets/{index-oyq0VRzp.js → index-D7gB7QU6.js} +1 -1
- package/dashboard/dist/assets/{info-nLiOCn95.js → info-DN-YTwZw.js} +1 -1
- package/dashboard/dist/assets/{link-2-DqpkQl5I.js → link-2-C5QllJZa.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-D9Uwv0lk.js → loader-circle-Br6MWmXZ.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BZqFpymh.js → maximize-2-DWQUOE9I.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-D5idO4uG.js → modifiers.esm-BW3ZY-q5.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-wrYSytV5.js → monitor-smartphone-C0lh9STU.js} +1 -1
- package/dashboard/dist/assets/{plus-Buu86Rru.js → plus-DGzzR4Ta.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DMDSBnrj.js → provider-display-C6cpMzsX.js} +1 -1
- package/dashboard/dist/assets/{react-Bj8RUcSV.js → react-Bl3I3D1L.js} +1 -1
- package/dashboard/dist/assets/{search-C8Vcia84.js → search-C9Xi6QaM.js} +1 -1
- package/dashboard/dist/assets/{skills-api-B9V7yAcx.js → skills-api-BPqKd9uy.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BSct19df.js → sortable.esm-CDX2JVl9.js} +1 -1
- package/dashboard/dist/assets/{store-DUbjqECL.js → store-BBp505Pu.js} +1 -1
- package/dashboard/dist/assets/{terminal-DVzgSayL.js → terminal-CS1tejfk.js} +1 -1
- package/dashboard/dist/assets/{trash-Bo7xY33E.js → trash-CBCEVUxu.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BCBvwoBJ.js → use-community-stats-W4PO2W2z.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Drj2VWM_.js → use-latest-request-guard-DkL6VAD-.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BGZ8e1eB.js → use-limits-display-prefs-BrBaoX2Q.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DskYb1p5.js → use-ordered-list-BH_LviiX.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CoktFwg_.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BbpqWKhz.js → use-session-efficiency-pref-8vzPyNum.js} +1 -1
- package/dashboard/dist/assets/{use-transform-C4LDbETT.js → use-transform-ILmR4oVe.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CeoHpc3u.js +1 -0
- package/dashboard/dist/assets/{useCurrency-fr7T9OGZ.js → useCurrency-_1mDCij1.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DKAbaR7B.js → useTokenFormat-DalbmT3S.js} +1 -1
- package/dashboard/dist/assets/{zap-CqDAEt0Z.js → zap-DanLIcSw.js} +1 -1
- package/dashboard/dist/brand-logos/cline.png +0 -0
- package/dashboard/dist/index.html +13 -11
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +25 -0
- package/src/commands/status.js +31 -0
- package/src/commands/sync.js +39 -0
- package/src/lib/pricing/curated-overrides.json +15 -0
- package/src/lib/pricing/index.js +6 -3
- package/src/lib/pricing/matcher.js +7 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +540 -0
- package/src/lib/zcode-limits.js +147 -16
- package/dashboard/dist/assets/DialogDescription-BS-3kTZ6.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DE2WaiPY.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-Cfx1w2nQ.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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,
|