tokentracker-cli 0.21.3 → 0.22.1

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 (48) hide show
  1. package/README.ja.md +459 -0
  2. package/README.ko.md +459 -0
  3. package/README.md +47 -6
  4. package/README.zh-CN.md +47 -6
  5. package/dashboard/dist/assets/{Card-Cv4wn6W8.js → Card-DdDoM-Gd.js} +1 -1
  6. package/dashboard/dist/assets/DashboardPage-DqsLRhn6.js +64 -0
  7. package/dashboard/dist/assets/DevicePage-Dz2WacAK.js +1 -0
  8. package/dashboard/dist/assets/{FadeIn-DjQyRfLZ.js → FadeIn-CoY8tCtI.js} +1 -1
  9. package/dashboard/dist/assets/{HeaderGithubStar-D2BjLT1b.js → HeaderGithubStar-hr5WjsdY.js} +1 -1
  10. package/dashboard/dist/assets/{IpCheckPage-D0uvbHPe.js → IpCheckPage-Dkp3KLz5.js} +1 -1
  11. package/dashboard/dist/assets/{LandingPage-DGJcVAg7.js → LandingPage-BrXJJj3l.js} +1 -1
  12. package/dashboard/dist/assets/{LeaderboardPage-Dnt_YLsP.js → LeaderboardPage-DvrAcWn2.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-DM7S9_kG.js → LeaderboardProfilePage-KRQhdatN.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-COomwRa6.js → LimitsPage-Ds-pKbQw.js} +2 -2
  15. package/dashboard/dist/assets/{LoginPage-k0k50kws.js → LoginPage-t-VKaIRw.js} +1 -1
  16. package/dashboard/dist/assets/{PopoverPopup-DctOj5-q.js → PopoverPopup-Cyf_sjKX.js} +2 -2
  17. package/dashboard/dist/assets/{ProviderIcon-DGlYzr9I.js → ProviderIcon-Dqb9ThUH.js} +1 -1
  18. package/dashboard/dist/assets/SettingsPage-DhiG9bbD.js +1 -0
  19. package/dashboard/dist/assets/SkillsPage-BdtuTu9f.js +1 -0
  20. package/dashboard/dist/assets/{WidgetsPage-DsMj8Qcz.js → WidgetsPage--n-BUbXK.js} +1 -1
  21. package/dashboard/dist/assets/WrappedPage-DRoff4y0.js +1 -0
  22. package/dashboard/dist/assets/check-BuIsQw1B.js +1 -0
  23. package/dashboard/dist/assets/{chevron-down-kcaroSaH.js → chevron-down-DvuPNrFG.js} +1 -1
  24. package/dashboard/dist/assets/{download-DKMK6oF8.js → download-LwtMKzsl.js} +1 -1
  25. package/dashboard/dist/assets/{leaderboard-columns-BZ06dD2h.js → leaderboard-columns-C01mJBwn.js} +1 -1
  26. package/dashboard/dist/assets/main-A_x5MMU-.css +1 -0
  27. package/dashboard/dist/assets/{main-DKVBnAOd.js → main-Csid8osN.js} +62 -17
  28. package/dashboard/dist/assets/{use-limits-display-prefs-Bx-K-27B.js → use-limits-display-prefs-BP17xuqs.js} +1 -1
  29. package/dashboard/dist/assets/{use-native-settings-DtuifRKC.js → use-native-settings-C-RMuUxI.js} +1 -1
  30. package/dashboard/dist/assets/{use-reduced-motion-Cen-UCKO.js → use-reduced-motion-CPJKbom2.js} +1 -1
  31. package/dashboard/dist/assets/{use-usage-limits-CAWz6ijv.js → use-usage-limits-DIE-GP0H.js} +1 -1
  32. package/dashboard/dist/index.html +2 -2
  33. package/dashboard/dist/share.html +2 -2
  34. package/package.json +3 -2
  35. package/src/cli.js +11 -0
  36. package/src/commands/device-login.js +161 -0
  37. package/src/commands/status.js +199 -1
  38. package/src/commands/sync.js +85 -2
  39. package/src/commands/wrapped.js +150 -0
  40. package/src/lib/local-api.js +37 -2
  41. package/src/lib/passive-mode.js +185 -0
  42. package/src/lib/pricing/seed-snapshot.json +1 -1
  43. package/src/lib/rollout.js +913 -0
  44. package/src/lib/wrapped-aggregator.js +225 -0
  45. package/dashboard/dist/assets/DashboardPage-DsfcNgai.js +0 -1
  46. package/dashboard/dist/assets/SettingsPage-D2sqM9g_.js +0 -1
  47. package/dashboard/dist/assets/SkillsPage-B8K--edc.js +0 -1
  48. package/dashboard/dist/assets/main-DX38hz5f.css +0 -1
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Wrapped aggregator — turns a list of queue.jsonl rows into a year-end
5
+ * summary suitable for the `tracker wrapped` CLI command and the
6
+ * /wrapped dashboard page.
7
+ *
8
+ * Input rows: { source, model, hour_start, input_tokens, cached_input_tokens,
9
+ * cache_creation_input_tokens, output_tokens,
10
+ * reasoning_output_tokens, total_tokens, conversation_count, ... }
11
+ *
12
+ * Output is a plain object (JSON-serializable) — no UI or stringification
13
+ * concerns leak in here so the same aggregator powers the CLI and the
14
+ * React Wrapped page.
15
+ */
16
+
17
+ function isFiniteNumber(n) {
18
+ return typeof n === "number" && Number.isFinite(n);
19
+ }
20
+
21
+ function rowTokens(row) {
22
+ return isFiniteNumber(row.billable_total_tokens)
23
+ ? row.billable_total_tokens
24
+ : isFiniteNumber(row.total_tokens)
25
+ ? row.total_tokens
26
+ : 0;
27
+ }
28
+
29
+ function rowDay(row) {
30
+ // hour_start is ISO 8601 UTC. Slice to YYYY-MM-DD.
31
+ return typeof row.hour_start === "string" ? row.hour_start.slice(0, 10) : null;
32
+ }
33
+
34
+ function rowYear(row) {
35
+ const d = rowDay(row);
36
+ if (!d) return null;
37
+ return Number(d.slice(0, 4));
38
+ }
39
+
40
+ function isoHour(row) {
41
+ // "2026-04-05T14:30:00.000Z" → 14
42
+ if (typeof row.hour_start !== "string") return null;
43
+ const m = /T(\d{2}):/.exec(row.hour_start);
44
+ return m ? Number(m[1]) : null;
45
+ }
46
+
47
+ function topByValue(map, n) {
48
+ return Array.from(map.entries())
49
+ .sort((a, b) => b[1] - a[1])
50
+ .slice(0, n);
51
+ }
52
+
53
+ /**
54
+ * Compute a year-end Wrapped summary from queue rows.
55
+ *
56
+ * @param {Array} rows — Array of queue.jsonl objects.
57
+ * @param {Object} [opts]
58
+ * @param {number} [opts.year] — Restrict to a single calendar year (UTC).
59
+ * Defaults to the most-recent year present.
60
+ * @returns {Object} Wrapped summary.
61
+ */
62
+ function aggregateWrapped(rows, opts = {}) {
63
+ // queue.jsonl is append-only: each sync re-emits the cumulative totals for
64
+ // every touched (source, model, hour_start). Naively summing every row
65
+ // double-counts every previously synced bucket. Keep only the latest entry
66
+ // per tuple — same dedup rule that local-api.js readQueueData enforces for
67
+ // the dashboard. This makes `tracker wrapped` totals match what the
68
+ // /wrapped dashboard page shows.
69
+ const dedup = new Map();
70
+ for (const row of Array.isArray(rows) ? rows : []) {
71
+ if (!row || typeof row !== "object") continue;
72
+ const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
73
+ dedup.set(key, row);
74
+ }
75
+ const all = Array.from(dedup.values());
76
+
77
+ // Decide year to summarize. Prefer caller's year; otherwise the most
78
+ // recent year that has data — which during a sync rollover may be the
79
+ // current year (even if it has only a week of data).
80
+ let year = isFiniteNumber(opts.year) ? Math.floor(opts.year) : null;
81
+ if (year == null) {
82
+ let latest = null;
83
+ for (const r of all) {
84
+ const y = rowYear(r);
85
+ if (y != null && (latest == null || y > latest)) latest = y;
86
+ }
87
+ year = latest ?? new Date().getUTCFullYear();
88
+ }
89
+
90
+ const filtered = all.filter((r) => rowYear(r) === year);
91
+
92
+ if (filtered.length === 0) {
93
+ return {
94
+ year,
95
+ range: { from: null, to: null },
96
+ totals: { tokens: 0, conversations: 0, sources: 0, models: 0, active_days: 0 },
97
+ top: { sources: [], models: [], days: [] },
98
+ peak_hour: null,
99
+ longest_streak: { days: 0, from: null, to: null },
100
+ highlights: [],
101
+ };
102
+ }
103
+
104
+ // Group: source, model, day, hour-of-day
105
+ const tokensBySource = new Map();
106
+ const tokensByModel = new Map();
107
+ const tokensByDay = new Map();
108
+ const tokensByHourOfDay = new Map();
109
+ let totalTokens = 0;
110
+ let totalConvs = 0;
111
+ let earliest = null;
112
+ let latest = null;
113
+
114
+ for (const row of filtered) {
115
+ const t = rowTokens(row);
116
+ totalTokens += t;
117
+ if (isFiniteNumber(row.conversation_count)) totalConvs += row.conversation_count;
118
+ if (row.source) tokensBySource.set(row.source, (tokensBySource.get(row.source) || 0) + t);
119
+ if (row.model) tokensByModel.set(row.model, (tokensByModel.get(row.model) || 0) + t);
120
+ const day = rowDay(row);
121
+ if (day) tokensByDay.set(day, (tokensByDay.get(day) || 0) + t);
122
+ const h = isoHour(row);
123
+ if (h != null) tokensByHourOfDay.set(h, (tokensByHourOfDay.get(h) || 0) + t);
124
+ if (row.hour_start) {
125
+ if (!earliest || row.hour_start < earliest) earliest = row.hour_start;
126
+ if (!latest || row.hour_start > latest) latest = row.hour_start;
127
+ }
128
+ }
129
+
130
+ // Longest consecutive day streak among days with non-zero tokens.
131
+ const activeDays = Array.from(tokensByDay.keys()).sort();
132
+ let longestStreak = { days: 0, from: null, to: null };
133
+ if (activeDays.length > 0) {
134
+ let runStart = activeDays[0];
135
+ let runPrev = activeDays[0];
136
+ let runLen = 1;
137
+ const stepDay = (iso, delta) => {
138
+ const dt = new Date(iso + "T00:00:00Z");
139
+ dt.setUTCDate(dt.getUTCDate() + delta);
140
+ return dt.toISOString().slice(0, 10);
141
+ };
142
+ longestStreak = { days: 1, from: runStart, to: runStart };
143
+ for (let i = 1; i < activeDays.length; i++) {
144
+ const day = activeDays[i];
145
+ if (day === stepDay(runPrev, 1)) {
146
+ runLen++;
147
+ runPrev = day;
148
+ } else {
149
+ runStart = day;
150
+ runPrev = day;
151
+ runLen = 1;
152
+ }
153
+ if (runLen > longestStreak.days) {
154
+ longestStreak = { days: runLen, from: runStart, to: runPrev };
155
+ }
156
+ }
157
+ }
158
+
159
+ // Peak hour-of-day (UTC).
160
+ let peakHour = null;
161
+ if (tokensByHourOfDay.size > 0) {
162
+ const top = topByValue(tokensByHourOfDay, 1)[0];
163
+ peakHour = { hour: top[0], tokens: top[1] };
164
+ }
165
+
166
+ const topSources = topByValue(tokensBySource, 5).map(([source, tokens]) => ({
167
+ source,
168
+ tokens,
169
+ share: totalTokens > 0 ? tokens / totalTokens : 0,
170
+ }));
171
+ const topModels = topByValue(tokensByModel, 5).map(([model, tokens]) => ({
172
+ model,
173
+ tokens,
174
+ share: totalTokens > 0 ? tokens / totalTokens : 0,
175
+ }));
176
+ const topDays = topByValue(tokensByDay, 5).map(([day, tokens]) => ({ day, tokens }));
177
+
178
+ // Highlights — short, share-friendly one-liners derived from the numbers.
179
+ const highlights = [];
180
+ if (topModels.length > 0) {
181
+ const m = topModels[0];
182
+ highlights.push(`${m.model} powered ${(m.share * 100).toFixed(0)}% of your year.`);
183
+ }
184
+ if (topDays.length > 0) {
185
+ const d = topDays[0];
186
+ highlights.push(`Your busiest day was ${d.day} (${formatCompact(d.tokens)} tokens).`);
187
+ }
188
+ if (peakHour) {
189
+ highlights.push(`You hit peak flow around ${String(peakHour.hour).padStart(2, "0")}:00 UTC.`);
190
+ }
191
+ if (longestStreak.days >= 2) {
192
+ highlights.push(`Longest streak: ${longestStreak.days} consecutive days of coding with AI.`);
193
+ }
194
+ if (tokensBySource.size >= 4) {
195
+ highlights.push(`You touched ${tokensBySource.size} different AI tools — that's range.`);
196
+ }
197
+
198
+ return {
199
+ year,
200
+ range: { from: earliest, to: latest },
201
+ totals: {
202
+ tokens: totalTokens,
203
+ conversations: totalConvs,
204
+ sources: tokensBySource.size,
205
+ models: tokensByModel.size,
206
+ active_days: activeDays.length,
207
+ },
208
+ top: { sources: topSources, models: topModels, days: topDays },
209
+ peak_hour: peakHour,
210
+ longest_streak: longestStreak,
211
+ highlights,
212
+ };
213
+ }
214
+
215
+ function formatCompact(n) {
216
+ if (!Number.isFinite(n)) return "0";
217
+ const abs = Math.abs(n);
218
+ if (abs >= 1e12) return (n / 1e12).toFixed(2).replace(/\.?0+$/, "") + "T";
219
+ if (abs >= 1e9) return (n / 1e9).toFixed(2).replace(/\.?0+$/, "") + "B";
220
+ if (abs >= 1e6) return (n / 1e6).toFixed(2).replace(/\.?0+$/, "") + "M";
221
+ if (abs >= 1e3) return (n / 1e3).toFixed(2).replace(/\.?0+$/, "") + "K";
222
+ return String(Math.round(n));
223
+ }
224
+
225
+ module.exports = { aggregateWrapped, formatCompact };