tokentracker-cli 0.84.9 → 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-BgBFpxmU.js → AchievementBadge-DEqbHlmn.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DHPD3Fnu.js → AchievementsPage-BOaEu1AQ.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DouWVD-3.js → AchievementsSection-C370ngO3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Df9eQiA3.js → BadgeDetailModal-D7Wkef0K.js} +1 -1
- package/dashboard/dist/assets/{Card-CQ9wBnz6.js → Card-mDEVjNdM.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-XIO-I-5v.js → ClawdAnimated-DdsX-z8n.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Cy1cJ91B.js → CommandPalette-DhxRIuHC.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CwKG4LQM.js → CommunityStatsModal-CG-9JPS6.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BNcWqick.js → ConfirmModal-COhxErLm.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-tX0DKgFE.js +1 -0
- package/dashboard/dist/assets/{DevicePage-pJ2jNkZV.js → DevicePage-BsSeHx84.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C2SOtkJN.js → DialogClose-eyOW2cbx.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CGSbB8G7.js → DialogTitle-H8sGiH7c.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CZSiw7me.js → FadeIn-pq2ja50M.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-ZmQhX_kR.js → HeaderGithubStar-DWgcpDHr.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D_YdG369.js → HoverTooltip-CcJ_VR65.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-du886dF0.js → IpCheckPage-CNUN346Q.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BuAak8lU.js → LandingPage-yaoRGsNI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-nU0ARYnN.js → LeaderboardAvatar-Ddai4I12.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CngwCvC7.js → LeaderboardPage-Bajkxf63.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CXZJ0e-T.js → LeaderboardProfileModal-BGYKDgIi.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CZve6s6b.js → LeaderboardProfilePage-Ck5vPaeN.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CWvoJPk8.js → LimitsPage-Bqobtsw2.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DyUAAyyE.js → LocalOnlyNotice-e9KSE3wj.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-eEiRfbdv.js → LoginCard-BhTSm2tb.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-C16WdIQm.js → LoginModal-CRwuThde.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-B2WqpE1t.js → LoginPage-j1i74lcc.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DtQWrGmI.js → PetPage-r62ViU1X.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CRhqtFLB.js → PopoverPopup-DopNTyD4.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DWNH_wYu.js → ProviderIcon-c1LkPhYM.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DzCczcWZ.js → ResetPasswordPage-DJdDNvtq.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-CV9ccq6R.js → SegmentedControl-DhWVOTPN.js} +1 -1
- package/dashboard/dist/assets/{Select-BrogNNk0.js → Select-DeUmTmsJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bpu3swt0.js → SelectItemText-pX0B5f7h.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-D_FIrMJP.js → ServiceStatusPage-ejIPz6oF.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-C8TvTy1o.js → SessionsPage-WEbn90GW.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-Bd-tzJAR.js → SettingsPage-B8t5gCeI.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CKGU9wQ4.js → SkillsPage-BWMlzeAp.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DhHW6L-U.js → TokenGalaxy-DIBRBvMj.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DQo-4pPB.js → ToolbarRootContext-C6KxLvnI.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-Ejl8d45g.js → TrendMonitor-BaMNh67w.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CY7f4wCD.js → WidgetsPage-DDsy4M3x.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B4-B2v-v.js → WrappedPage-CiNnUFRI.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BNUFXCfp.js → __vite-browser-external-CM-H8-LW.js} +23 -16
- package/dashboard/dist/assets/{agent-logos-BRzutmeR.js → agent-logos-BjVa6oe7.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BcrbPk8c.js → arrow-up-right-CCum1mqX.js} +1 -1
- package/dashboard/dist/assets/{check-BMxeFPfQ.js → check-ySRQc1YP.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DOstgiRf.js → chevron-right-5vFloHew.js} +1 -1
- package/dashboard/dist/assets/{copy-BsKO524t.js → copy-mhfKVMKo.js} +1 -1
- package/dashboard/dist/assets/{download-dadqxej7.js → download-GdrXglSx.js} +1 -1
- package/dashboard/dist/assets/{external-link-CTu3BMeU.js → external-link-KGFaKc8s.js} +1 -1
- package/dashboard/dist/assets/{flame-B_IvWcMn.js → flame-BmNtNoS1.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DIT6hLSb.js → format-tokens-CCpCNw3d.js} +1 -1
- package/dashboard/dist/assets/{icons-mWku76bu.js → icons-XLKZavTf.js} +1 -1
- package/dashboard/dist/assets/{index-D-gQV-hF.js → index-CsywTeCQ.js} +1 -1
- package/dashboard/dist/assets/{index-CboN9E82.js → index-IKpmeZte.js} +1 -1
- package/dashboard/dist/assets/{index-D5-w-aGy.js → index-_xU5U5Ct.js} +1 -1
- package/dashboard/dist/assets/{info-x6l352SW.js → info-DUV5UpmT.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-Bo12VlLJ.js → limits-providers-DFkCZCZj.js} +1 -1
- package/dashboard/dist/assets/{link-2-eo_xPxH1.js → link-2-D7c64yLY.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C4n1iGBv.js → loader-circle-sMONRK2M.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-pR8xwsI_.js → maximize-2-CVWRgrk9.js} +1 -1
- package/dashboard/dist/assets/{provider-display-ErggQWu4.js → provider-display-DSKqVjgm.js} +1 -1
- package/dashboard/dist/assets/{react-DYKg02Aq.js → react-c1q4mGkS.js} +1 -1
- package/dashboard/dist/assets/{search-B_jbUwPS.js → search-CwD3s-_C.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DY5ruGmw.js → skills-api-BRXj8kjT.js} +1 -1
- package/dashboard/dist/assets/{store-Cu8i1y26.js → store-lqTW2gfK.js} +1 -1
- package/dashboard/dist/assets/{terminal-D3j6X68s.js → terminal-DU1_KbKo.js} +1 -1
- package/dashboard/dist/assets/{trash-2-CFpwgJw5.js → trash-2-B8Y5Xc-K.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-C0fKq66G.js → use-community-stats-DYwwimvp.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-TOp4UAnI.js → use-limits-display-prefs-Bst6ybK3.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BZie_7z1.js → use-native-settings-Cqle4lgw.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-Bq92YyUp.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-BcpytkES.js → use-session-efficiency-pref-gwbDMHXa.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Dmmrne2z.js → use-transform-CU0yxXpX.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Cw0Z9Tew.js → use-usage-limits-5y1I1T5_.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DyyonINj.js → useCurrency-BfVNrywe.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DvWw86lJ.js → useScrollLock-CF1m7AMJ.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CBkvGfJ1.js → useTokenFormat-DiRwVvRr.js} +1 -1
- package/dashboard/dist/assets/{zap-CnYA39Ug.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 +337 -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,
|
|
@@ -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" ||
|
|
@@ -5307,6 +5510,22 @@ const KIRO_CLI_CREDITS_SIDECAR = "kiro-credits.json";
|
|
|
5307
5510
|
function resolveKiroCliDbPath(env = process.env) {
|
|
5308
5511
|
if (env.KIRO_CLI_DB_PATH) return env.KIRO_CLI_DB_PATH;
|
|
5309
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
|
+
}
|
|
5310
5529
|
return path.join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3");
|
|
5311
5530
|
}
|
|
5312
5531
|
|
|
@@ -5870,6 +6089,7 @@ async function readKiroCliV2SessionTurns(messagesPath) {
|
|
|
5870
6089
|
|
|
5871
6090
|
async function writeKiroCliCreditsSidecar({
|
|
5872
6091
|
queuePath,
|
|
6092
|
+
installKey,
|
|
5873
6093
|
totalCredits,
|
|
5874
6094
|
recordCount,
|
|
5875
6095
|
sessionCount,
|
|
@@ -5882,15 +6102,64 @@ async function writeKiroCliCreditsSidecar({
|
|
|
5882
6102
|
KIRO_CLI_CREDITS_SIDECAR,
|
|
5883
6103
|
);
|
|
5884
6104
|
try {
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
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] = {
|
|
5888
6129
|
total_credits: Number(totalCredits.toFixed(12)),
|
|
5889
6130
|
record_count: recordCount,
|
|
5890
6131
|
session_count: sessionCount,
|
|
5891
6132
|
file_count: fileCount,
|
|
5892
6133
|
latest_at: latestAt,
|
|
5893
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,
|
|
5894
6163
|
});
|
|
5895
6164
|
await chmod600IfPossible(sidecarPath);
|
|
5896
6165
|
} catch {
|
|
@@ -5953,13 +6222,29 @@ function readKiroCliRequests(dbPath, env = process.env, sqliteOptions = {}) {
|
|
|
5953
6222
|
"json_extract(value, '$.user_turn_metadata.requests') AS requests_json " +
|
|
5954
6223
|
"FROM conversations_v2 " +
|
|
5955
6224
|
"WHERE json_extract(value, '$.user_turn_metadata.requests') IS NOT NULL";
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
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
|
+
}
|
|
5963
6248
|
const flat = [];
|
|
5964
6249
|
for (const row of rows) {
|
|
5965
6250
|
let requests;
|
|
@@ -6058,6 +6343,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
6058
6343
|
}
|
|
6059
6344
|
await writeKiroCliCreditsSidecar({
|
|
6060
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"),
|
|
6061
6351
|
totalCredits: kiroCreditTotal,
|
|
6062
6352
|
recordCount: kiroCreditRecords,
|
|
6063
6353
|
sessionCount: kiroCreditSessions,
|
|
@@ -14371,6 +14661,7 @@ module.exports = {
|
|
|
14371
14661
|
resolveQoderDbPath,
|
|
14372
14662
|
resolveQoderDbPaths,
|
|
14373
14663
|
readQoderDbMessages,
|
|
14664
|
+
resolveKiroBasePath,
|
|
14374
14665
|
resolveKiroDbPath,
|
|
14375
14666
|
resolveKiroJsonlPath,
|
|
14376
14667
|
resolveHermesPath,
|
|
@@ -14404,6 +14695,8 @@ module.exports = {
|
|
|
14404
14695
|
gooseInstallOwnsCursor,
|
|
14405
14696
|
zedInstallOwnsCursor,
|
|
14406
14697
|
hermesInstallOwnsCursor,
|
|
14698
|
+
kiroInstallOwnsCursor,
|
|
14699
|
+
kiroCliInstallOwnsCursor,
|
|
14407
14700
|
copilotOtelCursorHasLegacyCliUsage,
|
|
14408
14701
|
pruneCopilotUsageClaims,
|
|
14409
14702
|
parseCopilotIncremental,
|