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.
- package/README.md +9 -0
- package/dashboard/dist/assets/{Card-5ebScQbM.js → Card-D_q1XGfK.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-B32RhUB1.js → DashboardPage-CuBSoNgI.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CHjFUAEe.js → FadeIn-ClpHby-T.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CrbKufId.js → IpCheckPage-B5TpHE6G.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-CVKBhGzY.js → LeaderboardPage-BOrtGeIf.js} +1 -1
- package/dashboard/dist/assets/LeaderboardProfilePage-C8xexZ08.js +1 -0
- package/dashboard/dist/assets/{LimitsPage-BwQ4isbM.js → LimitsPage-CERWQyIU.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Y3Znzs1i.js → SettingsPage-DUwt4x-H.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-tNojCBaj.js → WidgetsPage-Cmr2tbD7.js} +1 -1
- package/dashboard/dist/assets/{download-CQASvwEL.js → download-M8e1PJC-.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-DFG1TWe9.js → leaderboard-columns-Dcg9r7R2.js} +1 -1
- package/dashboard/dist/assets/{main-Cv6YpYdZ.js → main-D1VdJk4V.js} +2 -2
- package/dashboard/dist/assets/{use-limits-display-prefs-BUrHqTkA.js → use-limits-display-prefs-CSj55sfK.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-D-gv2i1R.js → use-usage-limits-BuWINUAm.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/status.js +11 -1
- package/src/commands/sync.js +65 -1
- package/src/lib/diagnostics.js +30 -0
- package/src/lib/local-api.js +176 -57
- package/src/lib/rollout.js +667 -4
- package/dashboard/dist/assets/LeaderboardProfilePage-CbpzxvLA.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -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
|
|
128
|
+
function readProjectQueueData(projectQueuePath) {
|
|
129
|
+
let raw;
|
|
120
130
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
697
|
-
const todayStr =
|
|
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
|
|
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
|
|
723
|
-
|
|
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:
|
|
732
|
-
last_30d: { from:
|
|
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
|
|
754
|
-
const
|
|
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
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
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
|
-
|
|
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))
|
|
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.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
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 = {
|
|
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
|
+
};
|