tokentracker-cli 0.84.5 → 0.84.6

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 (94) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-CKIvD3lG.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-CZWCUiS5.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-BSk_fR8P.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal-CjHkcmuq.js} +1 -1
  5. package/dashboard/dist/assets/{Card-Ha3x9zIj.js → Card-D6hdzwvI.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Cl6gmF7u.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-B0rcodcF.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-BtlTJbf4.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-tJCWVZQH.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-Dtxq8dzI.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-9USuEsRu.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-DxHdqMcl.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-C1ZeQgf2.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-Cx4jLl9G.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-BOyuGkoA.js} +1 -1
  17. package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BDhqMR3L.js} +1 -1
  18. package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-DTvA9Wmx.js} +1 -1
  19. package/dashboard/dist/assets/{LandingPage-BCvNh3zE.js → LandingPage-k1271lWV.js} +2 -2
  20. package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-3ibNg6If.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-DZSxazHP.js → LeaderboardPage-Byww5UpT.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-DeGbJtvi.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-DEPzO6hW.js} +1 -1
  24. package/dashboard/dist/assets/{LimitsPage-JWZDa5_Y.js → LimitsPage-DPtqL0PS.js} +1 -1
  25. package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-C76LAT_3.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DEyCJT67.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CueMQgAZ.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-CwqBb5Fd.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-C6UMY2qD.js → PetPage--xQAjW4_.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-Dt9VY2VQ.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-CHx4ohfx.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-DB6R7fjf.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-BINOpXIY.js} +1 -1
  34. package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-DcYXcsKv.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-BDv2Kb3x.js} +1 -1
  36. package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-cL_w0ytB.js} +1 -1
  37. package/dashboard/dist/assets/{SettingsPage-CYlEboSl.js → SettingsPage-CHGA0bQS.js} +1 -1
  38. package/dashboard/dist/assets/{SkillsPage-Bzq8V5Pd.js → SkillsPage-I29SqkXO.js} +1 -1
  39. package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-BHq4YSv2.js} +1 -1
  40. package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-akXaaKb6.js} +1 -1
  41. package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DPhFXzQc.js} +1 -1
  42. package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-Nf0UEz7s.js} +1 -1
  43. package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-D4zOSsqD.js} +1 -1
  44. package/dashboard/dist/assets/{__vite-browser-external-D3llGq6p.js → __vite-browser-external-UqlSqI-1.js} +2 -2
  45. package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-Bja3VCAa.js} +1 -1
  46. package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CzcCNJJ2.js} +1 -1
  47. package/dashboard/dist/assets/{check-C3ARiiE_.js → check-DUHSZCYn.js} +1 -1
  48. package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-jLvhaNUy.js} +1 -1
  49. package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-ChyH-ynx.js} +1 -1
  50. package/dashboard/dist/assets/{download-DtRzJfcr.js → download-CgwvXrUe.js} +1 -1
  51. package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-ysf1F8uB.js} +1 -1
  52. package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-DAa2cubl.js} +1 -1
  53. package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-DccoWQq9.js} +1 -1
  54. package/dashboard/dist/assets/{index-4unZlm2w.js → index-CAycNPQf.js} +1 -1
  55. package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CxExExXh.js} +1 -1
  56. package/dashboard/dist/assets/{index-CZRlrK7w.js → index-kkQrDRat.js} +1 -1
  57. package/dashboard/dist/assets/{info-BkrO-fg3.js → info-DWQQIq9M.js} +1 -1
  58. package/dashboard/dist/assets/{limits-providers-BtwYmYHA.js → limits-providers-B_18X9Rp.js} +1 -1
  59. package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-CKeVVq4P.js} +1 -1
  60. package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-Dw0tu6IN.js} +1 -1
  61. package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-BMU0YzEF.js} +1 -1
  62. package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-Cf9I-VLS.js} +1 -1
  63. package/dashboard/dist/assets/{react-DD9jpv--.js → react-BhbcISKy.js} +1 -1
  64. package/dashboard/dist/assets/{search--TvX-fig.js → search-BoY8EW1z.js} +1 -1
  65. package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-KVH21GJw.js} +1 -1
  66. package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BL7rSajG.js} +1 -1
  67. package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CeMuITdm.js} +1 -1
  68. package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-jVUCKqrR.js} +1 -1
  69. package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-uWK0fE6l.js} +1 -1
  70. package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-DWsF4ShM.js} +1 -1
  71. package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-SEbqWDvc.js} +1 -1
  72. package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-D4UhrPvw.js} +1 -1
  73. package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +1 -0
  74. package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-DbysyUTJ.js} +1 -1
  75. package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-DaEssGaZ.js} +1 -1
  76. package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-CmmQ34Ov.js} +1 -1
  77. package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-DvBEn4rx.js} +1 -1
  78. package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-QozsdFng.js} +1 -1
  79. package/dashboard/dist/assets/{useTokenFormat-Li8BANKO.js → useTokenFormat-BTLrL7Mi.js} +1 -1
  80. package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-Dp_aJ1q6.js} +1 -1
  81. package/dashboard/dist/index.html +1 -1
  82. package/dashboard/dist/ip-check.html +1 -1
  83. package/dashboard/dist/leaderboard.html +1 -1
  84. package/dashboard/dist/share.html +1 -1
  85. package/package.json +1 -1
  86. package/src/commands/status.js +17 -7
  87. package/src/commands/sync.js +333 -5
  88. package/src/lib/codex-rollout-parser.js +17 -39
  89. package/src/lib/codex-token-usage.js +195 -0
  90. package/src/lib/pricing/seed-snapshot.json +1 -1
  91. package/src/lib/rollout.js +359 -91
  92. package/src/lib/usage-limits.js +211 -41
  93. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
  94. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
@@ -8,6 +8,11 @@ const { ensureDir } = require("./fs");
8
8
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
9
9
  const wsl = require("./wsl-probe");
10
10
  const { resolveInstallPaths } = require("./install-resolver");
11
+ const {
12
+ consumeUsageDelta,
13
+ createUsageDeltaState,
14
+ snapshotUsageBaselines,
15
+ } = require("./codex-token-usage");
11
16
 
12
17
  const DEFAULT_SOURCE = "codex";
13
18
  const DEFAULT_MODEL = "unknown";
@@ -360,6 +365,9 @@ async function parseRolloutIncremental({
360
365
  const lastTotal = sameInode && !truncated && !rebuildingCodexBaseline
361
366
  ? prev.lastTotal || null
362
367
  : null;
368
+ const tokenUsageBaselines = sameInode && !truncated && !rebuildingCodexBaseline
369
+ ? prev.tokenUsageBaselines || null
370
+ : null;
363
371
  const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
364
372
 
365
373
  const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
@@ -430,6 +438,7 @@ async function parseRolloutIncremental({
430
438
  filePath,
431
439
  fileStat: st,
432
440
  lastTotal,
441
+ tokenUsageBaselines,
433
442
  lastModel,
434
443
  projectState,
435
444
  projectMetaCache,
@@ -442,6 +451,7 @@ async function parseRolloutIncremental({
442
451
  fileStat: st,
443
452
  startOffset,
444
453
  lastTotal,
454
+ tokenUsageBaselines,
445
455
  lastModel,
446
456
  hourlyState,
447
457
  touchedBuckets,
@@ -462,6 +472,7 @@ async function parseRolloutIncremental({
462
472
  inode,
463
473
  offset: result.endOffset,
464
474
  lastTotal: result.lastTotal,
475
+ tokenUsageBaselines: result.tokenUsageBaselines,
465
476
  lastModel: result.lastModel,
466
477
  updatedAt: new Date().toISOString(),
467
478
  };
@@ -1731,6 +1742,7 @@ async function parseRolloutFile({
1731
1742
  fileStat,
1732
1743
  startOffset,
1733
1744
  lastTotal,
1745
+ tokenUsageBaselines,
1734
1746
  lastModel,
1735
1747
  hourlyState,
1736
1748
  touchedBuckets,
@@ -1751,14 +1763,25 @@ async function parseRolloutFile({
1751
1763
  const projectFileContexts = [];
1752
1764
  addProjectFileContext(projectFileContexts, projectContext);
1753
1765
  if (startOffset >= endOffset) {
1754
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1766
+ return {
1767
+ endOffset,
1768
+ lastTotal,
1769
+ tokenUsageBaselines,
1770
+ lastModel,
1771
+ eventsAggregated: 0,
1772
+ projectFileContexts,
1773
+ };
1755
1774
  }
1756
1775
 
1757
1776
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
1758
1777
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1759
1778
 
1760
1779
  let model = typeof lastModel === "string" ? lastModel : null;
1761
- let totals = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
1780
+ const usageDeltaState = createUsageDeltaState({
1781
+ lastTotal,
1782
+ baselines: tokenUsageBaselines,
1783
+ });
1784
+ let latestTotal = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
1762
1785
  let currentCwd = null;
1763
1786
  let currentDate = null;
1764
1787
  let isForkedRollout = false;
@@ -1840,15 +1863,13 @@ async function parseRolloutFile({
1840
1863
 
1841
1864
  const lastUsage = info.last_token_usage;
1842
1865
  const totalUsage = info.total_token_usage;
1866
+ if (totalUsage && typeof totalUsage === "object") latestTotal = totalUsage;
1843
1867
 
1844
- const delta = pickDelta(lastUsage, totalUsage, totals);
1845
- if (!delta) continue;
1868
+ const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
1869
+ const delta = rawDelta ? normalizeUsage(rawDelta) : null;
1870
+ if (!delta || isAllZeroUsage(delta)) continue;
1846
1871
  delta.conversation_count = 1;
1847
1872
 
1848
- if (totalUsage && typeof totalUsage === "object") {
1849
- totals = totalUsage;
1850
- }
1851
-
1852
1873
  // Forked Codex rollouts replay the parent session's entire token history
1853
1874
  // into the child file the moment the fork is created. The date guard below
1854
1875
  // (current_date < rolloutDate) catches cross-day forks, but same-day forks
@@ -1863,8 +1884,8 @@ async function parseRolloutFile({
1863
1884
  // flush spacing and well below genuine turn cadence (≥~4.6s observed). The
1864
1885
  // first replayed row cannot be identified without lookahead, so it is still
1865
1886
  // counted (a bounded, <1% residual over-count); dropping real usage is the
1866
- // worse failure, so we bias against it. `totals` is already advanced above,
1867
- // keeping the cumulative-delta baseline correct for the live turns we keep.
1887
+ // worse failure, so we bias against it. `usageDeltaState` is already advanced above,
1888
+ // keeping the cumulative lineage correct for the live turns we keep.
1868
1889
  // Scoped to forked codex rollouts. (issue #169 follow-up.)
1869
1890
  let forkedReplaySkip = false;
1870
1891
  if (isForkedRollout && source === DEFAULT_SOURCE && replayPrefixActive) {
@@ -1903,8 +1924,8 @@ async function parseRolloutFile({
1903
1924
  // buckets. External tools rewrite session files without changing the token
1904
1925
  // data — Codex-Manager atomically rewrites them (new inode) to patch the
1905
1926
  // provider on every account/channel switch — so without dedup each switch
1906
- // double-counts the rewritten sessions. `totals` is already advanced above,
1907
- // so skipping an already-seen event keeps the cumulative-delta chain intact
1927
+ // double-counts the rewritten sessions. `usageDeltaState` is already advanced above,
1928
+ // so skipping an already-seen event keeps the cumulative lineage intact
1908
1929
  // while preventing the re-add; genuinely new turns carry new timestamps and
1909
1930
  // are still counted. Key = sessionUUID:eventTimestamp (both stable across the
1910
1931
  // rewrite and across a sessions/ -> archived_sessions/ move).
@@ -1939,13 +1960,21 @@ async function parseRolloutFile({
1939
1960
  eventsAggregated += 1;
1940
1961
  }
1941
1962
 
1942
- return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated, projectFileContexts };
1963
+ return {
1964
+ endOffset,
1965
+ lastTotal: latestTotal,
1966
+ tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
1967
+ lastModel: model,
1968
+ eventsAggregated,
1969
+ projectFileContexts,
1970
+ };
1943
1971
  }
1944
1972
 
1945
1973
  async function scanRolloutProjectFileContexts({
1946
1974
  filePath,
1947
1975
  fileStat,
1948
1976
  lastTotal,
1977
+ tokenUsageBaselines,
1949
1978
  lastModel,
1950
1979
  projectState,
1951
1980
  projectMetaCache,
@@ -1958,7 +1987,14 @@ async function scanRolloutProjectFileContexts({
1958
1987
  const projectFileContexts = [];
1959
1988
  addProjectFileContext(projectFileContexts, projectContext);
1960
1989
  if (!projectState || endOffset <= 0) {
1961
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1990
+ return {
1991
+ endOffset,
1992
+ lastTotal,
1993
+ tokenUsageBaselines,
1994
+ lastModel,
1995
+ eventsAggregated: 0,
1996
+ projectFileContexts,
1997
+ };
1962
1998
  }
1963
1999
 
1964
2000
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: 0 });
@@ -2005,7 +2041,14 @@ async function scanRolloutProjectFileContexts({
2005
2041
  addProjectFileContext(projectFileContexts, context);
2006
2042
  }
2007
2043
 
2008
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
2044
+ return {
2045
+ endOffset,
2046
+ lastTotal,
2047
+ tokenUsageBaselines,
2048
+ lastModel,
2049
+ eventsAggregated: 0,
2050
+ projectFileContexts,
2051
+ };
2009
2052
  }
2010
2053
 
2011
2054
  async function parseClaudeFile({
@@ -3570,48 +3613,6 @@ function extractTokenCount(obj) {
3570
3613
  return null;
3571
3614
  }
3572
3615
 
3573
- function pickDelta(lastUsage, totalUsage, prevTotals) {
3574
- const hasLast = isNonEmptyObject(lastUsage);
3575
- const hasTotal = isNonEmptyObject(totalUsage);
3576
- const hasPrevTotals = isNonEmptyObject(prevTotals);
3577
-
3578
- if (hasTotal && hasPrevTotals) {
3579
- if (totalsReset(totalUsage, prevTotals)) {
3580
- const resetUsage = hasLast ? lastUsage : totalUsage;
3581
- const normalized = normalizeUsage(resetUsage);
3582
- return isAllZeroUsage(normalized) ? null : normalized;
3583
- }
3584
-
3585
- const delta = {};
3586
- for (const k of [
3587
- "input_tokens",
3588
- "cached_input_tokens",
3589
- "cache_creation_input_tokens",
3590
- "output_tokens",
3591
- "reasoning_output_tokens",
3592
- "total_tokens",
3593
- ]) {
3594
- const a = Number(totalUsage[k]);
3595
- const b = Number(prevTotals[k]);
3596
- if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
3597
- }
3598
- const normalized = normalizeUsage(delta);
3599
- return isAllZeroUsage(normalized) ? null : normalized;
3600
- }
3601
-
3602
- if (hasLast) {
3603
- const normalized = normalizeUsage(lastUsage);
3604
- return isAllZeroUsage(normalized) ? null : normalized;
3605
- }
3606
-
3607
- if (hasTotal) {
3608
- const normalized = normalizeUsage(totalUsage);
3609
- return isAllZeroUsage(normalized) ? null : normalized;
3610
- }
3611
-
3612
- return null;
3613
- }
3614
-
3615
3616
  function normalizeUsage(u) {
3616
3617
  const out = {};
3617
3618
  for (const k of [
@@ -3694,13 +3695,6 @@ function isAllZeroUsage(u) {
3694
3695
  return true;
3695
3696
  }
3696
3697
 
3697
- function totalsReset(curr, prev) {
3698
- const currTotal = curr?.total_tokens;
3699
- const prevTotal = prev?.total_tokens;
3700
- if (!isFiniteNumber(currTotal) || !isFiniteNumber(prevTotal)) return false;
3701
- return currTotal < prevTotal;
3702
- }
3703
-
3704
3698
  function isFiniteNumber(v) {
3705
3699
  return typeof v === "number" && Number.isFinite(v);
3706
3700
  }
@@ -4940,32 +4934,77 @@ function resolveKiroCliDbPath(env = process.env) {
4940
4934
  // no hyphens. kiro-cli writes proper UUIDs; lock to the canonical shape.
4941
4935
  const KIRO_CLI_SESSION_FILE_RE =
4942
4936
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
4943
-
4944
- // Lists ~/.kiro/sessions/cli/{uuid}.json files. Includes files whose sibling
4945
- // .lock is present we read those as tail-only snapshots so a running
4946
- // session's completed turns still land in the queue on the next sync. The
4947
- // .json files are rewritten atomically by kiro-cli on each turn flush, so
4948
- // a stale read just means we'll pick up the rest next time.
4937
+ // The directory prefix and direct messages.jsonl child are the stable
4938
+ // contract. Do not assume the suffix will always remain a UUID: Kiro has
4939
+ // already changed this layout once, and an opaque non-empty session id is
4940
+ // sufficient to keep discovery both bounded and forward-compatible.
4941
+ const KIRO_CLI_V2_SESSION_DIR_RE = /^sess_.+$/;
4942
+
4943
+ // Lists both Kiro CLI session layouts:
4944
+ // legacy: ~/.kiro/sessions/cli/{uuid}.json
4945
+ // 2.13+: ~/.kiro/sessions/{workspaceHash}/sess_{uuid}/messages.jsonl
4946
+ //
4947
+ // Legacy .json files are rewritten atomically per turn. The 2.13+ JSONL is
4948
+ // append-only and may be read while a session is active. Only the direct
4949
+ // messages.jsonl child is accepted; sub-executions and arbitrary nested JSONL
4950
+ // files are intentionally excluded.
4949
4951
  //
4950
4952
  // TASK-014: env.HOME is honored (symmetric with resolveKiroCliDbPath) so
4951
4953
  // callers can redirect to a tmp home for hermetic tests/CI.
4952
4954
  function resolveKiroCliSessionFiles(env = process.env) {
4953
4955
  const home = env.HOME || require("node:os").homedir();
4954
4956
  const kiroHome = env.KIRO_HOME || path.join(home, ".kiro");
4955
- const sessionsDir = path.join(kiroHome, "sessions", "cli");
4956
- if (!fssync.existsSync(sessionsDir)) return [];
4957
+ const sessionsRoot = path.join(kiroHome, "sessions");
4958
+ if (!fssync.existsSync(sessionsRoot)) return [];
4957
4959
  const files = [];
4960
+
4961
+ const legacyDir = path.join(sessionsRoot, "cli");
4958
4962
  try {
4959
- for (const entry of fssync.readdirSync(sessionsDir)) {
4963
+ for (const entry of fssync.readdirSync(legacyDir)) {
4960
4964
  // TASK-003: only canonical {uuid}.json files; backups, scratch,
4961
4965
  // typos are skipped so they don't feed JSON.parse garbage.
4962
4966
  if (!KIRO_CLI_SESSION_FILE_RE.test(entry)) continue;
4963
- files.push(path.join(sessionsDir, entry));
4967
+ files.push(path.join(legacyDir, entry));
4964
4968
  }
4965
4969
  } catch {
4966
4970
  // ignore read errors
4967
4971
  }
4968
- return files;
4972
+
4973
+ try {
4974
+ const workspaceDirs = fssync.readdirSync(sessionsRoot, {
4975
+ withFileTypes: true,
4976
+ });
4977
+ for (const workspace of workspaceDirs) {
4978
+ if (!workspace.isDirectory() || workspace.name === "cli") continue;
4979
+ const workspacePath = path.join(sessionsRoot, workspace.name);
4980
+ let sessionDirs;
4981
+ try {
4982
+ sessionDirs = fssync.readdirSync(workspacePath, {
4983
+ withFileTypes: true,
4984
+ });
4985
+ } catch {
4986
+ continue;
4987
+ }
4988
+ for (const session of sessionDirs) {
4989
+ if (
4990
+ !session.isDirectory() ||
4991
+ !KIRO_CLI_V2_SESSION_DIR_RE.test(session.name)
4992
+ ) {
4993
+ continue;
4994
+ }
4995
+ const messagesPath = path.join(
4996
+ workspacePath,
4997
+ session.name,
4998
+ "messages.jsonl",
4999
+ );
5000
+ if (fssync.existsSync(messagesPath)) files.push(messagesPath);
5001
+ }
5002
+ }
5003
+ } catch {
5004
+ // ignore read errors
5005
+ }
5006
+
5007
+ return files.sort();
4969
5008
  }
4970
5009
 
4971
5010
  // Build char-count maps from a .jsonl sibling file. Lets us approximate
@@ -5183,6 +5222,199 @@ async function readKiroCliSessionTurns(jsonPath) {
5183
5222
  return flat;
5184
5223
  }
5185
5224
 
5225
+ function kiroCliV2ContentChars(value) {
5226
+ if (typeof value === "string") return value.length;
5227
+ if (Array.isArray(value)) {
5228
+ return value.reduce(
5229
+ (sum, item) => sum + kiroCliV2ContentChars(item),
5230
+ 0,
5231
+ );
5232
+ }
5233
+ if (!value || typeof value !== "object") return 0;
5234
+ for (const key of ["content", "text", "value", "parts", "entries"]) {
5235
+ if (value[key] !== undefined) {
5236
+ return kiroCliV2ContentChars(value[key]);
5237
+ }
5238
+ }
5239
+ return 0;
5240
+ }
5241
+
5242
+ // Kiro CLI 2.13+ writes event-sourced sessions under
5243
+ // ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. The records do
5244
+ // not carry token counts, but user/assistant text, turn boundaries, timestamps,
5245
+ // and assistant reasoningModelId are sufficient for the same 4 chars/token
5246
+ // approximation used by the legacy CLI reader.
5247
+ async function readKiroCliV2SessionTurns(messagesPath) {
5248
+ if (
5249
+ !messagesPath ||
5250
+ path.basename(messagesPath) !== "messages.jsonl" ||
5251
+ !fssync.existsSync(messagesPath)
5252
+ ) {
5253
+ return [];
5254
+ }
5255
+
5256
+ const sessionDir = path.dirname(messagesPath);
5257
+ let sessionMeta = {};
5258
+ try {
5259
+ sessionMeta = JSON.parse(
5260
+ fssync.readFileSync(path.join(sessionDir, "session.json"), "utf8"),
5261
+ );
5262
+ } catch {
5263
+ // messages.jsonl is self-contained enough to parse without session.json.
5264
+ }
5265
+
5266
+ const sessionId =
5267
+ typeof sessionMeta?.id === "string" && sessionMeta.id
5268
+ ? sessionMeta.id
5269
+ : path.basename(sessionDir);
5270
+ const fallbackModel =
5271
+ typeof sessionMeta?.modelId === "string" ? sessionMeta.modelId : null;
5272
+ const fallbackTimestamp = Date.parse(
5273
+ sessionMeta?.createdAt || sessionMeta?.lastModifiedAt || "",
5274
+ );
5275
+
5276
+ const flat = [];
5277
+ let stream;
5278
+ try {
5279
+ stream = fssync.createReadStream(messagesPath, { encoding: "utf8" });
5280
+ } catch {
5281
+ return flat;
5282
+ }
5283
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
5284
+
5285
+ let pendingUserChars = 0;
5286
+ let pendingUserTimestampMs = NaN;
5287
+ let turn = null;
5288
+ let fallbackTurnIndex = 0;
5289
+
5290
+ const flushTurn = () => {
5291
+ if (!turn) return;
5292
+ const hasUsage =
5293
+ turn.outputChars > 0 ||
5294
+ turn.reasoningChars > 0;
5295
+ const tsMs = Number.isFinite(turn.timestampMs)
5296
+ ? turn.timestampMs
5297
+ : Number.isFinite(pendingUserTimestampMs)
5298
+ ? pendingUserTimestampMs
5299
+ : fallbackTimestamp;
5300
+ if (hasUsage && Number.isFinite(tsMs) && tsMs > 0) {
5301
+ const requestId =
5302
+ turn.executionId ||
5303
+ `${sessionId}:v2:${fallbackTurnIndex}`;
5304
+ const messageIds = Array.from(turn.messageIds);
5305
+ flat.push({
5306
+ request_id: requestId,
5307
+ message_id: messageIds[0] || turn.executionId || null,
5308
+ all_message_ids: messageIds,
5309
+ session_id: sessionId,
5310
+ session_model_id: fallbackModel,
5311
+ model_id: turn.modelId || fallbackModel,
5312
+ request_start_timestamp_ms: tsMs,
5313
+ user_prompt_length: turn.inputChars,
5314
+ response_size: turn.outputChars,
5315
+ reasoning_size: turn.reasoningChars,
5316
+ });
5317
+ }
5318
+ fallbackTurnIndex++;
5319
+ turn = null;
5320
+ };
5321
+
5322
+ const startTurn = (payload, timestampMs) => {
5323
+ flushTurn();
5324
+ turn = {
5325
+ executionId:
5326
+ typeof payload?.executionId === "string" ? payload.executionId : null,
5327
+ timestampMs:
5328
+ Number.isFinite(timestampMs)
5329
+ ? timestampMs
5330
+ : pendingUserTimestampMs,
5331
+ inputChars: pendingUserChars,
5332
+ outputChars: 0,
5333
+ reasoningChars: 0,
5334
+ modelId: null,
5335
+ messageIds: new Set(),
5336
+ };
5337
+ pendingUserChars = 0;
5338
+ pendingUserTimestampMs = NaN;
5339
+ };
5340
+
5341
+ try {
5342
+ for await (const line of rl) {
5343
+ if (!line || !line.trim()) continue;
5344
+ let event;
5345
+ try {
5346
+ event = JSON.parse(line);
5347
+ } catch {
5348
+ continue;
5349
+ }
5350
+ const payload = event?.payload;
5351
+ if (!payload || typeof payload !== "object") continue;
5352
+ const type =
5353
+ typeof payload.type === "string"
5354
+ ? payload.type.toLowerCase()
5355
+ : "";
5356
+ const timestampMs = Date.parse(event.timestamp || "");
5357
+
5358
+ if (type === "user") {
5359
+ if (turn) flushTurn();
5360
+ pendingUserChars = kiroCliV2ContentChars(payload.content);
5361
+ pendingUserTimestampMs = timestampMs;
5362
+ continue;
5363
+ }
5364
+
5365
+ if (type === "turn_start") {
5366
+ startTurn(payload, timestampMs);
5367
+ continue;
5368
+ }
5369
+
5370
+ if (
5371
+ ["assistant", "tool_call", "tool_result", "usage_summary"].includes(
5372
+ type,
5373
+ ) &&
5374
+ !turn
5375
+ ) {
5376
+ startTurn(payload, timestampMs);
5377
+ }
5378
+ if (!turn) continue;
5379
+
5380
+ if (
5381
+ typeof payload.executionId === "string" &&
5382
+ payload.executionId &&
5383
+ !turn.executionId
5384
+ ) {
5385
+ turn.executionId = payload.executionId;
5386
+ }
5387
+ if (typeof event.id === "string" && event.id) {
5388
+ turn.messageIds.add(event.id);
5389
+ }
5390
+
5391
+ if (type === "assistant") {
5392
+ const chars = kiroCliV2ContentChars(payload.content);
5393
+ if (
5394
+ typeof payload.operationType === "string" &&
5395
+ payload.operationType.toLowerCase() === "reasoning"
5396
+ ) {
5397
+ turn.reasoningChars += chars;
5398
+ } else {
5399
+ turn.outputChars += chars;
5400
+ }
5401
+ if (
5402
+ typeof payload.reasoningModelId === "string" &&
5403
+ payload.reasoningModelId
5404
+ ) {
5405
+ turn.modelId = payload.reasoningModelId;
5406
+ }
5407
+ } else if (type === "turn_end") {
5408
+ flushTurn();
5409
+ }
5410
+ }
5411
+ } catch {
5412
+ // Return complete turns parsed before a concurrent truncate/delete.
5413
+ }
5414
+ flushTurn();
5415
+ return flat;
5416
+ }
5417
+
5186
5418
  // Canonicalize a Kiro-CLI-emitted model id so IDE and CLI rows collapse when
5187
5419
  // they refer to the same underlying Bedrock model. Examples:
5188
5420
  // anthropic.claude-sonnet-4-20250514-v1:0 -> claude-sonnet-4
@@ -5202,6 +5434,7 @@ function canonicalizeKiroCliModelId(raw) {
5202
5434
  if (!name) return null;
5203
5435
  name = name.toLowerCase();
5204
5436
  if (name === "auto") return null;
5437
+ name = name.replace(/^(?:qdev|kiro)::/, "");
5205
5438
  // Strip provider prefix (anthropic., aws., openai., or a full Bedrock ARN).
5206
5439
  name = name.replace(
5207
5440
  /^(?:arn:aws:bedrock:[^:]*:[^:]*:(?:foundation-model\/)?|anthropic\.|openai\.|aws\.)/,
@@ -5294,10 +5527,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5294
5527
  const resolvedEnv = env || process.env;
5295
5528
  const dbPath = resolveKiroCliDbPath(resolvedEnv);
5296
5529
 
5297
- // Combine two sources under the same (source='kiro', cursors.kiroCli)
5298
- // namespace: historical rows from the SQLite DB plus live session state
5299
- // from ~/.kiro/sessions/cli/{uuid}.json (covers turns from a running
5300
- // session that hasn't flushed to SQLite yet). Request ID shapes differ:
5530
+ // Combine three sources under the same (source='kiro', cursors.kiroCli)
5531
+ // namespace: historical rows from the SQLite DB, legacy live session state
5532
+ // from ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+ event logs at
5533
+ // ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. Request ID
5534
+ // shapes differ:
5301
5535
  // SQLite carries a persisted request_id UUID; session files synthesize
5302
5536
  // `${sessionId}:${loop_id.rand}`. When kiro-cli migrates a live session
5303
5537
  // into SQLite the same turn lands under a new request_id — the cross-
@@ -5309,8 +5543,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5309
5543
  : [];
5310
5544
  const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
5311
5545
  let flatSessions = [];
5312
- for (const jsonPath of sessionFilesList) {
5313
- const turns = await readKiroCliSessionTurns(jsonPath);
5546
+ for (const sessionPath of sessionFilesList) {
5547
+ const turns =
5548
+ path.basename(sessionPath) === "messages.jsonl"
5549
+ ? await readKiroCliV2SessionTurns(sessionPath)
5550
+ : await readKiroCliSessionTurns(sessionPath);
5314
5551
  for (const turn of turns) flatSessions.push(turn);
5315
5552
  }
5316
5553
  // Per-request state replaces the old seenIds set. Each entry captures
@@ -5382,7 +5619,14 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5382
5619
  toRetract.push([reqId, prev, sid]);
5383
5620
  }
5384
5621
  for (const [reqId, prev, sid] of toRetract) {
5385
- if (prev.input_tokens || prev.output_tokens) {
5622
+ if (
5623
+ prev.input_tokens ||
5624
+ prev.output_tokens ||
5625
+ prev.reasoning_output_tokens
5626
+ ) {
5627
+ const prevReasoning = toNonNegativeInt(
5628
+ prev.reasoning_output_tokens,
5629
+ );
5386
5630
  const prevBucket = getHourlyBucket(
5387
5631
  hourlyState,
5388
5632
  "kiro",
@@ -5394,8 +5638,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5394
5638
  cached_input_tokens: 0,
5395
5639
  cache_creation_input_tokens: 0,
5396
5640
  output_tokens: -prev.output_tokens,
5397
- reasoning_output_tokens: 0,
5398
- total_tokens: -(prev.input_tokens + prev.output_tokens),
5641
+ reasoning_output_tokens: -prevReasoning,
5642
+ total_tokens: -(
5643
+ prev.input_tokens +
5644
+ prev.output_tokens +
5645
+ prevReasoning
5646
+ ),
5399
5647
  conversation_count: -1,
5400
5648
  });
5401
5649
  touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
@@ -5488,8 +5736,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5488
5736
 
5489
5737
  const promptChars = toNonNegativeInt(r.user_prompt_length);
5490
5738
  const responseChars = toNonNegativeInt(r.response_size);
5739
+ const reasoningChars = toNonNegativeInt(r.reasoning_size);
5491
5740
  const approxInput = Math.floor(promptChars / KIRO_CLI_CHARS_PER_TOKEN);
5492
5741
  const approxOutput = Math.floor(responseChars / KIRO_CLI_CHARS_PER_TOKEN);
5742
+ const approxReasoning = Math.floor(
5743
+ reasoningChars / KIRO_CLI_CHARS_PER_TOKEN,
5744
+ );
5493
5745
 
5494
5746
  const tsMs = Number(r.request_start_timestamp_ms);
5495
5747
  if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
@@ -5502,37 +5754,52 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5502
5754
  const model = canonical || "kiro-cli-agent";
5503
5755
 
5504
5756
  // Fingerprint captures every field whose change should cause a re-bucket.
5505
- const fingerprint = `${promptChars}:${responseChars}:${model}:${tsMs}`;
5757
+ const fingerprint =
5758
+ `${promptChars}:${responseChars}:${reasoningChars}:${model}:${tsMs}`;
5506
5759
  const prev = requestState[requestId];
5507
5760
  if (prev && prev.fingerprint === fingerprint) continue; // unchanged
5508
5761
 
5509
5762
  // Subtract the prior contribution (if any) from its prior bucket so the
5510
5763
  // bucket's absolute totals reflect the CURRENT truth, not the historical
5511
5764
  // truth. enqueueTouchedBuckets will emit the net delta at flush time.
5512
- if (prev && (prev.input_tokens || prev.output_tokens)) {
5765
+ if (
5766
+ prev &&
5767
+ (
5768
+ prev.input_tokens ||
5769
+ prev.output_tokens ||
5770
+ prev.reasoning_output_tokens
5771
+ )
5772
+ ) {
5773
+ const prevReasoning = toNonNegativeInt(
5774
+ prev.reasoning_output_tokens,
5775
+ );
5513
5776
  const prevBucket = getHourlyBucket(hourlyState, "kiro", prev.model, prev.bucketStart);
5514
5777
  addTotals(prevBucket.totals, {
5515
5778
  input_tokens: -prev.input_tokens,
5516
5779
  cached_input_tokens: 0,
5517
5780
  cache_creation_input_tokens: 0,
5518
5781
  output_tokens: -prev.output_tokens,
5519
- reasoning_output_tokens: 0,
5520
- total_tokens: -(prev.input_tokens + prev.output_tokens),
5782
+ reasoning_output_tokens: -prevReasoning,
5783
+ total_tokens: -(
5784
+ prev.input_tokens +
5785
+ prev.output_tokens +
5786
+ prevReasoning
5787
+ ),
5521
5788
  conversation_count: -1,
5522
5789
  });
5523
5790
  touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
5524
5791
  }
5525
5792
 
5526
5793
  // Add the new contribution.
5527
- if (approxInput > 0 || approxOutput > 0) {
5794
+ if (approxInput > 0 || approxOutput > 0 || approxReasoning > 0) {
5528
5795
  const bucket = getHourlyBucket(hourlyState, "kiro", model, bucketStart);
5529
5796
  addTotals(bucket.totals, {
5530
5797
  input_tokens: approxInput,
5531
5798
  cached_input_tokens: 0,
5532
5799
  cache_creation_input_tokens: 0,
5533
5800
  output_tokens: approxOutput,
5534
- reasoning_output_tokens: 0,
5535
- total_tokens: approxInput + approxOutput,
5801
+ reasoning_output_tokens: approxReasoning,
5802
+ total_tokens: approxInput + approxOutput + approxReasoning,
5536
5803
  conversation_count: 1,
5537
5804
  });
5538
5805
  touchedBuckets.add(bucketKey("kiro", model, bucketStart));
@@ -5551,6 +5818,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5551
5818
  model,
5552
5819
  input_tokens: approxInput,
5553
5820
  output_tokens: approxOutput,
5821
+ reasoning_output_tokens: approxReasoning,
5554
5822
  ...(r.session_id ? { session_id: r.session_id } : {}),
5555
5823
  };
5556
5824