tokentracker-cli 0.21.2 → 0.22.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.
- package/README.ja.md +457 -0
- package/README.ko.md +457 -0
- package/README.md +45 -6
- package/README.zh-CN.md +45 -6
- package/dashboard/dist/assets/{Card-BlTjrLNe.js → Card-CD18G4Ge.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DKY_Mi9v.js +64 -0
- package/dashboard/dist/assets/DevicePage-BkavlAal.js +1 -0
- package/dashboard/dist/assets/{FadeIn-BPRZGKdg.js → FadeIn-CVNJ4aZy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DUExMcbl.js → HeaderGithubStar-COu1Xy3I.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-PLJuh2m5.js → IpCheckPage-B2HjZ3vY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-B85OvE31.js → LandingPage-9PSLFnys.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-dz85pWmv.js → LeaderboardPage-CQT5dBHU.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-BVntzReT.js → LeaderboardProfilePage-aPP-Raey.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BSmYsOGT.js → LimitsPage-eFrAHmoA.js} +2 -2
- package/dashboard/dist/assets/{LoginPage-YxDKzTXr.js → LoginPage-CczTNZ_P.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-C_Cq5Cd8.js → PopoverPopup-CEvWSWgZ.js} +2 -2
- package/dashboard/dist/assets/{ProviderIcon-rOxGmW9Z.js → ProviderIcon-ewev19y3.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-taBxq6ux.js +1 -0
- package/dashboard/dist/assets/SkillsPage-cqHO3rMB.js +1 -0
- package/dashboard/dist/assets/{WidgetsPage-CHnlcaHs.js → WidgetsPage-B53b1hwG.js} +1 -1
- package/dashboard/dist/assets/WrappedPage-DwAhprTa.js +1 -0
- package/dashboard/dist/assets/check-JnFJsHgI.js +1 -0
- package/dashboard/dist/assets/{chevron-down-CrDKy3YX.js → chevron-down-zOKEzHdv.js} +1 -1
- package/dashboard/dist/assets/{download-oH8QYt7L.js → download-BZZ4vKc1.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-B3psEJVP.js → leaderboard-columns-BNGlMUsD.js} +1 -1
- package/dashboard/dist/assets/main-A_x5MMU-.css +1 -0
- package/dashboard/dist/assets/{main-DFkO2vMJ.js → main-D0Irg9xR.js} +62 -17
- package/dashboard/dist/assets/{use-limits-display-prefs-DyeDzQ-s.js → use-limits-display-prefs-BTuSZo27.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CxdbRzEd.js → use-native-settings-BPXVZrWe.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-BPcu3IT5.js → use-reduced-motion-HUOV_JD1.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CmEZ5jjP.js → use-usage-limits-G6-vCBcN.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +3 -2
- package/src/cli.js +11 -0
- package/src/commands/device-login.js +161 -0
- package/src/commands/status.js +199 -1
- package/src/commands/sync.js +85 -2
- package/src/commands/wrapped.js +150 -0
- package/src/lib/local-api.js +37 -2
- package/src/lib/passive-mode.js +185 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +913 -0
- package/src/lib/wrapped-aggregator.js +225 -0
- package/dashboard/dist/assets/DashboardPage-Dn3eiHhn.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DzaUSufR.js +0 -1
- package/dashboard/dist/assets/SkillsPage-BoKJH6Il.js +0 -1
- 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 };
|