tokentracker-cli 0.24.5 → 0.24.7

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 (43) hide show
  1. package/README.ja.md +1 -0
  2. package/README.ko.md +1 -0
  3. package/README.md +1 -0
  4. package/README.zh-CN.md +1 -0
  5. package/dashboard/dist/assets/{Card-Dmmj336P.js → Card-Cy2qG2yw.js} +1 -1
  6. package/dashboard/dist/assets/{DashboardPage-BHYKJEV6.js → DashboardPage-CsG_Mq6k.js} +2 -2
  7. package/dashboard/dist/assets/{DevicePage-DwsKn4Ov.js → DevicePage-B1I6_Kxp.js} +1 -1
  8. package/dashboard/dist/assets/{FadeIn-B8VmShpI.js → FadeIn-xb0lBDdT.js} +1 -1
  9. package/dashboard/dist/assets/{HeaderGithubStar-B_KecfbF.js → HeaderGithubStar-BNxToTPs.js} +1 -1
  10. package/dashboard/dist/assets/{IpCheckPage-BgG1wCTr.js → IpCheckPage-CCysfQ-C.js} +1 -1
  11. package/dashboard/dist/assets/{LandingPage-ql_hYK8I.js → LandingPage-D7x7fS1R.js} +1 -1
  12. package/dashboard/dist/assets/{LeaderboardPage-D_jXlgYG.js → LeaderboardPage-DpT-Wb4Y.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-DFUaU6I1.js → LeaderboardProfilePage-BFx3cfEn.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-mQdxcN_z.js → LimitsPage-CSXwXe-W.js} +1 -1
  15. package/dashboard/dist/assets/{LoginPage-D1lzth57.js → LoginPage-BJckej8m.js} +1 -1
  16. package/dashboard/dist/assets/{PopoverPopup-DIB4uXcd.js → PopoverPopup-BKcg8Ouw.js} +1 -1
  17. package/dashboard/dist/assets/{ProviderIcon-CFDBvFyw.js → ProviderIcon-BRpynAI7.js} +1 -1
  18. package/dashboard/dist/assets/{SettingsPage-BN1OqYbm.js → SettingsPage-tXhwdSnC.js} +1 -1
  19. package/dashboard/dist/assets/{SkillsPage-wBzXeval.js → SkillsPage-CSybau5n.js} +1 -1
  20. package/dashboard/dist/assets/{WidgetsPage-DhZSen2-.js → WidgetsPage-DZS8HDJi.js} +1 -1
  21. package/dashboard/dist/assets/{WrappedPage-DhDlrrUL.js → WrappedPage-CUWY3_0R.js} +1 -1
  22. package/dashboard/dist/assets/check-qNB7C4Ej.js +1 -0
  23. package/dashboard/dist/assets/{chevron-down-bXSVV-pK.js → chevron-down-C8arMd0V.js} +1 -1
  24. package/dashboard/dist/assets/{download-DvJmehef.js → download-CoTKvd8q.js} +1 -1
  25. package/dashboard/dist/assets/{info-lgmn3oZc.js → info-tb06m6pM.js} +1 -1
  26. package/dashboard/dist/assets/{leaderboard-columns-Bbx3-Y6N.js → leaderboard-columns-45nSY60u.js} +1 -1
  27. package/dashboard/dist/assets/{main-DVwx7yxG.js → main-DDbFansE.js} +2 -2
  28. package/dashboard/dist/assets/{use-limits-display-prefs-CeHiE8M0.js → use-limits-display-prefs-IJ650iD0.js} +1 -1
  29. package/dashboard/dist/assets/{use-native-settings-iXw-cXKZ.js → use-native-settings-B0Civ1m0.js} +1 -1
  30. package/dashboard/dist/assets/{use-reduced-motion-BWHrwYlb.js → use-reduced-motion-DnF5Mq2H.js} +1 -1
  31. package/dashboard/dist/assets/{use-usage-limits-DPaSUkzX.js → use-usage-limits-Ig-0cZyA.js} +1 -1
  32. package/dashboard/dist/assets/{useCurrency-DqvOkq1e.js → useCurrency-kbOycXmD.js} +1 -1
  33. package/dashboard/dist/index.html +1 -1
  34. package/dashboard/dist/share.html +1 -1
  35. package/package.json +1 -1
  36. package/src/commands/serve.js +114 -14
  37. package/src/commands/status.js +13 -0
  38. package/src/commands/sync.js +35 -2
  39. package/src/lib/local-api.js +66 -19
  40. package/src/lib/pricing/matcher.js +54 -3
  41. package/src/lib/pricing/seed-snapshot.json +1 -1
  42. package/src/lib/rollout.js +474 -0
  43. package/dashboard/dist/assets/check-MvbdiVTJ.js +0 -1
@@ -5663,6 +5663,469 @@ async function parseGooseIncremental({
5663
5663
  return { recordsProcessed, eventsAggregated, bucketsQueued };
5664
5664
  }
5665
5665
 
5666
+ // ─────────────────────────────────────────────────────────────────────────────
5667
+ // Droid (Factory CLI) — passive reader for ~/.factory/sessions/**/*.settings.json
5668
+ //
5669
+ // Each Droid session has two sibling files:
5670
+ // <session-id>.jsonl — per-message transcript (no token counts)
5671
+ // <session-id>.settings.json — JSON object whose tokenUsage holds the
5672
+ // CUMULATIVE session-level total:
5673
+ // {
5674
+ // "model": "custom:GLM-5.1-[Proxy]-0",
5675
+ // "providerLock": "anthropic",
5676
+ // "providerLockTimestamp": "2026-05-21T12:34:56.000Z",
5677
+ // "tokenUsage": {
5678
+ // "inputTokens": 12345, // already excludes cached reads
5679
+ // "outputTokens": 678,
5680
+ // "cacheCreationTokens": 0,
5681
+ // "cacheReadTokens": 0,
5682
+ // "thinkingTokens": 0
5683
+ // }
5684
+ // }
5685
+ //
5686
+ // Droid records totals at session granularity (not per message). We treat each
5687
+ // settings file as a cumulative counter and emit (current - previous) deltas,
5688
+ // the same cumulative-delta pattern as Goose/Cursor. Bucket timestamp is the
5689
+ // settings file's mtime — the file is rewritten each turn, so mtime is the
5690
+ // most accurate "when did these new tokens land" signal we have.
5691
+ // ─────────────────────────────────────────────────────────────────────────────
5692
+
5693
+ function resolveDroidSessionsDirs(env = process.env) {
5694
+ if (typeof env.DROID_SESSIONS_DIR === "string" && env.DROID_SESSIONS_DIR.trim()) {
5695
+ return env.DROID_SESSIONS_DIR.split(",")
5696
+ .map((d) => expandHomePath(d.trim(), env))
5697
+ .filter(Boolean);
5698
+ }
5699
+ if (typeof env.FACTORY_DIR === "string" && env.FACTORY_DIR.trim()) {
5700
+ return [path.join(expandHomePath(env.FACTORY_DIR.trim(), env), "sessions")];
5701
+ }
5702
+ const home = env.HOME || os.homedir();
5703
+ return [path.join(home, ".factory", "sessions")];
5704
+ }
5705
+
5706
+ function resolveDroidSessionsDir(env = process.env) {
5707
+ return resolveDroidSessionsDirs(env)[0];
5708
+ }
5709
+
5710
+ function listDroidSettingsFiles(env = process.env) {
5711
+ const dirs = resolveDroidSessionsDirs(env);
5712
+ const out = [];
5713
+ const walk = (dir) => {
5714
+ let entries;
5715
+ try {
5716
+ entries = fssync.readdirSync(dir, { withFileTypes: true });
5717
+ } catch {
5718
+ return;
5719
+ }
5720
+ for (const entry of entries) {
5721
+ const full = path.join(dir, entry.name);
5722
+ if (entry.isDirectory()) {
5723
+ walk(full);
5724
+ } else if (entry.isFile() && entry.name.endsWith(".settings.json")) {
5725
+ out.push(full);
5726
+ }
5727
+ }
5728
+ };
5729
+ for (const dir of dirs) {
5730
+ if (!fssync.existsSync(dir)) continue;
5731
+ walk(dir);
5732
+ }
5733
+ out.sort((a, b) => a.localeCompare(b));
5734
+ return out;
5735
+ }
5736
+
5737
+ // Strip Droid's wrapper to leave a comparable model id. Mirrors ccusage's
5738
+ // `normalize_droid_model_name` (rust/crates/ccusage/src/adapter/droid/parser.rs)
5739
+ // so the same input produces the same bucket key across both tools:
5740
+ // "custom:GLM-5.1-[Proxy]-0" -> "glm-5-1-0"
5741
+ // "anthropic/claude-sonnet-4-5" -> "anthropic/claude-sonnet-4-5"
5742
+ // "glm_5_1" -> "glm_5_1" (underscore preserved)
5743
+ // IMPORTANT: only whitespace, `.`, and existing dashes collapse to a single
5744
+ // `-`. Underscores are kept verbatim — diverging here would split `glm_5_1`
5745
+ // rows from ccusage's equivalent rows in cross-tool comparisons.
5746
+ function normalizeDroidModelName(raw) {
5747
+ if (typeof raw !== "string") return "";
5748
+ let s = raw.startsWith("custom:") ? raw.slice("custom:".length) : raw;
5749
+ s = s.replace(/\[[^\]]*\]/g, "");
5750
+ s = s.toLowerCase();
5751
+ s = s.replace(/[\s.]+/g, "-");
5752
+ s = s.replace(/-+/g, "-");
5753
+ s = s.replace(/^-+|-+$/g, "");
5754
+ return s;
5755
+ }
5756
+
5757
+ // Mirror ccusage's `normalize_droid_provider`: collapse aliases for the four
5758
+ // known upstream families. Anything else falls through to the literal value
5759
+ // (or "unknown" when the input is empty/garbage).
5760
+ function normalizeDroidProvider(raw) {
5761
+ if (typeof raw !== "string") return "unknown";
5762
+ const v = raw.trim().toLowerCase().replace(/-/g, "_");
5763
+ if (!v) return "unknown";
5764
+ if (v === "claude" || v === "anthropic") return "anthropic";
5765
+ if (v === "openai") return "openai";
5766
+ if (
5767
+ v === "google" ||
5768
+ v === "google_ai" ||
5769
+ v === "gemini" ||
5770
+ v === "vertex" ||
5771
+ v === "vertex_ai"
5772
+ )
5773
+ return "google";
5774
+ if (v === "xai" || v === "x_ai" || v === "grok") return "xai";
5775
+ return v;
5776
+ }
5777
+
5778
+ // When `providerLock` is missing, ccusage infers the family from the model
5779
+ // name itself. We replicate the same heuristic so empty-providerLock sessions
5780
+ // still bucket into `claude-unknown` / `gpt-unknown` / etc. rather than a
5781
+ // generic "unknown".
5782
+ function inferDroidProviderFromModel(model) {
5783
+ if (typeof model !== "string" || !model) return "unknown";
5784
+ const m = model.toLowerCase();
5785
+ if (
5786
+ m.includes("claude") ||
5787
+ m.includes("opus") ||
5788
+ m.includes("sonnet") ||
5789
+ m.includes("haiku")
5790
+ )
5791
+ return "anthropic";
5792
+ if (
5793
+ m.startsWith("gpt-") ||
5794
+ m.includes("-gpt-") ||
5795
+ m.includes("chatgpt") ||
5796
+ /^o\d/.test(m)
5797
+ )
5798
+ return "openai";
5799
+ if (m.includes("gemini")) return "google";
5800
+ if (m.includes("grok")) return "xai";
5801
+ return "unknown";
5802
+ }
5803
+
5804
+ function defaultDroidModelForProvider(provider) {
5805
+ switch (provider) {
5806
+ case "anthropic":
5807
+ return "claude-unknown";
5808
+ case "openai":
5809
+ return "gpt-unknown";
5810
+ case "google":
5811
+ return "gemini-unknown";
5812
+ case "xai":
5813
+ return "grok-unknown";
5814
+ default:
5815
+ return "unknown";
5816
+ }
5817
+ }
5818
+
5819
+ // When `settings.model` is missing, ccusage scans the sibling `<id>.jsonl`
5820
+ // transcript for a line containing `Model:` and pulls the name from there.
5821
+ // We mirror that exactly — same first-500-lines cap, same terminator chars
5822
+ // (`"`, `\`, `[`) — so empty-model droid sessions don't all bucket under
5823
+ // "unknown".
5824
+ function extractDroidModelFromSidecarJsonl(settingsPath) {
5825
+ if (typeof settingsPath !== "string") return "";
5826
+ if (!settingsPath.endsWith(".settings.json")) return "";
5827
+ const sidecar = settingsPath.slice(0, -".settings.json".length) + ".jsonl";
5828
+ let raw;
5829
+ try {
5830
+ raw = fssync.readFileSync(sidecar, "utf8");
5831
+ } catch {
5832
+ return "";
5833
+ }
5834
+ const lines = raw.split("\n");
5835
+ const limit = Math.min(lines.length, 500);
5836
+ for (let i = 0; i < limit; i++) {
5837
+ const idx = lines[i].indexOf("Model:");
5838
+ if (idx < 0) continue;
5839
+ const tail = lines[i].slice(idx + "Model:".length);
5840
+ // Stop at the first quote, backslash, or bracket — mirrors ccusage.
5841
+ let cut = tail.length;
5842
+ for (const ch of ['"', "\\", "["]) {
5843
+ const p = tail.indexOf(ch);
5844
+ if (p >= 0 && p < cut) cut = p;
5845
+ }
5846
+ const candidate = tail.slice(0, cut).trim();
5847
+ if (!candidate) continue;
5848
+ const normalized = normalizeDroidModelName(candidate);
5849
+ if (normalized) return normalized;
5850
+ }
5851
+ return "";
5852
+ }
5853
+
5854
+ // ccusage's `apply_total_token_fallback`: if the five detail counters
5855
+ // underflow the session's `totalTokens`, attribute the gap. Prefer assigning
5856
+ // it to output (the field most likely to be missing on older settings.json
5857
+ // schemas); if output is already populated, fold the extra into the thinking
5858
+ // (reasoning_output_tokens) channel so total stays consistent. Mirrors
5859
+ // rust/crates/ccusage/src/utils.rs verbatim.
5860
+ function applyDroidTotalFallback(usage) {
5861
+ const known =
5862
+ usage.input + usage.output + usage.cacheCreation + usage.cacheRead + usage.thinking;
5863
+ const total = usage.totalTokens || 0;
5864
+ const missing = total > known ? total - known : 0;
5865
+ if (missing === 0) return usage;
5866
+ if (usage.output === 0) {
5867
+ return { ...usage, output: missing };
5868
+ }
5869
+ return { ...usage, thinking: usage.thinking + missing };
5870
+ }
5871
+
5872
+ // Session id = basename minus `.settings.json`, mirroring ccusage's keying.
5873
+ // Stable across FACTORY_DIR / HOME / mount-point moves because Droid uses
5874
+ // UUID-style session ids (collision risk between projects is negligible).
5875
+ function droidSessionIdFromPath(filePath) {
5876
+ if (typeof filePath !== "string" || !filePath) return "";
5877
+ const base = path.basename(filePath);
5878
+ if (!base.endsWith(".settings.json")) return "";
5879
+ return base.slice(0, -".settings.json".length);
5880
+ }
5881
+
5882
+ async function parseDroidIncremental({
5883
+ settingsFiles,
5884
+ cursors,
5885
+ queuePath,
5886
+ onProgress,
5887
+ env,
5888
+ // `prune: true` (the production default) drops cursor entries whose session
5889
+ // id was not observed this run — handles `.settings.json` files removed
5890
+ // off disk so the cursor doesn't grow unbounded. Tests that pass an
5891
+ // intentionally partial `settingsFiles` list should set `prune: false` to
5892
+ // keep unobserved entries.
5893
+ prune = true,
5894
+ } = {}) {
5895
+ await ensureDir(path.dirname(queuePath));
5896
+ const droidState =
5897
+ cursors.droid && typeof cursors.droid === "object" ? cursors.droid : {};
5898
+ const sessionTotals =
5899
+ droidState.sessionTotals && typeof droidState.sessionTotals === "object"
5900
+ ? { ...droidState.sessionTotals }
5901
+ : {};
5902
+
5903
+ const files = Array.isArray(settingsFiles)
5904
+ ? settingsFiles
5905
+ : listDroidSettingsFiles(env || process.env);
5906
+
5907
+ if (files.length === 0) {
5908
+ cursors.droid = {
5909
+ ...droidState,
5910
+ sessionTotals,
5911
+ updatedAt: new Date().toISOString(),
5912
+ };
5913
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
5914
+ }
5915
+
5916
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
5917
+ const touchedBuckets = new Set();
5918
+ const cb = typeof onProgress === "function" ? onProgress : null;
5919
+ let recordsProcessed = 0;
5920
+ let eventsAggregated = 0;
5921
+
5922
+ // Track which session ids we observed this run so we can prune cursor
5923
+ // entries for files that disappeared off disk — keeps the cursor bounded
5924
+ // by actual session count without the false-first-sight re-emit bug that
5925
+ // a fixed-N cap would introduce (evicted-but-still-on-disk entries would
5926
+ // resurrect as zero-prev on the next sync and re-count their cumulative).
5927
+ const seenSessionIds = new Set();
5928
+
5929
+ for (let i = 0; i < files.length; i++) {
5930
+ const filePath = files[i];
5931
+ recordsProcessed++;
5932
+
5933
+ let mtimeMs = 0;
5934
+ try {
5935
+ mtimeMs = fssync.statSync(filePath).mtimeMs;
5936
+ } catch (e) {
5937
+ if (e && e.code === "ENOENT") continue;
5938
+ throw e;
5939
+ }
5940
+
5941
+ // Key by session id (the UUID-style filename without `.settings.json`)
5942
+ // so the cursor survives FACTORY_DIR / HOME / mount-point migrations.
5943
+ // Mirrors ccusage's session_id derivation (parser.rs::load_settings_file).
5944
+ const sessionId = droidSessionIdFromPath(filePath);
5945
+ if (!sessionId) continue;
5946
+ seenSessionIds.add(sessionId);
5947
+
5948
+ const prev = sessionTotals[sessionId] || {
5949
+ input: 0,
5950
+ output: 0,
5951
+ cacheCreation: 0,
5952
+ cacheRead: 0,
5953
+ thinking: 0,
5954
+ mtimeMs: 0,
5955
+ };
5956
+ const isFirstSeenSession = !sessionTotals[sessionId];
5957
+ if (mtimeMs && mtimeMs === prev.mtimeMs) continue;
5958
+
5959
+ let raw;
5960
+ try {
5961
+ raw = fssync.readFileSync(filePath, "utf8");
5962
+ } catch {
5963
+ continue;
5964
+ }
5965
+ let settings;
5966
+ try {
5967
+ settings = JSON.parse(raw);
5968
+ } catch {
5969
+ continue;
5970
+ }
5971
+ if (!settings || typeof settings !== "object") continue;
5972
+ const tokenUsage = settings.tokenUsage;
5973
+ if (!tokenUsage || typeof tokenUsage !== "object") continue;
5974
+
5975
+ const filled = applyDroidTotalFallback({
5976
+ input: Math.max(0, Number(tokenUsage.inputTokens || 0)),
5977
+ output: Math.max(0, Number(tokenUsage.outputTokens || 0)),
5978
+ cacheCreation: Math.max(0, Number(tokenUsage.cacheCreationTokens || 0)),
5979
+ cacheRead: Math.max(0, Number(tokenUsage.cacheReadTokens || 0)),
5980
+ thinking: Math.max(0, Number(tokenUsage.thinkingTokens || 0)),
5981
+ totalTokens: Math.max(0, Number(tokenUsage.totalTokens || 0)),
5982
+ });
5983
+ const inputNow = filled.input;
5984
+ const outputNow = filled.output;
5985
+ const cacheCreationNow = filled.cacheCreation;
5986
+ const cacheReadNow = filled.cacheRead;
5987
+ const thinkingNow = filled.thinking;
5988
+ const sumNow =
5989
+ inputNow + outputNow + cacheCreationNow + cacheReadNow + thinkingNow;
5990
+ const sumPrev =
5991
+ prev.input + prev.output + prev.cacheCreation + prev.cacheRead + prev.thinking;
5992
+
5993
+ // Transient empty: settings.json was observed with zero tokens (mid-write
5994
+ // or a brief wipe before the next turn restores totals). Do NOT clobber
5995
+ // the existing per-field baseline — only bump mtimeMs so we don't re-read
5996
+ // the same empty payload next sync. If we overwrote prev with zeros, a
5997
+ // later non-empty read would emit the full cumulative as a fresh delta.
5998
+ if (sumNow === 0) {
5999
+ if (sumPrev > 0) {
6000
+ sessionTotals[sessionId] = { ...prev, mtimeMs };
6001
+ } else {
6002
+ sessionTotals[sessionId] = {
6003
+ input: 0,
6004
+ output: 0,
6005
+ cacheCreation: 0,
6006
+ cacheRead: 0,
6007
+ thinking: 0,
6008
+ mtimeMs,
6009
+ };
6010
+ }
6011
+ continue;
6012
+ }
6013
+
6014
+ // Reset only when the TOTAL shrinks — a real session reuse (Droid wiped
6015
+ // tokenUsage and started over). A single field dropping while the sum
6016
+ // grows is a schema change or cache eviction; clamping per-field deltas
6017
+ // to >=0 is the right behavior for those.
6018
+ const isReset = sumNow < sumPrev;
6019
+
6020
+ const dInput = isReset ? inputNow : Math.max(0, inputNow - prev.input);
6021
+ const dOutput = isReset ? outputNow : Math.max(0, outputNow - prev.output);
6022
+ const dCacheCreation = isReset
6023
+ ? cacheCreationNow
6024
+ : Math.max(0, cacheCreationNow - prev.cacheCreation);
6025
+ const dCacheRead = isReset
6026
+ ? cacheReadNow
6027
+ : Math.max(0, cacheReadNow - prev.cacheRead);
6028
+ const dThinking = isReset
6029
+ ? thinkingNow
6030
+ : Math.max(0, thinkingNow - prev.thinking);
6031
+
6032
+ if (dInput + dOutput + dCacheCreation + dCacheRead + dThinking === 0) {
6033
+ sessionTotals[sessionId] = {
6034
+ input: inputNow,
6035
+ output: outputNow,
6036
+ cacheCreation: cacheCreationNow,
6037
+ cacheRead: cacheReadNow,
6038
+ thinking: thinkingNow,
6039
+ mtimeMs,
6040
+ };
6041
+ continue;
6042
+ }
6043
+
6044
+ const bucketStart = toUtcHalfHourStart(
6045
+ new Date(mtimeMs || Date.now()).toISOString(),
6046
+ );
6047
+ if (!bucketStart) continue;
6048
+
6049
+ // Model resolution mirrors ccusage's chain: settings.model → sidecar
6050
+ // <id>.jsonl scrape → `<provider>-unknown` derived from providerLock or
6051
+ // inferred from the model fragment we did find. Same fallback string set
6052
+ // (claude-unknown / gpt-unknown / gemini-unknown / grok-unknown) so
6053
+ // empty-model sessions bucket identically across both tools.
6054
+ let model = normalizeDroidModelName(settings.model);
6055
+ if (!model) model = extractDroidModelFromSidecarJsonl(filePath);
6056
+ if (!model) {
6057
+ let provider = normalizeDroidProvider(settings.providerLock);
6058
+ if (provider === "unknown") {
6059
+ provider = inferDroidProviderFromModel(settings.model || "");
6060
+ }
6061
+ model = defaultDroidModelForProvider(provider);
6062
+ }
6063
+
6064
+ // Token normalization: inputTokens already excludes cache reads (matches
6065
+ // Anthropic API convention), so cache columns slot in directly. Thinking
6066
+ // is reasoning_output_tokens — folded into cost via existing pricing path.
6067
+ const bucketDelta = {
6068
+ input_tokens: dInput,
6069
+ cached_input_tokens: dCacheRead,
6070
+ cache_creation_input_tokens: dCacheCreation,
6071
+ output_tokens: dOutput,
6072
+ reasoning_output_tokens: dThinking,
6073
+ total_tokens: dInput + dOutput + dCacheCreation + dCacheRead + dThinking,
6074
+ conversation_count: isFirstSeenSession || isReset ? 1 : 0,
6075
+ };
6076
+ const bucket = getHourlyBucket(hourlyState, "droid", model, bucketStart);
6077
+ addTotals(bucket.totals, bucketDelta);
6078
+ touchedBuckets.add(bucketKey("droid", model, bucketStart));
6079
+
6080
+ sessionTotals[sessionId] = {
6081
+ input: inputNow,
6082
+ output: outputNow,
6083
+ cacheCreation: cacheCreationNow,
6084
+ cacheRead: cacheReadNow,
6085
+ thinking: thinkingNow,
6086
+ mtimeMs,
6087
+ };
6088
+ eventsAggregated++;
6089
+
6090
+ if (cb) {
6091
+ cb({
6092
+ index: i + 1,
6093
+ total: files.length,
6094
+ recordsProcessed,
6095
+ eventsAggregated,
6096
+ bucketsQueued: touchedBuckets.size,
6097
+ });
6098
+ }
6099
+ }
6100
+
6101
+ // Prune cursor entries for sessions that no longer appear on disk. Driven
6102
+ // by an explicit `prune` flag (default true) — not by the shape of
6103
+ // `settingsFiles` — so production callers that pass an explicit file list
6104
+ // still get pruning, while tests passing an intentionally partial subset
6105
+ // can opt out with `prune: false`.
6106
+ if (prune) {
6107
+ for (const id of Object.keys(sessionTotals)) {
6108
+ if (!seenSessionIds.has(id)) delete sessionTotals[id];
6109
+ }
6110
+ }
6111
+
6112
+ const bucketsQueued = await enqueueTouchedBuckets({
6113
+ queuePath,
6114
+ hourlyState,
6115
+ touchedBuckets,
6116
+ });
6117
+ const updatedAt = new Date().toISOString();
6118
+ hourlyState.updatedAt = updatedAt;
6119
+ cursors.hourly = hourlyState;
6120
+ cursors.droid = {
6121
+ ...droidState,
6122
+ sessionTotals,
6123
+ updatedAt,
6124
+ };
6125
+
6126
+ return { recordsProcessed, eventsAggregated, bucketsQueued };
6127
+ }
6128
+
5666
6129
  async function parseKilocodeIncremental({
5667
6130
  taskFiles,
5668
6131
  cursors,
@@ -7722,6 +8185,17 @@ module.exports = {
7722
8185
  parseGooseModelName,
7723
8186
  parseGooseCreatedAt,
7724
8187
  parseGooseIncremental,
8188
+ resolveDroidSessionsDir,
8189
+ resolveDroidSessionsDirs,
8190
+ listDroidSettingsFiles,
8191
+ normalizeDroidModelName,
8192
+ normalizeDroidProvider,
8193
+ inferDroidProviderFromModel,
8194
+ defaultDroidModelForProvider,
8195
+ droidSessionIdFromPath,
8196
+ extractDroidModelFromSidecarJsonl,
8197
+ applyDroidTotalFallback,
8198
+ parseDroidIncremental,
7725
8199
  resolvePiHome,
7726
8200
  resolvePiAgentDir,
7727
8201
  resolvePiSessionFiles,
@@ -1 +0,0 @@
1
- import{c}from"./main-DVwx7yxG.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};