tokentracker-cli 0.84.8 → 0.85.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/dashboard/dist/assets/{AchievementBadge-DAAenS6A.js → AchievementBadge-DEqbHlmn.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DJ9GMKxL.js → AchievementsPage-BOaEu1AQ.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DtpLrh2Q.js → AchievementsSection-C370ngO3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CkE1DwdN.js → BadgeDetailModal-D7Wkef0K.js} +1 -1
- package/dashboard/dist/assets/{Card-Cs2vVMHG.js → Card-mDEVjNdM.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BCSNeob2.js → ClawdAnimated-DdsX-z8n.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-CFDTjp76.js → CommandPalette-DhxRIuHC.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-D2oJpI9i.js → CommunityStatsModal-CG-9JPS6.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-D0a_xdwF.js → ConfirmModal-COhxErLm.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-tX0DKgFE.js +1 -0
- package/dashboard/dist/assets/{DevicePage-c9Fk13gJ.js → DevicePage-BsSeHx84.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-jsfDvF2n.js → DialogClose-eyOW2cbx.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Dt835L7T.js → DialogTitle-H8sGiH7c.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DJssGrj8.js → FadeIn-pq2ja50M.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B9MwwA-P.js → HeaderGithubStar-DWgcpDHr.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DPLMJkai.js → HoverTooltip-CcJ_VR65.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BXKjxEHe.js → IpCheckPage-CNUN346Q.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BJNMQTec.js → LandingPage-yaoRGsNI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BNwbnOa3.js → LeaderboardAvatar-Ddai4I12.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BOxejyMy.js → LeaderboardPage-Bajkxf63.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BEqUZCOD.js → LeaderboardProfileModal-BGYKDgIi.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CUDV5iyh.js → LeaderboardProfilePage-Ck5vPaeN.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-Bqobtsw2.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B3BSeCHd.js → LocalOnlyNotice-e9KSE3wj.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-2mAb696j.js → LoginCard-BhTSm2tb.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-cacKhc6U.js → LoginModal-CRwuThde.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-rnBlie8p.js → LoginPage-j1i74lcc.js} +1 -1
- package/dashboard/dist/assets/{PetPage-D3DhuS6O.js → PetPage-r62ViU1X.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CxATX7M9.js → PopoverPopup-DopNTyD4.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-B5b9LkmM.js → ProviderIcon-c1LkPhYM.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BGKmQ_JW.js → ResetPasswordPage-DJdDNvtq.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BMeYCm2b.js → SegmentedControl-DhWVOTPN.js} +1 -1
- package/dashboard/dist/assets/{Select-CYHXq8im.js → Select-DeUmTmsJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DvI4i06h.js → SelectItemText-pX0B5f7h.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-dv26-VvI.js → ServiceStatusPage-ejIPz6oF.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DBQruqkO.js → SessionsPage-WEbn90GW.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-BWRbQ3Iq.js → SettingsPage-B8t5gCeI.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-BonhKgAt.js → SkillsPage-BWMlzeAp.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BBUD1kpU.js → TokenGalaxy-DIBRBvMj.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DPMm8uYz.js → ToolbarRootContext-C6KxLvnI.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CZVE6gID.js → TrendMonitor-BaMNh67w.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bz9cieeZ.js → WidgetsPage-DDsy4M3x.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-CruMolZd.js → WrappedPage-CiNnUFRI.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-C3VH6nS4.js → __vite-browser-external-CM-H8-LW.js} +24 -16
- package/dashboard/dist/assets/{agent-logos-HIeYwIqN.js → agent-logos-BjVa6oe7.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-n7Bi4Tvd.js → arrow-up-right-CCum1mqX.js} +1 -1
- package/dashboard/dist/assets/{check-C5Q44G5k.js → check-ySRQc1YP.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-BsnptUU0.js → chevron-right-5vFloHew.js} +1 -1
- package/dashboard/dist/assets/{copy-BGuL9uFa.js → copy-mhfKVMKo.js} +1 -1
- package/dashboard/dist/assets/{download-BljgPwR2.js → download-GdrXglSx.js} +1 -1
- package/dashboard/dist/assets/{external-link-B4Oz8hUe.js → external-link-KGFaKc8s.js} +1 -1
- package/dashboard/dist/assets/{flame-ChLG_NZE.js → flame-BmNtNoS1.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DjuNk13Z.js → format-tokens-CCpCNw3d.js} +1 -1
- package/dashboard/dist/assets/{icons-CkO42M-u.js → icons-XLKZavTf.js} +1 -1
- package/dashboard/dist/assets/{index-CEsvYBWY.js → index-CsywTeCQ.js} +1 -1
- package/dashboard/dist/assets/{index-CGxawuWx.js → index-IKpmeZte.js} +1 -1
- package/dashboard/dist/assets/{index-D87oE_rV.js → index-_xU5U5Ct.js} +1 -1
- package/dashboard/dist/assets/{info-et8xtJ4v.js → info-DUV5UpmT.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-vO_86yRy.js → limits-providers-DFkCZCZj.js} +1 -1
- package/dashboard/dist/assets/{link-2-Dge2rBkr.js → link-2-D7c64yLY.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Cs3wpqJe.js → loader-circle-sMONRK2M.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-zn71uZX3.js → maximize-2-CVWRgrk9.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CTt6mzLe.js → provider-display-DSKqVjgm.js} +1 -1
- package/dashboard/dist/assets/{react-BbHDQe6Q.js → react-c1q4mGkS.js} +1 -1
- package/dashboard/dist/assets/{search-3CMhatWb.js → search-CwD3s-_C.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Cm1aiqWL.js → skills-api-BRXj8kjT.js} +1 -1
- package/dashboard/dist/assets/{store-DclXV77t.js → store-lqTW2gfK.js} +1 -1
- package/dashboard/dist/assets/{terminal-BC7uFcA7.js → terminal-DU1_KbKo.js} +1 -1
- package/dashboard/dist/assets/{trash-2-BT8V531F.js → trash-2-B8Y5Xc-K.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DJr59SGu.js → use-community-stats-DYwwimvp.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DU8OOFVb.js → use-limits-display-prefs-Bst6ybK3.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BvSwI4BL.js → use-native-settings-Cqle4lgw.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Cq5AyV2x.js → use-ordered-list-Dtze0ahb.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-WS8NhXuW.js → use-session-efficiency-pref-gwbDMHXa.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CLYsi8xc.js → use-transform-CU0yxXpX.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-5y1I1T5_.js +1 -0
- package/dashboard/dist/assets/{useCurrency-D0GtEoJD.js → useCurrency-BfVNrywe.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-TIUBSeIM.js → useScrollLock-CF1m7AMJ.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-B0TBL9La.js → useTokenFormat-DiRwVvRr.js} +1 -1
- package/dashboard/dist/assets/{zap-D89769CL.js → zap-BZtlWtuN.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/status.js +114 -9
- package/src/commands/sync.js +151 -46
- 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 +480 -53
- package/src/lib/session-analytics.js +332 -9
- package/src/lib/usage-limits.js +47 -2
- package/dashboard/dist/assets/DashboardPage-CMCqvpkA.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Dwxj5h5w.js +0 -1
- package/dashboard/dist/assets/LimitsPage-ejlJN1vi.js +0 -4
- package/dashboard/dist/assets/use-reduced-motion-D1_q3aRK.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-CGoQQDG-.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require("node:path");
|
|
|
5
5
|
const readline = require("node:readline");
|
|
6
6
|
|
|
7
7
|
const crypto = require("node:crypto");
|
|
8
|
-
const { ensureDir } = require("./fs");
|
|
8
|
+
const { ensureDir, writeJson, chmod600IfPossible } = require("./fs");
|
|
9
9
|
const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
10
10
|
const wsl = require("./wsl-probe");
|
|
11
11
|
const { resolveInstallPaths } = require("./install-resolver");
|
|
@@ -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,
|
|
@@ -2075,7 +2169,10 @@ async function parseClaudeFile({
|
|
|
2075
2169
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2076
2170
|
|
|
2077
2171
|
let eventsAggregated = 0;
|
|
2078
|
-
|
|
2172
|
+
// Separator-agnostic: WSL installs are scanned over \\wsl$ UNC paths where
|
|
2173
|
+
// path.join produces backslashes, and a forward-slash-only check would
|
|
2174
|
+
// misclassify subagent transcripts as main sessions (#307).
|
|
2175
|
+
const isMainSession = !/[\\/]subagents[\\/]/.test(filePath);
|
|
2079
2176
|
for await (const line of rl) {
|
|
2080
2177
|
if (!line) continue;
|
|
2081
2178
|
|
|
@@ -2095,13 +2192,25 @@ async function parseClaudeFile({
|
|
|
2095
2192
|
typeof content === "string" ||
|
|
2096
2193
|
(Array.isArray(content) && content.some((b) => b?.type === "text"));
|
|
2097
2194
|
if (hasText) {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2195
|
+
// Dedup by the line's uuid so a synced copy of the session file
|
|
2196
|
+
// (WSL mirror of a divergent native file, inode-reset re-read)
|
|
2197
|
+
// cannot re-count the conversation. Usage rows get the same
|
|
2198
|
+
// guarantee from claudeMessageDedupKey; user lines have no
|
|
2199
|
+
// message.id, so the line uuid is the identity.
|
|
2200
|
+
const userKey =
|
|
2201
|
+
seenMessageHashes && typeof userObj?.uuid === "string" && userObj.uuid
|
|
2202
|
+
? `u:${userObj.uuid}`
|
|
2203
|
+
: null;
|
|
2204
|
+
if (!userKey || !seenMessageHashes.has(userKey)) {
|
|
2205
|
+
if (userKey) seenMessageHashes.add(userKey);
|
|
2206
|
+
const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
|
|
2207
|
+
const userBucketStart = userTs ? toUtcHalfHourStart(userTs) : null;
|
|
2208
|
+
if (userBucketStart) {
|
|
2209
|
+
const userModel = DEFAULT_MODEL;
|
|
2210
|
+
const userBucket = getHourlyBucket(hourlyState, source, userModel, userBucketStart);
|
|
2211
|
+
userBucket.totals.conversation_count += 1;
|
|
2212
|
+
touchedBuckets.add(bucketKey(source, userModel, userBucketStart));
|
|
2213
|
+
}
|
|
2105
2214
|
}
|
|
2106
2215
|
}
|
|
2107
2216
|
}
|
|
@@ -4554,37 +4663,60 @@ async function parseCursorApiIncremental({
|
|
|
4554
4663
|
// Kiro token tracking (reads from devdata.sqlite or tokens_generated.jsonl)
|
|
4555
4664
|
// ---------------------------------------------------------------------------
|
|
4556
4665
|
|
|
4557
|
-
|
|
4666
|
+
// Kiro IDE (VS Code fork) globalStorage lives under the editor config root,
|
|
4667
|
+
// which differs per platform: %APPDATA% on Windows, ~/.config on Linux/WSL,
|
|
4668
|
+
// ~/Library/Application Support on macOS.
|
|
4669
|
+
function resolveKiroBasePath(env = process.env) {
|
|
4558
4670
|
const home = require("node:os").homedir();
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
"
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4671
|
+
const suffix = ["Kiro", "User", "globalStorage", "kiro.kiroagent"];
|
|
4672
|
+
if (process.platform === "win32") {
|
|
4673
|
+
const appData = typeof env.APPDATA === "string" && env.APPDATA.trim().length > 0
|
|
4674
|
+
? env.APPDATA.trim()
|
|
4675
|
+
: path.join(home, "AppData", "Roaming");
|
|
4676
|
+
return path.join(appData, ...suffix);
|
|
4677
|
+
}
|
|
4678
|
+
if (process.platform === "linux") {
|
|
4679
|
+
const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim().length > 0
|
|
4680
|
+
? env.XDG_CONFIG_HOME.trim()
|
|
4681
|
+
: path.join(home, ".config");
|
|
4682
|
+
return path.join(configHome, ...suffix);
|
|
4683
|
+
}
|
|
4684
|
+
return path.join(home, "Library", "Application Support", ...suffix);
|
|
4568
4685
|
}
|
|
4569
4686
|
|
|
4570
|
-
function resolveKiroDbPath() {
|
|
4571
|
-
return path.join(resolveKiroBasePath(), "dev_data", "devdata.sqlite");
|
|
4687
|
+
function resolveKiroDbPath(basePath) {
|
|
4688
|
+
return path.join(basePath || resolveKiroBasePath(), "dev_data", "devdata.sqlite");
|
|
4572
4689
|
}
|
|
4573
4690
|
|
|
4574
|
-
function resolveKiroJsonlPath() {
|
|
4575
|
-
return path.join(resolveKiroBasePath(), "dev_data", "tokens_generated.jsonl");
|
|
4691
|
+
function resolveKiroJsonlPath(basePath) {
|
|
4692
|
+
return path.join(basePath || resolveKiroBasePath(), "dev_data", "tokens_generated.jsonl");
|
|
4576
4693
|
}
|
|
4577
4694
|
|
|
4578
4695
|
function readKiroDbTokens(dbPath, sinceId, sqliteOptions = {}) {
|
|
4579
4696
|
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
4580
4697
|
const minId = Number.isFinite(sinceId) && sinceId > 0 ? sinceId : 0;
|
|
4581
4698
|
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
|
-
|
|
4699
|
+
|
|
4700
|
+
// WSL installs are read over the \\wsl$ UNC bridge; sqlite3 cannot safely
|
|
4701
|
+
// open WAL databases there, so copy db (+ sidecars) to tmp first.
|
|
4702
|
+
let snapshot = null;
|
|
4703
|
+
let effectiveDbPath = dbPath;
|
|
4704
|
+
if (isUncPath(dbPath)) {
|
|
4705
|
+
try {
|
|
4706
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
4707
|
+
effectiveDbPath = snapshot.path;
|
|
4708
|
+
} catch (_e) { }
|
|
4709
|
+
}
|
|
4710
|
+
try {
|
|
4711
|
+
return readSqliteJsonRows(effectiveDbPath, sql, {
|
|
4712
|
+
label: "Kiro",
|
|
4713
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
4714
|
+
timeout: 15_000,
|
|
4715
|
+
...sqliteOptions,
|
|
4716
|
+
});
|
|
4717
|
+
} finally {
|
|
4718
|
+
if (snapshot) snapshot.cleanup();
|
|
4719
|
+
}
|
|
4588
4720
|
}
|
|
4589
4721
|
|
|
4590
4722
|
// Read Kiro token data from JSONL fallback (tokens_generated.jsonl).
|
|
@@ -4726,7 +4858,7 @@ function normalizeKiroModelName(raw) {
|
|
|
4726
4858
|
return name || null;
|
|
4727
4859
|
}
|
|
4728
4860
|
|
|
4729
|
-
async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
|
|
4861
|
+
async function parseKiroIncremental({ basePath, dbPath, jsonlPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
|
|
4730
4862
|
await ensureDir(path.dirname(queuePath));
|
|
4731
4863
|
const kiroState = cursors.kiro && typeof cursors.kiro === "object" ? cursors.kiro : {};
|
|
4732
4864
|
const lastDbId = typeof kiroState.lastDbId === "number"
|
|
@@ -4735,8 +4867,8 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
4735
4867
|
const jsonlState = kiroState.jsonl && typeof kiroState.jsonl === "object" ? kiroState.jsonl : {};
|
|
4736
4868
|
const lastJsonlLine = typeof jsonlState.lastLine === "number" ? jsonlState.lastLine : 0;
|
|
4737
4869
|
|
|
4738
|
-
const resolvedDbPath = dbPath || resolveKiroDbPath();
|
|
4739
|
-
const resolvedJsonlPath = jsonlPath || resolveKiroJsonlPath();
|
|
4870
|
+
const resolvedDbPath = dbPath || resolveKiroDbPath(basePath);
|
|
4871
|
+
const resolvedJsonlPath = jsonlPath || resolveKiroJsonlPath(basePath);
|
|
4740
4872
|
|
|
4741
4873
|
// Try SQLite first, fall back to JSONL.
|
|
4742
4874
|
let rows = [];
|
|
@@ -4784,9 +4916,12 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
4784
4916
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
4785
4917
|
}
|
|
4786
4918
|
|
|
4787
|
-
// Build model timeline from .chat files for model name resolution
|
|
4788
|
-
|
|
4789
|
-
|
|
4919
|
+
// Build model timeline from .chat files for model name resolution.
|
|
4920
|
+
// Must come from the SAME install as the rows being parsed (dual-install:
|
|
4921
|
+
// a WSL install's rows must not be resolved against native .chat files).
|
|
4922
|
+
const timelineBase = basePath
|
|
4923
|
+
|| (dbPath ? path.dirname(path.dirname(dbPath)) : resolveKiroBasePath());
|
|
4924
|
+
const modelTimeline = buildKiroModelTimeline(timelineBase);
|
|
4790
4925
|
|
|
4791
4926
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4792
4927
|
const touchedBuckets = new Set();
|
|
@@ -5075,6 +5210,74 @@ function hermesInstallOwnsCursor(hermesPath, flatState, sqliteOptions) {
|
|
|
5075
5210
|
return allDbs.some((db) => sqliteDbContainsIds(db, "sessions", sample, sqliteOptions));
|
|
5076
5211
|
}
|
|
5077
5212
|
|
|
5213
|
+
function kiroInstallOwnsCursor(dbPath, flatState, sqliteOptions) {
|
|
5214
|
+
// The flat cursor tracks the max AUTOINCREMENT id it consumed. An install
|
|
5215
|
+
// whose tokens_generated table never reached that id cannot be the flat
|
|
5216
|
+
// cursor's host. Both installs containing the id (short/equal histories)
|
|
5217
|
+
// yields two hits upstream → caller seeds every namespace (safe).
|
|
5218
|
+
const lastDbId = typeof flatState?.lastDbId === "number" && Number.isInteger(flatState.lastDbId) && flatState.lastDbId > 0
|
|
5219
|
+
? flatState.lastDbId
|
|
5220
|
+
: (typeof flatState?.lastId === "number" && Number.isInteger(flatState.lastId) && flatState.lastId > 0
|
|
5221
|
+
? flatState.lastId
|
|
5222
|
+
: 0);
|
|
5223
|
+
if (!lastDbId) return false;
|
|
5224
|
+
return sqliteDbContainsIds(dbPath, "tokens_generated", [String(lastDbId)], sqliteOptions);
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
// Kiro CLI request_ids live inside the conversations_v2 JSON payload, not in
|
|
5228
|
+
// an id column — probe via json_each instead of sqliteDbContainsIds.
|
|
5229
|
+
function kiroCliDbContainsRequestIds(dbPath, requestIds, sqliteOptions = {}) {
|
|
5230
|
+
if (!dbPath || !Array.isArray(requestIds) || requestIds.length === 0) return false;
|
|
5231
|
+
if (!fssync.existsSync(dbPath)) return false;
|
|
5232
|
+
const inList = requestIds.map(sqliteStringLiteral).join(",");
|
|
5233
|
+
// Alias both tables: conversations_v2.value and json_each's own `value`
|
|
5234
|
+
// column collide — a bare `value` is "ambiguous column name" and sqlite
|
|
5235
|
+
// errors out (which readSqliteJsonRows swallows into []).
|
|
5236
|
+
const sql =
|
|
5237
|
+
"SELECT 1 FROM conversations_v2 AS c, " +
|
|
5238
|
+
"json_each(json_extract(c.value, '$.user_turn_metadata.requests')) AS je " +
|
|
5239
|
+
`WHERE json_extract(je.value, '$.request_id') IN (${inList}) LIMIT 1`;
|
|
5240
|
+
|
|
5241
|
+
let snapshot = null;
|
|
5242
|
+
let effectiveDbPath = dbPath;
|
|
5243
|
+
if (isUncPath(dbPath)) {
|
|
5244
|
+
try {
|
|
5245
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
5246
|
+
effectiveDbPath = snapshot.path;
|
|
5247
|
+
} catch (_e) { }
|
|
5248
|
+
}
|
|
5249
|
+
try {
|
|
5250
|
+
const rows = readSqliteJsonRows(effectiveDbPath, sql, {
|
|
5251
|
+
label: "InstallProbe",
|
|
5252
|
+
maxBuffer: 1024 * 1024,
|
|
5253
|
+
timeout: 10_000,
|
|
5254
|
+
...sqliteOptions,
|
|
5255
|
+
});
|
|
5256
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
5257
|
+
} catch (_e) {
|
|
5258
|
+
return false;
|
|
5259
|
+
} finally {
|
|
5260
|
+
if (snapshot) snapshot.cleanup();
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
function kiroCliInstallOwnsCursor(dbPath, flatState, sqliteOptions) {
|
|
5265
|
+
// Only SQLite-origin request_id UUIDs are probe evidence: session-file
|
|
5266
|
+
// entries synthesize `${sessionId}:${loopRand}` keys or carry a session_id
|
|
5267
|
+
// tag, and neither appears in conversations_v2. Filter BEFORE sampling so
|
|
5268
|
+
// recent session-file churn cannot blind the probe to older SQLite ids.
|
|
5269
|
+
const requests =
|
|
5270
|
+
flatState?.requests && typeof flatState.requests === "object" ? flatState.requests : {};
|
|
5271
|
+
const sqliteKeyed = {};
|
|
5272
|
+
for (const [k, v] of Object.entries(requests)) {
|
|
5273
|
+
if (k.includes(":")) continue;
|
|
5274
|
+
if (v && typeof v === "object" && typeof v.session_id === "string" && v.session_id) continue;
|
|
5275
|
+
sqliteKeyed[k] = v;
|
|
5276
|
+
}
|
|
5277
|
+
const sample = sampleRecentKeys(sqliteKeyed);
|
|
5278
|
+
return kiroCliDbContainsRequestIds(dbPath, sample, sqliteOptions);
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5078
5281
|
function hasLegacyHermesDefaultState(hermesState) {
|
|
5079
5282
|
return (
|
|
5080
5283
|
typeof hermesState.lastStartedAt === "number" ||
|
|
@@ -5302,10 +5505,27 @@ function resolveKimiDefaultModel(env = process.env) {
|
|
|
5302
5505
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5303
5506
|
|
|
5304
5507
|
const KIRO_CLI_CHARS_PER_TOKEN = 4;
|
|
5508
|
+
const KIRO_CLI_CREDITS_SIDECAR = "kiro-credits.json";
|
|
5305
5509
|
|
|
5306
5510
|
function resolveKiroCliDbPath(env = process.env) {
|
|
5307
5511
|
if (env.KIRO_CLI_DB_PATH) return env.KIRO_CLI_DB_PATH;
|
|
5308
5512
|
const home = env.HOME || require("node:os").homedir();
|
|
5513
|
+
if (process.platform === "win32") {
|
|
5514
|
+
// Speculative: Kiro CLI descends from Amazon Q CLI, which ships
|
|
5515
|
+
// macOS/Linux only — Windows users run it inside WSL (handled by the
|
|
5516
|
+
// dual-install path in sync). Kept as the conventional %LOCALAPPDATA%
|
|
5517
|
+
// location so a future native build is picked up; harmless when absent.
|
|
5518
|
+
const localAppData = typeof env.LOCALAPPDATA === "string" && env.LOCALAPPDATA.trim().length > 0
|
|
5519
|
+
? env.LOCALAPPDATA.trim()
|
|
5520
|
+
: path.join(home, "AppData", "Local");
|
|
5521
|
+
return path.join(localAppData, "kiro-cli", "data.sqlite3");
|
|
5522
|
+
}
|
|
5523
|
+
if (process.platform === "linux") {
|
|
5524
|
+
const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.trim().length > 0
|
|
5525
|
+
? env.XDG_DATA_HOME.trim()
|
|
5526
|
+
: path.join(home, ".local", "share");
|
|
5527
|
+
return path.join(dataHome, "kiro-cli", "data.sqlite3");
|
|
5528
|
+
}
|
|
5309
5529
|
return path.join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3");
|
|
5310
5530
|
}
|
|
5311
5531
|
|
|
@@ -5619,18 +5839,45 @@ function kiroCliV2ContentChars(value) {
|
|
|
5619
5839
|
return 0;
|
|
5620
5840
|
}
|
|
5621
5841
|
|
|
5842
|
+
function kiroCliV2UsageCredits(payload) {
|
|
5843
|
+
const summaries = Array.isArray(payload?.promptTurnSummaries)
|
|
5844
|
+
? payload.promptTurnSummaries
|
|
5845
|
+
: [];
|
|
5846
|
+
let totalCredits = 0;
|
|
5847
|
+
let hasCreditEntry = false;
|
|
5848
|
+
for (const summary of summaries) {
|
|
5849
|
+
if (!summary || typeof summary !== "object") continue;
|
|
5850
|
+
const unit = String(summary.unit || "").trim().toLowerCase();
|
|
5851
|
+
if (unit !== "credit" && unit !== "credits") continue;
|
|
5852
|
+
const usage = Number(summary.usage);
|
|
5853
|
+
if (!Number.isFinite(usage) || usage < 0) continue;
|
|
5854
|
+
totalCredits += usage;
|
|
5855
|
+
hasCreditEntry = true;
|
|
5856
|
+
}
|
|
5857
|
+
return {
|
|
5858
|
+
totalCredits,
|
|
5859
|
+
recordCount: hasCreditEntry ? 1 : 0,
|
|
5860
|
+
};
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5622
5863
|
// Kiro CLI 2.13+ writes event-sourced sessions under
|
|
5623
5864
|
// ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. The records do
|
|
5624
|
-
// not carry token counts, but user/assistant text,
|
|
5625
|
-
// and assistant reasoningModelId are sufficient for
|
|
5626
|
-
// approximation used by the legacy CLI reader.
|
|
5865
|
+
// not carry token counts, but user/assistant text, tool_result payloads, turn
|
|
5866
|
+
// boundaries, timestamps, and assistant reasoningModelId are sufficient for
|
|
5867
|
+
// the same 4 chars/token approximation used by the legacy CLI reader.
|
|
5868
|
+
// tool_result content counts as INPUT: tool output (file reads, command
|
|
5869
|
+
// output, search results) is fed back to the model as context on the next
|
|
5870
|
+
// request — skipping it undercounted tool-heavy sessions by ~67% (#366).
|
|
5627
5871
|
async function readKiroCliV2SessionTurns(messagesPath) {
|
|
5628
5872
|
if (
|
|
5629
5873
|
!messagesPath ||
|
|
5630
5874
|
path.basename(messagesPath) !== "messages.jsonl" ||
|
|
5631
5875
|
!fssync.existsSync(messagesPath)
|
|
5632
5876
|
) {
|
|
5633
|
-
return
|
|
5877
|
+
return {
|
|
5878
|
+
turns: [],
|
|
5879
|
+
credits: { totalCredits: 0, recordCount: 0, latestAt: null },
|
|
5880
|
+
};
|
|
5634
5881
|
}
|
|
5635
5882
|
|
|
5636
5883
|
const sessionDir = path.dirname(messagesPath);
|
|
@@ -5658,7 +5905,10 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5658
5905
|
try {
|
|
5659
5906
|
stream = fssync.createReadStream(messagesPath, { encoding: "utf8" });
|
|
5660
5907
|
} catch {
|
|
5661
|
-
return
|
|
5908
|
+
return {
|
|
5909
|
+
turns: flat,
|
|
5910
|
+
credits: { totalCredits: 0, recordCount: 0, latestAt: null },
|
|
5911
|
+
};
|
|
5662
5912
|
}
|
|
5663
5913
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
5664
5914
|
|
|
@@ -5666,6 +5916,10 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5666
5916
|
let pendingUserTimestampMs = NaN;
|
|
5667
5917
|
let turn = null;
|
|
5668
5918
|
let fallbackTurnIndex = 0;
|
|
5919
|
+
const seenCreditEvents = new Set();
|
|
5920
|
+
let totalCredits = 0;
|
|
5921
|
+
let creditRecordCount = 0;
|
|
5922
|
+
let latestCreditAt = null;
|
|
5669
5923
|
|
|
5670
5924
|
const flushTurn = () => {
|
|
5671
5925
|
if (!turn) return;
|
|
@@ -5747,8 +6001,35 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5747
6001
|
continue;
|
|
5748
6002
|
}
|
|
5749
6003
|
|
|
6004
|
+
if (type === "usage_summary") {
|
|
6005
|
+
const creditEventKey =
|
|
6006
|
+
typeof event.id === "string" && event.id
|
|
6007
|
+
? event.id
|
|
6008
|
+
: `${payload.executionId || ""}:${event.timestamp || ""}:${creditRecordCount}`;
|
|
6009
|
+
if (!seenCreditEvents.has(creditEventKey)) {
|
|
6010
|
+
seenCreditEvents.add(creditEventKey);
|
|
6011
|
+
const usage = kiroCliV2UsageCredits(payload);
|
|
6012
|
+
if (usage.recordCount > 0) {
|
|
6013
|
+
totalCredits += usage.totalCredits;
|
|
6014
|
+
creditRecordCount += usage.recordCount;
|
|
6015
|
+
if (
|
|
6016
|
+
Number.isFinite(timestampMs) &&
|
|
6017
|
+
(
|
|
6018
|
+
!latestCreditAt ||
|
|
6019
|
+
timestampMs > Date.parse(latestCreditAt)
|
|
6020
|
+
)
|
|
6021
|
+
) {
|
|
6022
|
+
latestCreditAt = new Date(timestampMs).toISOString();
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
// Billing summaries are independent metadata. They can arrive after
|
|
6027
|
+
// turn_end, so they must not create or mutate token turns.
|
|
6028
|
+
continue;
|
|
6029
|
+
}
|
|
6030
|
+
|
|
5750
6031
|
if (
|
|
5751
|
-
["assistant", "tool_call", "tool_result"
|
|
6032
|
+
["assistant", "tool_call", "tool_result"].includes(
|
|
5752
6033
|
type,
|
|
5753
6034
|
) &&
|
|
5754
6035
|
!turn
|
|
@@ -5784,6 +6065,10 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5784
6065
|
) {
|
|
5785
6066
|
turn.modelId = payload.reasoningModelId;
|
|
5786
6067
|
}
|
|
6068
|
+
} else if (type === "tool_result") {
|
|
6069
|
+
// #366: tool output re-enters the model as input context on the
|
|
6070
|
+
// next request within the turn. Same ÷4 heuristic as user text.
|
|
6071
|
+
turn.inputChars += kiroCliV2ContentChars(payload.content);
|
|
5787
6072
|
} else if (type === "turn_end") {
|
|
5788
6073
|
flushTurn();
|
|
5789
6074
|
}
|
|
@@ -5792,7 +6077,95 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5792
6077
|
// Return complete turns parsed before a concurrent truncate/delete.
|
|
5793
6078
|
}
|
|
5794
6079
|
flushTurn();
|
|
5795
|
-
return
|
|
6080
|
+
return {
|
|
6081
|
+
turns: flat,
|
|
6082
|
+
credits: {
|
|
6083
|
+
totalCredits,
|
|
6084
|
+
recordCount: creditRecordCount,
|
|
6085
|
+
latestAt: latestCreditAt,
|
|
6086
|
+
},
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
async function writeKiroCliCreditsSidecar({
|
|
6091
|
+
queuePath,
|
|
6092
|
+
installKey,
|
|
6093
|
+
totalCredits,
|
|
6094
|
+
recordCount,
|
|
6095
|
+
sessionCount,
|
|
6096
|
+
fileCount,
|
|
6097
|
+
latestAt,
|
|
6098
|
+
} = {}) {
|
|
6099
|
+
if (!queuePath || !(fileCount > 0)) return;
|
|
6100
|
+
const sidecarPath = path.join(
|
|
6101
|
+
path.dirname(queuePath),
|
|
6102
|
+
KIRO_CLI_CREDITS_SIDECAR,
|
|
6103
|
+
);
|
|
6104
|
+
try {
|
|
6105
|
+
// Dual-install (#306): keep one entry per install and aggregate at the
|
|
6106
|
+
// top level, so a second install's write never clobbers the first. The
|
|
6107
|
+
// top-level fields keep the v1 shape readKiroCreditsSummary consumes.
|
|
6108
|
+
let installs = {};
|
|
6109
|
+
try {
|
|
6110
|
+
const prev = JSON.parse(await fs.readFile(sidecarPath, "utf8"));
|
|
6111
|
+
if (prev?.version === 1 && prev.installs && typeof prev.installs === "object") {
|
|
6112
|
+
installs = prev.installs;
|
|
6113
|
+
}
|
|
6114
|
+
} catch {
|
|
6115
|
+
// first write or pre-dual-install sidecar — start fresh
|
|
6116
|
+
}
|
|
6117
|
+
// Expire entries whose install stopped syncing (deleted WSL distro,
|
|
6118
|
+
// distro rename / UNC alias flip leaving a duplicate key, mode switched
|
|
6119
|
+
// to *-only). Without this the read-modify-write merge would inflate the
|
|
6120
|
+
// aggregate with phantom installs forever; a 30-day grace keeps entries
|
|
6121
|
+
// alive across transient wsl.exe probe failures.
|
|
6122
|
+
const staleMs = 30 * 24 * 60 * 60 * 1000;
|
|
6123
|
+
for (const [k, entry] of Object.entries(installs)) {
|
|
6124
|
+
const ts = Date.parse(entry?.updated_at || "");
|
|
6125
|
+
if (!Number.isFinite(ts) || Date.now() - ts > staleMs) delete installs[k];
|
|
6126
|
+
}
|
|
6127
|
+
const key = typeof installKey === "string" && installKey ? installKey : "default";
|
|
6128
|
+
installs[key] = {
|
|
6129
|
+
total_credits: Number(totalCredits.toFixed(12)),
|
|
6130
|
+
record_count: recordCount,
|
|
6131
|
+
session_count: sessionCount,
|
|
6132
|
+
file_count: fileCount,
|
|
6133
|
+
latest_at: latestAt,
|
|
6134
|
+
updated_at: new Date().toISOString(),
|
|
6135
|
+
};
|
|
6136
|
+
let aggCredits = 0;
|
|
6137
|
+
let aggRecords = 0;
|
|
6138
|
+
let aggSessions = 0;
|
|
6139
|
+
let aggFiles = 0;
|
|
6140
|
+
let aggLatestAt = null;
|
|
6141
|
+
for (const entry of Object.values(installs)) {
|
|
6142
|
+
aggCredits += Number(entry.total_credits) || 0;
|
|
6143
|
+
aggRecords += Number(entry.record_count) || 0;
|
|
6144
|
+
aggSessions += Number(entry.session_count) || 0;
|
|
6145
|
+
aggFiles += Number(entry.file_count) || 0;
|
|
6146
|
+
if (
|
|
6147
|
+
typeof entry.latest_at === "string" &&
|
|
6148
|
+
(!aggLatestAt || Date.parse(entry.latest_at) > Date.parse(aggLatestAt))
|
|
6149
|
+
) {
|
|
6150
|
+
aggLatestAt = entry.latest_at;
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
await writeJson(sidecarPath, {
|
|
6154
|
+
version: 1,
|
|
6155
|
+
source: "kiro-cli-usage-summary",
|
|
6156
|
+
total_credits: Number(aggCredits.toFixed(12)),
|
|
6157
|
+
record_count: aggRecords,
|
|
6158
|
+
session_count: aggSessions,
|
|
6159
|
+
file_count: aggFiles,
|
|
6160
|
+
latest_at: aggLatestAt,
|
|
6161
|
+
updated_at: new Date().toISOString(),
|
|
6162
|
+
installs,
|
|
6163
|
+
});
|
|
6164
|
+
await chmod600IfPossible(sidecarPath);
|
|
6165
|
+
} catch {
|
|
6166
|
+
// Credits are supplemental billing metadata. A sidecar write failure must
|
|
6167
|
+
// never block the canonical token queue.
|
|
6168
|
+
}
|
|
5796
6169
|
}
|
|
5797
6170
|
|
|
5798
6171
|
// Canonicalize a Kiro-CLI-emitted model id so IDE and CLI rows collapse when
|
|
@@ -5849,13 +6222,29 @@ function readKiroCliRequests(dbPath, env = process.env, sqliteOptions = {}) {
|
|
|
5849
6222
|
"json_extract(value, '$.user_turn_metadata.requests') AS requests_json " +
|
|
5850
6223
|
"FROM conversations_v2 " +
|
|
5851
6224
|
"WHERE json_extract(value, '$.user_turn_metadata.requests') IS NOT NULL";
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
6225
|
+
|
|
6226
|
+
// WSL installs are read over the \\wsl$ UNC bridge; snapshot to tmp so
|
|
6227
|
+
// sqlite3 never opens a WAL database across the 9p bridge.
|
|
6228
|
+
let snapshot = null;
|
|
6229
|
+
let effectiveDbPath = dbPath;
|
|
6230
|
+
if (isUncPath(dbPath)) {
|
|
6231
|
+
try {
|
|
6232
|
+
snapshot = snapshotSqliteDb(dbPath);
|
|
6233
|
+
effectiveDbPath = snapshot.path;
|
|
6234
|
+
} catch (_e) { }
|
|
6235
|
+
}
|
|
6236
|
+
let rows;
|
|
6237
|
+
try {
|
|
6238
|
+
rows = readSqliteJsonRows(effectiveDbPath, sql, {
|
|
6239
|
+
label: "Kiro CLI",
|
|
6240
|
+
env,
|
|
6241
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
6242
|
+
timeout: 120_000,
|
|
6243
|
+
...sqliteOptions,
|
|
6244
|
+
});
|
|
6245
|
+
} finally {
|
|
6246
|
+
if (snapshot) snapshot.cleanup();
|
|
6247
|
+
}
|
|
5859
6248
|
const flat = [];
|
|
5860
6249
|
for (const row of rows) {
|
|
5861
6250
|
let requests;
|
|
@@ -5923,13 +6312,48 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5923
6312
|
: [];
|
|
5924
6313
|
const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
|
|
5925
6314
|
let flatSessions = [];
|
|
6315
|
+
let kiroCreditTotal = 0;
|
|
6316
|
+
let kiroCreditRecords = 0;
|
|
6317
|
+
let kiroCreditSessions = 0;
|
|
6318
|
+
let kiroCreditFiles = 0;
|
|
6319
|
+
let latestKiroCreditAt = null;
|
|
5926
6320
|
for (const sessionPath of sessionFilesList) {
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
6321
|
+
let turns;
|
|
6322
|
+
if (path.basename(sessionPath) === "messages.jsonl") {
|
|
6323
|
+
kiroCreditFiles++;
|
|
6324
|
+
const parsed = await readKiroCliV2SessionTurns(sessionPath);
|
|
6325
|
+
turns = parsed.turns;
|
|
6326
|
+
const credits = parsed.credits;
|
|
6327
|
+
kiroCreditTotal += credits.totalCredits;
|
|
6328
|
+
kiroCreditRecords += credits.recordCount;
|
|
6329
|
+
if (credits.recordCount > 0) kiroCreditSessions++;
|
|
6330
|
+
if (
|
|
6331
|
+
credits.latestAt &&
|
|
6332
|
+
(
|
|
6333
|
+
!latestKiroCreditAt ||
|
|
6334
|
+
Date.parse(credits.latestAt) > Date.parse(latestKiroCreditAt)
|
|
6335
|
+
)
|
|
6336
|
+
) {
|
|
6337
|
+
latestKiroCreditAt = credits.latestAt;
|
|
6338
|
+
}
|
|
6339
|
+
} else {
|
|
6340
|
+
turns = await readKiroCliSessionTurns(sessionPath);
|
|
6341
|
+
}
|
|
5931
6342
|
for (const turn of turns) flatSessions.push(turn);
|
|
5932
6343
|
}
|
|
6344
|
+
await writeKiroCliCreditsSidecar({
|
|
6345
|
+
queuePath,
|
|
6346
|
+
// Install identity mirrors resolveKiroCliSessionFiles' sessions home so
|
|
6347
|
+
// dual installs keep separate sidecar entries.
|
|
6348
|
+
installKey:
|
|
6349
|
+
resolvedEnv.KIRO_HOME ||
|
|
6350
|
+
path.join(resolvedEnv.HOME || require("node:os").homedir(), ".kiro"),
|
|
6351
|
+
totalCredits: kiroCreditTotal,
|
|
6352
|
+
recordCount: kiroCreditRecords,
|
|
6353
|
+
sessionCount: kiroCreditSessions,
|
|
6354
|
+
fileCount: kiroCreditFiles,
|
|
6355
|
+
latestAt: latestKiroCreditAt,
|
|
6356
|
+
});
|
|
5933
6357
|
// Per-request state replaces the old seenIds set. Each entry captures
|
|
5934
6358
|
// what we contributed for that request_id last time, so a later mutation
|
|
5935
6359
|
// (same request_id, different fingerprint) can subtract-old/add-new
|
|
@@ -14237,6 +14661,7 @@ module.exports = {
|
|
|
14237
14661
|
resolveQoderDbPath,
|
|
14238
14662
|
resolveQoderDbPaths,
|
|
14239
14663
|
readQoderDbMessages,
|
|
14664
|
+
resolveKiroBasePath,
|
|
14240
14665
|
resolveKiroDbPath,
|
|
14241
14666
|
resolveKiroJsonlPath,
|
|
14242
14667
|
resolveHermesPath,
|
|
@@ -14270,6 +14695,8 @@ module.exports = {
|
|
|
14270
14695
|
gooseInstallOwnsCursor,
|
|
14271
14696
|
zedInstallOwnsCursor,
|
|
14272
14697
|
hermesInstallOwnsCursor,
|
|
14698
|
+
kiroInstallOwnsCursor,
|
|
14699
|
+
kiroCliInstallOwnsCursor,
|
|
14273
14700
|
copilotOtelCursorHasLegacyCliUsage,
|
|
14274
14701
|
pruneCopilotUsageClaims,
|
|
14275
14702
|
parseCopilotIncremental,
|