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.
- package/README.de.md +7 -4
- package/README.ja.md +7 -4
- package/README.ko.md +7 -4
- package/README.md +10 -7
- package/README.zh-CN.md +7 -4
- package/dashboard/dist/assets/{AchievementBadge-DEqbHlmn.js → AchievementBadge-BHWjTRZh.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BOaEu1AQ.js → AchievementsPage-X6Z6fiAx.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-C370ngO3.js → AchievementsSection-DL2CccQ8.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D7Wkef0K.js → BadgeDetailModal-DKn88JDX.js} +1 -1
- package/dashboard/dist/assets/{Card-mDEVjNdM.js → Card-B_gKsFZR.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-DdsX-z8n.js → ClawdAnimated-vEB5WCNu.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DhxRIuHC.js → CommandPalette-BoxeCsUF.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CG-9JPS6.js → CommunityStatsModal-BwELjQld.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-COhxErLm.js → ConfirmModal-j8yHWm2k.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-tX0DKgFE.js → DashboardPage-CkvAOuBF.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BsSeHx84.js → DevicePage-k4Lr11LG.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-eyOW2cbx.js → DialogClose-C-ZhX9eY.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BRiqm-uZ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-H8sGiH7c.js → DialogTitle-XCGSHZDz.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-pq2ja50M.js → FadeIn-ljP_7vPC.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DWgcpDHr.js → HeaderGithubStar-D4QupeSb.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CcJ_VR65.js → HoverTooltip-Dkpz8RmH.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CNUN346Q.js → IpCheckPage-WkZK9Tr3.js} +1 -1
- package/dashboard/dist/assets/LandingPage-Dp3m0uJg.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-Ddai4I12.js → LeaderboardAvatar-DleP6bK7.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Bajkxf63.js → LeaderboardPage-DTwMMaF2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BGYKDgIi.js → LeaderboardProfileModal-xdDlcRDd.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Ck5vPaeN.js → LeaderboardProfilePage-Ckytgq9h.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-Bqobtsw2.js → LimitsPage-D_2cV3S4.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-e9KSE3wj.js → LocalOnlyNotice-DpCb8VIO.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BhTSm2tb.js → LoginCard-BeAb35_3.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CRwuThde.js → LoginModal-DNdAgKeE.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-j1i74lcc.js → LoginPage-nuWkBuqy.js} +1 -1
- package/dashboard/dist/assets/{PetPage-r62ViU1X.js → PetPage-CCLFROsF.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DopNTyD4.js → PopoverPopup-BCaeRKf4.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-c1LkPhYM.js → ProviderIcon-C_Fpc2jL.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DJdDNvtq.js → ResetPasswordPage-lLDxtpC9.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DhWVOTPN.js → SegmentedControl-Bnfv5lDS.js} +1 -1
- package/dashboard/dist/assets/{Select-DeUmTmsJ.js → Select-CvY_Y0WS.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-pX0B5f7h.js → SelectItemText-DX4QYfBW.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ejIPz6oF.js → ServiceStatusPage-DcUrtacl.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-WEbn90GW.js → SessionsPage-CJXyaHQA.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B8t5gCeI.js → SettingsPage-BkgPxhGn.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-BWMlzeAp.js → SkillsPage-BGvK3HZ0.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DIBRBvMj.js → TokenGalaxy-D9_H79bK.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-C6KxLvnI.js → ToolbarRootContext-BsM_3QBs.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BaMNh67w.js → TrendMonitor-D2QSVlSi.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DDsy4M3x.js → WidgetsPage-BpWOr7-c.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-CiNnUFRI.js → WrappedPage-DliB3OZk.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CM-H8-LW.js → __vite-browser-external-CzDhMv0q.js} +15 -14
- package/dashboard/dist/assets/{agent-logos-BjVa6oe7.js → agent-logos-Q7rTqkGv.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CCum1mqX.js → arrow-up-right-DwqQgMsR.js} +1 -1
- package/dashboard/dist/assets/{check-ySRQc1YP.js → check-Cypjnyke.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-5vFloHew.js → chevron-right-LZRe0crm.js} +1 -1
- package/dashboard/dist/assets/{copy-mhfKVMKo.js → copy-BUe15mXV.js} +1 -1
- package/dashboard/dist/assets/{download-GdrXglSx.js → download-BhjaQuUD.js} +1 -1
- package/dashboard/dist/assets/{external-link-KGFaKc8s.js → external-link-BHU7m_Df.js} +1 -1
- package/dashboard/dist/assets/{flame-BmNtNoS1.js → flame-BPQ7FpRu.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CCpCNw3d.js → format-tokens-C0XEJ96L.js} +1 -1
- package/dashboard/dist/assets/{icons-XLKZavTf.js → icons-De5Qo1lp.js} +1 -1
- package/dashboard/dist/assets/{index-IKpmeZte.js → index-Cl8hqOBU.js} +1 -1
- package/dashboard/dist/assets/{index-_xU5U5Ct.js → index-UfiJKvuV.js} +1 -1
- package/dashboard/dist/assets/{index-CsywTeCQ.js → index-lmWJH5W9.js} +1 -1
- package/dashboard/dist/assets/{info-DUV5UpmT.js → info-CCPDu6hQ.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DFkCZCZj.js → limits-providers-B2Qjydn9.js} +1 -1
- package/dashboard/dist/assets/{link-2-D7c64yLY.js → link-2-Dl1DAfTL.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-sMONRK2M.js → loader-circle-C-mC8s6j.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CVWRgrk9.js → maximize-2-C9YpCcsr.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSKqVjgm.js → provider-display-DNXQkneb.js} +2 -2
- package/dashboard/dist/assets/{react-c1q4mGkS.js → react-LsYzFOQW.js} +1 -1
- package/dashboard/dist/assets/{search-CwD3s-_C.js → search-D5SGjOTl.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BRXj8kjT.js → skills-api-CGRg1lZw.js} +1 -1
- package/dashboard/dist/assets/{store-lqTW2gfK.js → store-BMdm145i.js} +1 -1
- package/dashboard/dist/assets/{terminal-DU1_KbKo.js → terminal-HhtP7nqJ.js} +1 -1
- package/dashboard/dist/assets/{trash-2-B8Y5Xc-K.js → trash-2-CeEoITVO.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DYwwimvp.js → use-community-stats-BTZHzzWd.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Bst6ybK3.js → use-limits-display-prefs-DPNlMKcg.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-Cqle4lgw.js → use-native-settings-DFF0ouY5.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Dtze0ahb.js → use-ordered-list-CCLtmxXg.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DUtjZjIj.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-gwbDMHXa.js → use-session-efficiency-pref-CrvLKyhz.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CU0yxXpX.js → use-transform-BnJ0PftW.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-5y1I1T5_.js → use-usage-limits-BY3ZXlDo.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BfVNrywe.js → useCurrency-BiFDQKMu.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-CF1m7AMJ.js → useScrollLock-BXUoxe5p.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DiRwVvRr.js → useTokenFormat-C6DBFS-R.js} +1 -1
- package/dashboard/dist/assets/{zap-BZtlWtuN.js → zap-9H5t1Kye.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 +117 -10
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +471 -7
- package/src/lib/wsl-probe.js +36 -0
- package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +0 -1
- package/dashboard/dist/assets/LandingPage-yaoRGsNI.js +0 -2
- package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/lib/wsl-probe.js
CHANGED
|
@@ -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};
|