tokentracker-cli 0.84.9 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +8 -7
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-BgBFpxmU.js → AchievementBadge-BgbH_mX4.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DHPD3Fnu.js → AchievementsPage-CQqkh1JS.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DouWVD-3.js → AchievementsSection-Dmkxpn0s.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Df9eQiA3.js → BadgeDetailModal-7OQf37lk.js} +1 -1
- package/dashboard/dist/assets/{Card-CQ9wBnz6.js → Card-1PmWZRTK.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-XIO-I-5v.js → ClawdAnimated-CMJFagpX.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Cy1cJ91B.js → CommandPalette-D4OrwF1w.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CwKG4LQM.js → CommunityStatsModal-BDC9EcwZ.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BNcWqick.js → ConfirmModal-22DL5C_9.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CEXWzDtt.js +1 -0
- package/dashboard/dist/assets/{DevicePage-pJ2jNkZV.js → DevicePage-8uhHB7kX.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C2SOtkJN.js → DialogClose-BcJx6VK-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-D6zxtOM5.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CGSbB8G7.js → DialogTitle-gRzunAqv.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CZSiw7me.js → FadeIn-4rlPdBVe.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-ZmQhX_kR.js → HeaderGithubStar-CR003aO3.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D_YdG369.js → HoverTooltip-Dgz-dM8Q.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-du886dF0.js → IpCheckPage-B4PJRJP_.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BuAak8lU.js → LandingPage-3JQe-jOZ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-nU0ARYnN.js → LeaderboardAvatar-CL9LxiEf.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CngwCvC7.js → LeaderboardPage-DhIzM2Z2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CXZJ0e-T.js → LeaderboardProfileModal-bbf5uJR5.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CZve6s6b.js → LeaderboardProfilePage-CzuPxscw.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CWvoJPk8.js → LimitsPage-B59rb2gg.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DyUAAyyE.js → LocalOnlyNotice-BkK-6n1R.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-eEiRfbdv.js → LoginCard-BFGCzEjE.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-C16WdIQm.js → LoginModal-yhkyscFn.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-B2WqpE1t.js → LoginPage-DwU5s0F7.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DtQWrGmI.js → PetPage-BPscYR6z.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CRhqtFLB.js → PopoverPopup-E7PyGTTB.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DWNH_wYu.js → ProviderIcon-CvmnzwKU.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DzCczcWZ.js → ResetPasswordPage-DD6qX616.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-CV9ccq6R.js → SegmentedControl-DjGBL_l1.js} +1 -1
- package/dashboard/dist/assets/{Select-BrogNNk0.js → Select-B2MGmsi2.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bpu3swt0.js → SelectItemText-BaTwA_ZF.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-D_FIrMJP.js → ServiceStatusPage-0K0y8A6m.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-C8TvTy1o.js → SessionsPage-CxrtRm3B.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-Bd-tzJAR.js → SettingsPage-CyvSWvDP.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CKGU9wQ4.js → SkillsPage-BVOU8t3t.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DhHW6L-U.js → TokenGalaxy-w7bwGloC.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DQo-4pPB.js → ToolbarRootContext-BAzpDZej.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-Ejl8d45g.js → TrendMonitor-DyT7Q4xp.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CY7f4wCD.js → WidgetsPage-eDDtopQT.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B4-B2v-v.js → WrappedPage-CKkbdWHF.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BNUFXCfp.js → __vite-browser-external-SU8sCTLB.js} +23 -16
- package/dashboard/dist/assets/{agent-logos-BRzutmeR.js → agent-logos-bkh9XZJd.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BcrbPk8c.js → arrow-up-right-D16zCIw8.js} +1 -1
- package/dashboard/dist/assets/{check-BMxeFPfQ.js → check-BFL17JWQ.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DOstgiRf.js → chevron-right-CUJgrtkp.js} +1 -1
- package/dashboard/dist/assets/{copy-BsKO524t.js → copy-CRQiCjgs.js} +1 -1
- package/dashboard/dist/assets/{download-dadqxej7.js → download-Cd12nDrQ.js} +1 -1
- package/dashboard/dist/assets/{external-link-CTu3BMeU.js → external-link-CvAfnsG-.js} +1 -1
- package/dashboard/dist/assets/{flame-B_IvWcMn.js → flame-CCi681Yl.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DIT6hLSb.js → format-tokens-BAVt8uTZ.js} +1 -1
- package/dashboard/dist/assets/{icons-mWku76bu.js → icons-CbKC96gR.js} +1 -1
- package/dashboard/dist/assets/{index-D5-w-aGy.js → index--TDvaL1y.js} +1 -1
- package/dashboard/dist/assets/{index-D-gQV-hF.js → index-B5b6PCh5.js} +1 -1
- package/dashboard/dist/assets/{index-CboN9E82.js → index-B9LWn-mo.js} +1 -1
- package/dashboard/dist/assets/{info-x6l352SW.js → info-BPFpD--M.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Bo12VlLJ.js → limits-providers-Cyedt-WJ.js} +1 -1
- package/dashboard/dist/assets/{link-2-eo_xPxH1.js → link-2-BkEPPdfC.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C4n1iGBv.js → loader-circle-CzASiylJ.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-pR8xwsI_.js → maximize-2-CkwnHUJn.js} +1 -1
- package/dashboard/dist/assets/{provider-display-ErggQWu4.js → provider-display-BXBBedC-.js} +2 -2
- package/dashboard/dist/assets/{react-DYKg02Aq.js → react-BXSctDQj.js} +1 -1
- package/dashboard/dist/assets/{search-B_jbUwPS.js → search-_qGma9tM.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DY5ruGmw.js → skills-api-DdmJjnJ9.js} +1 -1
- package/dashboard/dist/assets/{store-Cu8i1y26.js → store-owYCN530.js} +1 -1
- package/dashboard/dist/assets/{terminal-D3j6X68s.js → terminal-Clzw-KEF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-CFpwgJw5.js → trash-2-C5Giwp_E.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-C0fKq66G.js → use-community-stats-Dk8UjLEr.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-TOp4UAnI.js → use-limits-display-prefs-CArM595Q.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BZie_7z1.js → use-native-settings-Cpd69kiF.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Bq92YyUp.js → use-ordered-list--CzwhBTx.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CcILnpU1.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BcpytkES.js → use-session-efficiency-pref-DUXO8cCg.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Dmmrne2z.js → use-transform-Cg3kuMj_.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Cw0Z9Tew.js → use-usage-limits-DY9_mV28.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DyyonINj.js → useCurrency-BIe86hMn.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DvWw86lJ.js → useScrollLock-7-hejkNC.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CBkvGfJ1.js → useTokenFormat-DEtLbRgQ.js} +1 -1
- package/dashboard/dist/assets/{zap-CnYA39Ug.js → zap-DUxY1d-O.js} +1 -1
- package/dashboard/dist/index.html +12 -10
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +1 -0
- package/src/commands/status.js +129 -9
- package/src/commands/sync.js +265 -54
- package/src/lib/claude-categorizer.js +36 -9
- package/src/lib/diagnostics.js +86 -13
- package/src/lib/doctor.js +17 -0
- package/src/lib/grok-context-breakdown.js +794 -0
- package/src/lib/install-resolver.js +10 -2
- package/src/lib/local-api.js +21 -3
- package/src/lib/multi-install-parser.js +9 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +788 -44
- package/src/lib/session-analytics.js +332 -9
- package/dashboard/dist/assets/DashboardPage-Dns_1yXL.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Bhfqqmll.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-ZiVeOkvb.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -714,6 +714,30 @@ function formatLocalDate(value) {
|
|
|
714
714
|
return `${year}-${month}-${day}`;
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
+
// Identity hash for cross-environment duplicate session files (native and
|
|
718
|
+
// \\wsl$ views of a synced ~/.claude). sha256 of the first 64KB plus the
|
|
719
|
+
// file size: Claude session headers carry sessionId/cwd so the prefix is
|
|
720
|
+
// discriminating, and folding in the size keeps a grown file distinct from
|
|
721
|
+
// a stale copy. Deliberately excludes inode (unstable across the 9p bridge)
|
|
722
|
+
// and mtime (rewritten by sync tools).
|
|
723
|
+
const CLAUDE_FILE_ID_PREFIX_BYTES = 64 * 1024;
|
|
724
|
+
async function claudeFileIdentityHash(filePath, st) {
|
|
725
|
+
const length = Math.min(CLAUDE_FILE_ID_PREFIX_BYTES, st.size);
|
|
726
|
+
if (length <= 0) return null;
|
|
727
|
+
let fh = null;
|
|
728
|
+
try {
|
|
729
|
+
fh = await fs.open(filePath, "r");
|
|
730
|
+
const buf = Buffer.alloc(length);
|
|
731
|
+
const { bytesRead } = await fh.read(buf, 0, length, 0);
|
|
732
|
+
const hash = crypto.createHash("sha256").update(buf.subarray(0, bytesRead)).digest("hex");
|
|
733
|
+
return `${hash}:${st.size}`;
|
|
734
|
+
} catch (_e) {
|
|
735
|
+
return null;
|
|
736
|
+
} finally {
|
|
737
|
+
if (fh) await fh.close().catch(() => { });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
717
741
|
async function parseClaudeIncremental({
|
|
718
742
|
projectFiles,
|
|
719
743
|
cursors,
|
|
@@ -747,6 +771,22 @@ async function parseClaudeIncremental({
|
|
|
747
771
|
cursors.files = {};
|
|
748
772
|
}
|
|
749
773
|
|
|
774
|
+
// Cross-environment file dedup (#307): when the scan list mixes native and
|
|
775
|
+
// \\wsl$ paths, a session file synced between the environments shows up
|
|
776
|
+
// twice under different paths. Identify never-parsed files by content hash
|
|
777
|
+
// and skip the duplicate wholesale — the message-hash layer alone is capped
|
|
778
|
+
// at 100k entries, and eviction would let bulk history double count.
|
|
779
|
+
let sawUncFile = false;
|
|
780
|
+
let sawLocalFile = false;
|
|
781
|
+
for (const entry of files) {
|
|
782
|
+
const p = typeof entry === "string" ? entry : entry?.path;
|
|
783
|
+
if (typeof p !== "string" || !p) continue;
|
|
784
|
+
if (isUncPath(p)) sawUncFile = true;
|
|
785
|
+
else sawLocalFile = true;
|
|
786
|
+
if (sawUncFile && sawLocalFile) break;
|
|
787
|
+
}
|
|
788
|
+
const seenFileIds = sawUncFile && sawLocalFile ? new Map() : null;
|
|
789
|
+
|
|
750
790
|
for (let idx = 0; idx < files.length; idx++) {
|
|
751
791
|
const entry = files[idx];
|
|
752
792
|
const filePath = typeof entry === "string" ? entry : entry?.path;
|
|
@@ -766,6 +806,59 @@ async function parseClaudeIncremental({
|
|
|
766
806
|
const truncated = sameInode && prevOffset > st.size;
|
|
767
807
|
const startOffset = sameInode && !truncated ? prevOffset : 0;
|
|
768
808
|
|
|
809
|
+
let fileId = null;
|
|
810
|
+
if (seenFileIds) {
|
|
811
|
+
const cachedFileId =
|
|
812
|
+
sameInode && typeof prev?.fileId === "string" && prev?.fileIdSize === st.size
|
|
813
|
+
? prev.fileId
|
|
814
|
+
: null;
|
|
815
|
+
fileId = cachedFileId || (await claudeFileIdentityHash(filePath, st));
|
|
816
|
+
if (fileId) {
|
|
817
|
+
// Cache on the live cursor object so idle files don't re-hash on
|
|
818
|
+
// every sync (the idle short-circuit below never rewrites cursors).
|
|
819
|
+
if (prev && (prev.fileId !== fileId || prev.fileIdSize !== st.size)) {
|
|
820
|
+
prev.fileId = fileId;
|
|
821
|
+
prev.fileIdSize = st.size;
|
|
822
|
+
}
|
|
823
|
+
const primary = seenFileIds.get(fileId);
|
|
824
|
+
if (primary === undefined) {
|
|
825
|
+
seenFileIds.set(fileId, filePath);
|
|
826
|
+
} else if (!prev && primary !== filePath) {
|
|
827
|
+
// Never-parsed duplicate of a file already accounted this run (the
|
|
828
|
+
// native copy sorts first). Mark it caught-up so future syncs only
|
|
829
|
+
// look at genuinely new tail bytes — message hashes still guard the
|
|
830
|
+
// tail if the copies diverge later.
|
|
831
|
+
cursors.files[key] = {
|
|
832
|
+
inode,
|
|
833
|
+
offset: st.size,
|
|
834
|
+
updatedAt: new Date().toISOString(),
|
|
835
|
+
fileId,
|
|
836
|
+
fileIdSize: st.size,
|
|
837
|
+
duplicateOf: primary,
|
|
838
|
+
...(projectEnabled
|
|
839
|
+
? {
|
|
840
|
+
claudeCwd: null,
|
|
841
|
+
projectFileContext: buildProjectFileContext(null),
|
|
842
|
+
projectRef: null,
|
|
843
|
+
projectKey: null,
|
|
844
|
+
}
|
|
845
|
+
: {}),
|
|
846
|
+
};
|
|
847
|
+
if (cb) {
|
|
848
|
+
cb({
|
|
849
|
+
index: idx + 1,
|
|
850
|
+
total: totalFiles,
|
|
851
|
+
filePath,
|
|
852
|
+
filesProcessed,
|
|
853
|
+
eventsAggregated,
|
|
854
|
+
bucketsQueued: touchedBuckets.size,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
769
862
|
// Claude's launch cwd is fixed for a session file's lifetime, so once
|
|
770
863
|
// resolved (found, or confirmed absent) from content it never needs
|
|
771
864
|
// re-extracting. Cache it — and the resolved project's git-config
|
|
@@ -850,6 +943,7 @@ async function parseClaudeIncremental({
|
|
|
850
943
|
inode,
|
|
851
944
|
offset: result.endOffset,
|
|
852
945
|
updatedAt: new Date().toISOString(),
|
|
946
|
+
...(fileId ? { fileId, fileIdSize: st.size } : {}),
|
|
853
947
|
...(projectEnabled
|
|
854
948
|
? {
|
|
855
949
|
claudeCwd: nextCwd === undefined ? null : nextCwd,
|
|
@@ -1205,6 +1299,85 @@ async function parseOpencodeIncremental({
|
|
|
1205
1299
|
return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
1206
1300
|
}
|
|
1207
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
|
+
|
|
1208
1381
|
async function parseOpenclawIncremental({
|
|
1209
1382
|
sessionFiles,
|
|
1210
1383
|
cursors,
|
|
@@ -1368,6 +1541,20 @@ async function parseOpenclawIncremental({
|
|
|
1368
1541
|
usageFingerprint: accepted.usageFingerprint,
|
|
1369
1542
|
updatedAt: new Date().toISOString(),
|
|
1370
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,
|
|
1371
1558
|
};
|
|
1372
1559
|
cursorKeys.set(key, key);
|
|
1373
1560
|
|
|
@@ -2075,7 +2262,10 @@ async function parseClaudeFile({
|
|
|
2075
2262
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2076
2263
|
|
|
2077
2264
|
let eventsAggregated = 0;
|
|
2078
|
-
|
|
2265
|
+
// Separator-agnostic: WSL installs are scanned over \\wsl$ UNC paths where
|
|
2266
|
+
// path.join produces backslashes, and a forward-slash-only check would
|
|
2267
|
+
// misclassify subagent transcripts as main sessions (#307).
|
|
2268
|
+
const isMainSession = !/[\\/]subagents[\\/]/.test(filePath);
|
|
2079
2269
|
for await (const line of rl) {
|
|
2080
2270
|
if (!line) continue;
|
|
2081
2271
|
|
|
@@ -2095,13 +2285,25 @@ async function parseClaudeFile({
|
|
|
2095
2285
|
typeof content === "string" ||
|
|
2096
2286
|
(Array.isArray(content) && content.some((b) => b?.type === "text"));
|
|
2097
2287
|
if (hasText) {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2288
|
+
// Dedup by the line's uuid so a synced copy of the session file
|
|
2289
|
+
// (WSL mirror of a divergent native file, inode-reset re-read)
|
|
2290
|
+
// cannot re-count the conversation. Usage rows get the same
|
|
2291
|
+
// guarantee from claudeMessageDedupKey; user lines have no
|
|
2292
|
+
// message.id, so the line uuid is the identity.
|
|
2293
|
+
const userKey =
|
|
2294
|
+
seenMessageHashes && typeof userObj?.uuid === "string" && userObj.uuid
|
|
2295
|
+
? `u:${userObj.uuid}`
|
|
2296
|
+
: null;
|
|
2297
|
+
if (!userKey || !seenMessageHashes.has(userKey)) {
|
|
2298
|
+
if (userKey) seenMessageHashes.add(userKey);
|
|
2299
|
+
const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
|
|
2300
|
+
const userBucketStart = userTs ? toUtcHalfHourStart(userTs) : null;
|
|
2301
|
+
if (userBucketStart) {
|
|
2302
|
+
const userModel = DEFAULT_MODEL;
|
|
2303
|
+
const userBucket = getHourlyBucket(hourlyState, source, userModel, userBucketStart);
|
|
2304
|
+
userBucket.totals.conversation_count += 1;
|
|
2305
|
+
touchedBuckets.add(bucketKey(source, userModel, userBucketStart));
|
|
2306
|
+
}
|
|
2105
2307
|
}
|
|
2106
2308
|
}
|
|
2107
2309
|
}
|
|
@@ -4420,6 +4622,356 @@ async function parseQoderDbIncremental({
|
|
|
4420
4622
|
return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
4421
4623
|
}
|
|
4422
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
|
+
|
|
4423
4975
|
// Resolve project context from DB message (no file path available)
|
|
4424
4976
|
async function resolveProjectContextForDb({
|
|
4425
4977
|
msg,
|
|
@@ -4554,37 +5106,60 @@ async function parseCursorApiIncremental({
|
|
|
4554
5106
|
// Kiro token tracking (reads from devdata.sqlite or tokens_generated.jsonl)
|
|
4555
5107
|
// ---------------------------------------------------------------------------
|
|
4556
5108
|
|
|
4557
|
-
|
|
5109
|
+
// Kiro IDE (VS Code fork) globalStorage lives under the editor config root,
|
|
5110
|
+
// which differs per platform: %APPDATA% on Windows, ~/.config on Linux/WSL,
|
|
5111
|
+
// ~/Library/Application Support on macOS.
|
|
5112
|
+
function resolveKiroBasePath(env = process.env) {
|
|
4558
5113
|
const home = require("node:os").homedir();
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
"
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
5114
|
+
const suffix = ["Kiro", "User", "globalStorage", "kiro.kiroagent"];
|
|
5115
|
+
if (process.platform === "win32") {
|
|
5116
|
+
const appData = typeof env.APPDATA === "string" && env.APPDATA.trim().length > 0
|
|
5117
|
+
? env.APPDATA.trim()
|
|
5118
|
+
: path.join(home, "AppData", "Roaming");
|
|
5119
|
+
return path.join(appData, ...suffix);
|
|
5120
|
+
}
|
|
5121
|
+
if (process.platform === "linux") {
|
|
5122
|
+
const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim().length > 0
|
|
5123
|
+
? env.XDG_CONFIG_HOME.trim()
|
|
5124
|
+
: path.join(home, ".config");
|
|
5125
|
+
return path.join(configHome, ...suffix);
|
|
5126
|
+
}
|
|
5127
|
+
return path.join(home, "Library", "Application Support", ...suffix);
|
|
4568
5128
|
}
|
|
4569
5129
|
|
|
4570
|
-
function resolveKiroDbPath() {
|
|
4571
|
-
return path.join(resolveKiroBasePath(), "dev_data", "devdata.sqlite");
|
|
5130
|
+
function resolveKiroDbPath(basePath) {
|
|
5131
|
+
return path.join(basePath || resolveKiroBasePath(), "dev_data", "devdata.sqlite");
|
|
4572
5132
|
}
|
|
4573
5133
|
|
|
4574
|
-
function resolveKiroJsonlPath() {
|
|
4575
|
-
return path.join(resolveKiroBasePath(), "dev_data", "tokens_generated.jsonl");
|
|
5134
|
+
function resolveKiroJsonlPath(basePath) {
|
|
5135
|
+
return path.join(basePath || resolveKiroBasePath(), "dev_data", "tokens_generated.jsonl");
|
|
4576
5136
|
}
|
|
4577
5137
|
|
|
4578
5138
|
function readKiroDbTokens(dbPath, sinceId, sqliteOptions = {}) {
|
|
4579
5139
|
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
4580
5140
|
const minId = Number.isFinite(sinceId) && sinceId > 0 ? sinceId : 0;
|
|
4581
5141
|
const sql = `SELECT id, model, provider, tokens_prompt, tokens_generated, timestamp FROM tokens_generated WHERE id > ${minId} ORDER BY id ASC`;
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
5142
|
+
|
|
5143
|
+
// WSL installs are read over the \\wsl$ UNC bridge; sqlite3 cannot safely
|
|
5144
|
+
// open WAL databases there, so copy db (+ sidecars) to tmp first.
|
|
5145
|
+
let snapshot = null;
|
|
5146
|
+
let effectiveDbPath = dbPath;
|
|
5147
|
+
if (isUncPath(dbPath)) {
|
|
5148
|
+
try {
|
|
5149
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
5150
|
+
effectiveDbPath = snapshot.path;
|
|
5151
|
+
} catch (_e) { }
|
|
5152
|
+
}
|
|
5153
|
+
try {
|
|
5154
|
+
return readSqliteJsonRows(effectiveDbPath, sql, {
|
|
5155
|
+
label: "Kiro",
|
|
5156
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
5157
|
+
timeout: 15_000,
|
|
5158
|
+
...sqliteOptions,
|
|
5159
|
+
});
|
|
5160
|
+
} finally {
|
|
5161
|
+
if (snapshot) snapshot.cleanup();
|
|
5162
|
+
}
|
|
4588
5163
|
}
|
|
4589
5164
|
|
|
4590
5165
|
// Read Kiro token data from JSONL fallback (tokens_generated.jsonl).
|
|
@@ -4726,7 +5301,7 @@ function normalizeKiroModelName(raw) {
|
|
|
4726
5301
|
return name || null;
|
|
4727
5302
|
}
|
|
4728
5303
|
|
|
4729
|
-
async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
|
|
5304
|
+
async function parseKiroIncremental({ basePath, dbPath, jsonlPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
|
|
4730
5305
|
await ensureDir(path.dirname(queuePath));
|
|
4731
5306
|
const kiroState = cursors.kiro && typeof cursors.kiro === "object" ? cursors.kiro : {};
|
|
4732
5307
|
const lastDbId = typeof kiroState.lastDbId === "number"
|
|
@@ -4735,8 +5310,8 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
4735
5310
|
const jsonlState = kiroState.jsonl && typeof kiroState.jsonl === "object" ? kiroState.jsonl : {};
|
|
4736
5311
|
const lastJsonlLine = typeof jsonlState.lastLine === "number" ? jsonlState.lastLine : 0;
|
|
4737
5312
|
|
|
4738
|
-
const resolvedDbPath = dbPath || resolveKiroDbPath();
|
|
4739
|
-
const resolvedJsonlPath = jsonlPath || resolveKiroJsonlPath();
|
|
5313
|
+
const resolvedDbPath = dbPath || resolveKiroDbPath(basePath);
|
|
5314
|
+
const resolvedJsonlPath = jsonlPath || resolveKiroJsonlPath(basePath);
|
|
4740
5315
|
|
|
4741
5316
|
// Try SQLite first, fall back to JSONL.
|
|
4742
5317
|
let rows = [];
|
|
@@ -4784,9 +5359,12 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
4784
5359
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
4785
5360
|
}
|
|
4786
5361
|
|
|
4787
|
-
// Build model timeline from .chat files for model name resolution
|
|
4788
|
-
|
|
4789
|
-
|
|
5362
|
+
// Build model timeline from .chat files for model name resolution.
|
|
5363
|
+
// Must come from the SAME install as the rows being parsed (dual-install:
|
|
5364
|
+
// a WSL install's rows must not be resolved against native .chat files).
|
|
5365
|
+
const timelineBase = basePath
|
|
5366
|
+
|| (dbPath ? path.dirname(path.dirname(dbPath)) : resolveKiroBasePath());
|
|
5367
|
+
const modelTimeline = buildKiroModelTimeline(timelineBase);
|
|
4790
5368
|
|
|
4791
5369
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4792
5370
|
const touchedBuckets = new Set();
|
|
@@ -5075,6 +5653,74 @@ function hermesInstallOwnsCursor(hermesPath, flatState, sqliteOptions) {
|
|
|
5075
5653
|
return allDbs.some((db) => sqliteDbContainsIds(db, "sessions", sample, sqliteOptions));
|
|
5076
5654
|
}
|
|
5077
5655
|
|
|
5656
|
+
function kiroInstallOwnsCursor(dbPath, flatState, sqliteOptions) {
|
|
5657
|
+
// The flat cursor tracks the max AUTOINCREMENT id it consumed. An install
|
|
5658
|
+
// whose tokens_generated table never reached that id cannot be the flat
|
|
5659
|
+
// cursor's host. Both installs containing the id (short/equal histories)
|
|
5660
|
+
// yields two hits upstream → caller seeds every namespace (safe).
|
|
5661
|
+
const lastDbId = typeof flatState?.lastDbId === "number" && Number.isInteger(flatState.lastDbId) && flatState.lastDbId > 0
|
|
5662
|
+
? flatState.lastDbId
|
|
5663
|
+
: (typeof flatState?.lastId === "number" && Number.isInteger(flatState.lastId) && flatState.lastId > 0
|
|
5664
|
+
? flatState.lastId
|
|
5665
|
+
: 0);
|
|
5666
|
+
if (!lastDbId) return false;
|
|
5667
|
+
return sqliteDbContainsIds(dbPath, "tokens_generated", [String(lastDbId)], sqliteOptions);
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
// Kiro CLI request_ids live inside the conversations_v2 JSON payload, not in
|
|
5671
|
+
// an id column — probe via json_each instead of sqliteDbContainsIds.
|
|
5672
|
+
function kiroCliDbContainsRequestIds(dbPath, requestIds, sqliteOptions = {}) {
|
|
5673
|
+
if (!dbPath || !Array.isArray(requestIds) || requestIds.length === 0) return false;
|
|
5674
|
+
if (!fssync.existsSync(dbPath)) return false;
|
|
5675
|
+
const inList = requestIds.map(sqliteStringLiteral).join(",");
|
|
5676
|
+
// Alias both tables: conversations_v2.value and json_each's own `value`
|
|
5677
|
+
// column collide — a bare `value` is "ambiguous column name" and sqlite
|
|
5678
|
+
// errors out (which readSqliteJsonRows swallows into []).
|
|
5679
|
+
const sql =
|
|
5680
|
+
"SELECT 1 FROM conversations_v2 AS c, " +
|
|
5681
|
+
"json_each(json_extract(c.value, '$.user_turn_metadata.requests')) AS je " +
|
|
5682
|
+
`WHERE json_extract(je.value, '$.request_id') IN (${inList}) LIMIT 1`;
|
|
5683
|
+
|
|
5684
|
+
let snapshot = null;
|
|
5685
|
+
let effectiveDbPath = dbPath;
|
|
5686
|
+
if (isUncPath(dbPath)) {
|
|
5687
|
+
try {
|
|
5688
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
5689
|
+
effectiveDbPath = snapshot.path;
|
|
5690
|
+
} catch (_e) { }
|
|
5691
|
+
}
|
|
5692
|
+
try {
|
|
5693
|
+
const rows = readSqliteJsonRows(effectiveDbPath, sql, {
|
|
5694
|
+
label: "InstallProbe",
|
|
5695
|
+
maxBuffer: 1024 * 1024,
|
|
5696
|
+
timeout: 10_000,
|
|
5697
|
+
...sqliteOptions,
|
|
5698
|
+
});
|
|
5699
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
5700
|
+
} catch (_e) {
|
|
5701
|
+
return false;
|
|
5702
|
+
} finally {
|
|
5703
|
+
if (snapshot) snapshot.cleanup();
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
function kiroCliInstallOwnsCursor(dbPath, flatState, sqliteOptions) {
|
|
5708
|
+
// Only SQLite-origin request_id UUIDs are probe evidence: session-file
|
|
5709
|
+
// entries synthesize `${sessionId}:${loopRand}` keys or carry a session_id
|
|
5710
|
+
// tag, and neither appears in conversations_v2. Filter BEFORE sampling so
|
|
5711
|
+
// recent session-file churn cannot blind the probe to older SQLite ids.
|
|
5712
|
+
const requests =
|
|
5713
|
+
flatState?.requests && typeof flatState.requests === "object" ? flatState.requests : {};
|
|
5714
|
+
const sqliteKeyed = {};
|
|
5715
|
+
for (const [k, v] of Object.entries(requests)) {
|
|
5716
|
+
if (k.includes(":")) continue;
|
|
5717
|
+
if (v && typeof v === "object" && typeof v.session_id === "string" && v.session_id) continue;
|
|
5718
|
+
sqliteKeyed[k] = v;
|
|
5719
|
+
}
|
|
5720
|
+
const sample = sampleRecentKeys(sqliteKeyed);
|
|
5721
|
+
return kiroCliDbContainsRequestIds(dbPath, sample, sqliteOptions);
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5078
5724
|
function hasLegacyHermesDefaultState(hermesState) {
|
|
5079
5725
|
return (
|
|
5080
5726
|
typeof hermesState.lastStartedAt === "number" ||
|
|
@@ -5307,6 +5953,22 @@ const KIRO_CLI_CREDITS_SIDECAR = "kiro-credits.json";
|
|
|
5307
5953
|
function resolveKiroCliDbPath(env = process.env) {
|
|
5308
5954
|
if (env.KIRO_CLI_DB_PATH) return env.KIRO_CLI_DB_PATH;
|
|
5309
5955
|
const home = env.HOME || require("node:os").homedir();
|
|
5956
|
+
if (process.platform === "win32") {
|
|
5957
|
+
// Speculative: Kiro CLI descends from Amazon Q CLI, which ships
|
|
5958
|
+
// macOS/Linux only — Windows users run it inside WSL (handled by the
|
|
5959
|
+
// dual-install path in sync). Kept as the conventional %LOCALAPPDATA%
|
|
5960
|
+
// location so a future native build is picked up; harmless when absent.
|
|
5961
|
+
const localAppData = typeof env.LOCALAPPDATA === "string" && env.LOCALAPPDATA.trim().length > 0
|
|
5962
|
+
? env.LOCALAPPDATA.trim()
|
|
5963
|
+
: path.join(home, "AppData", "Local");
|
|
5964
|
+
return path.join(localAppData, "kiro-cli", "data.sqlite3");
|
|
5965
|
+
}
|
|
5966
|
+
if (process.platform === "linux") {
|
|
5967
|
+
const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.trim().length > 0
|
|
5968
|
+
? env.XDG_DATA_HOME.trim()
|
|
5969
|
+
: path.join(home, ".local", "share");
|
|
5970
|
+
return path.join(dataHome, "kiro-cli", "data.sqlite3");
|
|
5971
|
+
}
|
|
5310
5972
|
return path.join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3");
|
|
5311
5973
|
}
|
|
5312
5974
|
|
|
@@ -5870,6 +6532,7 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5870
6532
|
|
|
5871
6533
|
async function writeKiroCliCreditsSidecar({
|
|
5872
6534
|
queuePath,
|
|
6535
|
+
installKey,
|
|
5873
6536
|
totalCredits,
|
|
5874
6537
|
recordCount,
|
|
5875
6538
|
sessionCount,
|
|
@@ -5882,15 +6545,64 @@ async function writeKiroCliCreditsSidecar({
|
|
|
5882
6545
|
KIRO_CLI_CREDITS_SIDECAR,
|
|
5883
6546
|
);
|
|
5884
6547
|
try {
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
6548
|
+
// Dual-install (#306): keep one entry per install and aggregate at the
|
|
6549
|
+
// top level, so a second install's write never clobbers the first. The
|
|
6550
|
+
// top-level fields keep the v1 shape readKiroCreditsSummary consumes.
|
|
6551
|
+
let installs = {};
|
|
6552
|
+
try {
|
|
6553
|
+
const prev = JSON.parse(await fs.readFile(sidecarPath, "utf8"));
|
|
6554
|
+
if (prev?.version === 1 && prev.installs && typeof prev.installs === "object") {
|
|
6555
|
+
installs = prev.installs;
|
|
6556
|
+
}
|
|
6557
|
+
} catch {
|
|
6558
|
+
// first write or pre-dual-install sidecar — start fresh
|
|
6559
|
+
}
|
|
6560
|
+
// Expire entries whose install stopped syncing (deleted WSL distro,
|
|
6561
|
+
// distro rename / UNC alias flip leaving a duplicate key, mode switched
|
|
6562
|
+
// to *-only). Without this the read-modify-write merge would inflate the
|
|
6563
|
+
// aggregate with phantom installs forever; a 30-day grace keeps entries
|
|
6564
|
+
// alive across transient wsl.exe probe failures.
|
|
6565
|
+
const staleMs = 30 * 24 * 60 * 60 * 1000;
|
|
6566
|
+
for (const [k, entry] of Object.entries(installs)) {
|
|
6567
|
+
const ts = Date.parse(entry?.updated_at || "");
|
|
6568
|
+
if (!Number.isFinite(ts) || Date.now() - ts > staleMs) delete installs[k];
|
|
6569
|
+
}
|
|
6570
|
+
const key = typeof installKey === "string" && installKey ? installKey : "default";
|
|
6571
|
+
installs[key] = {
|
|
5888
6572
|
total_credits: Number(totalCredits.toFixed(12)),
|
|
5889
6573
|
record_count: recordCount,
|
|
5890
6574
|
session_count: sessionCount,
|
|
5891
6575
|
file_count: fileCount,
|
|
5892
6576
|
latest_at: latestAt,
|
|
5893
6577
|
updated_at: new Date().toISOString(),
|
|
6578
|
+
};
|
|
6579
|
+
let aggCredits = 0;
|
|
6580
|
+
let aggRecords = 0;
|
|
6581
|
+
let aggSessions = 0;
|
|
6582
|
+
let aggFiles = 0;
|
|
6583
|
+
let aggLatestAt = null;
|
|
6584
|
+
for (const entry of Object.values(installs)) {
|
|
6585
|
+
aggCredits += Number(entry.total_credits) || 0;
|
|
6586
|
+
aggRecords += Number(entry.record_count) || 0;
|
|
6587
|
+
aggSessions += Number(entry.session_count) || 0;
|
|
6588
|
+
aggFiles += Number(entry.file_count) || 0;
|
|
6589
|
+
if (
|
|
6590
|
+
typeof entry.latest_at === "string" &&
|
|
6591
|
+
(!aggLatestAt || Date.parse(entry.latest_at) > Date.parse(aggLatestAt))
|
|
6592
|
+
) {
|
|
6593
|
+
aggLatestAt = entry.latest_at;
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
await writeJson(sidecarPath, {
|
|
6597
|
+
version: 1,
|
|
6598
|
+
source: "kiro-cli-usage-summary",
|
|
6599
|
+
total_credits: Number(aggCredits.toFixed(12)),
|
|
6600
|
+
record_count: aggRecords,
|
|
6601
|
+
session_count: aggSessions,
|
|
6602
|
+
file_count: aggFiles,
|
|
6603
|
+
latest_at: aggLatestAt,
|
|
6604
|
+
updated_at: new Date().toISOString(),
|
|
6605
|
+
installs,
|
|
5894
6606
|
});
|
|
5895
6607
|
await chmod600IfPossible(sidecarPath);
|
|
5896
6608
|
} catch {
|
|
@@ -5953,13 +6665,29 @@ function readKiroCliRequests(dbPath, env = process.env, sqliteOptions = {}) {
|
|
|
5953
6665
|
"json_extract(value, '$.user_turn_metadata.requests') AS requests_json " +
|
|
5954
6666
|
"FROM conversations_v2 " +
|
|
5955
6667
|
"WHERE json_extract(value, '$.user_turn_metadata.requests') IS NOT NULL";
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
6668
|
+
|
|
6669
|
+
// WSL installs are read over the \\wsl$ UNC bridge; snapshot to tmp so
|
|
6670
|
+
// sqlite3 never opens a WAL database across the 9p bridge.
|
|
6671
|
+
let snapshot = null;
|
|
6672
|
+
let effectiveDbPath = dbPath;
|
|
6673
|
+
if (isUncPath(dbPath)) {
|
|
6674
|
+
try {
|
|
6675
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
6676
|
+
effectiveDbPath = snapshot.path;
|
|
6677
|
+
} catch (_e) { }
|
|
6678
|
+
}
|
|
6679
|
+
let rows;
|
|
6680
|
+
try {
|
|
6681
|
+
rows = readSqliteJsonRows(effectiveDbPath, sql, {
|
|
6682
|
+
label: "Kiro CLI",
|
|
6683
|
+
env,
|
|
6684
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
6685
|
+
timeout: 120_000,
|
|
6686
|
+
...sqliteOptions,
|
|
6687
|
+
});
|
|
6688
|
+
} finally {
|
|
6689
|
+
if (snapshot) snapshot.cleanup();
|
|
6690
|
+
}
|
|
5963
6691
|
const flat = [];
|
|
5964
6692
|
for (const row of rows) {
|
|
5965
6693
|
let requests;
|
|
@@ -6058,6 +6786,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
6058
6786
|
}
|
|
6059
6787
|
await writeKiroCliCreditsSidecar({
|
|
6060
6788
|
queuePath,
|
|
6789
|
+
// Install identity mirrors resolveKiroCliSessionFiles' sessions home so
|
|
6790
|
+
// dual installs keep separate sidecar entries.
|
|
6791
|
+
installKey:
|
|
6792
|
+
resolvedEnv.KIRO_HOME ||
|
|
6793
|
+
path.join(resolvedEnv.HOME || require("node:os").homedir(), ".kiro"),
|
|
6061
6794
|
totalCredits: kiroCreditTotal,
|
|
6062
6795
|
recordCount: kiroCreditRecords,
|
|
6063
6796
|
sessionCount: kiroCreditSessions,
|
|
@@ -14371,6 +15104,7 @@ module.exports = {
|
|
|
14371
15104
|
resolveQoderDbPath,
|
|
14372
15105
|
resolveQoderDbPaths,
|
|
14373
15106
|
readQoderDbMessages,
|
|
15107
|
+
resolveKiroBasePath,
|
|
14374
15108
|
resolveKiroDbPath,
|
|
14375
15109
|
resolveKiroJsonlPath,
|
|
14376
15110
|
resolveHermesPath,
|
|
@@ -14398,12 +15132,22 @@ module.exports = {
|
|
|
14398
15132
|
parseQoderDbIncremental,
|
|
14399
15133
|
openclawCursorKey,
|
|
14400
15134
|
parseOpenclawIncremental,
|
|
15135
|
+
resolveOpenclawHome,
|
|
15136
|
+
resolveOpenclawHomes,
|
|
15137
|
+
resolveOpenclawSessionFiles,
|
|
15138
|
+
resolveClaudeScienceDbPath,
|
|
15139
|
+
resolveClaudeScienceDbPaths,
|
|
15140
|
+
buildClaudeScienceFramesQuery,
|
|
15141
|
+
readClaudeScienceFrames,
|
|
15142
|
+
parseClaudeScienceIncremental,
|
|
14401
15143
|
parseCursorApiIncremental,
|
|
14402
15144
|
parseKiroIncremental,
|
|
14403
15145
|
parseHermesIncremental,
|
|
14404
15146
|
gooseInstallOwnsCursor,
|
|
14405
15147
|
zedInstallOwnsCursor,
|
|
14406
15148
|
hermesInstallOwnsCursor,
|
|
15149
|
+
kiroInstallOwnsCursor,
|
|
15150
|
+
kiroCliInstallOwnsCursor,
|
|
14407
15151
|
copilotOtelCursorHasLegacyCliUsage,
|
|
14408
15152
|
pruneCopilotUsageClaims,
|
|
14409
15153
|
parseCopilotIncremental,
|