tokentracker-cli 0.5.70 → 0.5.72
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 +3 -2
- package/dashboard/dist/assets/{Card-Bmd_CiIj.js → Card-D_q1XGfK.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-o3xjHuSr.js → DashboardPage-CuBSoNgI.js} +2 -2
- package/dashboard/dist/assets/{FadeIn-Dqezp4M6.js → FadeIn-ClpHby-T.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Dvo1fWrV.js → IpCheckPage-B5TpHE6G.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-sarzdedp.js → LeaderboardPage-BOrtGeIf.js} +2 -2
- package/dashboard/dist/assets/LeaderboardProfilePage-C8xexZ08.js +1 -0
- package/dashboard/dist/assets/{LimitsPage-BYVQqh0y.js → LimitsPage-CERWQyIU.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-C0bLU9fQ.js → SettingsPage-DUwt4x-H.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DH1pg04g.js → WidgetsPage-Cmr2tbD7.js} +1 -1
- package/dashboard/dist/assets/{download-Dqdzgt-u.js → download-M8e1PJC-.js} +1 -1
- package/dashboard/dist/assets/leaderboard-columns-Dcg9r7R2.js +1 -0
- package/dashboard/dist/assets/{main-Vhl9rhYB.js → main-D1VdJk4V.js} +16 -15
- package/dashboard/dist/assets/{use-limits-display-prefs-CJuYd6SN.js → use-limits-display-prefs-CSj55sfK.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BEbR2ySJ.js → use-usage-limits-BuWINUAm.js} +1 -1
- package/dashboard/dist/brand-logos/kimi.svg +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +11 -0
- package/src/commands/status.js +9 -0
- package/src/commands/sync.js +52 -1
- package/src/lib/local-api.js +159 -56
- package/src/lib/rollout.js +217 -4
- package/dashboard/dist/assets/LeaderboardProfilePage-DsoEom4U.js +0 -1
- package/dashboard/dist/assets/leaderboard-columns-eDzDr_Xs.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a}from"./main-
|
|
1
|
+
import{r as a}from"./main-D1VdJk4V.js";const d=["claude","codex","cursor","gemini","kiro","copilot","antigravity"],v={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},k={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function y(){if(typeof window>"u")return[...d];try{const r=window.localStorage.getItem(l);if(!r)return[...d];const s=JSON.parse(r);if(!Array.isArray(s))return[...d];const n=s.filter(c=>d.includes(c));for(const c of d)n.includes(c)||n.push(c);return n}catch{return[...d]}}function m(){const r=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return r;try{const s=window.localStorage.getItem(g);if(!s)return r;const n=JSON.parse(s);if(!n||typeof n!="object")return r;const c={...r};for(const u of d)typeof n[u]=="boolean"&&(c[u]=n[u]);return c}catch{return r}}function C(){const[r,s]=a.useState(y),[n,c]=a.useState(m);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(n))}catch{}},[n]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(y()),t.key===g&&c(m())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const i=[...t];return[i[e-1],i[e]]=[i[e],i[e-1]],i})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const i=[...t];return[i[e],i[e+1]]=[i[e+1],i[e]],i})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const i=e.indexOf(o),w=e.indexOf(t);if(i<0||w<0)return e;const f=[...e],[E]=f.splice(i,1);return f.splice(w,0,E),f})},[]),x=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),I=a.useMemo(()=>r.filter(o=>n[o]!==!1),[r,n]);return{order:r,visibility:n,visibleOrdered:I,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:x}}export{k as L,v as a,C as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r,aM as l}from"./main-
|
|
1
|
+
import{r,aM as l}from"./main-D1VdJk4V.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"></path><path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"></path></svg>
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
]
|
|
136
136
|
}
|
|
137
137
|
</script>
|
|
138
|
-
<script type="module" crossorigin src="/assets/main-
|
|
138
|
+
<script type="module" crossorigin src="/assets/main-D1VdJk4V.js"></script>
|
|
139
139
|
<link rel="stylesheet" crossorigin href="/assets/main-CNzfq4Ln.css">
|
|
140
140
|
</head>
|
|
141
141
|
<body>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"description": "Shareable Token Tracker dashboard snapshot."
|
|
52
52
|
}
|
|
53
53
|
</script>
|
|
54
|
-
<script type="module" crossorigin src="/assets/main-
|
|
54
|
+
<script type="module" crossorigin src="/assets/main-D1VdJk4V.js"></script>
|
|
55
55
|
<link rel="stylesheet" crossorigin href="/assets/main-CNzfq4Ln.css">
|
|
56
56
|
</head>
|
|
57
57
|
<body>
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -404,6 +404,17 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
404
404
|
summary.push({ label: "Cursor", status: "skipped", detail: "Not installed" });
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// Kimi: passive reader — no hook installation needed.
|
|
408
|
+
// TokenTracker reads ~/.kimi/sessions/**/wire.jsonl directly.
|
|
409
|
+
{
|
|
410
|
+
const kimiHome = process.env.KIMI_HOME || path.join(home, ".kimi");
|
|
411
|
+
const kimiSessions = path.join(kimiHome, "sessions");
|
|
412
|
+
const fssync = require("node:fs");
|
|
413
|
+
if (fssync.existsSync(kimiSessions)) {
|
|
414
|
+
summary.push({ label: "Kimi Code", status: "detected", detail: "Passive reader (no hook needed)" });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
407
418
|
const openclawBefore = await probeOpenclawSessionPluginState({
|
|
408
419
|
home,
|
|
409
420
|
trackerDir,
|
package/src/commands/status.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const os = require("node:os");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
|
+
const fssync = require("node:fs");
|
|
4
5
|
|
|
5
6
|
const { readJson } = require("../lib/fs");
|
|
6
7
|
const { readCodexNotify, readEveryCodeNotify } = require("../lib/codex-config");
|
|
@@ -19,6 +20,7 @@ const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
|
19
20
|
const { probeOpenclawHookState } = require("../lib/openclaw-hook");
|
|
20
21
|
const { probeOpenclawSessionPluginState } = require("../lib/openclaw-session-plugin");
|
|
21
22
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
23
|
+
const { resolveKimiWireFiles } = require("../lib/rollout");
|
|
22
24
|
|
|
23
25
|
async function cmdStatus(argv = []) {
|
|
24
26
|
const opts = parseArgs(argv);
|
|
@@ -112,6 +114,10 @@ async function cmdStatus(argv = []) {
|
|
|
112
114
|
const subscriptionLines =
|
|
113
115
|
subscriptions.length > 0 ? subscriptions.map(formatSubscriptionLine) : [];
|
|
114
116
|
|
|
117
|
+
const kimiWireFiles = resolveKimiWireFiles(process.env);
|
|
118
|
+
const kimiHome = process.env.KIMI_HOME || path.join(home, ".kimi");
|
|
119
|
+
const kimiInstalled = fssync.existsSync(path.join(kimiHome, "sessions"));
|
|
120
|
+
|
|
115
121
|
const copilotToken = readCopilotOauthToken({ home });
|
|
116
122
|
const copilotOtel = describeCopilotOtelStatus({ home, env: process.env });
|
|
117
123
|
const copilotLines = formatCopilotLines({ token: copilotToken, otel: copilotOtel });
|
|
@@ -138,6 +144,9 @@ async function cmdStatus(argv = []) {
|
|
|
138
144
|
`- Opencode plugin: ${opencodePluginConfigured ? "set" : "unset"}`,
|
|
139
145
|
`- OpenClaw session plugin: ${openclawSessionPluginState?.configured ? "set" : "unset"}`,
|
|
140
146
|
`- OpenClaw hook (legacy): ${openclawHookState?.configured ? "set" : "unset"}`,
|
|
147
|
+
kimiInstalled
|
|
148
|
+
? `- Kimi Code: passive reader (${kimiWireFiles.length} wire.jsonl file${kimiWireFiles.length !== 1 ? "s" : ""} found)`
|
|
149
|
+
: null,
|
|
141
150
|
...copilotLines,
|
|
142
151
|
...subscriptionLines,
|
|
143
152
|
"",
|
package/src/commands/sync.js
CHANGED
|
@@ -25,11 +25,16 @@ const {
|
|
|
25
25
|
parseKiroIncremental,
|
|
26
26
|
parseHermesIncremental,
|
|
27
27
|
parseCopilotIncremental,
|
|
28
|
+
resolveKimiWireFiles,
|
|
29
|
+
parseKimiIncremental,
|
|
28
30
|
} = require("../lib/rollout");
|
|
29
31
|
const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
|
|
30
32
|
const {
|
|
31
33
|
normalizeState: normalizeUploadState,
|
|
32
34
|
decideAutoUpload,
|
|
35
|
+
recordUploadFailure,
|
|
36
|
+
recordUploadSuccess,
|
|
37
|
+
parseRetryAfterMs,
|
|
33
38
|
} = require("../lib/upload-throttle");
|
|
34
39
|
const {
|
|
35
40
|
isCursorInstalled,
|
|
@@ -354,6 +359,28 @@ async function cmdSync(argv) {
|
|
|
354
359
|
});
|
|
355
360
|
}
|
|
356
361
|
|
|
362
|
+
// ── Kimi (passive wire.jsonl reader) ──
|
|
363
|
+
let kimiResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
364
|
+
const kimiWireFiles = resolveKimiWireFiles(process.env);
|
|
365
|
+
if (kimiWireFiles.length > 0) {
|
|
366
|
+
if (progress?.enabled) {
|
|
367
|
+
progress.start(`Parsing Kimi Code ${renderBar(0)} | buckets 0`);
|
|
368
|
+
}
|
|
369
|
+
kimiResult = await parseKimiIncremental({
|
|
370
|
+
wireFiles: kimiWireFiles,
|
|
371
|
+
cursors,
|
|
372
|
+
queuePath,
|
|
373
|
+
env: process.env,
|
|
374
|
+
onProgress: (p) => {
|
|
375
|
+
if (!progress?.enabled) return;
|
|
376
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
377
|
+
progress.update(
|
|
378
|
+
`Parsing Kimi Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
357
384
|
// ── GitHub Copilot CLI (OTEL JSONL files) ──
|
|
358
385
|
let copilotResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
359
386
|
const copilotPaths = resolveCopilotOtelPaths(process.env);
|
|
@@ -412,7 +439,24 @@ async function cmdSync(argv) {
|
|
|
412
439
|
maxBatches: opts.drain ? 100 : 5,
|
|
413
440
|
batchSize: 200,
|
|
414
441
|
});
|
|
442
|
+
// Record success so the exponential backoff step resets — otherwise
|
|
443
|
+
// a single past failure keeps us pessimistically throttled forever.
|
|
444
|
+
uploadThrottleState = recordUploadSuccess({
|
|
445
|
+
nowMs: Date.now(),
|
|
446
|
+
state: uploadThrottleState,
|
|
447
|
+
});
|
|
448
|
+
await writeJson(uploadThrottlePath, uploadThrottleState);
|
|
415
449
|
} catch (e) {
|
|
450
|
+
// Persist a backoff on 429 / 5xx so the next auto-sync waits instead
|
|
451
|
+
// of retrying immediately and making the rate-limit worse. The
|
|
452
|
+
// throttle module already parses Retry-After when we surface it on
|
|
453
|
+
// the error object (drainQueueToCloud stamps err.status + err.retryAfterMs).
|
|
454
|
+
uploadThrottleState = recordUploadFailure({
|
|
455
|
+
nowMs: Date.now(),
|
|
456
|
+
state: uploadThrottleState,
|
|
457
|
+
error: e,
|
|
458
|
+
});
|
|
459
|
+
await writeJson(uploadThrottlePath, uploadThrottleState);
|
|
416
460
|
if (!opts.auto) {
|
|
417
461
|
process.stderr.write(`Upload error: ${e?.message || e}\n`);
|
|
418
462
|
}
|
|
@@ -453,6 +497,7 @@ async function cmdSync(argv) {
|
|
|
453
497
|
cursorResult.recordsProcessed +
|
|
454
498
|
kiroResult.recordsProcessed +
|
|
455
499
|
hermesResult.recordsProcessed +
|
|
500
|
+
kimiResult.recordsProcessed +
|
|
456
501
|
copilotResult.recordsProcessed;
|
|
457
502
|
const totalBuckets =
|
|
458
503
|
parseResult.bucketsQueued +
|
|
@@ -463,6 +508,7 @@ async function cmdSync(argv) {
|
|
|
463
508
|
cursorResult.bucketsQueued +
|
|
464
509
|
kiroResult.bucketsQueued +
|
|
465
510
|
hermesResult.bucketsQueued +
|
|
511
|
+
kimiResult.bucketsQueued +
|
|
466
512
|
copilotResult.bucketsQueued;
|
|
467
513
|
process.stdout.write(
|
|
468
514
|
[
|
|
@@ -822,7 +868,12 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
|
|
|
822
868
|
let data = {};
|
|
823
869
|
try { data = JSON.parse(rawText); } catch { data = {}; }
|
|
824
870
|
if (!res.ok) {
|
|
825
|
-
|
|
871
|
+
const err = new Error(`HTTP ${res.status}: ${rawText.substring(0, 500)}`);
|
|
872
|
+
err.status = res.status;
|
|
873
|
+
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
874
|
+
const retryAfterMs = parseRetryAfterMs(retryAfter);
|
|
875
|
+
if (retryAfterMs !== null) err.retryAfterMs = retryAfterMs;
|
|
876
|
+
throw err;
|
|
826
877
|
}
|
|
827
878
|
|
|
828
879
|
inserted += Number(data?.inserted || 0);
|
package/src/lib/local-api.js
CHANGED
|
@@ -116,30 +116,90 @@ function resolveQueuePath() {
|
|
|
116
116
|
return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
function
|
|
119
|
+
function readProjectQueueData(projectQueuePath) {
|
|
120
|
+
let raw;
|
|
120
121
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
raw = fs.readFileSync(projectQueuePath, "utf8");
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (e?.code !== "ENOENT") {
|
|
125
|
+
console.error("[LocalAPI] readProjectQueueData: failed to read:", e?.message || e);
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
130
|
+
const seen = new Map();
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
try {
|
|
133
|
+
const row = JSON.parse(line);
|
|
134
|
+
const key = `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`;
|
|
129
135
|
seen.set(key, row);
|
|
136
|
+
} catch {
|
|
137
|
+
// skip malformed
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return Array.from(seen.values());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readQueueData(queuePath) {
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(queuePath, "utf8");
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// ENOENT is legitimate (never synced yet); anything else is a signal we
|
|
149
|
+
// don't want to hide behind an empty array forever — the dashboard would
|
|
150
|
+
// otherwise render "0 tokens" with no clue the queue was unreadable.
|
|
151
|
+
if (e?.code !== "ENOENT") {
|
|
152
|
+
console.error("[LocalAPI] readQueueData: failed to read queue:", e?.message || e);
|
|
130
153
|
}
|
|
131
|
-
return Array.from(seen.values());
|
|
132
|
-
} catch (_e) {
|
|
133
154
|
return [];
|
|
134
155
|
}
|
|
156
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
157
|
+
// Parse row-by-row so a single corrupted line (partial write, disk-full
|
|
158
|
+
// truncation, …) does not wipe out every other row with it.
|
|
159
|
+
const parsed = [];
|
|
160
|
+
let malformed = 0;
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
try {
|
|
163
|
+
parsed.push(JSON.parse(line));
|
|
164
|
+
} catch {
|
|
165
|
+
malformed += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (malformed > 0) {
|
|
169
|
+
console.error(
|
|
170
|
+
`[LocalAPI] readQueueData: skipped ${malformed}/${lines.length} malformed line(s) in ${queuePath}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
// Deduplicate: each sync appends cumulative totals per bucket, so for
|
|
174
|
+
// each (source, model, hour_start) keep only the latest (last) entry.
|
|
175
|
+
const seen = new Map();
|
|
176
|
+
for (const row of parsed) {
|
|
177
|
+
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
178
|
+
seen.set(key, row);
|
|
179
|
+
}
|
|
180
|
+
return Array.from(seen.values());
|
|
135
181
|
}
|
|
136
182
|
|
|
137
|
-
function
|
|
183
|
+
function rowDayKey(row, timeZoneContext) {
|
|
184
|
+
const hs = row.hour_start;
|
|
185
|
+
if (!hs) return "";
|
|
186
|
+
if (
|
|
187
|
+
timeZoneContext &&
|
|
188
|
+
(timeZoneContext.timeZone || Number.isFinite(timeZoneContext.offsetMinutes))
|
|
189
|
+
) {
|
|
190
|
+
const parts = getZonedParts(new Date(hs), timeZoneContext);
|
|
191
|
+
const key = formatPartsDayKey(parts);
|
|
192
|
+
if (key) return key;
|
|
193
|
+
}
|
|
194
|
+
return hs.slice(0, 10);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function aggregateByDay(rows, timeZoneContext = null) {
|
|
138
198
|
const byDay = new Map();
|
|
139
199
|
for (const row of rows) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
200
|
+
if (!row.hour_start) continue;
|
|
201
|
+
const day = rowDayKey(row, timeZoneContext);
|
|
202
|
+
if (!day) continue;
|
|
143
203
|
if (!byDay.has(day)) {
|
|
144
204
|
byDay.set(day, {
|
|
145
205
|
day,
|
|
@@ -674,8 +734,9 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
674
734
|
if (p === "/functions/tokentracker-usage-summary") {
|
|
675
735
|
const from = url.searchParams.get("from") || "";
|
|
676
736
|
const to = url.searchParams.get("to") || "";
|
|
737
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
677
738
|
const rows = readQueueData(qp);
|
|
678
|
-
const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
|
|
739
|
+
const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
|
|
679
740
|
const totals = daily.reduce(
|
|
680
741
|
(acc, r) => {
|
|
681
742
|
acc.total_tokens += r.total_tokens;
|
|
@@ -693,16 +754,19 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
693
754
|
);
|
|
694
755
|
const totalCost = totals.total_cost_usd;
|
|
695
756
|
|
|
696
|
-
const
|
|
697
|
-
const todayStr =
|
|
698
|
-
const allDaily = aggregateByDay(rows);
|
|
757
|
+
const todayParts = getZonedParts(new Date(), timeZoneContext);
|
|
758
|
+
const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
|
|
759
|
+
const allDaily = aggregateByDay(rows, timeZoneContext);
|
|
699
760
|
|
|
761
|
+
const shiftDay = (dayStr, delta) => {
|
|
762
|
+
const d = new Date(`${dayStr}T00:00:00Z`);
|
|
763
|
+
d.setUTCDate(d.getUTCDate() + delta);
|
|
764
|
+
return d.toISOString().slice(0, 10);
|
|
765
|
+
};
|
|
700
766
|
const collectDays = (n) => {
|
|
701
767
|
const out = [];
|
|
702
768
|
for (let i = n - 1; i >= 0; i--) {
|
|
703
|
-
const
|
|
704
|
-
d.setUTCDate(d.getUTCDate() - i);
|
|
705
|
-
const ds = d.toISOString().slice(0, 10);
|
|
769
|
+
const ds = shiftDay(todayStr, -i);
|
|
706
770
|
const dd = allDaily.find((x) => x.day === ds);
|
|
707
771
|
if (dd) out.push(dd);
|
|
708
772
|
}
|
|
@@ -719,17 +783,15 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
719
783
|
const l30 = collectDays(30);
|
|
720
784
|
const l7t = sumDays(l7);
|
|
721
785
|
const l30t = sumDays(l30);
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
const l30from = new Date(today);
|
|
725
|
-
l30from.setUTCDate(l30from.getUTCDate() - 29);
|
|
786
|
+
const l7fromStr = shiftDay(todayStr, -6);
|
|
787
|
+
const l30fromStr = shiftDay(todayStr, -29);
|
|
726
788
|
|
|
727
789
|
json(res, {
|
|
728
790
|
from, to, days: daily.length,
|
|
729
791
|
totals: { ...totals, total_cost_usd: totalCost.toFixed(6) },
|
|
730
792
|
rolling: {
|
|
731
|
-
last_7d: { from:
|
|
732
|
-
last_30d: { from:
|
|
793
|
+
last_7d: { from: l7fromStr, to: todayStr, active_days: l7.length, totals: l7t },
|
|
794
|
+
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
795
|
},
|
|
734
796
|
});
|
|
735
797
|
return true;
|
|
@@ -739,8 +801,9 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
739
801
|
if (p === "/functions/tokentracker-usage-daily") {
|
|
740
802
|
const from = url.searchParams.get("from") || "";
|
|
741
803
|
const to = url.searchParams.get("to") || "";
|
|
804
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
742
805
|
const rows = readQueueData(qp);
|
|
743
|
-
const daily = aggregateByDay(rows).filter((d) => d.day >= from && d.day <= to);
|
|
806
|
+
const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
|
|
744
807
|
json(res, { from, to, data: daily });
|
|
745
808
|
return true;
|
|
746
809
|
}
|
|
@@ -748,10 +811,12 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
748
811
|
// --- usage-heatmap ---
|
|
749
812
|
if (p === "/functions/tokentracker-usage-heatmap") {
|
|
750
813
|
const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
|
|
814
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
751
815
|
const rows = readQueueData(qp);
|
|
752
|
-
const daily = aggregateByDay(rows);
|
|
753
|
-
const
|
|
754
|
-
const
|
|
816
|
+
const daily = aggregateByDay(rows, timeZoneContext);
|
|
817
|
+
const todayParts = getZonedParts(new Date(), timeZoneContext);
|
|
818
|
+
const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
|
|
819
|
+
const end = new Date(`${todayStr}T00:00:00Z`);
|
|
755
820
|
const start = new Date(end);
|
|
756
821
|
start.setUTCDate(start.getUTCDate() - weeks * 7 + 1);
|
|
757
822
|
const from = start.toISOString().slice(0, 10);
|
|
@@ -792,9 +857,10 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
792
857
|
if (p === "/functions/tokentracker-usage-model-breakdown") {
|
|
793
858
|
const from = url.searchParams.get("from") || "";
|
|
794
859
|
const to = url.searchParams.get("to") || "";
|
|
860
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
795
861
|
const rows = readQueueData(qp).filter((r) => {
|
|
796
862
|
if (!r.hour_start) return false;
|
|
797
|
-
const d = r
|
|
863
|
+
const d = rowDayKey(r, timeZoneContext);
|
|
798
864
|
return d >= from && d <= to;
|
|
799
865
|
});
|
|
800
866
|
|
|
@@ -852,35 +918,71 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
852
918
|
|
|
853
919
|
// --- project-usage-summary ---
|
|
854
920
|
if (p === "/functions/tokentracker-project-usage-summary") {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
921
|
+
// Use the per-project bucket log that rollout.js emits — it already
|
|
922
|
+
// carries the actual tokens attributed to each (project_key, source,
|
|
923
|
+
// hour_start). Falling back to "session-file count × total tokens"
|
|
924
|
+
// (the old behavior) produced pure fiction: every short-and-hot
|
|
925
|
+
// project got the same weight as every long-and-cold one.
|
|
926
|
+
const projectQueuePath = path.join(
|
|
927
|
+
path.dirname(qp),
|
|
928
|
+
"project.queue.jsonl",
|
|
929
|
+
);
|
|
930
|
+
const projectRows = readProjectQueueData(projectQueuePath);
|
|
931
|
+
|
|
932
|
+
const byProject = new Map();
|
|
933
|
+
for (const row of projectRows) {
|
|
934
|
+
const key = row.project_key || "unknown";
|
|
935
|
+
if (!byProject.has(key)) {
|
|
936
|
+
byProject.set(key, {
|
|
937
|
+
project_key: key,
|
|
938
|
+
project_ref: row.project_ref || key,
|
|
939
|
+
total_tokens: 0,
|
|
940
|
+
billable_total_tokens: 0,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const agg = byProject.get(key);
|
|
944
|
+
agg.total_tokens += Number(row.total_tokens || 0);
|
|
945
|
+
agg.billable_total_tokens += Number(row.total_tokens || 0);
|
|
946
|
+
if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
|
|
947
|
+
}
|
|
862
948
|
|
|
863
|
-
|
|
949
|
+
// If no project-attributed rows exist yet (user hasn't synced project
|
|
950
|
+
// attribution, or never used a project-capable CLI), fall back to
|
|
951
|
+
// per-source aggregation over the main queue so the panel isn't
|
|
952
|
+
// totally empty. This path used to also exist for the non-empty case
|
|
953
|
+
// and produce wrong numbers; keep it only as the empty fallback.
|
|
954
|
+
let entries;
|
|
955
|
+
if (byProject.size === 0) {
|
|
956
|
+
const rows = readQueueData(qp);
|
|
864
957
|
const bySrc = new Map();
|
|
865
958
|
for (const row of rows) {
|
|
866
959
|
const src = row.source || "unknown";
|
|
867
|
-
if (!bySrc.has(src))
|
|
960
|
+
if (!bySrc.has(src)) {
|
|
961
|
+
bySrc.set(src, {
|
|
962
|
+
project_key: src,
|
|
963
|
+
project_ref: `https://${src}.ai`,
|
|
964
|
+
total_tokens: 0,
|
|
965
|
+
billable_total_tokens: 0,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
868
968
|
bySrc.get(src).total_tokens += row.total_tokens || 0;
|
|
869
969
|
bySrc.get(src).billable_total_tokens += row.total_tokens || 0;
|
|
870
970
|
}
|
|
871
|
-
entries.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
971
|
+
entries = Array.from(bySrc.values())
|
|
972
|
+
.sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
|
|
973
|
+
.map((e) => ({
|
|
974
|
+
...e,
|
|
975
|
+
total_tokens: String(e.total_tokens),
|
|
976
|
+
billable_total_tokens: String(e.billable_total_tokens),
|
|
977
|
+
}));
|
|
876
978
|
} else {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
979
|
+
entries = Array.from(byProject.values())
|
|
980
|
+
.sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
|
|
981
|
+
.map((e) => ({
|
|
982
|
+
...e,
|
|
983
|
+
total_tokens: String(e.total_tokens),
|
|
984
|
+
billable_total_tokens: String(e.billable_total_tokens),
|
|
985
|
+
}));
|
|
884
986
|
}
|
|
885
987
|
|
|
886
988
|
json(res, { generated_at: new Date().toISOString(), entries });
|
|
@@ -911,12 +1013,13 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
911
1013
|
if (p === "/functions/tokentracker-usage-monthly") {
|
|
912
1014
|
const from = url.searchParams.get("from") || "";
|
|
913
1015
|
const to = url.searchParams.get("to") || "";
|
|
1016
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
914
1017
|
const rows = readQueueData(qp);
|
|
915
1018
|
const byMonth = new Map();
|
|
916
1019
|
for (const row of rows) {
|
|
917
1020
|
if (!row.hour_start) continue;
|
|
918
|
-
const day = row
|
|
919
|
-
if (day < from || day > to) continue;
|
|
1021
|
+
const day = rowDayKey(row, timeZoneContext);
|
|
1022
|
+
if (!day || day < from || day > to) continue;
|
|
920
1023
|
const month = day.slice(0, 7);
|
|
921
1024
|
if (!byMonth.has(month))
|
|
922
1025
|
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 });
|