tokentracker-cli 0.5.71 → 0.5.73

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 (24) hide show
  1. package/README.md +9 -0
  2. package/dashboard/dist/assets/{Card-5ebScQbM.js → Card-D_q1XGfK.js} +1 -1
  3. package/dashboard/dist/assets/{DashboardPage-B32RhUB1.js → DashboardPage-CuBSoNgI.js} +1 -1
  4. package/dashboard/dist/assets/{FadeIn-CHjFUAEe.js → FadeIn-ClpHby-T.js} +1 -1
  5. package/dashboard/dist/assets/{IpCheckPage-CrbKufId.js → IpCheckPage-B5TpHE6G.js} +1 -1
  6. package/dashboard/dist/assets/{LeaderboardPage-CVKBhGzY.js → LeaderboardPage-BOrtGeIf.js} +1 -1
  7. package/dashboard/dist/assets/LeaderboardProfilePage-C8xexZ08.js +1 -0
  8. package/dashboard/dist/assets/{LimitsPage-BwQ4isbM.js → LimitsPage-CERWQyIU.js} +1 -1
  9. package/dashboard/dist/assets/{SettingsPage-Y3Znzs1i.js → SettingsPage-DUwt4x-H.js} +1 -1
  10. package/dashboard/dist/assets/{WidgetsPage-tNojCBaj.js → WidgetsPage-Cmr2tbD7.js} +1 -1
  11. package/dashboard/dist/assets/{download-CQASvwEL.js → download-M8e1PJC-.js} +1 -1
  12. package/dashboard/dist/assets/{leaderboard-columns-DFG1TWe9.js → leaderboard-columns-Dcg9r7R2.js} +1 -1
  13. package/dashboard/dist/assets/{main-Cv6YpYdZ.js → main-D1VdJk4V.js} +2 -2
  14. package/dashboard/dist/assets/{use-limits-display-prefs-BUrHqTkA.js → use-limits-display-prefs-CSj55sfK.js} +1 -1
  15. package/dashboard/dist/assets/{use-usage-limits-D-gv2i1R.js → use-usage-limits-BuWINUAm.js} +1 -1
  16. package/dashboard/dist/index.html +1 -1
  17. package/dashboard/dist/share.html +1 -1
  18. package/package.json +1 -1
  19. package/src/commands/status.js +11 -1
  20. package/src/commands/sync.js +65 -1
  21. package/src/lib/diagnostics.js +30 -0
  22. package/src/lib/local-api.js +176 -57
  23. package/src/lib/rollout.js +667 -4
  24. package/dashboard/dist/assets/LeaderboardProfilePage-CbpzxvLA.js +0 -1
@@ -63,6 +63,14 @@ const MODEL_PRICING = {
63
63
  "kimi-for-coding": { input: 0.6, output: 2, cache_read: 0.15 },
64
64
  "kimi-k2.5": { input: 0.6, output: 2, cache_read: 0.15 },
65
65
  "kimi-k2.5-free": { input: 0, output: 0, cache_read: 0 },
66
+ // ── AWS Kiro (Kiro IDE + Kiro CLI — both route through Bedrock, most
67
+ // commonly claude-sonnet-4; rates mirror the sonnet-4 table below so
68
+ // costs stay consistent with the real underlying model when a Bedrock
69
+ // model_id isn't exposed). Mirrored byte-for-byte in
70
+ // dashboard/edge-patches/tokentracker-leaderboard-refresh.ts for
71
+ // leaderboard estimated_cost_usd. ──
72
+ "kiro-agent": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
73
+ "kiro-cli-agent": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
66
74
  // ── Misc / Free ──
67
75
  "glm-4.7-free": { input: 0, output: 0, cache_read: 0 },
68
76
  "nemotron-3-super-free": { input: 0, output: 0, cache_read: 0 },
@@ -90,6 +98,7 @@ function getModelPricing(model) {
90
98
  if (lower.includes("gemini-3")) return MODEL_PRICING["gemini-3-flash-preview"];
91
99
  if (lower.includes("gemini-2.5")) return MODEL_PRICING["gemini-2.5-pro"];
92
100
  if (lower.includes("kimi")) return MODEL_PRICING["kimi-k2.5"];
101
+ if (lower.includes("kiro")) return MODEL_PRICING["kiro-cli-agent"];
93
102
  if (lower.includes("composer")) return MODEL_PRICING["composer-1"];
94
103
  if (lower === "auto") return MODEL_PRICING["composer-1"];
95
104
  return ZERO_PRICING;
@@ -116,30 +125,90 @@ function resolveQueuePath() {
116
125
  return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
117
126
  }
118
127
 
119
- function readQueueData(queuePath) {
128
+ function readProjectQueueData(projectQueuePath) {
129
+ let raw;
120
130
  try {
121
- const raw = fs.readFileSync(queuePath, "utf8");
122
- const lines = raw.split("\n").filter((l) => l.trim());
123
- const parsed = lines.map((l) => JSON.parse(l));
124
- // Deduplicate: each sync appends cumulative totals per bucket, so for
125
- // each (source, model, hour_start) keep only the latest (last) entry.
126
- const seen = new Map();
127
- for (const row of parsed) {
128
- const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
131
+ raw = fs.readFileSync(projectQueuePath, "utf8");
132
+ } catch (e) {
133
+ if (e?.code !== "ENOENT") {
134
+ console.error("[LocalAPI] readProjectQueueData: failed to read:", e?.message || e);
135
+ }
136
+ return [];
137
+ }
138
+ const lines = raw.split("\n").filter((l) => l.trim());
139
+ const seen = new Map();
140
+ for (const line of lines) {
141
+ try {
142
+ const row = JSON.parse(line);
143
+ const key = `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`;
129
144
  seen.set(key, row);
145
+ } catch {
146
+ // skip malformed
147
+ }
148
+ }
149
+ return Array.from(seen.values());
150
+ }
151
+
152
+ function readQueueData(queuePath) {
153
+ let raw;
154
+ try {
155
+ raw = fs.readFileSync(queuePath, "utf8");
156
+ } catch (e) {
157
+ // ENOENT is legitimate (never synced yet); anything else is a signal we
158
+ // don't want to hide behind an empty array forever — the dashboard would
159
+ // otherwise render "0 tokens" with no clue the queue was unreadable.
160
+ if (e?.code !== "ENOENT") {
161
+ console.error("[LocalAPI] readQueueData: failed to read queue:", e?.message || e);
130
162
  }
131
- return Array.from(seen.values());
132
- } catch (_e) {
133
163
  return [];
134
164
  }
165
+ const lines = raw.split("\n").filter((l) => l.trim());
166
+ // Parse row-by-row so a single corrupted line (partial write, disk-full
167
+ // truncation, …) does not wipe out every other row with it.
168
+ const parsed = [];
169
+ let malformed = 0;
170
+ for (const line of lines) {
171
+ try {
172
+ parsed.push(JSON.parse(line));
173
+ } catch {
174
+ malformed += 1;
175
+ }
176
+ }
177
+ if (malformed > 0) {
178
+ console.error(
179
+ `[LocalAPI] readQueueData: skipped ${malformed}/${lines.length} malformed line(s) in ${queuePath}`,
180
+ );
181
+ }
182
+ // Deduplicate: each sync appends cumulative totals per bucket, so for
183
+ // each (source, model, hour_start) keep only the latest (last) entry.
184
+ const seen = new Map();
185
+ for (const row of parsed) {
186
+ const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
187
+ seen.set(key, row);
188
+ }
189
+ return Array.from(seen.values());
135
190
  }
136
191
 
137
- function aggregateByDay(rows) {
192
+ function rowDayKey(row, timeZoneContext) {
193
+ const hs = row.hour_start;
194
+ if (!hs) return "";
195
+ if (
196
+ timeZoneContext &&
197
+ (timeZoneContext.timeZone || Number.isFinite(timeZoneContext.offsetMinutes))
198
+ ) {
199
+ const parts = getZonedParts(new Date(hs), timeZoneContext);
200
+ const key = formatPartsDayKey(parts);
201
+ if (key) return key;
202
+ }
203
+ return hs.slice(0, 10);
204
+ }
205
+
206
+ function aggregateByDay(rows, timeZoneContext = null) {
138
207
  const byDay = new Map();
139
208
  for (const row of rows) {
140
- const hs = row.hour_start;
141
- if (!hs) continue;
142
- const day = hs.slice(0, 10);
209
+ if (!row.hour_start) continue;
210
+ const day = rowDayKey(row, timeZoneContext);
211
+ if (!day) continue;
143
212
  if (!byDay.has(day)) {
144
213
  byDay.set(day, {
145
214
  day,
@@ -674,8 +743,9 @@ function createLocalApiHandler({ queuePath }) {
674
743
  if (p === "/functions/tokentracker-usage-summary") {
675
744
  const from = url.searchParams.get("from") || "";
676
745
  const to = url.searchParams.get("to") || "";
746
+ const timeZoneContext = getTimeZoneContext(url);
677
747
  const rows = readQueueData(qp);
678
- const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
748
+ const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
679
749
  const totals = daily.reduce(
680
750
  (acc, r) => {
681
751
  acc.total_tokens += r.total_tokens;
@@ -693,16 +763,19 @@ function createLocalApiHandler({ queuePath }) {
693
763
  );
694
764
  const totalCost = totals.total_cost_usd;
695
765
 
696
- const today = new Date();
697
- const todayStr = today.toISOString().slice(0, 10);
698
- const allDaily = aggregateByDay(rows);
766
+ const todayParts = getZonedParts(new Date(), timeZoneContext);
767
+ const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
768
+ const allDaily = aggregateByDay(rows, timeZoneContext);
699
769
 
770
+ const shiftDay = (dayStr, delta) => {
771
+ const d = new Date(`${dayStr}T00:00:00Z`);
772
+ d.setUTCDate(d.getUTCDate() + delta);
773
+ return d.toISOString().slice(0, 10);
774
+ };
700
775
  const collectDays = (n) => {
701
776
  const out = [];
702
777
  for (let i = n - 1; i >= 0; i--) {
703
- const d = new Date(today);
704
- d.setUTCDate(d.getUTCDate() - i);
705
- const ds = d.toISOString().slice(0, 10);
778
+ const ds = shiftDay(todayStr, -i);
706
779
  const dd = allDaily.find((x) => x.day === ds);
707
780
  if (dd) out.push(dd);
708
781
  }
@@ -719,17 +792,15 @@ function createLocalApiHandler({ queuePath }) {
719
792
  const l30 = collectDays(30);
720
793
  const l7t = sumDays(l7);
721
794
  const l30t = sumDays(l30);
722
- const l7from = new Date(today);
723
- l7from.setUTCDate(l7from.getUTCDate() - 6);
724
- const l30from = new Date(today);
725
- l30from.setUTCDate(l30from.getUTCDate() - 29);
795
+ const l7fromStr = shiftDay(todayStr, -6);
796
+ const l30fromStr = shiftDay(todayStr, -29);
726
797
 
727
798
  json(res, {
728
799
  from, to, days: daily.length,
729
800
  totals: { ...totals, total_cost_usd: totalCost.toFixed(6) },
730
801
  rolling: {
731
- last_7d: { from: l7from.toISOString().slice(0, 10), to: todayStr, active_days: l7.length, totals: l7t },
732
- last_30d: { from: l30from.toISOString().slice(0, 10), to: todayStr, active_days: l30.length, totals: l30t, avg_per_active_day: l30.length > 0 ? Math.round(l30t.billable_total_tokens / l30.length) : 0 },
802
+ last_7d: { from: l7fromStr, to: todayStr, active_days: l7.length, totals: l7t },
803
+ last_30d: { from: l30fromStr, to: todayStr, active_days: l30.length, totals: l30t, avg_per_active_day: l30.length > 0 ? Math.round(l30t.billable_total_tokens / l30.length) : 0 },
733
804
  },
734
805
  });
735
806
  return true;
@@ -739,8 +810,9 @@ function createLocalApiHandler({ queuePath }) {
739
810
  if (p === "/functions/tokentracker-usage-daily") {
740
811
  const from = url.searchParams.get("from") || "";
741
812
  const to = url.searchParams.get("to") || "";
813
+ const timeZoneContext = getTimeZoneContext(url);
742
814
  const rows = readQueueData(qp);
743
- const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
815
+ const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
744
816
  json(res, { from, to, data: daily });
745
817
  return true;
746
818
  }
@@ -748,10 +820,12 @@ function createLocalApiHandler({ queuePath }) {
748
820
  // --- usage-heatmap ---
749
821
  if (p === "/functions/tokentracker-usage-heatmap") {
750
822
  const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
823
+ const timeZoneContext = getTimeZoneContext(url);
751
824
  const rows = readQueueData(qp);
752
- const daily = aggregateByDay(rows);
753
- const today = new Date();
754
- const end = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
825
+ const daily = aggregateByDay(rows, timeZoneContext);
826
+ const todayParts = getZonedParts(new Date(), timeZoneContext);
827
+ const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
828
+ const end = new Date(`${todayStr}T00:00:00Z`);
755
829
  const start = new Date(end);
756
830
  start.setUTCDate(start.getUTCDate() - weeks * 7 + 1);
757
831
  const from = start.toISOString().slice(0, 10);
@@ -792,9 +866,10 @@ function createLocalApiHandler({ queuePath }) {
792
866
  if (p === "/functions/tokentracker-usage-model-breakdown") {
793
867
  const from = url.searchParams.get("from") || "";
794
868
  const to = url.searchParams.get("to") || "";
869
+ const timeZoneContext = getTimeZoneContext(url);
795
870
  const rows = readQueueData(qp).filter((r) => {
796
871
  if (!r.hour_start) return false;
797
- const d = r.hour_start.slice(0, 10);
872
+ const d = rowDayKey(r, timeZoneContext);
798
873
  return d >= from && d <= to;
799
874
  });
800
875
 
@@ -852,35 +927,71 @@ function createLocalApiHandler({ queuePath }) {
852
927
 
853
928
  // --- project-usage-summary ---
854
929
  if (p === "/functions/tokentracker-project-usage-summary") {
855
- const projectMap = new Map();
856
- scanCodexProjects(projectMap);
857
- scanClaudeProjects(projectMap);
858
-
859
- const rows = readQueueData(qp);
860
- const totalTokens = rows.reduce((s, r) => s + (r.total_tokens || 0), 0);
861
- const entries = [];
930
+ // Use the per-project bucket log that rollout.js emits — it already
931
+ // carries the actual tokens attributed to each (project_key, source,
932
+ // hour_start). Falling back to "session-file count × total tokens"
933
+ // (the old behavior) produced pure fiction: every short-and-hot
934
+ // project got the same weight as every long-and-cold one.
935
+ const projectQueuePath = path.join(
936
+ path.dirname(qp),
937
+ "project.queue.jsonl",
938
+ );
939
+ const projectRows = readProjectQueueData(projectQueuePath);
940
+
941
+ const byProject = new Map();
942
+ for (const row of projectRows) {
943
+ const key = row.project_key || "unknown";
944
+ if (!byProject.has(key)) {
945
+ byProject.set(key, {
946
+ project_key: key,
947
+ project_ref: row.project_ref || key,
948
+ total_tokens: 0,
949
+ billable_total_tokens: 0,
950
+ });
951
+ }
952
+ const agg = byProject.get(key);
953
+ agg.total_tokens += Number(row.total_tokens || 0);
954
+ agg.billable_total_tokens += Number(row.total_tokens || 0);
955
+ if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
956
+ }
862
957
 
863
- if (projectMap.size === 0) {
958
+ // If no project-attributed rows exist yet (user hasn't synced project
959
+ // attribution, or never used a project-capable CLI), fall back to
960
+ // per-source aggregation over the main queue so the panel isn't
961
+ // totally empty. This path used to also exist for the non-empty case
962
+ // and produce wrong numbers; keep it only as the empty fallback.
963
+ let entries;
964
+ if (byProject.size === 0) {
965
+ const rows = readQueueData(qp);
864
966
  const bySrc = new Map();
865
967
  for (const row of rows) {
866
968
  const src = row.source || "unknown";
867
- if (!bySrc.has(src)) bySrc.set(src, { project_key: src, project_ref: `https://${src}.ai`, total_tokens: 0, billable_total_tokens: 0 });
969
+ if (!bySrc.has(src)) {
970
+ bySrc.set(src, {
971
+ project_key: src,
972
+ project_ref: `https://${src}.ai`,
973
+ total_tokens: 0,
974
+ billable_total_tokens: 0,
975
+ });
976
+ }
868
977
  bySrc.get(src).total_tokens += row.total_tokens || 0;
869
978
  bySrc.get(src).billable_total_tokens += row.total_tokens || 0;
870
979
  }
871
- entries.push(
872
- ...Array.from(bySrc.values())
873
- .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
874
- .map((e) => ({ ...e, total_tokens: String(e.total_tokens), billable_total_tokens: String(e.billable_total_tokens) })),
875
- );
980
+ entries = Array.from(bySrc.values())
981
+ .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
982
+ .map((e) => ({
983
+ ...e,
984
+ total_tokens: String(e.total_tokens),
985
+ billable_total_tokens: String(e.billable_total_tokens),
986
+ }));
876
987
  } else {
877
- const totalCount = Array.from(projectMap.values()).reduce((s, p) => s + p.count, 0);
878
- for (const [, proj] of projectMap) {
879
- const ratio = totalCount > 0 ? proj.count / totalCount : 1 / projectMap.size;
880
- const tokens = Math.floor(totalTokens * ratio);
881
- entries.push({ project_key: proj.project_key, project_ref: proj.project_ref, total_tokens: String(tokens), billable_total_tokens: String(tokens) });
882
- }
883
- entries.sort((a, b) => Number(b.billable_total_tokens) - Number(a.billable_total_tokens));
988
+ entries = Array.from(byProject.values())
989
+ .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
990
+ .map((e) => ({
991
+ ...e,
992
+ total_tokens: String(e.total_tokens),
993
+ billable_total_tokens: String(e.billable_total_tokens),
994
+ }));
884
995
  }
885
996
 
886
997
  json(res, { generated_at: new Date().toISOString(), entries });
@@ -911,12 +1022,13 @@ function createLocalApiHandler({ queuePath }) {
911
1022
  if (p === "/functions/tokentracker-usage-monthly") {
912
1023
  const from = url.searchParams.get("from") || "";
913
1024
  const to = url.searchParams.get("to") || "";
1025
+ const timeZoneContext = getTimeZoneContext(url);
914
1026
  const rows = readQueueData(qp);
915
1027
  const byMonth = new Map();
916
1028
  for (const row of rows) {
917
1029
  if (!row.hour_start) continue;
918
- const day = row.hour_start.slice(0, 10);
919
- if (day < from || day > to) continue;
1030
+ const day = rowDayKey(row, timeZoneContext);
1031
+ if (!day || day < from || day > to) continue;
920
1032
  const month = day.slice(0, 7);
921
1033
  if (!byMonth.has(month))
922
1034
  byMonth.set(month, { month, total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0 });
@@ -958,4 +1070,11 @@ function createLocalApiHandler({ queuePath }) {
958
1070
  };
959
1071
  }
960
1072
 
961
- module.exports = { createLocalApiHandler, resolveQueuePath };
1073
+ module.exports = {
1074
+ createLocalApiHandler,
1075
+ resolveQueuePath,
1076
+ // Exported for cross-consumer tests (pricing + native contract lock).
1077
+ MODEL_PRICING,
1078
+ getModelPricing,
1079
+ computeRowCost,
1080
+ };