tokentracker-cli 0.5.79 → 0.5.81

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 (22) hide show
  1. package/dashboard/dist/assets/{Card-Cv4eTIKD.js → Card-CvRCwmts.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-BLFOnvMn.js → DashboardPage-Q3m4isBc.js} +1 -1
  3. package/dashboard/dist/assets/{FadeIn-CIeY4GXM.js → FadeIn-7Sp96sG1.js} +1 -1
  4. package/dashboard/dist/assets/{IpCheckPage-IsYc44dj.js → IpCheckPage-CU5zi7km.js} +1 -1
  5. package/dashboard/dist/assets/{LeaderboardPage-Su4flsQc.js → LeaderboardPage-C9g0lTkN.js} +1 -1
  6. package/dashboard/dist/assets/{LeaderboardProfilePage-bt2zrvtb.js → LeaderboardProfilePage-U3Oww62u.js} +1 -1
  7. package/dashboard/dist/assets/{LimitsPage-CojL1PZS.js → LimitsPage-DW-j3hsV.js} +1 -1
  8. package/dashboard/dist/assets/{ProviderIcon-C2Qp69XI.js → ProviderIcon-BJl3TO7B.js} +1 -1
  9. package/dashboard/dist/assets/{SettingsPage-FSVM_ozY.js → SettingsPage-DnSupHC-.js} +1 -1
  10. package/dashboard/dist/assets/{WidgetsPage-CeLvw5tR.js → WidgetsPage-DwSoxQql.js} +1 -1
  11. package/dashboard/dist/assets/{download-BK4EqMpL.js → download-DtuvJNeF.js} +1 -1
  12. package/dashboard/dist/assets/{leaderboard-columns-CxdAz5_V.js → leaderboard-columns-B1_Q_WZh.js} +1 -1
  13. package/dashboard/dist/assets/{main-CPsqG3PW.js → main-MimZZsgW.js} +188 -188
  14. package/dashboard/dist/assets/{use-limits-display-prefs-C-Y8vFA9.js → use-limits-display-prefs-CPfPbjXC.js} +1 -1
  15. package/dashboard/dist/assets/{use-usage-limits-CiHD5lbg.js → use-usage-limits-BvCjl0IL.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/serve.js +14 -4
  20. package/src/lib/local-api.js +146 -27
  21. package/src/lib/rollout.js +19 -20
  22. package/src/lib/static-server.js +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-CPsqG3PW.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
+ import{r as a}from"./main-MimZZsgW.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-CPsqG3PW.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};
1
+ import{r,aM as l}from"./main-MimZZsgW.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};
@@ -135,7 +135,7 @@
135
135
  ]
136
136
  }
137
137
  </script>
138
- <script type="module" crossorigin src="/assets/main-CPsqG3PW.js"></script>
138
+ <script type="module" crossorigin src="/assets/main-MimZZsgW.js"></script>
139
139
  <link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.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-CPsqG3PW.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-MimZZsgW.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.css">
56
56
  </head>
57
57
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.5.79",
3
+ "version": "0.5.81",
4
4
  "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw, Hermes, GitHub Copilot)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -11,11 +11,16 @@ const { openInBrowser } = require("../lib/browser-auth");
11
11
 
12
12
  const DEFAULT_PORT = 7680;
13
13
  const NPM_PACKAGE_NAME = "tokentracker-cli";
14
+ const LOCAL_BIND_HOST = "127.0.0.1";
14
15
 
15
16
  function buildPortInUseHint(port) {
16
17
  return `Port ${port} is still in use after cleanup. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
17
18
  }
18
19
 
20
+ function getLocalServerUrl(port) {
21
+ return `http://${LOCAL_BIND_HOST}:${port}`;
22
+ }
23
+
19
24
  async function cmdServe(argv) {
20
25
  const opts = parseArgs(argv);
21
26
 
@@ -85,7 +90,6 @@ async function cmdServe(argv) {
85
90
  // CORS preflight
86
91
  if (req.method === "OPTIONS") {
87
92
  res.writeHead(204, {
88
- "Access-Control-Allow-Origin": "*",
89
93
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
90
94
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
91
95
  });
@@ -116,8 +120,8 @@ async function cmdServe(argv) {
116
120
  // 4. Listen (kill stale process on same port if needed)
117
121
  const port = opts.port;
118
122
  await ensurePortFree(port);
119
- server.listen(port, () => {
120
- const url = `http://localhost:${port}`;
123
+ server.listen(port, LOCAL_BIND_HOST, () => {
124
+ const url = getLocalServerUrl(port);
121
125
  process.stdout.write(
122
126
  [
123
127
  "",
@@ -226,4 +230,10 @@ function parseArgs(argv) {
226
230
  return opts;
227
231
  }
228
232
 
229
- module.exports = { cmdServe, buildPortInUseHint, NPM_PACKAGE_NAME };
233
+ module.exports = {
234
+ cmdServe,
235
+ buildPortInUseHint,
236
+ NPM_PACKAGE_NAME,
237
+ LOCAL_BIND_HOST,
238
+ getLocalServerUrl,
239
+ };
@@ -2,6 +2,8 @@ const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
4
  const { spawn } = require("node:child_process");
5
+ const crypto = require("node:crypto");
6
+ const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
5
7
 
6
8
  const SYNC_TIMEOUT_MS = 120_000;
7
9
  const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
@@ -106,12 +108,22 @@ function getModelPricing(model) {
106
108
 
107
109
  function computeRowCost(row) {
108
110
  const pricing = getModelPricing(row.model);
111
+ // For OpenAI/Codex-family rollouts, `output_tokens` already includes any
112
+ // reasoning tokens (the OpenAI API's `completion_tokens` is inclusive),
113
+ // so adding a separate `reasoning_output_tokens * output_rate` term
114
+ // double-charges that slice. ccusage models this the same way. For other
115
+ // sources we keep the explicit reasoning term because `reasoning` is not
116
+ // guaranteed to be folded into `output_tokens`.
117
+ const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
118
+ const reasoningCost = reasoningIncludedInOutput
119
+ ? 0
120
+ : (row.reasoning_output_tokens || 0) * (pricing.output || 0);
109
121
  return (
110
122
  ((row.input_tokens || 0) * (pricing.input || 0) +
111
123
  (row.output_tokens || 0) * (pricing.output || 0) +
112
124
  (row.cached_input_tokens || 0) * (pricing.cache_read || 0) +
113
125
  (row.cache_creation_input_tokens || 0) * (pricing.cache_write || 0) +
114
- (row.reasoning_output_tokens || 0) * (pricing.output || 0)) /
126
+ reasoningCost) /
115
127
  1_000_000
116
128
  );
117
129
  }
@@ -149,6 +161,29 @@ function readProjectQueueData(projectQueuePath) {
149
161
  return Array.from(seen.values());
150
162
  }
151
163
 
164
+ function isLegacyInclusiveCodexRow(row) {
165
+ if (!row || (row.source !== "codex" && row.source !== "every-code")) return false;
166
+ const inputTokens = Number(row.input_tokens || 0);
167
+ const cachedInputTokens = Number(row.cached_input_tokens || 0);
168
+ const outputTokens = Number(row.output_tokens || 0);
169
+ const totalTokens = Number(row.total_tokens || 0);
170
+ if (!Number.isFinite(inputTokens) || !Number.isFinite(cachedInputTokens)) return false;
171
+ if (cachedInputTokens <= 0 || inputTokens < cachedInputTokens) return false;
172
+ // Legacy Codex queue rows stored input inclusive of cache reads, while
173
+ // total_tokens remained input + output. Canonical rows keep input as pure
174
+ // non-cached input, so cache-heavy legacy rows can be identified by this
175
+ // exact invariant.
176
+ return totalTokens === inputTokens + outputTokens;
177
+ }
178
+
179
+ function normalizeQueueRow(row) {
180
+ if (!isLegacyInclusiveCodexRow(row)) return row;
181
+ return {
182
+ ...row,
183
+ input_tokens: Number(row.input_tokens || 0) - Number(row.cached_input_tokens || 0),
184
+ };
185
+ }
186
+
152
187
  function readQueueData(queuePath) {
153
188
  let raw;
154
189
  try {
@@ -184,7 +219,7 @@ function readQueueData(queuePath) {
184
219
  const seen = new Map();
185
220
  for (const row of parsed) {
186
221
  const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
187
- seen.set(key, row);
222
+ seen.set(key, normalizeQueueRow(row));
188
223
  }
189
224
  return Array.from(seen.values());
190
225
  }
@@ -351,6 +386,67 @@ function trimOutput(value, max = 4000) {
351
386
  return t.length <= max ? t : t.slice(t.length - max);
352
387
  }
353
388
 
389
+ function normalizeRemoteHttpBaseUrl(value) {
390
+ if (typeof value !== "string") return null;
391
+ const trimmed = value.trim();
392
+ if (!trimmed) return null;
393
+ try {
394
+ const url = new URL(trimmed);
395
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
396
+ url.username = "";
397
+ url.password = "";
398
+ url.hash = "";
399
+ return url.toString().replace(/\/$/, "");
400
+ } catch (_e) {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ function resolveAllowedInsforgeBaseUrl(value) {
406
+ const requested = normalizeRemoteHttpBaseUrl(value);
407
+ if (!requested) return null;
408
+
409
+ const runtime = resolveRuntimeConfig();
410
+ const allowed = new Set(
411
+ [runtime.baseUrl, DEFAULT_BASE_URL]
412
+ .map((entry) => normalizeRemoteHttpBaseUrl(entry))
413
+ .filter(Boolean),
414
+ );
415
+
416
+ return allowed.has(requested) ? requested : null;
417
+ }
418
+
419
+ function parseCookieHeader(value) {
420
+ const out = new Map();
421
+ if (typeof value !== "string" || !value.trim()) return out;
422
+ for (const part of value.split(";")) {
423
+ const idx = part.indexOf("=");
424
+ if (idx < 1) continue;
425
+ const key = part.slice(0, idx).trim();
426
+ const rawValue = part.slice(idx + 1).trim();
427
+ if (key) out.set(key, rawValue);
428
+ }
429
+ return out;
430
+ }
431
+
432
+ function isLoopbackHostname(hostname) {
433
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
434
+ }
435
+
436
+ function hasAllowedLoopbackOrigin(headers = {}) {
437
+ const candidates = [headers.origin, headers.referer];
438
+ for (const raw of candidates) {
439
+ if (raw == null || raw === "") continue;
440
+ try {
441
+ const url = new URL(String(raw));
442
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return false;
443
+ } catch (_e) {
444
+ return false;
445
+ }
446
+ }
447
+ return true;
448
+ }
449
+
354
450
  function readJsonBody(req) {
355
451
  return new Promise((resolve, reject) => {
356
452
  const chunks = [];
@@ -516,7 +612,7 @@ function scanClaudeProjects(projectMap) {
516
612
  // ---------------------------------------------------------------------------
517
613
 
518
614
  function json(res, data, status) {
519
- res.writeHead(status || 200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
615
+ res.writeHead(status || 200, { "Content-Type": "application/json" });
520
616
  res.end(JSON.stringify(data));
521
617
  }
522
618
 
@@ -531,6 +627,7 @@ function createLocalApiHandler({ queuePath }) {
531
627
  // so that both browser and WKWebView share the same login session via the proxy.
532
628
  // Persisted to disk so cookies survive server restarts.
533
629
  let relayCookies = new Map();
630
+ const localAuthToken = crypto.randomBytes(24).toString("hex");
534
631
  const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
535
632
  const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
536
633
 
@@ -643,13 +740,40 @@ function createLocalApiHandler({ queuePath }) {
643
740
  let _nativeAuthPending = false;
644
741
  let _nativeAuthExpiry = 0;
645
742
 
743
+ function isAuthorizedLocalMutation(req) {
744
+ const headerToken = req?.headers?.["x-tokentracker-local-auth"];
745
+ const cookieToken = parseCookieHeader(req?.headers?.cookie).get("tokentracker_local_auth");
746
+ const token = typeof headerToken === "string" && headerToken.trim()
747
+ ? headerToken.trim()
748
+ : cookieToken || "";
749
+ if (!token || token !== localAuthToken) return false;
750
+ return hasAllowedLoopbackOrigin(req?.headers || {});
751
+ }
752
+
646
753
  return async function handleLocalApi(req, res, url) {
647
754
  const p = url.pathname;
648
755
 
756
+ if (p === "/api/local-auth") {
757
+ if (String(req.method || "GET").toUpperCase() !== "GET") {
758
+ json(res, { error: "Method Not Allowed" }, 405);
759
+ return true;
760
+ }
761
+ res.writeHead(200, {
762
+ "Content-Type": "application/json",
763
+ "Cache-Control": "no-store",
764
+ });
765
+ res.end(JSON.stringify({ token: localAuthToken }));
766
+ return true;
767
+ }
768
+
649
769
  // --- Auth bridge: native OAuth flag (WebView ↔ system browser) ---
650
770
  if (p === "/api/auth-bridge/verifier") {
651
771
  const method = String(req.method || "GET").toUpperCase();
652
772
  if (method === "PUT" || method === "POST") {
773
+ if (!isAuthorizedLocalMutation(req)) {
774
+ json(res, { error: "Unauthorized" }, 401);
775
+ return true;
776
+ }
653
777
  const body = await readJsonBody(req);
654
778
  _nativeAuthPending = Boolean(body?.native);
655
779
  _nativeAuthExpiry = Date.now() + 5 * 60 * 1000; // 5 min TTL
@@ -669,20 +793,8 @@ function createLocalApiHandler({ queuePath }) {
669
793
 
670
794
  // --- auth proxy: forward /api/auth/* to InsForge cloud ---
671
795
  if (p.startsWith("/api/auth/")) {
672
- const { DEFAULT_BASE_URL } = require("./runtime-config.js");
673
- let insforgeBase = process.env.TOKENTRACKER_INSFORGE_BASE_URL
674
- || process.env.INSFORGE_BASE_URL
675
- || "";
676
- if (!insforgeBase) {
677
- try {
678
- const cfgPath = path.join(os.homedir(), ".tokentracker", "tracker", "config.json");
679
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
680
- insforgeBase = cfg?.baseUrl || "";
681
- } catch { /* ignore */ }
682
- }
683
- if (!insforgeBase) {
684
- insforgeBase = DEFAULT_BASE_URL;
685
- }
796
+ const runtime = resolveRuntimeConfig();
797
+ const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
686
798
  try {
687
799
  const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
688
800
  const proxyHeaders = {};
@@ -748,6 +860,10 @@ function createLocalApiHandler({ queuePath }) {
748
860
  json(res, { ok: false, error: "Method Not Allowed" }, 405);
749
861
  return true;
750
862
  }
863
+ if (!isAuthorizedLocalMutation(req)) {
864
+ json(res, { ok: false, error: "Unauthorized" }, 401);
865
+ return true;
866
+ }
751
867
  try {
752
868
  let body = {};
753
869
  try {
@@ -759,8 +875,13 @@ function createLocalApiHandler({ queuePath }) {
759
875
  if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
760
876
  extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
761
877
  }
762
- if (typeof body.insforgeBaseUrl === "string" && /^https?:\/\//i.test(body.insforgeBaseUrl.trim())) {
763
- extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = body.insforgeBaseUrl.trim();
878
+ if (body.insforgeBaseUrl != null) {
879
+ const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
880
+ if (!allowedBaseUrl) {
881
+ json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
882
+ return true;
883
+ }
884
+ extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
764
885
  }
765
886
  const result = await runSyncCommand(extraEnv);
766
887
  try {
@@ -939,14 +1060,11 @@ function createLocalApiHandler({ queuePath }) {
939
1060
  const sources = Array.from(bySource.values()).map((s) => {
940
1061
  s.models = Array.from(s.models.values())
941
1062
  .map((m) => {
942
- const p = getModelPricing(m.model);
943
- const cost =
944
- ((m.totals.input_tokens || 0) * (p.input || 0) +
945
- (m.totals.output_tokens || 0) * (p.output || 0) +
946
- (m.totals.cached_input_tokens || 0) * (p.cache_read || 0) +
947
- (m.totals.cache_creation_input_tokens || 0) * (p.cache_write || 0) +
948
- (m.totals.reasoning_output_tokens || 0) * (p.output || 0)) /
949
- 1_000_000;
1063
+ const cost = computeRowCost({
1064
+ ...m.totals,
1065
+ model: m.model,
1066
+ source: s.source,
1067
+ });
950
1068
  return { ...m, totals: { ...m.totals, total_cost_usd: cost.toFixed(6) } };
951
1069
  })
952
1070
  .sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
@@ -1109,6 +1227,7 @@ function createLocalApiHandler({ queuePath }) {
1109
1227
 
1110
1228
  module.exports = {
1111
1229
  createLocalApiHandler,
1230
+ resolveAllowedInsforgeBaseUrl,
1112
1231
  resolveQueuePath,
1113
1232
  // Exported for cross-consumer tests (pricing + native contract lock).
1114
1233
  MODEL_PRICING,
@@ -2150,12 +2150,12 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
2150
2150
  const hasTotal = isNonEmptyObject(totalUsage);
2151
2151
  const hasPrevTotals = isNonEmptyObject(prevTotals);
2152
2152
 
2153
- // Codex rollout logs sometimes emit duplicate token_count records where total_token_usage does not
2154
- // change between adjacent entries. Counting last_token_usage in those cases will double-count.
2155
- if (hasTotal && hasPrevTotals && sameUsage(totalUsage, prevTotals)) {
2156
- return null;
2157
- }
2158
-
2153
+ // NOTE: We used to guard against "duplicate token_count records where
2154
+ // total_token_usage is unchanged" by returning null here. We removed that
2155
+ // guard to align token counts with ccusage exactly (audited against 10 days
2156
+ // of real rollouts). When last_token_usage is present we trust it as the
2157
+ // per-turn delta; when it's absent the cumulative-subtract path naturally
2158
+ // yields an all-zero delta on duplicates and is still filtered below.
2159
2159
  if (!hasLast && hasTotal && hasPrevTotals && totalsReset(totalUsage, prevTotals)) {
2160
2160
  const normalized = normalizeUsage(totalUsage);
2161
2161
  return isAllZeroUsage(normalized) ? null : normalized;
@@ -2203,6 +2203,19 @@ function normalizeUsage(u) {
2203
2203
  const n = Number(u[k] || 0);
2204
2204
  out[k] = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
2205
2205
  }
2206
+ // Codex rollouts (and Every Code, which shares the format) report
2207
+ // `input_tokens` as the TOTAL prompt, with `cached_input_tokens` as the
2208
+ // cached subset — i.e. the cached slice is INSIDE the input count. Our
2209
+ // queue schema (CLAUDE.md → Token Normalization Convention) stores
2210
+ // `input_tokens` as pure non-cached input and `cached_input_tokens`
2211
+ // separately. Without this subtraction the cost formula bills the cached
2212
+ // bytes twice: once at the full input rate and again at the cache_read
2213
+ // rate, producing ~6–7x cost inflation on cache-heavy Codex sessions
2214
+ // (verified against ccusage's per-day numbers on the same rollouts).
2215
+ // We intentionally leave `total_tokens` unchanged: Codex reports
2216
+ // total = input(inclusive of cached) + output, which numerically equals
2217
+ // our schema's non_cached + cached + output + 0 (cache_creation=0 here).
2218
+ out.input_tokens = Math.max(0, out.input_tokens - out.cached_input_tokens);
2206
2219
  return out;
2207
2220
  }
2208
2221
 
@@ -2241,20 +2254,6 @@ function isAllZeroUsage(u) {
2241
2254
  return true;
2242
2255
  }
2243
2256
 
2244
- function sameUsage(a, b) {
2245
- for (const k of [
2246
- "input_tokens",
2247
- "cached_input_tokens",
2248
- "cache_creation_input_tokens",
2249
- "output_tokens",
2250
- "reasoning_output_tokens",
2251
- "total_tokens",
2252
- ]) {
2253
- if (toNonNegativeInt(a?.[k]) !== toNonNegativeInt(b?.[k])) return false;
2254
- }
2255
- return true;
2256
- }
2257
-
2258
2257
  function totalsReset(curr, prev) {
2259
2258
  const currTotal = curr?.total_tokens;
2260
2259
  const prevTotal = prev?.total_tokens;
@@ -46,7 +46,6 @@ async function serveStaticFile(baseDir, pathname, res) {
46
46
  "Content-Type": contentType,
47
47
  "Content-Length": stat.size,
48
48
  "Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
49
- "Access-Control-Allow-Origin": "*",
50
49
  });
51
50
 
52
51
  const stream = fs.createReadStream(filePath);