tokentracker-cli 0.85.0 → 0.86.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 +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +8 -7
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-DEqbHlmn.js → AchievementBadge-BgbH_mX4.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BOaEu1AQ.js → AchievementsPage-CQqkh1JS.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-C370ngO3.js → AchievementsSection-Dmkxpn0s.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D7Wkef0K.js → BadgeDetailModal-7OQf37lk.js} +1 -1
- package/dashboard/dist/assets/{Card-mDEVjNdM.js → Card-1PmWZRTK.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-DdsX-z8n.js → ClawdAnimated-CMJFagpX.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DhxRIuHC.js → CommandPalette-D4OrwF1w.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CG-9JPS6.js → CommunityStatsModal-BDC9EcwZ.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-COhxErLm.js → ConfirmModal-22DL5C_9.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-tX0DKgFE.js → DashboardPage-CEXWzDtt.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BsSeHx84.js → DevicePage-8uhHB7kX.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-eyOW2cbx.js → DialogClose-BcJx6VK-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-D6zxtOM5.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-H8sGiH7c.js → DialogTitle-gRzunAqv.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-pq2ja50M.js → FadeIn-4rlPdBVe.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DWgcpDHr.js → HeaderGithubStar-CR003aO3.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CcJ_VR65.js → HoverTooltip-Dgz-dM8Q.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CNUN346Q.js → IpCheckPage-B4PJRJP_.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-yaoRGsNI.js → LandingPage-3JQe-jOZ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-Ddai4I12.js → LeaderboardAvatar-CL9LxiEf.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Bajkxf63.js → LeaderboardPage-DhIzM2Z2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BGYKDgIi.js → LeaderboardProfileModal-bbf5uJR5.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Ck5vPaeN.js → LeaderboardProfilePage-CzuPxscw.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-Bqobtsw2.js → LimitsPage-B59rb2gg.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-e9KSE3wj.js → LocalOnlyNotice-BkK-6n1R.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BhTSm2tb.js → LoginCard-BFGCzEjE.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CRwuThde.js → LoginModal-yhkyscFn.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-j1i74lcc.js → LoginPage-DwU5s0F7.js} +1 -1
- package/dashboard/dist/assets/{PetPage-r62ViU1X.js → PetPage-BPscYR6z.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DopNTyD4.js → PopoverPopup-E7PyGTTB.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-c1LkPhYM.js → ProviderIcon-CvmnzwKU.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DJdDNvtq.js → ResetPasswordPage-DD6qX616.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DhWVOTPN.js → SegmentedControl-DjGBL_l1.js} +1 -1
- package/dashboard/dist/assets/{Select-DeUmTmsJ.js → Select-B2MGmsi2.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-pX0B5f7h.js → SelectItemText-BaTwA_ZF.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ejIPz6oF.js → ServiceStatusPage-0K0y8A6m.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-WEbn90GW.js → SessionsPage-CxrtRm3B.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B8t5gCeI.js → SettingsPage-CyvSWvDP.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-BWMlzeAp.js → SkillsPage-BVOU8t3t.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DIBRBvMj.js → TokenGalaxy-w7bwGloC.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-C6KxLvnI.js → ToolbarRootContext-BAzpDZej.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BaMNh67w.js → TrendMonitor-DyT7Q4xp.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DDsy4M3x.js → WidgetsPage-eDDtopQT.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-CiNnUFRI.js → WrappedPage-CKkbdWHF.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CM-H8-LW.js → __vite-browser-external-SU8sCTLB.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-BjVa6oe7.js → agent-logos-bkh9XZJd.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CCum1mqX.js → arrow-up-right-D16zCIw8.js} +1 -1
- package/dashboard/dist/assets/{check-ySRQc1YP.js → check-BFL17JWQ.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-5vFloHew.js → chevron-right-CUJgrtkp.js} +1 -1
- package/dashboard/dist/assets/{copy-mhfKVMKo.js → copy-CRQiCjgs.js} +1 -1
- package/dashboard/dist/assets/{download-GdrXglSx.js → download-Cd12nDrQ.js} +1 -1
- package/dashboard/dist/assets/{external-link-KGFaKc8s.js → external-link-CvAfnsG-.js} +1 -1
- package/dashboard/dist/assets/{flame-BmNtNoS1.js → flame-CCi681Yl.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CCpCNw3d.js → format-tokens-BAVt8uTZ.js} +1 -1
- package/dashboard/dist/assets/{icons-XLKZavTf.js → icons-CbKC96gR.js} +1 -1
- package/dashboard/dist/assets/{index-_xU5U5Ct.js → index--TDvaL1y.js} +1 -1
- package/dashboard/dist/assets/{index-CsywTeCQ.js → index-B5b6PCh5.js} +1 -1
- package/dashboard/dist/assets/{index-IKpmeZte.js → index-B9LWn-mo.js} +1 -1
- package/dashboard/dist/assets/{info-DUV5UpmT.js → info-BPFpD--M.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DFkCZCZj.js → limits-providers-Cyedt-WJ.js} +1 -1
- package/dashboard/dist/assets/{link-2-D7c64yLY.js → link-2-BkEPPdfC.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-sMONRK2M.js → loader-circle-CzASiylJ.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CVWRgrk9.js → maximize-2-CkwnHUJn.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSKqVjgm.js → provider-display-BXBBedC-.js} +2 -2
- package/dashboard/dist/assets/{react-c1q4mGkS.js → react-BXSctDQj.js} +1 -1
- package/dashboard/dist/assets/{search-CwD3s-_C.js → search-_qGma9tM.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BRXj8kjT.js → skills-api-DdmJjnJ9.js} +1 -1
- package/dashboard/dist/assets/{store-lqTW2gfK.js → store-owYCN530.js} +1 -1
- package/dashboard/dist/assets/{terminal-DU1_KbKo.js → terminal-Clzw-KEF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-B8Y5Xc-K.js → trash-2-C5Giwp_E.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DYwwimvp.js → use-community-stats-Dk8UjLEr.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Bst6ybK3.js → use-limits-display-prefs-CArM595Q.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-Cqle4lgw.js → use-native-settings-Cpd69kiF.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Dtze0ahb.js → use-ordered-list--CzwhBTx.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CcILnpU1.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-gwbDMHXa.js → use-session-efficiency-pref-DUXO8cCg.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CU0yxXpX.js → use-transform-Cg3kuMj_.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-5y1I1T5_.js → use-usage-limits-DY9_mV28.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BfVNrywe.js → useCurrency-BIe86hMn.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-CF1m7AMJ.js → useScrollLock-7-hejkNC.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DiRwVvRr.js → useTokenFormat-DEtLbRgQ.js} +1 -1
- package/dashboard/dist/assets/{zap-BZtlWtuN.js → zap-DUxY1d-O.js} +1 -1
- package/dashboard/dist/index.html +12 -10
- 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 +1 -0
- package/src/commands/status.js +15 -0
- package/src/commands/sync.js +114 -8
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +451 -0
- package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -1299,6 +1299,85 @@ async function parseOpencodeIncremental({
|
|
|
1299
1299
|
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
|
+
// Passive discovery of OpenClaw session transcripts (issue #264). OpenClaw was
|
|
1303
|
+
// previously counted ONLY when its session plugin fired an `agent_end` hook and
|
|
1304
|
+
// pointed sync at one specific <sessionId>.jsonl. That hook silently fails for a
|
|
1305
|
+
// whole class of real usage — messages arriving through a channel (e.g. the
|
|
1306
|
+
// WeChat ClawBot) whose sessionKey the plugin can't map back to a sessions.json
|
|
1307
|
+
// entry, gateways where the plugin never loaded, or newer installs that keep
|
|
1308
|
+
// runtime rows in SQLite and only leave archived transcripts on disk. In all of
|
|
1309
|
+
// those cases usage exists but shows as 0. Scanning the on-disk transcripts on
|
|
1310
|
+
// every full sync (like every other passive provider) closes that gap; the
|
|
1311
|
+
// event-identity dedup in parseOpenclawSessionFile makes a plugin trigger and a
|
|
1312
|
+
// passive scan of the same file idempotent, so the two paths never double-count.
|
|
1313
|
+
// Mirrors OpenClaw's own precedence (OPENCLAW_HOME > state dir override), but
|
|
1314
|
+
// deliberately keeps os.homedir() — NOT env.HOME — as the base. OpenClaw itself
|
|
1315
|
+
// prefers env.HOME, yet every OpenClaw cursor we have ever written was keyed off
|
|
1316
|
+
// an os.homedir()-derived path; switching the base would make Git Bash / MSYS
|
|
1317
|
+
// (HOME=/c/Users/x vs C:\Users\x) miss its own cursor and re-count the whole
|
|
1318
|
+
// transcript history.
|
|
1319
|
+
function resolveOpenclawHome(env = process.env) {
|
|
1320
|
+
const override =
|
|
1321
|
+
(typeof env.TOKENTRACKER_OPENCLAW_HOME === "string" && env.TOKENTRACKER_OPENCLAW_HOME.trim()) ||
|
|
1322
|
+
(typeof env.OPENCLAW_HOME === "string" && env.OPENCLAW_HOME.trim()) ||
|
|
1323
|
+
(typeof env.OPENCLAW_STATE_DIR === "string" && env.OPENCLAW_STATE_DIR.trim()) ||
|
|
1324
|
+
"";
|
|
1325
|
+
return override || path.join(os.homedir(), ".openclaw");
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// On Windows the recommended OpenClaw install provisions an app-owned WSL
|
|
1329
|
+
// distro and runs the gateway inside it, so %USERPROFILE%\.openclaw is empty
|
|
1330
|
+
// while the real state dir sits on the distro's ext4 home (issue #264).
|
|
1331
|
+
function resolveOpenclawHomes(env = process.env, deps = {}) {
|
|
1332
|
+
const roots = [resolveOpenclawHome(env)];
|
|
1333
|
+
const platform = deps.platform || process.platform;
|
|
1334
|
+
const overridden =
|
|
1335
|
+
env.TOKENTRACKER_OPENCLAW_HOME || env.OPENCLAW_HOME || env.OPENCLAW_STATE_DIR;
|
|
1336
|
+
if (platform === "win32" && !overridden) {
|
|
1337
|
+
const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
|
|
1338
|
+
const wslRoot = wsl.shouldProbeWsl(env) ? discoverWslHome(".openclaw", { ...deps, env }) : null;
|
|
1339
|
+
if (wslRoot) roots.push(wslRoot);
|
|
1340
|
+
}
|
|
1341
|
+
return roots;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Transcript artifacts are not always a bare `<sessionId>.jsonl`: resets and
|
|
1345
|
+
// deletes leave `<sessionId>.jsonl.reset.<iso>` / `.deleted.<ts>` siblings, and
|
|
1346
|
+
// the SQLite migration moves still-hot transcripts into
|
|
1347
|
+
// `session-sqlite-import-archive/`. Match the same `*.jsonl*` shape other
|
|
1348
|
+
// OpenClaw readers use so those are not silently dropped.
|
|
1349
|
+
const OPENCLAW_TRANSCRIPT_SUBDIRS = ["sessions", "session-sqlite-import-archive"];
|
|
1350
|
+
|
|
1351
|
+
function isOpenclawTranscriptName(name) {
|
|
1352
|
+
return typeof name === "string" && name.includes(".jsonl") && !name.endsWith(".json");
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
async function resolveOpenclawSessionFiles(env = process.env, deps = {}) {
|
|
1356
|
+
const out = [];
|
|
1357
|
+
const seen = new Set();
|
|
1358
|
+
for (const openclawHome of resolveOpenclawHomes(env, deps)) {
|
|
1359
|
+
const agentsDir = path.join(openclawHome, "agents");
|
|
1360
|
+
const agents = await safeReadDir(agentsDir);
|
|
1361
|
+
for (const agent of agents) {
|
|
1362
|
+
if (!agent.isDirectory()) continue;
|
|
1363
|
+
for (const subdir of OPENCLAW_TRANSCRIPT_SUBDIRS) {
|
|
1364
|
+
const dir = path.join(agentsDir, agent.name, subdir);
|
|
1365
|
+
const entries = await safeReadDir(dir);
|
|
1366
|
+
for (const entry of entries) {
|
|
1367
|
+
if (!entry.isFile() || !isOpenclawTranscriptName(entry.name)) continue;
|
|
1368
|
+
const full = path.join(dir, entry.name);
|
|
1369
|
+
const key = openclawCursorKey(full);
|
|
1370
|
+
if (seen.has(key)) continue;
|
|
1371
|
+
seen.add(key);
|
|
1372
|
+
out.push(full);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
1378
|
+
return out;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1302
1381
|
async function parseOpenclawIncremental({
|
|
1303
1382
|
sessionFiles,
|
|
1304
1383
|
cursors,
|
|
@@ -1462,6 +1541,20 @@ async function parseOpenclawIncremental({
|
|
|
1462
1541
|
usageFingerprint: accepted.usageFingerprint,
|
|
1463
1542
|
updatedAt: new Date().toISOString(),
|
|
1464
1543
|
usageEvents: accepted.usageEvents,
|
|
1544
|
+
// Sticky: once a transcript has yielded real per-event usage, the
|
|
1545
|
+
// sessions.json totals fallback must defer to it to avoid double
|
|
1546
|
+
// counting the same tokens twice (see applyOpenclawTotalsFallback).
|
|
1547
|
+
//
|
|
1548
|
+
// The usageEvents check is what makes this work for EXISTING installs:
|
|
1549
|
+
// cursors written before this flag existed carry no `hasRealUsage`, and a
|
|
1550
|
+
// steady-state sync of an unchanged transcript aggregates 0 new events —
|
|
1551
|
+
// so keying off eventsAggregated alone would leave the flag false forever
|
|
1552
|
+
// and let the fallback keep double counting for exactly the users who
|
|
1553
|
+
// already have OpenClaw history.
|
|
1554
|
+
hasRealUsage:
|
|
1555
|
+
Boolean(prev?.hasRealUsage) ||
|
|
1556
|
+
accepted.result.eventsAggregated > 0 ||
|
|
1557
|
+
Object.keys(accepted.usageEvents || {}).length > 0,
|
|
1465
1558
|
};
|
|
1466
1559
|
cursorKeys.set(key, key);
|
|
1467
1560
|
|
|
@@ -4529,6 +4622,356 @@ async function parseQoderDbIncremental({
|
|
|
4529
4622
|
return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
4530
4623
|
}
|
|
4531
4624
|
|
|
4625
|
+
// ── Claude Science (Anthropic's local research workbench, issue #246) ──
|
|
4626
|
+
//
|
|
4627
|
+
// Claude Science keeps all of its state in a SQLite database named
|
|
4628
|
+
// `operon-cli.db`. There is no native Windows build — on Windows the app runs
|
|
4629
|
+
// INSIDE WSL, so the DB lives on the distro's ext4 home, not under %USERPROFILE%
|
|
4630
|
+
// (see resolveClaudeScienceDbPaths).
|
|
4631
|
+
//
|
|
4632
|
+
// Per-frame token usage lives on the `frames` table. A row is one agent
|
|
4633
|
+
// session/sub-session and records its OWN counters; Claude Science's own
|
|
4634
|
+
// per-conversation rollup (`sumTokensForRoot`) plainly sums every row under a
|
|
4635
|
+
// root, so summing across frames never double-counts a parent's children.
|
|
4636
|
+
//
|
|
4637
|
+
// TWO load-bearing quirks, both verified against real frames by reproducing
|
|
4638
|
+
// Claude Science's own cost function to 6 significant figures:
|
|
4639
|
+
//
|
|
4640
|
+
// 1. `input_tokens` is OpenAI-style — it ALREADY INCLUDES cache reads and
|
|
4641
|
+
// writes. Claude Science's accumulator adds `totalInputTokens`
|
|
4642
|
+
// (= cacheRead + cacheWrite + uncached), not the Anthropic API's
|
|
4643
|
+
// `input_tokens`. Treating the column as pure non-cached input prices cache
|
|
4644
|
+
// reads at the full input rate and inflates cost ~5.6x, while also counting
|
|
4645
|
+
// the cache columns twice in total_tokens (~1.97x). Subtract them back out.
|
|
4646
|
+
// 2. `aux_*` are real, billable usage from background helper calls (rolling
|
|
4647
|
+
// compaction, compaction summaries, artifact provenance extraction,
|
|
4648
|
+
// biosecurity screening, memory extraction). They are DISJOINT from the
|
|
4649
|
+
// headline columns — the summarizer call is issued with
|
|
4650
|
+
// `skip_cost_accumulation: true` so it bypasses the main accumulator — and
|
|
4651
|
+
// Claude Science's own rollup adds them in. Dropping them under-counts by
|
|
4652
|
+
// ~11.5%. `aux_input_tokens` carries the same cache-inclusive convention.
|
|
4653
|
+
//
|
|
4654
|
+
// Known approximation: aux tokens get priced at the frame's headline model
|
|
4655
|
+
// rate, but the helper calls actually run on a cheaper model, and Claude
|
|
4656
|
+
// Science does not persist which one (nor a per-aux cache split — the
|
|
4657
|
+
// aux_cache_* columns come back NULL even when aux_cost implies cache
|
|
4658
|
+
// reads). Measured on real frames, that overprices the aux slice ~2.4x,
|
|
4659
|
+
// i.e. ~20% high on the frame total. Counting the tokens and accepting a
|
|
4660
|
+
// bounded cost skew beats dropping 11.5% of usage outright, but if Claude
|
|
4661
|
+
// Science ever records the aux model, price that slice separately.
|
|
4662
|
+
//
|
|
4663
|
+
// `token_class_usage` is deliberately NOT read: it is an attribution *slice* of
|
|
4664
|
+
// the same tokens (assistant_prose / tool_calls / thinking / …), so its classes
|
|
4665
|
+
// sum back to the columns above and adding it would double count.
|
|
4666
|
+
// The cache and aux columns arrived in later drizzle migrations, so build the
|
|
4667
|
+
// SELECT from PRAGMA table_info rather than naming them blindly — an older
|
|
4668
|
+
// operon-cli.db must degrade to the columns it has instead of erroring the
|
|
4669
|
+
// whole provider out.
|
|
4670
|
+
//
|
|
4671
|
+
// The usage predicate doubles as the seed-frame filter: a fresh install ships
|
|
4672
|
+
// four Anthropic demo frames whose token columns are all NULL. Their
|
|
4673
|
+
// `context_data` JSON still carries the demo run's real numbers (~23.6M tokens
|
|
4674
|
+
// / $28.5), which is exactly why this parser reads columns and never that blob.
|
|
4675
|
+
const CLAUDE_SCIENCE_TOKEN_COLUMNS = [
|
|
4676
|
+
"input_tokens",
|
|
4677
|
+
"output_tokens",
|
|
4678
|
+
"cache_read_tokens",
|
|
4679
|
+
"cache_write_tokens",
|
|
4680
|
+
"aux_input_tokens",
|
|
4681
|
+
"aux_output_tokens",
|
|
4682
|
+
"aux_cache_read_tokens",
|
|
4683
|
+
"aux_cache_write_tokens",
|
|
4684
|
+
];
|
|
4685
|
+
|
|
4686
|
+
function buildClaudeScienceFramesQuery(columnNames) {
|
|
4687
|
+
const columns = new Set(columnNames);
|
|
4688
|
+
const optional = (col) => (columns.has(col) ? col : `NULL AS ${col}`);
|
|
4689
|
+
const present = CLAUDE_SCIENCE_TOKEN_COLUMNS.filter((col) => columns.has(col));
|
|
4690
|
+
const where = present.length
|
|
4691
|
+
? `WHERE ${present.map((col) => `COALESCE(${col}, 0) <> 0`).join("\n OR ")}`
|
|
4692
|
+
: "WHERE 0";
|
|
4693
|
+
return `
|
|
4694
|
+
SELECT
|
|
4695
|
+
id,
|
|
4696
|
+
parent_frame_id,
|
|
4697
|
+
model,
|
|
4698
|
+
${CLAUDE_SCIENCE_TOKEN_COLUMNS.map(optional).join(",\n ")},
|
|
4699
|
+
created_at,
|
|
4700
|
+
${optional("updated_at")},
|
|
4701
|
+
${optional("completed_at")}
|
|
4702
|
+
FROM frames
|
|
4703
|
+
${where}
|
|
4704
|
+
ORDER BY created_at, id
|
|
4705
|
+
`;
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
// `operon.db` is the pre-rename filename; both are still probed.
|
|
4709
|
+
const CLAUDE_SCIENCE_DB_FILENAMES = ["operon-cli.db", "operon.db"];
|
|
4710
|
+
|
|
4711
|
+
function resolveClaudeScienceRoot({ home = os.homedir(), env = process.env } = {}) {
|
|
4712
|
+
const override =
|
|
4713
|
+
typeof env.CLAUDE_SCIENCE_HOME === "string" && env.CLAUDE_SCIENCE_HOME.trim()
|
|
4714
|
+
? path.resolve(env.CLAUDE_SCIENCE_HOME.trim())
|
|
4715
|
+
: null;
|
|
4716
|
+
return override || path.join(home, ".claude-science");
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
function claudeScienceDbsUnderRoot(root) {
|
|
4720
|
+
const out = [];
|
|
4721
|
+
if (!root) return out;
|
|
4722
|
+
for (const name of CLAUDE_SCIENCE_DB_FILENAMES) {
|
|
4723
|
+
const candidate = path.join(root, name);
|
|
4724
|
+
if (fssync.existsSync(candidate)) out.push(candidate);
|
|
4725
|
+
}
|
|
4726
|
+
// Multi-org installs keep one DB per org under orgs/<slug>/.
|
|
4727
|
+
const orgsDir = path.join(root, "orgs");
|
|
4728
|
+
let orgs = [];
|
|
4729
|
+
try {
|
|
4730
|
+
orgs = fssync.readdirSync(orgsDir, { withFileTypes: true });
|
|
4731
|
+
} catch (_e) {
|
|
4732
|
+
orgs = [];
|
|
4733
|
+
}
|
|
4734
|
+
for (const org of orgs) {
|
|
4735
|
+
if (!org.isDirectory()) continue;
|
|
4736
|
+
for (const name of CLAUDE_SCIENCE_DB_FILENAMES) {
|
|
4737
|
+
const candidate = path.join(orgsDir, org.name, name);
|
|
4738
|
+
if (fssync.existsSync(candidate)) out.push(candidate);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
return out;
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
// Every Claude Science DB on this machine. There is no native Windows build —
|
|
4745
|
+
// Windows users run the app inside WSL, so on win32 the distro home is probed
|
|
4746
|
+
// too (same pattern as the other WSL-aware providers). Returns [] when nothing
|
|
4747
|
+
// is installed.
|
|
4748
|
+
function resolveClaudeScienceDbPaths({ home = os.homedir(), env = process.env, deps = {} } = {}) {
|
|
4749
|
+
const explicit =
|
|
4750
|
+
typeof env.CLAUDE_SCIENCE_DB_PATH === "string" && env.CLAUDE_SCIENCE_DB_PATH.trim()
|
|
4751
|
+
? path.resolve(env.CLAUDE_SCIENCE_DB_PATH.trim())
|
|
4752
|
+
: null;
|
|
4753
|
+
if (explicit) return fssync.existsSync(explicit) ? [explicit] : [];
|
|
4754
|
+
|
|
4755
|
+
const roots = [resolveClaudeScienceRoot({ home, env })];
|
|
4756
|
+
const platform = deps.platform || process.platform;
|
|
4757
|
+
if (platform === "win32" && !env.CLAUDE_SCIENCE_HOME) {
|
|
4758
|
+
const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
|
|
4759
|
+
const wslRoot = wsl.shouldProbeWsl(env)
|
|
4760
|
+
? discoverWslHome(".claude-science", { ...deps, env })
|
|
4761
|
+
: null;
|
|
4762
|
+
if (wslRoot) roots.push(wslRoot);
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
const seen = new Set();
|
|
4766
|
+
const out = [];
|
|
4767
|
+
for (const root of roots) {
|
|
4768
|
+
for (const dbPath of claudeScienceDbsUnderRoot(root)) {
|
|
4769
|
+
if (seen.has(dbPath)) continue;
|
|
4770
|
+
seen.add(dbPath);
|
|
4771
|
+
out.push(dbPath);
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
return out;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
// Kept for the default single-install path (and its existing callers/tests):
|
|
4778
|
+
// the conventional location, whether or not it exists yet.
|
|
4779
|
+
function resolveClaudeScienceDbPath({ home = os.homedir(), env = process.env } = {}) {
|
|
4780
|
+
const explicit =
|
|
4781
|
+
typeof env.CLAUDE_SCIENCE_DB_PATH === "string" && env.CLAUDE_SCIENCE_DB_PATH.trim()
|
|
4782
|
+
? env.CLAUDE_SCIENCE_DB_PATH.trim()
|
|
4783
|
+
: null;
|
|
4784
|
+
if (explicit) return path.resolve(explicit);
|
|
4785
|
+
return path.join(resolveClaudeScienceRoot({ home, env }), "operon-cli.db");
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
async function readClaudeScienceFrames(dbPath, sqliteOptions = {}) {
|
|
4789
|
+
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
4790
|
+
const options = { label: "Claude Science", readOnly: true, ...sqliteOptions };
|
|
4791
|
+
|
|
4792
|
+
// On Windows the DB lives inside WSL and is read over a \\wsl.localhost\ UNC
|
|
4793
|
+
// path. operon-cli.db runs in WAL mode (its -wal/-shm sidecars must be read
|
|
4794
|
+
// consistently with the main file), and SQLite over the 9p/UNC bridge can
|
|
4795
|
+
// fail to open the WAL or read a torn state. Snapshot to a local temp copy
|
|
4796
|
+
// first — same guard every other WSL-aware provider uses — and run BOTH the
|
|
4797
|
+
// PRAGMA probe and the frames read against that one consistent copy.
|
|
4798
|
+
let snapshot = null;
|
|
4799
|
+
let effectiveDbPath = dbPath;
|
|
4800
|
+
if (isUncPath(dbPath)) {
|
|
4801
|
+
try {
|
|
4802
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
4803
|
+
effectiveDbPath = snapshot.path;
|
|
4804
|
+
} catch (_e) {
|
|
4805
|
+
// Snapshot failed (permissions, transient I/O) — fall through to a direct
|
|
4806
|
+
// read so the non-UNC case never regresses.
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
try {
|
|
4811
|
+
const pragmaRows = await readSqliteJsonRowsAsync(
|
|
4812
|
+
effectiveDbPath,
|
|
4813
|
+
"PRAGMA table_info(frames)",
|
|
4814
|
+
options,
|
|
4815
|
+
);
|
|
4816
|
+
const columnNames = pragmaRows.map((row) => row?.name).filter(Boolean);
|
|
4817
|
+
// No `frames` table at all (wrong DB / pre-frames build) — not an error.
|
|
4818
|
+
if (columnNames.length === 0) return [];
|
|
4819
|
+
return await readSqliteJsonRowsAsync(
|
|
4820
|
+
effectiveDbPath,
|
|
4821
|
+
buildClaudeScienceFramesQuery(columnNames),
|
|
4822
|
+
options,
|
|
4823
|
+
);
|
|
4824
|
+
} finally {
|
|
4825
|
+
if (snapshot) snapshot.cleanup();
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
// `input` here is cache-inclusive (see the header note), so peel the cache
|
|
4830
|
+
// columns back off to recover pure non-cached input. Main and aux counters are
|
|
4831
|
+
// clamped independently — mixing them before the subtraction would let one
|
|
4832
|
+
// group's cache mask the other's shortfall.
|
|
4833
|
+
function claudeScienceUncachedInput(input, cacheRead, cacheWrite) {
|
|
4834
|
+
return Math.max(0, toNonNegativeInt(input) - toNonNegativeInt(cacheRead) - toNonNegativeInt(cacheWrite));
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
function normalizeClaudeScienceTokens(row) {
|
|
4838
|
+
const uncachedInput =
|
|
4839
|
+
claudeScienceUncachedInput(row?.input_tokens, row?.cache_read_tokens, row?.cache_write_tokens) +
|
|
4840
|
+
claudeScienceUncachedInput(
|
|
4841
|
+
row?.aux_input_tokens,
|
|
4842
|
+
row?.aux_cache_read_tokens,
|
|
4843
|
+
row?.aux_cache_write_tokens,
|
|
4844
|
+
);
|
|
4845
|
+
const output = toNonNegativeInt(row?.output_tokens) + toNonNegativeInt(row?.aux_output_tokens);
|
|
4846
|
+
const cacheRead =
|
|
4847
|
+
toNonNegativeInt(row?.cache_read_tokens) + toNonNegativeInt(row?.aux_cache_read_tokens);
|
|
4848
|
+
const cacheWrite =
|
|
4849
|
+
toNonNegativeInt(row?.cache_write_tokens) + toNonNegativeInt(row?.aux_cache_write_tokens);
|
|
4850
|
+
const total = uncachedInput + output + cacheRead + cacheWrite;
|
|
4851
|
+
if (total <= 0) return null;
|
|
4852
|
+
return {
|
|
4853
|
+
input_tokens: uncachedInput,
|
|
4854
|
+
cached_input_tokens: cacheRead,
|
|
4855
|
+
cache_creation_input_tokens: cacheWrite,
|
|
4856
|
+
output_tokens: output,
|
|
4857
|
+
reasoning_output_tokens: 0,
|
|
4858
|
+
total_tokens: total,
|
|
4859
|
+
billable_total_tokens: total,
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
function claudeScienceFrameTimestampMs(row) {
|
|
4864
|
+
return (
|
|
4865
|
+
coerceEpochMs(row?.completed_at) ||
|
|
4866
|
+
coerceEpochMs(row?.updated_at) ||
|
|
4867
|
+
coerceEpochMs(row?.created_at) ||
|
|
4868
|
+
// Tolerate ISO-8601 text timestamps in case a build stores them as strings.
|
|
4869
|
+
parseIsoTimestampMs(row?.completed_at) ||
|
|
4870
|
+
parseIsoTimestampMs(row?.updated_at) ||
|
|
4871
|
+
parseIsoTimestampMs(row?.created_at)
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
function parseIsoTimestampMs(value) {
|
|
4876
|
+
if (typeof value !== "string" || !value.trim()) return 0;
|
|
4877
|
+
const ms = Date.parse(value.trim());
|
|
4878
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
// Idempotent, subtract-on-change aggregation keyed by frame id (mirrors the
|
|
4882
|
+
// Qoder DB parser): frames are re-read in full on every sync, and a frame whose
|
|
4883
|
+
// counters grew between syncs has its previous contribution removed from its old
|
|
4884
|
+
// bucket before the new totals are added, so repeated syncs never inflate.
|
|
4885
|
+
async function parseClaudeScienceIncremental({
|
|
4886
|
+
dbRows,
|
|
4887
|
+
cursors,
|
|
4888
|
+
queuePath,
|
|
4889
|
+
onProgress,
|
|
4890
|
+
} = {}) {
|
|
4891
|
+
await ensureDir(path.dirname(queuePath));
|
|
4892
|
+
const rows = Array.isArray(dbRows) ? dbRows : [];
|
|
4893
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4894
|
+
const state = normalizeQoderState(cursors?.claudeScience);
|
|
4895
|
+
const touchedBuckets = new Set();
|
|
4896
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
4897
|
+
let recordsProcessed = 0;
|
|
4898
|
+
let eventsAggregated = 0;
|
|
4899
|
+
|
|
4900
|
+
for (let index = 0; index < rows.length; index++) {
|
|
4901
|
+
const row = rows[index];
|
|
4902
|
+
const frameId = normalizeMessageKeyPart(row?.id == null ? "" : String(row.id));
|
|
4903
|
+
const base = normalizeClaudeScienceTokens(row);
|
|
4904
|
+
const timestampMs = claudeScienceFrameTimestampMs(row);
|
|
4905
|
+
const bucketStart = timestampMs
|
|
4906
|
+
? toUtcHalfHourStart(new Date(timestampMs).toISOString())
|
|
4907
|
+
: null;
|
|
4908
|
+
if (!frameId || !base || !bucketStart) {
|
|
4909
|
+
recordsProcessed += 1;
|
|
4910
|
+
continue;
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
// A root frame (no parent) is one user-facing conversation; nested child
|
|
4914
|
+
// frames are sub-agent turns of the same conversation and must not inflate
|
|
4915
|
+
// the conversation count.
|
|
4916
|
+
const isRoot = !normalizeMessageKeyPart(
|
|
4917
|
+
row?.parent_frame_id == null ? "" : String(row.parent_frame_id),
|
|
4918
|
+
);
|
|
4919
|
+
const currentTotals = { ...base, conversation_count: isRoot ? 1 : 0 };
|
|
4920
|
+
const model = normalizeModelInput(row?.model) || "claude-science";
|
|
4921
|
+
|
|
4922
|
+
const previous = state.messages[frameId];
|
|
4923
|
+
const previousTotals =
|
|
4924
|
+
previous?.totals && typeof previous.totals === "object" ? previous.totals : null;
|
|
4925
|
+
const unchanged =
|
|
4926
|
+
previousTotals &&
|
|
4927
|
+
totalsKey(previousTotals) === totalsKey(currentTotals) &&
|
|
4928
|
+
previous.bucketStart === bucketStart &&
|
|
4929
|
+
previous.model === model;
|
|
4930
|
+
|
|
4931
|
+
if (!unchanged) {
|
|
4932
|
+
if (previousTotals && previous.bucketStart && previous.model) {
|
|
4933
|
+
const oldBucket = getHourlyBucket(
|
|
4934
|
+
hourlyState,
|
|
4935
|
+
"claude-science",
|
|
4936
|
+
previous.model,
|
|
4937
|
+
previous.bucketStart,
|
|
4938
|
+
);
|
|
4939
|
+
subtractTotals(oldBucket.totals, previousTotals);
|
|
4940
|
+
touchedBuckets.add(bucketKey("claude-science", previous.model, previous.bucketStart));
|
|
4941
|
+
}
|
|
4942
|
+
const bucket = getHourlyBucket(hourlyState, "claude-science", model, bucketStart);
|
|
4943
|
+
addTotals(bucket.totals, currentTotals);
|
|
4944
|
+
touchedBuckets.add(bucketKey("claude-science", model, bucketStart));
|
|
4945
|
+
state.messages[frameId] = {
|
|
4946
|
+
totals: currentTotals,
|
|
4947
|
+
bucketStart,
|
|
4948
|
+
model,
|
|
4949
|
+
updatedAt: new Date().toISOString(),
|
|
4950
|
+
};
|
|
4951
|
+
eventsAggregated += 1;
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
recordsProcessed += 1;
|
|
4955
|
+
if (cb) {
|
|
4956
|
+
cb({
|
|
4957
|
+
index: index + 1,
|
|
4958
|
+
total: rows.length,
|
|
4959
|
+
recordsProcessed,
|
|
4960
|
+
eventsAggregated,
|
|
4961
|
+
bucketsQueued: touchedBuckets.size,
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
4967
|
+
const updatedAt = new Date().toISOString();
|
|
4968
|
+
hourlyState.updatedAt = updatedAt;
|
|
4969
|
+
state.updatedAt = updatedAt;
|
|
4970
|
+
cursors.hourly = hourlyState;
|
|
4971
|
+
cursors.claudeScience = state;
|
|
4972
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4532
4975
|
// Resolve project context from DB message (no file path available)
|
|
4533
4976
|
async function resolveProjectContextForDb({
|
|
4534
4977
|
msg,
|
|
@@ -14689,6 +15132,14 @@ module.exports = {
|
|
|
14689
15132
|
parseQoderDbIncremental,
|
|
14690
15133
|
openclawCursorKey,
|
|
14691
15134
|
parseOpenclawIncremental,
|
|
15135
|
+
resolveOpenclawHome,
|
|
15136
|
+
resolveOpenclawHomes,
|
|
15137
|
+
resolveOpenclawSessionFiles,
|
|
15138
|
+
resolveClaudeScienceDbPath,
|
|
15139
|
+
resolveClaudeScienceDbPaths,
|
|
15140
|
+
buildClaudeScienceFramesQuery,
|
|
15141
|
+
readClaudeScienceFrames,
|
|
15142
|
+
parseClaudeScienceIncremental,
|
|
14692
15143
|
parseCursorApiIncremental,
|
|
14693
15144
|
parseKiroIncremental,
|
|
14694
15145
|
parseHermesIncremental,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,A as a}from"./__vite-browser-external-CM-H8-LW.js";import{u as p}from"./DialogTitle-H8sGiH7c.js";import{u as c}from"./useScrollLock-CF1m7AMJ.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as o}from"./__vite-browser-external-CM-H8-LW.js";import{h as r,i as t,p as s}from"./react-c1q4mGkS.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};
|