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.
- 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-BI_HiL08.js → AchievementBadge-CKSLpTB6.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CbK_pV0C.js → AchievementsPage-r42Lu6ll.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DEQycrwd.js → AchievementsSection-1L_uoCV4.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BdaHueq5.js → BadgeDetailModal-DlYT33qj.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-BATn4r97.js → BotAnimated-BJilDdvD.js} +1 -1
- package/dashboard/dist/assets/{Card-C3_Fzc5V.js → Card-DfMS7eJT.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BidgYaVn.js → ClawdAnimated-B2IKU3oX.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-D86_iwFa.js → CommandPalette-BQAnKMQp.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CJuy4O88.js → CommunityStatsModal-DrYW6eRx.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BaAc5rzt.js → ConfirmModal-DzIeh5dx.js} +1 -1
- package/dashboard/dist/assets/{Controls-Brnf_QJ_.js → Controls-Ce-II3Yn.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BmW622BX.js → DashboardPage-CeQYNbXo.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DDrrY5AJ.js → DevicePage-DmoCLkPn.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BNcKZCV5.js → DialogClose-DhDGRv1X.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-1rzvf7wG.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DL9AVJvk.js → DialogTitle-BI2-gsHD.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BYwYqoVv.js → FadeIn-Bm7hEdb4.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C71yS5fY.js → HeaderGithubStar-BDAWv2_t.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-nJQ-bK5U.js → HoverTooltip-Z8xawE-X.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CTwXZRZk.js → IpCheckPage-BjmCHK-1.js} +1 -1
- package/dashboard/dist/assets/{LandingPage--GOHgrB4.js → LandingPage-DPfKTaC7.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-B7SjpjnT.js → LeaderboardAvatar-Cd0_hasU.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BmExdKSM.js → LeaderboardPage-FbGCyz1h.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-C5cQqviO.js → LeaderboardProfileModal-CzL7x0ts.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJ-09rX6.js → LeaderboardProfilePage-CRU4_vJ_.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CazL_F1h.js → LimitsPage-C8j3oSMF.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-K3aCR52T.js → LocalOnlyNotice-DMuYjLqc.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-_FbeoBO0.js → LoginCard-BTT8hq7T.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CYXuWlcp.js → LoginModal-DFyh1GeQ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-NgvQXPTx.js → LoginPage-Cylxxqn4.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CQ1aHdDU.js → PetPage-CGgadvAb.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-BH948Enq.js → PopoverPopup-DiBUe7yt.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-CTBbzd0x.js → ProviderIcon-_jz2BcWs.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-GCsv0PKq.js → ResetPasswordPage-Fv4MB_xt.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D-a1T6Ay.js → SegmentedControl-yi9ALUzr.js} +1 -1
- package/dashboard/dist/assets/{Select-BuBpeT1X.js → Select-CiF8kiQJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BJl0lY5n.js → SelectItemText-Dbzv_uem.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-Xz-rDYxA.js → ServiceStatusPage-nIhZrGkz.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BvJjRxbT.js → SessionsPage-BSyoUCDc.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-CDrrA4Z_.js → SettingsPage-Cl2eEX84.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-C3YDKdBr.js → SkillsPage-Cl8kb4jo.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CwBN1Nw2.js → TokenGalaxy-Da8tynGB.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-2RKWzsAn.js → ToolbarRootContext-IQUwRnDp.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CrCYxmL5.js → TrendMonitor-lpOCu-La.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CdJhuvNK.js → WidgetsPage-Cmby6Cw0.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-xQN9f_Lg.js → WrappedPage-CE3GkR5a.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CMMzxCk7.js → __vite-browser-external-dDN0mi9f.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-fd9xH1K3.js → agent-logos-D9dYPUzL.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DWTEaioI.js → arrow-up-right-DwAAGP7p.js} +1 -1
- package/dashboard/dist/assets/{check-D7d6q9Tz.js → check-D1Lq-9ts.js} +1 -1
- package/dashboard/dist/assets/{composite-xFyWluZ9.js → composite-JEjFHOYa.js} +1 -1
- package/dashboard/dist/assets/{copy-BlQDmjZB.js → copy-Cgep9txD.js} +1 -1
- package/dashboard/dist/assets/{download-CCvNpDTW.js → download-BvCJfrsP.js} +1 -1
- package/dashboard/dist/assets/{external-link-D3CFEoOU.js → external-link-BvrBuvDE.js} +1 -1
- package/dashboard/dist/assets/{flame-BVD3QAGl.js → flame-CpFTS5jY.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BeU_fDKc.js → format-tokens-DMy_T00F.js} +1 -1
- package/dashboard/dist/assets/{icons-DT5tCQC8.js → icons-BF-Gt_W6.js} +1 -1
- package/dashboard/dist/assets/{index-CuRJmltu.js → index-BV_fWDL1.js} +1 -1
- package/dashboard/dist/assets/{index-7Rxzbfye.js → index-CG8txBGV.js} +1 -1
- package/dashboard/dist/assets/{index-CB5EF37x.js → index-D7gB7QU6.js} +1 -1
- package/dashboard/dist/assets/{info-Ct8mnVg8.js → info-DN-YTwZw.js} +1 -1
- package/dashboard/dist/assets/{link-2-BkEYcjI7.js → link-2-C5QllJZa.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Cwlqvkcz.js → loader-circle-Br6MWmXZ.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Bfi31lVA.js → maximize-2-DWQUOE9I.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DC5Ku6C4.js → modifiers.esm-BW3ZY-q5.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-CjsPxNBN.js → monitor-smartphone-C0lh9STU.js} +1 -1
- package/dashboard/dist/assets/{plus-CgM_CX0H.js → plus-DGzzR4Ta.js} +1 -1
- package/dashboard/dist/assets/{provider-display-TEnGljXd.js → provider-display-C6cpMzsX.js} +1 -1
- package/dashboard/dist/assets/{react-CwlX45ob.js → react-Bl3I3D1L.js} +1 -1
- package/dashboard/dist/assets/{search-CKtoGzmW.js → search-C9Xi6QaM.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DuBly_gc.js → skills-api-BPqKd9uy.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BNsp-WHM.js → sortable.esm-CDX2JVl9.js} +1 -1
- package/dashboard/dist/assets/{store-BTs6kZtj.js → store-BBp505Pu.js} +1 -1
- package/dashboard/dist/assets/{terminal-DPMN8gR5.js → terminal-CS1tejfk.js} +1 -1
- package/dashboard/dist/assets/{trash-BDR38M5I.js → trash-CBCEVUxu.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Deg7skO9.js → use-community-stats-W4PO2W2z.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Bs2kD7b1.js → use-latest-request-guard-DkL6VAD-.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BgdguDzu.js → use-limits-display-prefs-BrBaoX2Q.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-ezT84-W6.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-CTxl79WJ.js → use-session-efficiency-pref-8vzPyNum.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BpIPybsm.js → use-transform-ILmR4oVe.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CeoHpc3u.js +1 -0
- package/dashboard/dist/assets/{useCurrency-e-C23rLv.js → useCurrency-_1mDCij1.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-D_LHEY77.js → useTokenFormat-DalbmT3S.js} +1 -1
- package/dashboard/dist/assets/{zap-CjrBTjSn.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 +497 -0
- package/src/lib/zcode-limits.js +147 -16
- package/dashboard/dist/assets/DialogDescription-CSvbgRdI.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-TNdjFH8O.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-ts6E8fNZ.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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,
|