tokentracker-cli 0.81.2 → 0.82.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 (137) hide show
  1. package/dashboard/dist/assets/AchievementBadge-D6iPEEbz.js +1 -0
  2. package/dashboard/dist/assets/AchievementsPage-Jry355YE.js +2 -0
  3. package/dashboard/dist/assets/AchievementsSection-ah-kG00E.js +1 -0
  4. package/dashboard/dist/assets/BadgeDetailModal-FyvUrVvF.js +1 -0
  5. package/dashboard/dist/assets/Card-C3nLScju.js +1 -0
  6. package/dashboard/dist/assets/ClawdAnimated-CLHhE26z.js +1 -0
  7. package/dashboard/dist/assets/CommunityStatsModal-DA3PHLV5.js +1 -0
  8. package/dashboard/dist/assets/ConfirmModal-CTDarLrD.js +1 -0
  9. package/dashboard/dist/assets/DashboardPage-DZIiWc3Y.js +1 -0
  10. package/dashboard/dist/assets/DevicePage-CyXjUO8W.js +1 -0
  11. package/dashboard/dist/assets/DialogDescription-ahxSNUcB.js +1 -0
  12. package/dashboard/dist/assets/DialogTitle-z86PHHyk.js +1 -0
  13. package/dashboard/dist/assets/FadeIn-BDWtfIsO.js +1 -0
  14. package/dashboard/dist/assets/HeaderGithubStar-DE1USQbb.js +1 -0
  15. package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-B2SkgT0P.js} +3 -3
  16. package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-PU5gxsfY.js} +165 -165
  17. package/dashboard/dist/assets/LeaderboardAvatar-CaOV_hFE.js +1 -0
  18. package/dashboard/dist/assets/LeaderboardPage-B0JtEyRP.js +2 -0
  19. package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-DtNEbVH8.js} +4 -4
  20. package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-DRklax3-.js} +3 -3
  21. package/dashboard/dist/assets/LimitsPage-Dg0cJa40.js +4 -0
  22. package/dashboard/dist/assets/LocalOnlyNotice-si9PMKya.js +1 -0
  23. package/dashboard/dist/assets/LoginPage-DgLwhXKo.js +1 -0
  24. package/dashboard/dist/assets/PetPage-DEZbjk99.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-DX7v1XfE.js +1 -0
  26. package/dashboard/dist/assets/ResetPasswordPage-C6NvRO-Q.js +1 -0
  27. package/dashboard/dist/assets/Select-CH2xVU8X.js +1 -0
  28. package/dashboard/dist/assets/SelectItemText-CCW6JeeN.js +1 -0
  29. package/dashboard/dist/assets/SettingsPage-Cm3uT11D.js +1 -0
  30. package/dashboard/dist/assets/SkillsPage-CljRL6yF.js +1 -0
  31. package/dashboard/dist/assets/TrendMonitor-DM1_WNfw.js +19 -0
  32. package/dashboard/dist/assets/WidgetsPage-mJ1j6hly.js +1 -0
  33. package/dashboard/dist/assets/WrappedPage-Dz71ln4t.js +1 -0
  34. package/dashboard/dist/assets/__vite-browser-external-C7JwykID.css +1 -0
  35. package/dashboard/dist/assets/__vite-browser-external-Eoyj4eG-.js +1341 -0
  36. package/dashboard/dist/assets/agent-logos-D3K_ZRUj.js +1 -0
  37. package/dashboard/dist/assets/arrow-up-right-CT4-gmOS.js +1 -0
  38. package/dashboard/dist/assets/check-B87_qvAg.js +1 -0
  39. package/dashboard/dist/assets/copy-Dtr68epm.js +1 -0
  40. package/dashboard/dist/assets/download-DaWsA_Gj.js +1 -0
  41. package/dashboard/dist/assets/flame-vLBOq0X0.js +1 -0
  42. package/dashboard/dist/assets/format-tokens-C6Pidz3s.js +1 -0
  43. package/dashboard/dist/assets/icons-BaaYJxUN.js +1 -0
  44. package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
  45. package/dashboard/dist/assets/info-B46Qt16h.js +1 -0
  46. package/dashboard/dist/assets/limits-providers-13af4PEg.js +1 -0
  47. package/dashboard/dist/assets/link-2-DeN6MJpT.js +1 -0
  48. package/dashboard/dist/assets/loader-circle-ByEuaaSw.js +1 -0
  49. package/dashboard/dist/assets/provider-display-CKbOKeV4.js +42 -0
  50. package/dashboard/dist/assets/terminal-DyicX7lG.js +1 -0
  51. package/dashboard/dist/assets/trash-2-CVCC3DtQ.js +1 -0
  52. package/dashboard/dist/assets/use-community-stats-CExk3D6G.js +1 -0
  53. package/dashboard/dist/assets/use-limits-display-prefs-CRl4uU95.js +1 -0
  54. package/dashboard/dist/assets/use-native-settings-Bi6E9dsE.js +1 -0
  55. package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-Dk0p2otw.js} +2 -2
  56. package/dashboard/dist/assets/use-session-efficiency-pref-YAG-OYIh.js +1 -0
  57. package/dashboard/dist/assets/use-transform-lflyghWN.js +1 -0
  58. package/dashboard/dist/assets/use-usage-limits-DJXvJ25Q.js +1 -0
  59. package/dashboard/dist/assets/useCurrency-BtHtcsO5.js +1 -0
  60. package/dashboard/dist/assets/useScrollLock-C2C-Yazq.js +1 -0
  61. package/dashboard/dist/assets/useTokenFormat-BdD057ih.js +1 -0
  62. package/dashboard/dist/assets/zap-DWWma29E.js +1 -0
  63. package/dashboard/dist/index.html +2 -2
  64. package/dashboard/dist/share.html +2 -2
  65. package/package.json +1 -1
  66. package/src/cli.js +6 -0
  67. package/src/commands/serve.js +8 -6
  68. package/src/commands/sessions.js +29 -0
  69. package/src/commands/status.js +2 -1
  70. package/src/commands/sync.js +164 -36
  71. package/src/lib/codex-context-breakdown.js +330 -13
  72. package/src/lib/codex-rollout-parser.js +323 -42
  73. package/src/lib/context-health.js +124 -0
  74. package/src/lib/cursor-store.js +1289 -0
  75. package/src/lib/diagnostics.js +8 -5
  76. package/src/lib/fs.js +254 -16
  77. package/src/lib/git-outcomes.js +147 -0
  78. package/src/lib/local-api.js +151 -20
  79. package/src/lib/outcomes-engine.js +23 -0
  80. package/src/lib/pricing/index.js +8 -0
  81. package/src/lib/pricing/seed-snapshot.json +1 -1
  82. package/src/lib/rollout.js +101 -9
  83. package/src/lib/session-analytics.js +612 -0
  84. package/src/lib/usage-limits.js +21 -0
  85. package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
  86. package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
  87. package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
  88. package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
  89. package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
  90. package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
  91. package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
  92. package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
  93. package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
  94. package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
  95. package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
  96. package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
  97. package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
  98. package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
  99. package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
  100. package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
  101. package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
  102. package/dashboard/dist/assets/LimitsPage-BiQpqxj8.js +0 -4
  103. package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
  104. package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
  105. package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
  106. package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
  107. package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
  108. package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
  109. package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
  110. package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
  111. package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
  112. package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
  113. package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
  114. package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
  115. package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
  116. package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
  117. package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
  118. package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
  119. package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
  120. package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
  121. package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
  122. package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
  123. package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
  124. package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
  125. package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
  126. package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
  127. package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
  128. package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
  129. package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
  130. package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
  131. package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
  132. package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
  133. package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
  134. package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
  135. package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
  136. package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
  137. package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
@@ -35,6 +35,7 @@ const {
35
35
  getModelPricing,
36
36
  computeRowCost,
37
37
  ensurePricingLoaded,
38
+ getPricingRevision,
38
39
  } = require("./pricing");
39
40
 
40
41
  const {
@@ -66,6 +67,33 @@ const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
66
67
  CLAUDE_MEM_OBSERVER_PROJECT_REF,
67
68
  );
68
69
  const PROJECT_USAGE_MAX_ENTRIES = 10;
70
+ const MAX_TIME_ZONE_CACHE_ENTRIES = 16;
71
+
72
+ // The native dashboard fans one refresh out across several local endpoints.
73
+ // Keep one immutable, deduped view of the current queue so those endpoints do
74
+ // not all read and JSON.parse the same append-only file. A file identity +
75
+ // nanosecond timestamp signature invalidates the cache immediately on append,
76
+ // in-place rewrite, or atomic replacement.
77
+ let queueDataCache = null;
78
+ const dailyAggregationCache = new WeakMap();
79
+ const zonedPartsFormatters = new Map();
80
+
81
+ function boundedCacheSet(cache, key, value, maxEntries) {
82
+ if (cache.has(key)) cache.delete(key);
83
+ cache.set(key, value);
84
+ while (cache.size > maxEntries) {
85
+ cache.delete(cache.keys().next().value);
86
+ }
87
+ }
88
+
89
+ function queueFileSignature(queuePath) {
90
+ const stat = fs.statSync(queuePath, { bigint: true });
91
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
92
+ }
93
+
94
+ function timeZoneContextKey({ timeZone, offsetMinutes } = {}) {
95
+ return `${timeZone || ""}|${Number.isFinite(offsetMinutes) ? offsetMinutes : ""}|p${getPricingRevision()}`;
96
+ }
69
97
 
70
98
  // Shared by project-usage-summary and project-usage-detail: reads the
71
99
  // deduped project bucket log, drops the claude-mem pseudo project, and
@@ -160,6 +188,21 @@ function normalizeQueueRow(row) {
160
188
  }
161
189
 
162
190
  function readQueueData(queuePath) {
191
+ let signature;
192
+ try {
193
+ signature = queueFileSignature(queuePath);
194
+ } catch (e) {
195
+ if (queueDataCache?.queuePath === queuePath) queueDataCache = null;
196
+ if (e?.code !== "ENOENT") {
197
+ console.error("[LocalAPI] readQueueData: failed to stat queue:", e?.message || e);
198
+ }
199
+ return [];
200
+ }
201
+
202
+ if (queueDataCache?.queuePath === queuePath && queueDataCache.signature === signature) {
203
+ return queueDataCache.rows;
204
+ }
205
+
163
206
  let raw;
164
207
  try {
165
208
  raw = fs.readFileSync(queuePath, "utf8");
@@ -196,7 +239,9 @@ function readQueueData(queuePath) {
196
239
  const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
197
240
  seen.set(key, normalizeQueueRow(row));
198
241
  }
199
- return Array.from(seen.values());
242
+ const rows = Array.from(seen.values());
243
+ queueDataCache = { queuePath, signature, rows };
244
+ return rows;
200
245
  }
201
246
 
202
247
  function rowDayKey(row, timeZoneContext) {
@@ -214,8 +259,19 @@ function rowDayKey(row, timeZoneContext) {
214
259
  }
215
260
 
216
261
  function aggregateByDay(rows, timeZoneContext = null) {
262
+ const normalizedRows = Array.isArray(rows) ? rows : [];
263
+ const cacheKey = timeZoneContextKey(timeZoneContext || {});
264
+ let cachedByTimeZone = dailyAggregationCache.get(normalizedRows);
265
+ if (cachedByTimeZone?.has(cacheKey)) {
266
+ const cached = cachedByTimeZone.get(cacheKey);
267
+ // Touch the entry so the small per-queue map behaves as an LRU.
268
+ cachedByTimeZone.delete(cacheKey);
269
+ cachedByTimeZone.set(cacheKey, cached);
270
+ return cached;
271
+ }
272
+
217
273
  const byDay = new Map();
218
- for (const row of rows) {
274
+ for (const row of normalizedRows) {
219
275
  if (!row.hour_start) continue;
220
276
  const day = rowDayKey(row, timeZoneContext);
221
277
  if (!day) continue;
@@ -250,7 +306,13 @@ function aggregateByDay(rows, timeZoneContext = null) {
250
306
  const model = row.model || "unknown";
251
307
  a.models[model] = (a.models[model] || 0) + (row.total_tokens || 0);
252
308
  }
253
- return Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
309
+ const daily = Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
310
+ if (!cachedByTimeZone) {
311
+ cachedByTimeZone = new Map();
312
+ dailyAggregationCache.set(normalizedRows, cachedByTimeZone);
313
+ }
314
+ boundedCacheSet(cachedByTimeZone, cacheKey, daily, MAX_TIME_ZONE_CACHE_ENTRIES);
315
+ return daily;
254
316
  }
255
317
 
256
318
  function buildCodexCategoryFallbackFromQueue(queueRows, { from, to, timeZoneContext }) {
@@ -477,17 +539,34 @@ function getZonedParts(date, { timeZone, offsetMinutes } = {}) {
477
539
 
478
540
  if (timeZone && typeof Intl !== "undefined" && Intl.DateTimeFormat) {
479
541
  try {
480
- const formatter = new Intl.DateTimeFormat("en-CA", {
481
- timeZone,
482
- year: "numeric",
483
- month: "2-digit",
484
- day: "2-digit",
485
- hour: "2-digit",
486
- minute: "2-digit",
487
- second: "2-digit",
488
- hourCycle: "h23",
489
- });
490
- const parts = formatter.formatToParts(dt);
542
+ let formatter;
543
+ if (zonedPartsFormatters.has(timeZone)) {
544
+ formatter = zonedPartsFormatters.get(timeZone);
545
+ } else {
546
+ try {
547
+ formatter = new Intl.DateTimeFormat("en-CA", {
548
+ timeZone,
549
+ year: "numeric",
550
+ month: "2-digit",
551
+ day: "2-digit",
552
+ hour: "2-digit",
553
+ minute: "2-digit",
554
+ second: "2-digit",
555
+ hourCycle: "h23",
556
+ });
557
+ } catch {
558
+ // Cache invalid zone ids too; otherwise a bad local query would
559
+ // throw once per queue row before falling back to its fixed offset.
560
+ formatter = null;
561
+ }
562
+ boundedCacheSet(
563
+ zonedPartsFormatters,
564
+ timeZone,
565
+ formatter,
566
+ MAX_TIME_ZONE_CACHE_ENTRIES,
567
+ );
568
+ }
569
+ const parts = formatter?.formatToParts(dt) || [];
491
570
  const values = parts.reduce((acc, part) => {
492
571
  if (part.type && part.value) acc[part.type] = part.value;
493
572
  return acc;
@@ -685,6 +764,7 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
685
764
  const args = [TRACKER_BIN, "sync"];
686
765
  if (opts.auto === true) args.push("--auto");
687
766
  if (opts.background === true) args.push("--background");
767
+ if (opts.publishAccount === true) args.push("--publish-account");
688
768
  if (opts.allLocalSources === true) args.push("--all-local-sources");
689
769
  if (opts.drain === true) args.push("--drain");
690
770
  const child = spawn(process.execPath, args, {
@@ -1696,6 +1776,11 @@ function createLocalApiHandler({ queuePath }) {
1696
1776
  const auto = body.auto === true && !drain;
1697
1777
  const background =
1698
1778
  auto && (body.background === true || body.lightweight === true);
1779
+ // The local server is the trust boundary for cloud-sync preferences.
1780
+ // A persisted device token must not let a native background request
1781
+ // upload after the user has explicitly disabled cloud sync.
1782
+ const publishAccount =
1783
+ background && body.publishAccount === true && getCloudSyncPref();
1699
1784
  const allLocalSources = background && body.allLocalSources === true;
1700
1785
  if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
1701
1786
  extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
@@ -1710,7 +1795,10 @@ function createLocalApiHandler({ queuePath }) {
1710
1795
  extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
1711
1796
  localSyncBaseUrl = allowedBaseUrl;
1712
1797
  }
1713
- if (!background && !extraEnv.TOKENTRACKER_DEVICE_TOKEN && getCloudSyncPref() && getRefreshTokenForCloud()) {
1798
+ if ((!background || publishAccount) &&
1799
+ !extraEnv.TOKENTRACKER_DEVICE_TOKEN &&
1800
+ getCloudSyncPref() &&
1801
+ getRefreshTokenForCloud()) {
1714
1802
  let issuedToken = null;
1715
1803
  try {
1716
1804
  issuedToken = await issueDeviceTokenForLocalSync(qp, { baseUrl: localSyncBaseUrl });
@@ -1732,6 +1820,7 @@ function createLocalApiHandler({ queuePath }) {
1732
1820
  drain,
1733
1821
  auto,
1734
1822
  background,
1823
+ publishAccount,
1735
1824
  allLocalSources,
1736
1825
  });
1737
1826
  try {
@@ -1778,7 +1867,8 @@ function createLocalApiHandler({ queuePath }) {
1778
1867
  const to = url.searchParams.get("to") || "";
1779
1868
  const timeZoneContext = getTimeZoneContext(url);
1780
1869
  const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
1781
- const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
1870
+ const allDaily = aggregateByDay(rows, timeZoneContext);
1871
+ const daily = allDaily.filter((d) => d.day >= from && d.day <= to);
1782
1872
  const totals = daily.reduce(
1783
1873
  (acc, r) => {
1784
1874
  acc.total_tokens += r.total_tokens;
@@ -1798,7 +1888,6 @@ function createLocalApiHandler({ queuePath }) {
1798
1888
 
1799
1889
  const todayParts = getZonedParts(new Date(), timeZoneContext);
1800
1890
  const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
1801
- const allDaily = aggregateByDay(rows, timeZoneContext);
1802
1891
 
1803
1892
  const shiftDay = (dayStr, delta) => {
1804
1893
  const d = new Date(`${dayStr}T00:00:00Z`);
@@ -1860,11 +1949,20 @@ function createLocalApiHandler({ queuePath }) {
1860
1949
  const from = url.searchParams.get("from") || "";
1861
1950
  const to = url.searchParams.get("to") || "";
1862
1951
  const {
1863
- readOutcomesData,
1864
- resolveOutcomesPath,
1952
+ readAllOutcomesData,
1865
1953
  computeQualityPerDollar,
1866
1954
  } = require("./outcomes-engine");
1867
- const outcomes = readOutcomesData(resolveOutcomesPath());
1955
+ // Refresh the independent metadata-only sidecars before joining. Any
1956
+ // scanner failure degrades to the existing manual outcomes file.
1957
+ try {
1958
+ const { buildSessionAnalytics } = require("./session-analytics");
1959
+ const { buildGitOutcomes } = require("./git-outcomes");
1960
+ const sessions = await buildSessionAnalytics();
1961
+ await buildGitOutcomes(sessions);
1962
+ } catch (error) {
1963
+ console.error("[outcomes] automatic Git attribution failed:", error?.message || error);
1964
+ }
1965
+ const outcomes = readAllOutcomesData();
1868
1966
  if (!outcomes.length) {
1869
1967
  json(res, { available: false, from, to, by_model: [], by_tool: [], totals: null });
1870
1968
  return true;
@@ -1875,6 +1973,39 @@ function createLocalApiHandler({ queuePath }) {
1875
1973
  return true;
1876
1974
  }
1877
1975
 
1976
+ // --- metadata-only Claude/Codex session efficiency ---
1977
+ if (p === "/functions/tokentracker-session-insights") {
1978
+ const from = url.searchParams.get("from") || "";
1979
+ const to = url.searchParams.get("to") || "";
1980
+ const refresh = ["1", "true"].includes(url.searchParams.get("refresh"));
1981
+ try {
1982
+ const { buildSessionAnalytics, summarizeSessions, sessionsToCsv } = require("./session-analytics");
1983
+ const sessions = await buildSessionAnalytics({ force: refresh });
1984
+ const wantsCsv = url.searchParams.get("format") === "csv";
1985
+ const includeSessions = wantsCsv || ["1", "true"].includes(url.searchParams.get("include_sessions"));
1986
+ const result = summarizeSessions(sessions, { from, to, includeSessions });
1987
+ if (wantsCsv) {
1988
+ const content = sessionsToCsv(result.sessions);
1989
+ res.statusCode = 200;
1990
+ res.setHeader("Content-Type", "text/csv; charset=utf-8");
1991
+ res.setHeader("Content-Disposition", "attachment; filename=tokentracker-sessions.csv");
1992
+ res.end(content);
1993
+ return true;
1994
+ }
1995
+ json(res, { from, to, ...result });
1996
+ } catch (error) {
1997
+ json(res, { available: false, error: error?.message || "Session analytics failed" }, 500);
1998
+ }
1999
+ return true;
2000
+ }
2001
+
2002
+ // --- fixed context overhead audit (counts and estimates only) ---
2003
+ if (p === "/functions/tokentracker-context-health") {
2004
+ const { computeContextHealth } = require("./context-health");
2005
+ json(res, computeContextHealth({ home: os.homedir(), cwd: process.cwd(), env: process.env }));
2006
+ return true;
2007
+ }
2008
+
1878
2009
  // --- usage-heatmap ---
1879
2010
  if (p === "/functions/tokentracker-usage-heatmap") {
1880
2011
  const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
@@ -27,6 +27,10 @@ function resolveOutcomesPath() {
27
27
  return path.join(os.homedir(), ".tokentracker", "tracker", "outcomes.jsonl");
28
28
  }
29
29
 
30
+ function resolveAutoOutcomesPath() {
31
+ return path.join(os.homedir(), ".tokentracker", "tracker", "auto-outcomes.jsonl");
32
+ }
33
+
30
34
  // The ONLY fields ever lifted off an outcome record. Anything not on this list
31
35
  // — bodies, diffs, messages, arbitrary blobs — is dropped at read time. This is
32
36
  // the enforcement point for the metadata-only privacy invariant.
@@ -54,6 +58,10 @@ function sanitizeOutcome(raw) {
54
58
  if (typeof raw.task_type === "string" && raw.task_type.trim()) {
55
59
  out.task_type = raw.task_type.trim();
56
60
  }
61
+ for (const key of ["status", "session_hash", "commit_hash", "confidence", "methodology"]) {
62
+ if (typeof raw[key] === "string" && raw[key].trim()) out[key] = raw[key].trim();
63
+ }
64
+ if (Number.isFinite(Number(raw.parent_count))) out.parent_count = Number(raw.parent_count);
57
65
  return out;
58
66
  }
59
67
 
@@ -91,6 +99,19 @@ function readOutcomesData(outcomesPath) {
91
99
  return out;
92
100
  }
93
101
 
102
+ function readAllOutcomesData(paths = [resolveOutcomesPath(), resolveAutoOutcomesPath()]) {
103
+ const dedup = new Map();
104
+ for (const outcomePath of paths) {
105
+ for (const row of readOutcomesData(outcomePath)) {
106
+ const key = row.commit_hash
107
+ ? `commit:${row.commit_hash}`
108
+ : `${row.timestamp}|${row.model}|${row.tool}|${row.task_type || ""}`;
109
+ dedup.set(key, row);
110
+ }
111
+ }
112
+ return [...dedup.values()];
113
+ }
114
+
94
115
  function dayOf(value) {
95
116
  return typeof value === "string" ? value.slice(0, 10) : "";
96
117
  }
@@ -224,7 +245,9 @@ function computeQualityPerDollar(queueRows, outcomes, { from = "", to = "" } = {
224
245
 
225
246
  module.exports = {
226
247
  resolveOutcomesPath,
248
+ resolveAutoOutcomesPath,
227
249
  sanitizeOutcome,
228
250
  readOutcomesData,
251
+ readAllOutcomesData,
229
252
  computeQualityPerDollar,
230
253
  };
@@ -37,6 +37,7 @@ const seedRaw = loadSeedSync();
37
37
  const state = {
38
38
  loaded: false,
39
39
  loadingPromise: null,
40
+ revision: 0,
40
41
  litellmRawMap: seedRaw, // raw per-token; field shape from LiteLLM JSON
41
42
  litellmPerMillionMap: buildLitellmPerMillionMap(seedRaw), // USD/MTok
42
43
  source: Object.keys(seedRaw).length ? "seed-snapshot:sync" : null,
@@ -61,6 +62,7 @@ async function ensurePricingLoaded(opts = {}) {
61
62
  state.litellmPerMillionMap = buildLitellmPerMillionMap(state.litellmRawMap);
62
63
  state.source = source;
63
64
  state.loaded = true;
65
+ state.revision += 1;
64
66
  state.negativeCache.clear();
65
67
  return state;
66
68
  } finally {
@@ -79,9 +81,14 @@ function resetPricingForTests() {
79
81
  state.litellmRawMap = seedRaw;
80
82
  state.litellmPerMillionMap = buildLitellmPerMillionMap(seedRaw);
81
83
  state.source = Object.keys(seedRaw).length ? "seed-snapshot:sync" : null;
84
+ state.revision += 1;
82
85
  state.negativeCache.clear();
83
86
  }
84
87
 
88
+ function getPricingRevision() {
89
+ return state.revision;
90
+ }
91
+
85
92
  function getModelPricing(model, opts = {}) {
86
93
  if (!model) return ZERO_PRICING;
87
94
  let lookupSource = null;
@@ -133,6 +140,7 @@ const MODEL_PRICING = curatedOverrides.exact;
133
140
 
134
141
  module.exports = {
135
142
  ensurePricingLoaded,
143
+ getPricingRevision,
136
144
  getModelPricing,
137
145
  computeRowCost,
138
146
  resetPricingForTests,