tokentracker-cli 0.85.0 → 0.86.1

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 (102) hide show
  1. package/README.de.md +7 -4
  2. package/README.ja.md +7 -4
  3. package/README.ko.md +7 -4
  4. package/README.md +10 -7
  5. package/README.zh-CN.md +7 -4
  6. package/dashboard/dist/assets/{AchievementBadge-DEqbHlmn.js → AchievementBadge-BHWjTRZh.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BOaEu1AQ.js → AchievementsPage-X6Z6fiAx.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-C370ngO3.js → AchievementsSection-DL2CccQ8.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D7Wkef0K.js → BadgeDetailModal-DKn88JDX.js} +1 -1
  10. package/dashboard/dist/assets/{Card-mDEVjNdM.js → Card-B_gKsFZR.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-DdsX-z8n.js → ClawdAnimated-vEB5WCNu.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DhxRIuHC.js → CommandPalette-BoxeCsUF.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-CG-9JPS6.js → CommunityStatsModal-BwELjQld.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-COhxErLm.js → ConfirmModal-j8yHWm2k.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-tX0DKgFE.js → DashboardPage-CkvAOuBF.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-BsSeHx84.js → DevicePage-k4Lr11LG.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-eyOW2cbx.js → DialogClose-C-ZhX9eY.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-BRiqm-uZ.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-H8sGiH7c.js → DialogTitle-XCGSHZDz.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-pq2ja50M.js → FadeIn-ljP_7vPC.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-DWgcpDHr.js → HeaderGithubStar-D4QupeSb.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-CcJ_VR65.js → HoverTooltip-Dkpz8RmH.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-CNUN346Q.js → IpCheckPage-WkZK9Tr3.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-Dp3m0uJg.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-Ddai4I12.js → LeaderboardAvatar-DleP6bK7.js} +1 -1
  26. package/dashboard/dist/assets/{LeaderboardPage-Bajkxf63.js → LeaderboardPage-DTwMMaF2.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-BGYKDgIi.js → LeaderboardProfileModal-xdDlcRDd.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-Ck5vPaeN.js → LeaderboardProfilePage-Ckytgq9h.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-Bqobtsw2.js → LimitsPage-D_2cV3S4.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-e9KSE3wj.js → LocalOnlyNotice-DpCb8VIO.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-BhTSm2tb.js → LoginCard-BeAb35_3.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CRwuThde.js → LoginModal-DNdAgKeE.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-j1i74lcc.js → LoginPage-nuWkBuqy.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-r62ViU1X.js → PetPage-CCLFROsF.js} +1 -1
  35. package/dashboard/dist/assets/{PopoverPopup-DopNTyD4.js → PopoverPopup-BCaeRKf4.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-c1LkPhYM.js → ProviderIcon-C_Fpc2jL.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DJdDNvtq.js → ResetPasswordPage-lLDxtpC9.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-DhWVOTPN.js → SegmentedControl-Bnfv5lDS.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DeUmTmsJ.js → Select-CvY_Y0WS.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-pX0B5f7h.js → SelectItemText-DX4QYfBW.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-ejIPz6oF.js → ServiceStatusPage-DcUrtacl.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-WEbn90GW.js → SessionsPage-CJXyaHQA.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B8t5gCeI.js → SettingsPage-BkgPxhGn.js} +1 -1
  44. package/dashboard/dist/assets/{SkillsPage-BWMlzeAp.js → SkillsPage-BGvK3HZ0.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DIBRBvMj.js → TokenGalaxy-D9_H79bK.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-C6KxLvnI.js → ToolbarRootContext-BsM_3QBs.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-BaMNh67w.js → TrendMonitor-D2QSVlSi.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DDsy4M3x.js → WidgetsPage-BpWOr7-c.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-CiNnUFRI.js → WrappedPage-DliB3OZk.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-CM-H8-LW.js → __vite-browser-external-CzDhMv0q.js} +15 -14
  51. package/dashboard/dist/assets/{agent-logos-BjVa6oe7.js → agent-logos-Q7rTqkGv.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-up-right-CCum1mqX.js → arrow-up-right-DwqQgMsR.js} +1 -1
  53. package/dashboard/dist/assets/{check-ySRQc1YP.js → check-Cypjnyke.js} +1 -1
  54. package/dashboard/dist/assets/{chevron-right-5vFloHew.js → chevron-right-LZRe0crm.js} +1 -1
  55. package/dashboard/dist/assets/{copy-mhfKVMKo.js → copy-BUe15mXV.js} +1 -1
  56. package/dashboard/dist/assets/{download-GdrXglSx.js → download-BhjaQuUD.js} +1 -1
  57. package/dashboard/dist/assets/{external-link-KGFaKc8s.js → external-link-BHU7m_Df.js} +1 -1
  58. package/dashboard/dist/assets/{flame-BmNtNoS1.js → flame-BPQ7FpRu.js} +1 -1
  59. package/dashboard/dist/assets/{format-tokens-CCpCNw3d.js → format-tokens-C0XEJ96L.js} +1 -1
  60. package/dashboard/dist/assets/{icons-XLKZavTf.js → icons-De5Qo1lp.js} +1 -1
  61. package/dashboard/dist/assets/{index-IKpmeZte.js → index-Cl8hqOBU.js} +1 -1
  62. package/dashboard/dist/assets/{index-_xU5U5Ct.js → index-UfiJKvuV.js} +1 -1
  63. package/dashboard/dist/assets/{index-CsywTeCQ.js → index-lmWJH5W9.js} +1 -1
  64. package/dashboard/dist/assets/{info-DUV5UpmT.js → info-CCPDu6hQ.js} +1 -1
  65. package/dashboard/dist/assets/{limits-providers-DFkCZCZj.js → limits-providers-B2Qjydn9.js} +1 -1
  66. package/dashboard/dist/assets/{link-2-D7c64yLY.js → link-2-Dl1DAfTL.js} +1 -1
  67. package/dashboard/dist/assets/{loader-circle-sMONRK2M.js → loader-circle-C-mC8s6j.js} +1 -1
  68. package/dashboard/dist/assets/{maximize-2-CVWRgrk9.js → maximize-2-C9YpCcsr.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-DSKqVjgm.js → provider-display-DNXQkneb.js} +2 -2
  70. package/dashboard/dist/assets/{react-c1q4mGkS.js → react-LsYzFOQW.js} +1 -1
  71. package/dashboard/dist/assets/{search-CwD3s-_C.js → search-D5SGjOTl.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-BRXj8kjT.js → skills-api-CGRg1lZw.js} +1 -1
  73. package/dashboard/dist/assets/{store-lqTW2gfK.js → store-BMdm145i.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-DU1_KbKo.js → terminal-HhtP7nqJ.js} +1 -1
  75. package/dashboard/dist/assets/{trash-2-B8Y5Xc-K.js → trash-2-CeEoITVO.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-DYwwimvp.js → use-community-stats-BTZHzzWd.js} +1 -1
  77. package/dashboard/dist/assets/{use-limits-display-prefs-Bst6ybK3.js → use-limits-display-prefs-DPNlMKcg.js} +1 -1
  78. package/dashboard/dist/assets/{use-native-settings-Cqle4lgw.js → use-native-settings-DFF0ouY5.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-Dtze0ahb.js → use-ordered-list-CCLtmxXg.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-DUtjZjIj.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-gwbDMHXa.js → use-session-efficiency-pref-CrvLKyhz.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-CU0yxXpX.js → use-transform-BnJ0PftW.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-5y1I1T5_.js → use-usage-limits-BY3ZXlDo.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-BfVNrywe.js → useCurrency-BiFDQKMu.js} +1 -1
  85. package/dashboard/dist/assets/{useScrollLock-CF1m7AMJ.js → useScrollLock-BXUoxe5p.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-DiRwVvRr.js → useTokenFormat-C6DBFS-R.js} +1 -1
  87. package/dashboard/dist/assets/{zap-BZtlWtuN.js → zap-9H5t1Kye.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 +117 -10
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +471 -7
  99. package/src/lib/wsl-probe.js +36 -0
  100. package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +0 -1
  101. package/dashboard/dist/assets/LandingPage-yaoRGsNI.js +0 -2
  102. package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +0 -1
@@ -760,6 +760,11 @@ async function parseClaudeIncremental({
760
760
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
761
761
  const projectMetaCache = projectEnabled ? new Map() : null;
762
762
  const publicRepoCache = projectEnabled ? new Map() : null;
763
+ // Sessions cluster into few repos, so the per-file freshness checks below
764
+ // keep stat-ing the same handful of .git/config paths. Share one stat per
765
+ // config per run, as the Codex path does — it matters most for WSL installs,
766
+ // where every stat crosses the \\wsl$ bridge (#374).
767
+ const projectFreshnessCache = projectEnabled ? new Map() : null;
763
768
  const touchedBuckets = new Set();
764
769
  // Persist seenMessageHashes across syncs to prevent cross-file duplicates
765
770
  // (e.g. subagent file created after main session was already parsed).
@@ -878,7 +883,9 @@ async function parseClaudeIncremental({
878
883
  const cachedContextAbsent = cachedProjectFileContext?.absent === true;
879
884
  const projectContextFresh =
880
885
  projectEnabled && cachedProjectFileContext && !cachedContextAbsent
881
- ? await isProjectFileContextFresh(cachedProjectFileContext)
886
+ ? await isProjectFileContextFresh(cachedProjectFileContext, {
887
+ freshnessCache: projectFreshnessCache,
888
+ })
882
889
  : false;
883
890
  const projectKnownAbsent = cachedCwd === null || cachedContextAbsent;
884
891
  const projectSettled =
@@ -909,7 +916,7 @@ async function parseClaudeIncremental({
909
916
  }
910
917
  if (nextCwd) {
911
918
  const projectContext = await resolveProjectContextForPath({
912
- startDir: nextCwd,
919
+ startDir: wsl.mapWslCwdToUnc(nextCwd, filePath),
913
920
  projectMetaCache,
914
921
  publicRepoCache,
915
922
  publicRepoResolver,
@@ -1299,6 +1306,85 @@ async function parseOpencodeIncremental({
1299
1306
  return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
1300
1307
  }
1301
1308
 
1309
+ // Passive discovery of OpenClaw session transcripts (issue #264). OpenClaw was
1310
+ // previously counted ONLY when its session plugin fired an `agent_end` hook and
1311
+ // pointed sync at one specific <sessionId>.jsonl. That hook silently fails for a
1312
+ // whole class of real usage — messages arriving through a channel (e.g. the
1313
+ // WeChat ClawBot) whose sessionKey the plugin can't map back to a sessions.json
1314
+ // entry, gateways where the plugin never loaded, or newer installs that keep
1315
+ // runtime rows in SQLite and only leave archived transcripts on disk. In all of
1316
+ // those cases usage exists but shows as 0. Scanning the on-disk transcripts on
1317
+ // every full sync (like every other passive provider) closes that gap; the
1318
+ // event-identity dedup in parseOpenclawSessionFile makes a plugin trigger and a
1319
+ // passive scan of the same file idempotent, so the two paths never double-count.
1320
+ // Mirrors OpenClaw's own precedence (OPENCLAW_HOME > state dir override), but
1321
+ // deliberately keeps os.homedir() — NOT env.HOME — as the base. OpenClaw itself
1322
+ // prefers env.HOME, yet every OpenClaw cursor we have ever written was keyed off
1323
+ // an os.homedir()-derived path; switching the base would make Git Bash / MSYS
1324
+ // (HOME=/c/Users/x vs C:\Users\x) miss its own cursor and re-count the whole
1325
+ // transcript history.
1326
+ function resolveOpenclawHome(env = process.env) {
1327
+ const override =
1328
+ (typeof env.TOKENTRACKER_OPENCLAW_HOME === "string" && env.TOKENTRACKER_OPENCLAW_HOME.trim()) ||
1329
+ (typeof env.OPENCLAW_HOME === "string" && env.OPENCLAW_HOME.trim()) ||
1330
+ (typeof env.OPENCLAW_STATE_DIR === "string" && env.OPENCLAW_STATE_DIR.trim()) ||
1331
+ "";
1332
+ return override || path.join(os.homedir(), ".openclaw");
1333
+ }
1334
+
1335
+ // On Windows the recommended OpenClaw install provisions an app-owned WSL
1336
+ // distro and runs the gateway inside it, so %USERPROFILE%\.openclaw is empty
1337
+ // while the real state dir sits on the distro's ext4 home (issue #264).
1338
+ function resolveOpenclawHomes(env = process.env, deps = {}) {
1339
+ const roots = [resolveOpenclawHome(env)];
1340
+ const platform = deps.platform || process.platform;
1341
+ const overridden =
1342
+ env.TOKENTRACKER_OPENCLAW_HOME || env.OPENCLAW_HOME || env.OPENCLAW_STATE_DIR;
1343
+ if (platform === "win32" && !overridden) {
1344
+ const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
1345
+ const wslRoot = wsl.shouldProbeWsl(env) ? discoverWslHome(".openclaw", { ...deps, env }) : null;
1346
+ if (wslRoot) roots.push(wslRoot);
1347
+ }
1348
+ return roots;
1349
+ }
1350
+
1351
+ // Transcript artifacts are not always a bare `<sessionId>.jsonl`: resets and
1352
+ // deletes leave `<sessionId>.jsonl.reset.<iso>` / `.deleted.<ts>` siblings, and
1353
+ // the SQLite migration moves still-hot transcripts into
1354
+ // `session-sqlite-import-archive/`. Match the same `*.jsonl*` shape other
1355
+ // OpenClaw readers use so those are not silently dropped.
1356
+ const OPENCLAW_TRANSCRIPT_SUBDIRS = ["sessions", "session-sqlite-import-archive"];
1357
+
1358
+ function isOpenclawTranscriptName(name) {
1359
+ return typeof name === "string" && name.includes(".jsonl") && !name.endsWith(".json");
1360
+ }
1361
+
1362
+ async function resolveOpenclawSessionFiles(env = process.env, deps = {}) {
1363
+ const out = [];
1364
+ const seen = new Set();
1365
+ for (const openclawHome of resolveOpenclawHomes(env, deps)) {
1366
+ const agentsDir = path.join(openclawHome, "agents");
1367
+ const agents = await safeReadDir(agentsDir);
1368
+ for (const agent of agents) {
1369
+ if (!agent.isDirectory()) continue;
1370
+ for (const subdir of OPENCLAW_TRANSCRIPT_SUBDIRS) {
1371
+ const dir = path.join(agentsDir, agent.name, subdir);
1372
+ const entries = await safeReadDir(dir);
1373
+ for (const entry of entries) {
1374
+ if (!entry.isFile() || !isOpenclawTranscriptName(entry.name)) continue;
1375
+ const full = path.join(dir, entry.name);
1376
+ const key = openclawCursorKey(full);
1377
+ if (seen.has(key)) continue;
1378
+ seen.add(key);
1379
+ out.push(full);
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
+ out.sort((a, b) => a.localeCompare(b));
1385
+ return out;
1386
+ }
1387
+
1302
1388
  async function parseOpenclawIncremental({
1303
1389
  sessionFiles,
1304
1390
  cursors,
@@ -1462,6 +1548,20 @@ async function parseOpenclawIncremental({
1462
1548
  usageFingerprint: accepted.usageFingerprint,
1463
1549
  updatedAt: new Date().toISOString(),
1464
1550
  usageEvents: accepted.usageEvents,
1551
+ // Sticky: once a transcript has yielded real per-event usage, the
1552
+ // sessions.json totals fallback must defer to it to avoid double
1553
+ // counting the same tokens twice (see applyOpenclawTotalsFallback).
1554
+ //
1555
+ // The usageEvents check is what makes this work for EXISTING installs:
1556
+ // cursors written before this flag existed carry no `hasRealUsage`, and a
1557
+ // steady-state sync of an unchanged transcript aggregates 0 new events —
1558
+ // so keying off eventsAggregated alone would leave the flag false forever
1559
+ // and let the fallback keep double counting for exactly the users who
1560
+ // already have OpenClaw history.
1561
+ hasRealUsage:
1562
+ Boolean(prev?.hasRealUsage) ||
1563
+ accepted.result.eventsAggregated > 0 ||
1564
+ Object.keys(accepted.usageEvents || {}).length > 0,
1465
1565
  };
1466
1566
  cursorKeys.set(key, key);
1467
1567
 
@@ -1932,7 +2032,7 @@ async function parseRolloutFile({
1932
2032
  const nextCwd = obj.payload.cwd.trim();
1933
2033
  if (nextCwd && nextCwd !== currentCwd) {
1934
2034
  const context = await resolveProjectContextForPath({
1935
- startDir: nextCwd,
2035
+ startDir: wsl.mapWslCwdToUnc(nextCwd, filePath),
1936
2036
  projectMetaCache,
1937
2037
  publicRepoCache,
1938
2038
  publicRepoResolver,
@@ -2126,7 +2226,7 @@ async function scanRolloutProjectFileContexts({
2126
2226
  const nextCwd = obj.payload.cwd.trim();
2127
2227
  if (!nextCwd || nextCwd === currentCwd) continue;
2128
2228
  const context = await resolveProjectContextForPath({
2129
- startDir: nextCwd,
2229
+ startDir: wsl.mapWslCwdToUnc(nextCwd, filePath),
2130
2230
  projectMetaCache,
2131
2231
  publicRepoCache,
2132
2232
  publicRepoResolver,
@@ -4036,6 +4136,7 @@ function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
4036
4136
 
4037
4137
  async function parseOpencodeDbIncremental({
4038
4138
  dbMessages,
4139
+ dbPath,
4039
4140
  cursors,
4040
4141
  queuePath,
4041
4142
  projectQueuePath,
@@ -4132,6 +4233,7 @@ async function parseOpencodeDbIncremental({
4132
4233
  if (projectEnabled) {
4133
4234
  const projectContext = await resolveProjectContextForDb({
4134
4235
  msg,
4236
+ dbPath,
4135
4237
  projectMetaCache,
4136
4238
  publicRepoCache,
4137
4239
  publicRepoResolver,
@@ -4351,6 +4453,7 @@ function qoderProjectPath(row) {
4351
4453
 
4352
4454
  async function parseQoderDbIncremental({
4353
4455
  dbMessages,
4456
+ dbPath,
4354
4457
  cursors,
4355
4458
  queuePath,
4356
4459
  projectQueuePath,
@@ -4422,7 +4525,7 @@ async function parseQoderDbIncremental({
4422
4525
  const projectPath = qoderProjectPath(row);
4423
4526
  if (projectPath) {
4424
4527
  const context = await resolveProjectContextForPath({
4425
- startDir: projectPath,
4528
+ startDir: wsl.mapWslCwdToUnc(projectPath, dbPath),
4426
4529
  projectMetaCache,
4427
4530
  publicRepoCache,
4428
4531
  publicRepoResolver,
@@ -4529,9 +4632,360 @@ async function parseQoderDbIncremental({
4529
4632
  return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
4530
4633
  }
4531
4634
 
4635
+ // ── Claude Science (Anthropic's local research workbench, issue #246) ──
4636
+ //
4637
+ // Claude Science keeps all of its state in a SQLite database named
4638
+ // `operon-cli.db`. There is no native Windows build — on Windows the app runs
4639
+ // INSIDE WSL, so the DB lives on the distro's ext4 home, not under %USERPROFILE%
4640
+ // (see resolveClaudeScienceDbPaths).
4641
+ //
4642
+ // Per-frame token usage lives on the `frames` table. A row is one agent
4643
+ // session/sub-session and records its OWN counters; Claude Science's own
4644
+ // per-conversation rollup (`sumTokensForRoot`) plainly sums every row under a
4645
+ // root, so summing across frames never double-counts a parent's children.
4646
+ //
4647
+ // TWO load-bearing quirks, both verified against real frames by reproducing
4648
+ // Claude Science's own cost function to 6 significant figures:
4649
+ //
4650
+ // 1. `input_tokens` is OpenAI-style — it ALREADY INCLUDES cache reads and
4651
+ // writes. Claude Science's accumulator adds `totalInputTokens`
4652
+ // (= cacheRead + cacheWrite + uncached), not the Anthropic API's
4653
+ // `input_tokens`. Treating the column as pure non-cached input prices cache
4654
+ // reads at the full input rate and inflates cost ~5.6x, while also counting
4655
+ // the cache columns twice in total_tokens (~1.97x). Subtract them back out.
4656
+ // 2. `aux_*` are real, billable usage from background helper calls (rolling
4657
+ // compaction, compaction summaries, artifact provenance extraction,
4658
+ // biosecurity screening, memory extraction). They are DISJOINT from the
4659
+ // headline columns — the summarizer call is issued with
4660
+ // `skip_cost_accumulation: true` so it bypasses the main accumulator — and
4661
+ // Claude Science's own rollup adds them in. Dropping them under-counts by
4662
+ // ~11.5%. `aux_input_tokens` carries the same cache-inclusive convention.
4663
+ //
4664
+ // Known approximation: aux tokens get priced at the frame's headline model
4665
+ // rate, but the helper calls actually run on a cheaper model, and Claude
4666
+ // Science does not persist which one (nor a per-aux cache split — the
4667
+ // aux_cache_* columns come back NULL even when aux_cost implies cache
4668
+ // reads). Measured on real frames, that overprices the aux slice ~2.4x,
4669
+ // i.e. ~20% high on the frame total. Counting the tokens and accepting a
4670
+ // bounded cost skew beats dropping 11.5% of usage outright, but if Claude
4671
+ // Science ever records the aux model, price that slice separately.
4672
+ //
4673
+ // `token_class_usage` is deliberately NOT read: it is an attribution *slice* of
4674
+ // the same tokens (assistant_prose / tool_calls / thinking / …), so its classes
4675
+ // sum back to the columns above and adding it would double count.
4676
+ // The cache and aux columns arrived in later drizzle migrations, so build the
4677
+ // SELECT from PRAGMA table_info rather than naming them blindly — an older
4678
+ // operon-cli.db must degrade to the columns it has instead of erroring the
4679
+ // whole provider out.
4680
+ //
4681
+ // The usage predicate doubles as the seed-frame filter: a fresh install ships
4682
+ // four Anthropic demo frames whose token columns are all NULL. Their
4683
+ // `context_data` JSON still carries the demo run's real numbers (~23.6M tokens
4684
+ // / $28.5), which is exactly why this parser reads columns and never that blob.
4685
+ const CLAUDE_SCIENCE_TOKEN_COLUMNS = [
4686
+ "input_tokens",
4687
+ "output_tokens",
4688
+ "cache_read_tokens",
4689
+ "cache_write_tokens",
4690
+ "aux_input_tokens",
4691
+ "aux_output_tokens",
4692
+ "aux_cache_read_tokens",
4693
+ "aux_cache_write_tokens",
4694
+ ];
4695
+
4696
+ function buildClaudeScienceFramesQuery(columnNames) {
4697
+ const columns = new Set(columnNames);
4698
+ const optional = (col) => (columns.has(col) ? col : `NULL AS ${col}`);
4699
+ const present = CLAUDE_SCIENCE_TOKEN_COLUMNS.filter((col) => columns.has(col));
4700
+ const where = present.length
4701
+ ? `WHERE ${present.map((col) => `COALESCE(${col}, 0) <> 0`).join("\n OR ")}`
4702
+ : "WHERE 0";
4703
+ return `
4704
+ SELECT
4705
+ id,
4706
+ parent_frame_id,
4707
+ model,
4708
+ ${CLAUDE_SCIENCE_TOKEN_COLUMNS.map(optional).join(",\n ")},
4709
+ created_at,
4710
+ ${optional("updated_at")},
4711
+ ${optional("completed_at")}
4712
+ FROM frames
4713
+ ${where}
4714
+ ORDER BY created_at, id
4715
+ `;
4716
+ }
4717
+
4718
+ // `operon.db` is the pre-rename filename; both are still probed.
4719
+ const CLAUDE_SCIENCE_DB_FILENAMES = ["operon-cli.db", "operon.db"];
4720
+
4721
+ function resolveClaudeScienceRoot({ home = os.homedir(), env = process.env } = {}) {
4722
+ const override =
4723
+ typeof env.CLAUDE_SCIENCE_HOME === "string" && env.CLAUDE_SCIENCE_HOME.trim()
4724
+ ? path.resolve(env.CLAUDE_SCIENCE_HOME.trim())
4725
+ : null;
4726
+ return override || path.join(home, ".claude-science");
4727
+ }
4728
+
4729
+ function claudeScienceDbsUnderRoot(root) {
4730
+ const out = [];
4731
+ if (!root) return out;
4732
+ for (const name of CLAUDE_SCIENCE_DB_FILENAMES) {
4733
+ const candidate = path.join(root, name);
4734
+ if (fssync.existsSync(candidate)) out.push(candidate);
4735
+ }
4736
+ // Multi-org installs keep one DB per org under orgs/<slug>/.
4737
+ const orgsDir = path.join(root, "orgs");
4738
+ let orgs = [];
4739
+ try {
4740
+ orgs = fssync.readdirSync(orgsDir, { withFileTypes: true });
4741
+ } catch (_e) {
4742
+ orgs = [];
4743
+ }
4744
+ for (const org of orgs) {
4745
+ if (!org.isDirectory()) continue;
4746
+ for (const name of CLAUDE_SCIENCE_DB_FILENAMES) {
4747
+ const candidate = path.join(orgsDir, org.name, name);
4748
+ if (fssync.existsSync(candidate)) out.push(candidate);
4749
+ }
4750
+ }
4751
+ return out;
4752
+ }
4753
+
4754
+ // Every Claude Science DB on this machine. There is no native Windows build —
4755
+ // Windows users run the app inside WSL, so on win32 the distro home is probed
4756
+ // too (same pattern as the other WSL-aware providers). Returns [] when nothing
4757
+ // is installed.
4758
+ function resolveClaudeScienceDbPaths({ home = os.homedir(), env = process.env, deps = {} } = {}) {
4759
+ const explicit =
4760
+ typeof env.CLAUDE_SCIENCE_DB_PATH === "string" && env.CLAUDE_SCIENCE_DB_PATH.trim()
4761
+ ? path.resolve(env.CLAUDE_SCIENCE_DB_PATH.trim())
4762
+ : null;
4763
+ if (explicit) return fssync.existsSync(explicit) ? [explicit] : [];
4764
+
4765
+ const roots = [resolveClaudeScienceRoot({ home, env })];
4766
+ const platform = deps.platform || process.platform;
4767
+ if (platform === "win32" && !env.CLAUDE_SCIENCE_HOME) {
4768
+ const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
4769
+ const wslRoot = wsl.shouldProbeWsl(env)
4770
+ ? discoverWslHome(".claude-science", { ...deps, env })
4771
+ : null;
4772
+ if (wslRoot) roots.push(wslRoot);
4773
+ }
4774
+
4775
+ const seen = new Set();
4776
+ const out = [];
4777
+ for (const root of roots) {
4778
+ for (const dbPath of claudeScienceDbsUnderRoot(root)) {
4779
+ if (seen.has(dbPath)) continue;
4780
+ seen.add(dbPath);
4781
+ out.push(dbPath);
4782
+ }
4783
+ }
4784
+ return out;
4785
+ }
4786
+
4787
+ // Kept for the default single-install path (and its existing callers/tests):
4788
+ // the conventional location, whether or not it exists yet.
4789
+ function resolveClaudeScienceDbPath({ home = os.homedir(), env = process.env } = {}) {
4790
+ const explicit =
4791
+ typeof env.CLAUDE_SCIENCE_DB_PATH === "string" && env.CLAUDE_SCIENCE_DB_PATH.trim()
4792
+ ? env.CLAUDE_SCIENCE_DB_PATH.trim()
4793
+ : null;
4794
+ if (explicit) return path.resolve(explicit);
4795
+ return path.join(resolveClaudeScienceRoot({ home, env }), "operon-cli.db");
4796
+ }
4797
+
4798
+ async function readClaudeScienceFrames(dbPath, sqliteOptions = {}) {
4799
+ if (!dbPath || !fssync.existsSync(dbPath)) return [];
4800
+ const options = { label: "Claude Science", readOnly: true, ...sqliteOptions };
4801
+
4802
+ // On Windows the DB lives inside WSL and is read over a \\wsl.localhost\ UNC
4803
+ // path. operon-cli.db runs in WAL mode (its -wal/-shm sidecars must be read
4804
+ // consistently with the main file), and SQLite over the 9p/UNC bridge can
4805
+ // fail to open the WAL or read a torn state. Snapshot to a local temp copy
4806
+ // first — same guard every other WSL-aware provider uses — and run BOTH the
4807
+ // PRAGMA probe and the frames read against that one consistent copy.
4808
+ let snapshot = null;
4809
+ let effectiveDbPath = dbPath;
4810
+ if (isUncPath(dbPath)) {
4811
+ try {
4812
+ snapshot = snapshotSqliteDb(dbPath);
4813
+ effectiveDbPath = snapshot.path;
4814
+ } catch (_e) {
4815
+ // Snapshot failed (permissions, transient I/O) — fall through to a direct
4816
+ // read so the non-UNC case never regresses.
4817
+ }
4818
+ }
4819
+
4820
+ try {
4821
+ const pragmaRows = await readSqliteJsonRowsAsync(
4822
+ effectiveDbPath,
4823
+ "PRAGMA table_info(frames)",
4824
+ options,
4825
+ );
4826
+ const columnNames = pragmaRows.map((row) => row?.name).filter(Boolean);
4827
+ // No `frames` table at all (wrong DB / pre-frames build) — not an error.
4828
+ if (columnNames.length === 0) return [];
4829
+ return await readSqliteJsonRowsAsync(
4830
+ effectiveDbPath,
4831
+ buildClaudeScienceFramesQuery(columnNames),
4832
+ options,
4833
+ );
4834
+ } finally {
4835
+ if (snapshot) snapshot.cleanup();
4836
+ }
4837
+ }
4838
+
4839
+ // `input` here is cache-inclusive (see the header note), so peel the cache
4840
+ // columns back off to recover pure non-cached input. Main and aux counters are
4841
+ // clamped independently — mixing them before the subtraction would let one
4842
+ // group's cache mask the other's shortfall.
4843
+ function claudeScienceUncachedInput(input, cacheRead, cacheWrite) {
4844
+ return Math.max(0, toNonNegativeInt(input) - toNonNegativeInt(cacheRead) - toNonNegativeInt(cacheWrite));
4845
+ }
4846
+
4847
+ function normalizeClaudeScienceTokens(row) {
4848
+ const uncachedInput =
4849
+ claudeScienceUncachedInput(row?.input_tokens, row?.cache_read_tokens, row?.cache_write_tokens) +
4850
+ claudeScienceUncachedInput(
4851
+ row?.aux_input_tokens,
4852
+ row?.aux_cache_read_tokens,
4853
+ row?.aux_cache_write_tokens,
4854
+ );
4855
+ const output = toNonNegativeInt(row?.output_tokens) + toNonNegativeInt(row?.aux_output_tokens);
4856
+ const cacheRead =
4857
+ toNonNegativeInt(row?.cache_read_tokens) + toNonNegativeInt(row?.aux_cache_read_tokens);
4858
+ const cacheWrite =
4859
+ toNonNegativeInt(row?.cache_write_tokens) + toNonNegativeInt(row?.aux_cache_write_tokens);
4860
+ const total = uncachedInput + output + cacheRead + cacheWrite;
4861
+ if (total <= 0) return null;
4862
+ return {
4863
+ input_tokens: uncachedInput,
4864
+ cached_input_tokens: cacheRead,
4865
+ cache_creation_input_tokens: cacheWrite,
4866
+ output_tokens: output,
4867
+ reasoning_output_tokens: 0,
4868
+ total_tokens: total,
4869
+ billable_total_tokens: total,
4870
+ };
4871
+ }
4872
+
4873
+ function claudeScienceFrameTimestampMs(row) {
4874
+ return (
4875
+ coerceEpochMs(row?.completed_at) ||
4876
+ coerceEpochMs(row?.updated_at) ||
4877
+ coerceEpochMs(row?.created_at) ||
4878
+ // Tolerate ISO-8601 text timestamps in case a build stores them as strings.
4879
+ parseIsoTimestampMs(row?.completed_at) ||
4880
+ parseIsoTimestampMs(row?.updated_at) ||
4881
+ parseIsoTimestampMs(row?.created_at)
4882
+ );
4883
+ }
4884
+
4885
+ function parseIsoTimestampMs(value) {
4886
+ if (typeof value !== "string" || !value.trim()) return 0;
4887
+ const ms = Date.parse(value.trim());
4888
+ return Number.isFinite(ms) && ms > 0 ? ms : 0;
4889
+ }
4890
+
4891
+ // Idempotent, subtract-on-change aggregation keyed by frame id (mirrors the
4892
+ // Qoder DB parser): frames are re-read in full on every sync, and a frame whose
4893
+ // counters grew between syncs has its previous contribution removed from its old
4894
+ // bucket before the new totals are added, so repeated syncs never inflate.
4895
+ async function parseClaudeScienceIncremental({
4896
+ dbRows,
4897
+ cursors,
4898
+ queuePath,
4899
+ onProgress,
4900
+ } = {}) {
4901
+ await ensureDir(path.dirname(queuePath));
4902
+ const rows = Array.isArray(dbRows) ? dbRows : [];
4903
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
4904
+ const state = normalizeQoderState(cursors?.claudeScience);
4905
+ const touchedBuckets = new Set();
4906
+ const cb = typeof onProgress === "function" ? onProgress : null;
4907
+ let recordsProcessed = 0;
4908
+ let eventsAggregated = 0;
4909
+
4910
+ for (let index = 0; index < rows.length; index++) {
4911
+ const row = rows[index];
4912
+ const frameId = normalizeMessageKeyPart(row?.id == null ? "" : String(row.id));
4913
+ const base = normalizeClaudeScienceTokens(row);
4914
+ const timestampMs = claudeScienceFrameTimestampMs(row);
4915
+ const bucketStart = timestampMs
4916
+ ? toUtcHalfHourStart(new Date(timestampMs).toISOString())
4917
+ : null;
4918
+ if (!frameId || !base || !bucketStart) {
4919
+ recordsProcessed += 1;
4920
+ continue;
4921
+ }
4922
+
4923
+ // A root frame (no parent) is one user-facing conversation; nested child
4924
+ // frames are sub-agent turns of the same conversation and must not inflate
4925
+ // the conversation count.
4926
+ const isRoot = !normalizeMessageKeyPart(
4927
+ row?.parent_frame_id == null ? "" : String(row.parent_frame_id),
4928
+ );
4929
+ const currentTotals = { ...base, conversation_count: isRoot ? 1 : 0 };
4930
+ const model = normalizeModelInput(row?.model) || "claude-science";
4931
+
4932
+ const previous = state.messages[frameId];
4933
+ const previousTotals =
4934
+ previous?.totals && typeof previous.totals === "object" ? previous.totals : null;
4935
+ const unchanged =
4936
+ previousTotals &&
4937
+ totalsKey(previousTotals) === totalsKey(currentTotals) &&
4938
+ previous.bucketStart === bucketStart &&
4939
+ previous.model === model;
4940
+
4941
+ if (!unchanged) {
4942
+ if (previousTotals && previous.bucketStart && previous.model) {
4943
+ const oldBucket = getHourlyBucket(
4944
+ hourlyState,
4945
+ "claude-science",
4946
+ previous.model,
4947
+ previous.bucketStart,
4948
+ );
4949
+ subtractTotals(oldBucket.totals, previousTotals);
4950
+ touchedBuckets.add(bucketKey("claude-science", previous.model, previous.bucketStart));
4951
+ }
4952
+ const bucket = getHourlyBucket(hourlyState, "claude-science", model, bucketStart);
4953
+ addTotals(bucket.totals, currentTotals);
4954
+ touchedBuckets.add(bucketKey("claude-science", model, bucketStart));
4955
+ state.messages[frameId] = {
4956
+ totals: currentTotals,
4957
+ bucketStart,
4958
+ model,
4959
+ updatedAt: new Date().toISOString(),
4960
+ };
4961
+ eventsAggregated += 1;
4962
+ }
4963
+
4964
+ recordsProcessed += 1;
4965
+ if (cb) {
4966
+ cb({
4967
+ index: index + 1,
4968
+ total: rows.length,
4969
+ recordsProcessed,
4970
+ eventsAggregated,
4971
+ bucketsQueued: touchedBuckets.size,
4972
+ });
4973
+ }
4974
+ }
4975
+
4976
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
4977
+ const updatedAt = new Date().toISOString();
4978
+ hourlyState.updatedAt = updatedAt;
4979
+ state.updatedAt = updatedAt;
4980
+ cursors.hourly = hourlyState;
4981
+ cursors.claudeScience = state;
4982
+ return { recordsProcessed, eventsAggregated, bucketsQueued };
4983
+ }
4984
+
4532
4985
  // Resolve project context from DB message (no file path available)
4533
4986
  async function resolveProjectContextForDb({
4534
4987
  msg,
4988
+ dbPath,
4535
4989
  projectMetaCache,
4536
4990
  publicRepoCache,
4537
4991
  publicRepoResolver,
@@ -4540,7 +4994,9 @@ async function resolveProjectContextForDb({
4540
4994
  const cwd = msg?.path?.cwd;
4541
4995
  if (!cwd || typeof cwd !== "string") return null;
4542
4996
  return resolveProjectContextForPath({
4543
- startDir: cwd,
4997
+ // The DB itself is read over the distro's UNC bridge on Windows, so its
4998
+ // path carries the prefix the recorded POSIX cwd needs (#374).
4999
+ startDir: wsl.mapWslCwdToUnc(cwd, dbPath),
4544
5000
  projectMetaCache,
4545
5001
  publicRepoCache,
4546
5002
  publicRepoResolver,
@@ -10698,7 +11154,7 @@ async function parseOmpIncremental({
10698
11154
  const cwd = await resolveOmpFileCwd(filePath);
10699
11155
  const projectContext = cwd
10700
11156
  ? await resolveProjectContextForPath({
10701
- startDir: cwd,
11157
+ startDir: wsl.mapWslCwdToUnc(cwd, filePath),
10702
11158
  projectMetaCache,
10703
11159
  publicRepoCache,
10704
11160
  publicRepoResolver,
@@ -14689,6 +15145,14 @@ module.exports = {
14689
15145
  parseQoderDbIncremental,
14690
15146
  openclawCursorKey,
14691
15147
  parseOpenclawIncremental,
15148
+ resolveOpenclawHome,
15149
+ resolveOpenclawHomes,
15150
+ resolveOpenclawSessionFiles,
15151
+ resolveClaudeScienceDbPath,
15152
+ resolveClaudeScienceDbPaths,
15153
+ buildClaudeScienceFramesQuery,
15154
+ readClaudeScienceFrames,
15155
+ parseClaudeScienceIncremental,
14692
15156
  parseCursorApiIncremental,
14693
15157
  parseKiroIncremental,
14694
15158
  parseHermesIncremental,
@@ -164,6 +164,40 @@ function isUncPath(p) {
164
164
  return typeof p === "string" && (p.startsWith("\\\\") || p.startsWith("//"));
165
165
  }
166
166
 
167
+ const WSL_UNC_ROOT_RE = /^[\\/]{2}(wsl\$|wsl\.localhost)[\\/]([^\\/]+)[\\/]/i;
168
+ const WINDOWS_DRIVE_PREFIX_RE = /^\/[A-Za-z]:[\\/]/;
169
+
170
+ // `\\wsl$\Ubuntu-24.04\home\u\...` -> `\\wsl$\Ubuntu-24.04\`. Returns null for
171
+ // native and non-WSL UNC paths.
172
+ function wslUncRoot(p) {
173
+ if (typeof p !== "string") return null;
174
+ const match = WSL_UNC_ROOT_RE.exec(p);
175
+ return match ? `\\\\${match[1]}\\${match[2]}\\` : null;
176
+ }
177
+
178
+ // A WSL session records its cwd as a POSIX path (`/home/u/dev/app`) while the
179
+ // transcript itself is read over the distro's UNC bridge. Probing that raw
180
+ // POSIX path from Windows resolves it against the current drive
181
+ // (`C:\home\u\dev\app`), which never exists — so project attribution finds no
182
+ // `.git`, and every WSL session ends up with a null projectKey (#374).
183
+ // Re-anchor the cwd onto the UNC prefix the transcript came from.
184
+ //
185
+ // The prefix is taken from the transcript path rather than re-probed, which
186
+ // also picks whichever of `\\wsl$` / `\\wsl.localhost` actually resolves on
187
+ // this machine — they are not interchangeable everywhere. Anything that isn't
188
+ // a POSIX cwd from a WSL-hosted file is returned untouched.
189
+ function mapWslCwdToUnc(cwd, transcriptPath) {
190
+ if (typeof cwd !== "string" || !cwd.startsWith("/")) return cwd;
191
+ // `new URL("file:///C:/dev/app").pathname` is "/C:/dev/app" — a leading
192
+ // slash, but a Windows path rather than a POSIX one. Qoder feeds exactly
193
+ // that shape in via `project_uri`; re-anchoring it would invent
194
+ // `\\wsl$\Distro\C:\dev\app`.
195
+ if (WINDOWS_DRIVE_PREFIX_RE.test(cwd)) return cwd;
196
+ const root = wslUncRoot(transcriptPath);
197
+ if (!root) return cwd;
198
+ return root + cwd.slice(1).replaceAll("/", "\\");
199
+ }
200
+
167
201
  function snapshotSqliteDb(dbPath) {
168
202
  const tmpRoot = fssync.mkdtempSync(
169
203
  path.join(require("node:os").tmpdir(), "tokentracker-wsl-snap-"),
@@ -191,6 +225,8 @@ module.exports = {
191
225
  resetWslProbeCache,
192
226
  discoverWslHome,
193
227
  isUncPath,
228
+ wslUncRoot,
229
+ mapWslCwdToUnc,
194
230
  snapshotSqliteDb,
195
231
  getWslMode,
196
232
  isInvalidWslMode,
@@ -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};