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.
Files changed (100) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +8 -7
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-DEqbHlmn.js → AchievementBadge-BgbH_mX4.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BOaEu1AQ.js → AchievementsPage-CQqkh1JS.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-C370ngO3.js → AchievementsSection-Dmkxpn0s.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D7Wkef0K.js → BadgeDetailModal-7OQf37lk.js} +1 -1
  10. package/dashboard/dist/assets/{Card-mDEVjNdM.js → Card-1PmWZRTK.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-DdsX-z8n.js → ClawdAnimated-CMJFagpX.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DhxRIuHC.js → CommandPalette-D4OrwF1w.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-CG-9JPS6.js → CommunityStatsModal-BDC9EcwZ.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-COhxErLm.js → ConfirmModal-22DL5C_9.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-tX0DKgFE.js → DashboardPage-CEXWzDtt.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-BsSeHx84.js → DevicePage-8uhHB7kX.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-eyOW2cbx.js → DialogClose-BcJx6VK-.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-D6zxtOM5.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-H8sGiH7c.js → DialogTitle-gRzunAqv.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-pq2ja50M.js → FadeIn-4rlPdBVe.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-DWgcpDHr.js → HeaderGithubStar-CR003aO3.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-CcJ_VR65.js → HoverTooltip-Dgz-dM8Q.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-CNUN346Q.js → IpCheckPage-B4PJRJP_.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-yaoRGsNI.js → LandingPage-3JQe-jOZ.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardAvatar-Ddai4I12.js → LeaderboardAvatar-CL9LxiEf.js} +1 -1
  26. package/dashboard/dist/assets/{LeaderboardPage-Bajkxf63.js → LeaderboardPage-DhIzM2Z2.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-BGYKDgIi.js → LeaderboardProfileModal-bbf5uJR5.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-Ck5vPaeN.js → LeaderboardProfilePage-CzuPxscw.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-Bqobtsw2.js → LimitsPage-B59rb2gg.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-e9KSE3wj.js → LocalOnlyNotice-BkK-6n1R.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-BhTSm2tb.js → LoginCard-BFGCzEjE.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CRwuThde.js → LoginModal-yhkyscFn.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-j1i74lcc.js → LoginPage-DwU5s0F7.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-r62ViU1X.js → PetPage-BPscYR6z.js} +1 -1
  35. package/dashboard/dist/assets/{PopoverPopup-DopNTyD4.js → PopoverPopup-E7PyGTTB.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-c1LkPhYM.js → ProviderIcon-CvmnzwKU.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DJdDNvtq.js → ResetPasswordPage-DD6qX616.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-DhWVOTPN.js → SegmentedControl-DjGBL_l1.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DeUmTmsJ.js → Select-B2MGmsi2.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-pX0B5f7h.js → SelectItemText-BaTwA_ZF.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-ejIPz6oF.js → ServiceStatusPage-0K0y8A6m.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-WEbn90GW.js → SessionsPage-CxrtRm3B.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B8t5gCeI.js → SettingsPage-CyvSWvDP.js} +1 -1
  44. package/dashboard/dist/assets/{SkillsPage-BWMlzeAp.js → SkillsPage-BVOU8t3t.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DIBRBvMj.js → TokenGalaxy-w7bwGloC.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-C6KxLvnI.js → ToolbarRootContext-BAzpDZej.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-BaMNh67w.js → TrendMonitor-DyT7Q4xp.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DDsy4M3x.js → WidgetsPage-eDDtopQT.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-CiNnUFRI.js → WrappedPage-CKkbdWHF.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-CM-H8-LW.js → __vite-browser-external-SU8sCTLB.js} +3 -3
  51. package/dashboard/dist/assets/{agent-logos-BjVa6oe7.js → agent-logos-bkh9XZJd.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-up-right-CCum1mqX.js → arrow-up-right-D16zCIw8.js} +1 -1
  53. package/dashboard/dist/assets/{check-ySRQc1YP.js → check-BFL17JWQ.js} +1 -1
  54. package/dashboard/dist/assets/{chevron-right-5vFloHew.js → chevron-right-CUJgrtkp.js} +1 -1
  55. package/dashboard/dist/assets/{copy-mhfKVMKo.js → copy-CRQiCjgs.js} +1 -1
  56. package/dashboard/dist/assets/{download-GdrXglSx.js → download-Cd12nDrQ.js} +1 -1
  57. package/dashboard/dist/assets/{external-link-KGFaKc8s.js → external-link-CvAfnsG-.js} +1 -1
  58. package/dashboard/dist/assets/{flame-BmNtNoS1.js → flame-CCi681Yl.js} +1 -1
  59. package/dashboard/dist/assets/{format-tokens-CCpCNw3d.js → format-tokens-BAVt8uTZ.js} +1 -1
  60. package/dashboard/dist/assets/{icons-XLKZavTf.js → icons-CbKC96gR.js} +1 -1
  61. package/dashboard/dist/assets/{index-_xU5U5Ct.js → index--TDvaL1y.js} +1 -1
  62. package/dashboard/dist/assets/{index-CsywTeCQ.js → index-B5b6PCh5.js} +1 -1
  63. package/dashboard/dist/assets/{index-IKpmeZte.js → index-B9LWn-mo.js} +1 -1
  64. package/dashboard/dist/assets/{info-DUV5UpmT.js → info-BPFpD--M.js} +1 -1
  65. package/dashboard/dist/assets/{limits-providers-DFkCZCZj.js → limits-providers-Cyedt-WJ.js} +1 -1
  66. package/dashboard/dist/assets/{link-2-D7c64yLY.js → link-2-BkEPPdfC.js} +1 -1
  67. package/dashboard/dist/assets/{loader-circle-sMONRK2M.js → loader-circle-CzASiylJ.js} +1 -1
  68. package/dashboard/dist/assets/{maximize-2-CVWRgrk9.js → maximize-2-CkwnHUJn.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-DSKqVjgm.js → provider-display-BXBBedC-.js} +2 -2
  70. package/dashboard/dist/assets/{react-c1q4mGkS.js → react-BXSctDQj.js} +1 -1
  71. package/dashboard/dist/assets/{search-CwD3s-_C.js → search-_qGma9tM.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-BRXj8kjT.js → skills-api-DdmJjnJ9.js} +1 -1
  73. package/dashboard/dist/assets/{store-lqTW2gfK.js → store-owYCN530.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-DU1_KbKo.js → terminal-Clzw-KEF.js} +1 -1
  75. package/dashboard/dist/assets/{trash-2-B8Y5Xc-K.js → trash-2-C5Giwp_E.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-DYwwimvp.js → use-community-stats-Dk8UjLEr.js} +1 -1
  77. package/dashboard/dist/assets/{use-limits-display-prefs-Bst6ybK3.js → use-limits-display-prefs-CArM595Q.js} +1 -1
  78. package/dashboard/dist/assets/{use-native-settings-Cqle4lgw.js → use-native-settings-Cpd69kiF.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-Dtze0ahb.js → use-ordered-list--CzwhBTx.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-CcILnpU1.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-gwbDMHXa.js → use-session-efficiency-pref-DUXO8cCg.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-CU0yxXpX.js → use-transform-Cg3kuMj_.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-5y1I1T5_.js → use-usage-limits-DY9_mV28.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-BfVNrywe.js → useCurrency-BIe86hMn.js} +1 -1
  85. package/dashboard/dist/assets/{useScrollLock-CF1m7AMJ.js → useScrollLock-7-hejkNC.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-DiRwVvRr.js → useTokenFormat-DEtLbRgQ.js} +1 -1
  87. package/dashboard/dist/assets/{zap-BZtlWtuN.js → zap-DUxY1d-O.js} +1 -1
  88. package/dashboard/dist/index.html +12 -10
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/llms.txt +3 -2
  92. package/dashboard/dist/share.html +1 -1
  93. package/package.json +2 -2
  94. package/src/commands/init.js +1 -0
  95. package/src/commands/status.js +15 -0
  96. package/src/commands/sync.js +114 -8
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +451 -0
  99. package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +0 -1
  100. package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +0 -1
@@ -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};