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.
Files changed (105) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DAAenS6A.js → AchievementBadge-DEqbHlmn.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DJ9GMKxL.js → AchievementsPage-BOaEu1AQ.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DtpLrh2Q.js → AchievementsSection-C370ngO3.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-CkE1DwdN.js → BadgeDetailModal-D7Wkef0K.js} +1 -1
  5. package/dashboard/dist/assets/{Card-Cs2vVMHG.js → Card-mDEVjNdM.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-BCSNeob2.js → ClawdAnimated-DdsX-z8n.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-CFDTjp76.js → CommandPalette-DhxRIuHC.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-D2oJpI9i.js → CommunityStatsModal-CG-9JPS6.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-D0a_xdwF.js → ConfirmModal-COhxErLm.js} +1 -1
  10. package/dashboard/dist/assets/DashboardPage-tX0DKgFE.js +1 -0
  11. package/dashboard/dist/assets/{DevicePage-c9Fk13gJ.js → DevicePage-BsSeHx84.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-jsfDvF2n.js → DialogClose-eyOW2cbx.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-Dt835L7T.js → DialogTitle-H8sGiH7c.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-DJssGrj8.js → FadeIn-pq2ja50M.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-B9MwwA-P.js → HeaderGithubStar-DWgcpDHr.js} +1 -1
  17. package/dashboard/dist/assets/{HoverTooltip-DPLMJkai.js → HoverTooltip-CcJ_VR65.js} +1 -1
  18. package/dashboard/dist/assets/{IpCheckPage-BXKjxEHe.js → IpCheckPage-CNUN346Q.js} +1 -1
  19. package/dashboard/dist/assets/{LandingPage-BJNMQTec.js → LandingPage-yaoRGsNI.js} +2 -2
  20. package/dashboard/dist/assets/{LeaderboardAvatar-BNwbnOa3.js → LeaderboardAvatar-Ddai4I12.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-BOxejyMy.js → LeaderboardPage-Bajkxf63.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-BEqUZCOD.js → LeaderboardProfileModal-BGYKDgIi.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CUDV5iyh.js → LeaderboardProfilePage-Ck5vPaeN.js} +1 -1
  24. package/dashboard/dist/assets/LimitsPage-Bqobtsw2.js +4 -0
  25. package/dashboard/dist/assets/{LocalOnlyNotice-B3BSeCHd.js → LocalOnlyNotice-e9KSE3wj.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-2mAb696j.js → LoginCard-BhTSm2tb.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-cacKhc6U.js → LoginModal-CRwuThde.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-rnBlie8p.js → LoginPage-j1i74lcc.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-D3DhuS6O.js → PetPage-r62ViU1X.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-CxATX7M9.js → PopoverPopup-DopNTyD4.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-B5b9LkmM.js → ProviderIcon-c1LkPhYM.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-BGKmQ_JW.js → ResetPasswordPage-DJdDNvtq.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-BMeYCm2b.js → SegmentedControl-DhWVOTPN.js} +1 -1
  34. package/dashboard/dist/assets/{Select-CYHXq8im.js → Select-DeUmTmsJ.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-DvI4i06h.js → SelectItemText-pX0B5f7h.js} +1 -1
  36. package/dashboard/dist/assets/{ServiceStatusPage-dv26-VvI.js → ServiceStatusPage-ejIPz6oF.js} +1 -1
  37. package/dashboard/dist/assets/{SessionsPage-DBQruqkO.js → SessionsPage-WEbn90GW.js} +2 -2
  38. package/dashboard/dist/assets/{SettingsPage-BWRbQ3Iq.js → SettingsPage-B8t5gCeI.js} +1 -1
  39. package/dashboard/dist/assets/{SkillsPage-BonhKgAt.js → SkillsPage-BWMlzeAp.js} +1 -1
  40. package/dashboard/dist/assets/{TokenGalaxy-BBUD1kpU.js → TokenGalaxy-DIBRBvMj.js} +1 -1
  41. package/dashboard/dist/assets/{ToolbarRootContext-DPMm8uYz.js → ToolbarRootContext-C6KxLvnI.js} +1 -1
  42. package/dashboard/dist/assets/{TrendMonitor-CZVE6gID.js → TrendMonitor-BaMNh67w.js} +1 -1
  43. package/dashboard/dist/assets/{WidgetsPage-Bz9cieeZ.js → WidgetsPage-DDsy4M3x.js} +1 -1
  44. package/dashboard/dist/assets/{WrappedPage-CruMolZd.js → WrappedPage-CiNnUFRI.js} +1 -1
  45. package/dashboard/dist/assets/{__vite-browser-external-C3VH6nS4.js → __vite-browser-external-CM-H8-LW.js} +24 -16
  46. package/dashboard/dist/assets/{agent-logos-HIeYwIqN.js → agent-logos-BjVa6oe7.js} +1 -1
  47. package/dashboard/dist/assets/{arrow-up-right-n7Bi4Tvd.js → arrow-up-right-CCum1mqX.js} +1 -1
  48. package/dashboard/dist/assets/{check-C5Q44G5k.js → check-ySRQc1YP.js} +1 -1
  49. package/dashboard/dist/assets/{chevron-right-BsnptUU0.js → chevron-right-5vFloHew.js} +1 -1
  50. package/dashboard/dist/assets/{copy-BGuL9uFa.js → copy-mhfKVMKo.js} +1 -1
  51. package/dashboard/dist/assets/{download-BljgPwR2.js → download-GdrXglSx.js} +1 -1
  52. package/dashboard/dist/assets/{external-link-B4Oz8hUe.js → external-link-KGFaKc8s.js} +1 -1
  53. package/dashboard/dist/assets/{flame-ChLG_NZE.js → flame-BmNtNoS1.js} +1 -1
  54. package/dashboard/dist/assets/{format-tokens-DjuNk13Z.js → format-tokens-CCpCNw3d.js} +1 -1
  55. package/dashboard/dist/assets/{icons-CkO42M-u.js → icons-XLKZavTf.js} +1 -1
  56. package/dashboard/dist/assets/{index-CEsvYBWY.js → index-CsywTeCQ.js} +1 -1
  57. package/dashboard/dist/assets/{index-CGxawuWx.js → index-IKpmeZte.js} +1 -1
  58. package/dashboard/dist/assets/{index-D87oE_rV.js → index-_xU5U5Ct.js} +1 -1
  59. package/dashboard/dist/assets/{info-et8xtJ4v.js → info-DUV5UpmT.js} +1 -1
  60. package/dashboard/dist/assets/{limits-providers-vO_86yRy.js → limits-providers-DFkCZCZj.js} +1 -1
  61. package/dashboard/dist/assets/{link-2-Dge2rBkr.js → link-2-D7c64yLY.js} +1 -1
  62. package/dashboard/dist/assets/{loader-circle-Cs3wpqJe.js → loader-circle-sMONRK2M.js} +1 -1
  63. package/dashboard/dist/assets/{maximize-2-zn71uZX3.js → maximize-2-CVWRgrk9.js} +1 -1
  64. package/dashboard/dist/assets/{provider-display-CTt6mzLe.js → provider-display-DSKqVjgm.js} +1 -1
  65. package/dashboard/dist/assets/{react-BbHDQe6Q.js → react-c1q4mGkS.js} +1 -1
  66. package/dashboard/dist/assets/{search-3CMhatWb.js → search-CwD3s-_C.js} +1 -1
  67. package/dashboard/dist/assets/{skills-api-Cm1aiqWL.js → skills-api-BRXj8kjT.js} +1 -1
  68. package/dashboard/dist/assets/{store-DclXV77t.js → store-lqTW2gfK.js} +1 -1
  69. package/dashboard/dist/assets/{terminal-BC7uFcA7.js → terminal-DU1_KbKo.js} +1 -1
  70. package/dashboard/dist/assets/{trash-2-BT8V531F.js → trash-2-B8Y5Xc-K.js} +1 -1
  71. package/dashboard/dist/assets/{use-community-stats-DJr59SGu.js → use-community-stats-DYwwimvp.js} +1 -1
  72. package/dashboard/dist/assets/{use-limits-display-prefs-DU8OOFVb.js → use-limits-display-prefs-Bst6ybK3.js} +1 -1
  73. package/dashboard/dist/assets/{use-native-settings-BvSwI4BL.js → use-native-settings-Cqle4lgw.js} +1 -1
  74. package/dashboard/dist/assets/{use-ordered-list-Cq5AyV2x.js → use-ordered-list-Dtze0ahb.js} +1 -1
  75. package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +1 -0
  76. package/dashboard/dist/assets/{use-session-efficiency-pref-WS8NhXuW.js → use-session-efficiency-pref-gwbDMHXa.js} +1 -1
  77. package/dashboard/dist/assets/{use-transform-CLYsi8xc.js → use-transform-CU0yxXpX.js} +1 -1
  78. package/dashboard/dist/assets/use-usage-limits-5y1I1T5_.js +1 -0
  79. package/dashboard/dist/assets/{useCurrency-D0GtEoJD.js → useCurrency-BfVNrywe.js} +1 -1
  80. package/dashboard/dist/assets/{useScrollLock-TIUBSeIM.js → useScrollLock-CF1m7AMJ.js} +1 -1
  81. package/dashboard/dist/assets/{useTokenFormat-B0TBL9La.js → useTokenFormat-DiRwVvRr.js} +1 -1
  82. package/dashboard/dist/assets/{zap-D89769CL.js → zap-BZtlWtuN.js} +1 -1
  83. package/dashboard/dist/index.html +1 -1
  84. package/dashboard/dist/ip-check.html +1 -1
  85. package/dashboard/dist/leaderboard.html +1 -1
  86. package/dashboard/dist/share.html +1 -1
  87. package/package.json +1 -1
  88. package/src/commands/status.js +114 -9
  89. package/src/commands/sync.js +151 -46
  90. package/src/lib/claude-categorizer.js +36 -9
  91. package/src/lib/diagnostics.js +86 -13
  92. package/src/lib/doctor.js +17 -0
  93. package/src/lib/grok-context-breakdown.js +794 -0
  94. package/src/lib/install-resolver.js +10 -2
  95. package/src/lib/local-api.js +21 -3
  96. package/src/lib/multi-install-parser.js +9 -1
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +480 -53
  99. package/src/lib/session-analytics.js +332 -9
  100. package/src/lib/usage-limits.js +47 -2
  101. package/dashboard/dist/assets/DashboardPage-CMCqvpkA.js +0 -1
  102. package/dashboard/dist/assets/DialogDescription-Dwxj5h5w.js +0 -1
  103. package/dashboard/dist/assets/LimitsPage-ejlJN1vi.js +0 -4
  104. package/dashboard/dist/assets/use-reduced-motion-D1_q3aRK.js +0 -1
  105. package/dashboard/dist/assets/use-usage-limits-CGoQQDG-.js +0 -1
@@ -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
- const isMainSession = !filePath.includes("/subagents/");
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
- const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
2099
- const userBucketStart = userTs ? toUtcHalfHourStart(userTs) : null;
2100
- if (userBucketStart) {
2101
- const userModel = DEFAULT_MODEL;
2102
- const userBucket = getHourlyBucket(hourlyState, source, userModel, userBucketStart);
2103
- userBucket.totals.conversation_count += 1;
2104
- touchedBuckets.add(bucketKey(source, userModel, userBucketStart));
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
- function resolveKiroBasePath() {
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
- return path.join(
4560
- home,
4561
- "Library",
4562
- "Application Support",
4563
- "Kiro",
4564
- "User",
4565
- "globalStorage",
4566
- "kiro.kiroagent",
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
- return readSqliteJsonRows(dbPath, sql, {
4583
- label: "Kiro",
4584
- maxBuffer: 10 * 1024 * 1024,
4585
- timeout: 15_000,
4586
- ...sqliteOptions,
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
- const basePath = resolveKiroBasePath();
4789
- const modelTimeline = buildKiroModelTimeline(basePath);
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, turn boundaries, timestamps,
5625
- // and assistant reasoningModelId are sufficient for the same 4 chars/token
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 flat;
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", "usage_summary"].includes(
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 flat;
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
- const rows = readSqliteJsonRows(dbPath, sql, {
5853
- label: "Kiro CLI",
5854
- env,
5855
- maxBuffer: 128 * 1024 * 1024,
5856
- timeout: 120_000,
5857
- ...sqliteOptions,
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
- const turns =
5928
- path.basename(sessionPath) === "messages.jsonl"
5929
- ? await readKiroCliV2SessionTurns(sessionPath)
5930
- : await readKiroCliSessionTurns(sessionPath);
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,