tokentracker-cli 0.24.6 → 0.24.8
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.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dashboard/dist/assets/{Card-BfayTmBt.js → Card-Bm2ZOfFA.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-C_ExwqoB.js → DashboardPage-BCwneqYv.js} +2 -2
- package/dashboard/dist/assets/{DevicePage-Bzc-tOQ7.js → DevicePage-CtqTSoxC.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-C1nCEQAI.js → FadeIn-BGmXw8rD.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CalgbIws.js → HeaderGithubStar-7oF7WLZx.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C2_FdWEa.js → IpCheckPage-DLGIFtxp.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Cab2gSJk.js → LandingPage-DblVIECc.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BoWSvv8E.js → LeaderboardPage-BSIiXBXB.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-BxvvRB0p.js → LeaderboardProfilePage-rnzMxGm7.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-JTd7ZYkv.js → LimitsPage-BIIloMLr.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-fU320alu.js → LoginPage-K0pFbm2P.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-C76-ba7G.js → PopoverPopup-CdYV60oX.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-xv4cUgTy.js → ProviderIcon-CyeZHlvD.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-UtWH1_mF.js → SettingsPage-CwNF9bwF.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-9W2jGGps.js → SkillsPage-DJtcsqCs.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BedujTKv.js → WidgetsPage-DnQgY7YQ.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Bms62oTH.js → WrappedPage-Ci0XyXL_.js} +1 -1
- package/dashboard/dist/assets/check-CDwpiGWn.js +1 -0
- package/dashboard/dist/assets/{chevron-down-g6db-hJJ.js → chevron-down-BTjZUclo.js} +1 -1
- package/dashboard/dist/assets/{download-UDqrzLfH.js → download-TnEyNsSy.js} +1 -1
- package/dashboard/dist/assets/{info-BB9X3uhm.js → info-8F30IXmF.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-CTGzd-uH.js → leaderboard-columns-C6mnviZx.js} +1 -1
- package/dashboard/dist/assets/{main-QPJFCBQm.js → main-Ch22JpOj.js} +15 -15
- package/dashboard/dist/assets/main-D_iR1XAB.css +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-DccYvUNZ.js → use-limits-display-prefs-Rfcf8uOp.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-DAbSUv-n.js → use-native-settings-7r5luMxr.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-B_GaKtWC.js → use-reduced-motion-lvUJO2cx.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-3pTcFcoF.js → use-usage-limits-BDH7ZuiN.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CLZ2MqvV.js → useCurrency-9qQotA-7.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +114 -14
- package/src/commands/status.js +13 -0
- package/src/commands/sync.js +35 -2
- package/src/lib/local-api.js +112 -0
- package/src/lib/pricing/matcher.js +54 -3
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +474 -0
- package/dashboard/dist/assets/check-DHKWR9eH.js +0 -1
- package/dashboard/dist/assets/main-C8k06i2w.css +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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-QPJFCBQm.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
|