tokentracker-cli 0.5.97 → 0.5.99

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.
@@ -135,7 +135,7 @@
135
135
  ]
136
136
  }
137
137
  </script>
138
- <script type="module" crossorigin src="/assets/main-h-vFy9lS.js"></script>
138
+ <script type="module" crossorigin src="/assets/main-BhjD_pKB.js"></script>
139
139
  <link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.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-h-vFy9lS.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-BhjD_pKB.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.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.97",
3
+ "version": "0.5.99",
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": {
@@ -29,6 +29,7 @@
29
29
  "dashboard:open": "bash scripts/open-dashboard.sh",
30
30
  "dashboard:preview": "npm --prefix dashboard run preview",
31
31
  "dev:shim": "node scripts/dev-bin-shim.cjs",
32
+ "audit:tokens": "node scripts/audit-token-correctness.cjs",
32
33
  "graph:auto-index": "node scripts/graph/auto-index.cjs",
33
34
  "graph:scip": "node scripts/graph/generate-scip.cjs",
34
35
  "pricing:build-seed": "node scripts/build-pricing-seed.cjs",
@@ -879,7 +879,7 @@ function isRunnableCommand(command) {
879
879
  `;
880
880
  }
881
881
 
882
- module.exports = { cmdInit, buildNotifyHandler };
882
+ module.exports = { cmdInit, buildNotifyHandler, installLocalTrackerApp };
883
883
 
884
884
  async function probeFile(p) {
885
885
  try {
@@ -908,7 +908,9 @@ async function installLocalTrackerApp({ appDir }) {
908
908
  const packageRoot = path.resolve(__dirname, "../..");
909
909
  const srcFrom = path.join(packageRoot, "src");
910
910
  const binFrom = path.join(packageRoot, "bin", "tracker.js");
911
+ const packageJsonFrom = path.join(packageRoot, "package.json");
911
912
  const nodeModulesFrom = path.join(packageRoot, "node_modules");
913
+ const dashboardDistFrom = path.join(packageRoot, "dashboard", "dist");
912
914
 
913
915
  // When running from the installed local runtime (or when appDir is symlinked to this package),
914
916
  // source and destination resolve to the same place. Do not delete appDir in that case.
@@ -920,6 +922,7 @@ async function installLocalTrackerApp({ appDir }) {
920
922
  const binToDir = path.join(appDir, "bin");
921
923
  const binTo = path.join(binToDir, "tracker.js");
922
924
  const nodeModulesTo = path.join(appDir, "node_modules");
925
+ const dashboardDistTo = path.join(appDir, "dashboard", "dist");
923
926
 
924
927
  await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
925
928
  await ensureDir(appDir);
@@ -927,6 +930,10 @@ async function installLocalTrackerApp({ appDir }) {
927
930
  await ensureDir(binToDir);
928
931
  await fs.copyFile(binFrom, binTo);
929
932
  await fs.chmod(binTo, 0o755).catch(() => {});
933
+ await fs.copyFile(packageJsonFrom, path.join(appDir, "package.json")).catch(() => {});
934
+ if (await isDir(dashboardDistFrom)) {
935
+ await fs.cp(dashboardDistFrom, dashboardDistTo, { recursive: true });
936
+ }
930
937
  await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
931
938
  }
932
939
 
@@ -37,6 +37,13 @@ async function cmdServe(argv) {
37
37
  }
38
38
  }
39
39
 
40
+ try {
41
+ const { installLocalTrackerApp } = require("./init");
42
+ await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
43
+ } catch (e) {
44
+ process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
45
+ }
46
+
40
47
  // 0.1 Ensure config.json baseUrl matches the canonical default
41
48
  try {
42
49
  const { DEFAULT_BASE_URL } = require("../lib/runtime-config");
@@ -52,6 +52,7 @@ const { resolveTrackerPaths } = require("../lib/tracker-paths");
52
52
  const { resolveRuntimeConfig } = require("../lib/runtime-config");
53
53
 
54
54
  const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
55
+ const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
55
56
 
56
57
  async function cmdSync(argv) {
57
58
  const opts = parseArgs(argv);
@@ -114,6 +115,8 @@ async function cmdSync(argv) {
114
115
  }
115
116
  }
116
117
 
118
+ await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
119
+
117
120
  const openclawFiles = openclawSignal?.sessionFile
118
121
  ? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
119
122
  : [];
@@ -621,7 +624,13 @@ function parseArgs(argv) {
621
624
  return out;
622
625
  }
623
626
 
624
- module.exports = { cmdSync, migrateCursorUnknownBuckets, CURSOR_UNKNOWN_MIGRATION_KEY };
627
+ module.exports = {
628
+ cmdSync,
629
+ migrateCursorUnknownBuckets,
630
+ migrateRolloutCumulativeDeltaBuckets,
631
+ CURSOR_UNKNOWN_MIGRATION_KEY,
632
+ ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
633
+ };
625
634
 
626
635
  function normalizeString(value) {
627
636
  if (typeof value !== "string") return null;
@@ -1034,3 +1043,67 @@ async function migrateCursorUnknownBuckets({ cursors, queuePath }) {
1034
1043
 
1035
1044
  cursors.migrations[CURSOR_UNKNOWN_MIGRATION_KEY] = new Date().toISOString();
1036
1045
  }
1046
+
1047
+ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles }) {
1048
+ if (!cursors || typeof cursors !== "object") return;
1049
+ cursors.migrations = cursors.migrations || {};
1050
+ if (cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY]) return;
1051
+
1052
+ const rolloutPathSources = new Map();
1053
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
1054
+ const filePath = typeof entry === "string" ? entry : entry?.path;
1055
+ const source = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
1056
+ if (!filePath) continue;
1057
+ if (source === "codex" || source === "every-code") {
1058
+ rolloutPathSources.set(filePath, source);
1059
+ }
1060
+ }
1061
+
1062
+ if (cursors.files && typeof cursors.files === "object") {
1063
+ for (const filePath of rolloutPathSources.keys()) {
1064
+ delete cursors.files[filePath];
1065
+ }
1066
+ }
1067
+
1068
+ const buckets = cursors.hourly?.buckets;
1069
+ const retractions = [];
1070
+ if (buckets && typeof buckets === "object") {
1071
+ for (const key of Object.keys(buckets)) {
1072
+ const [source, model, ...hourParts] = key.split("|");
1073
+ if (source !== "codex" && source !== "every-code") continue;
1074
+ const hourStart = hourParts.join("|");
1075
+ retractions.push(
1076
+ JSON.stringify({
1077
+ source,
1078
+ model: model || "unknown",
1079
+ hour_start: hourStart,
1080
+ input_tokens: 0,
1081
+ cached_input_tokens: 0,
1082
+ cache_creation_input_tokens: 0,
1083
+ output_tokens: 0,
1084
+ reasoning_output_tokens: 0,
1085
+ total_tokens: 0,
1086
+ billable_total_tokens: 0,
1087
+ conversation_count: 0,
1088
+ }),
1089
+ );
1090
+ delete buckets[key];
1091
+ }
1092
+ }
1093
+
1094
+ const groupQueued = cursors.hourly?.groupQueued;
1095
+ if (groupQueued && typeof groupQueued === "object") {
1096
+ for (const key of Object.keys(groupQueued)) {
1097
+ if (key.startsWith("codex|") || key.startsWith("every-code|")) {
1098
+ delete groupQueued[key];
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ if (retractions.length > 0) {
1104
+ await ensureDir(path.dirname(queuePath));
1105
+ await fs.appendFile(queuePath, retractions.join("\n") + "\n");
1106
+ }
1107
+
1108
+ cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
1109
+ }
@@ -107,6 +107,7 @@ function extractUserIdFromJwt(jwt) {
107
107
 
108
108
  const CURSOR_CSV_URL = "https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens";
109
109
  const CURSOR_SUMMARY_URL = "https://cursor.com/api/usage-summary";
110
+ const CURSOR_SOURCE_SCOPE = "account";
110
111
 
111
112
  /**
112
113
  * Fetch full usage CSV from Cursor API.
@@ -280,6 +281,10 @@ function parseCursorCsv(csvText) {
280
281
  kind: kindIdx !== undefined ? stripQuotes(fields[kindIdx]) : "unknown",
281
282
  model: stripQuotes(fields[modelIdx]),
282
283
  maxMode: maxModeIdx !== undefined ? stripQuotes(fields[maxModeIdx]) : "No",
284
+ sourceScope: CURSOR_SOURCE_SCOPE,
285
+ billableKind: isCursorBillableKind(kindIdx !== undefined ? fields[kindIdx] : "unknown")
286
+ ? "billable"
287
+ : "non_billable",
283
288
  inputTokens: inputWithoutCache,
284
289
  cacheWriteTokens: Math.max(0, inputWithCache - inputWithoutCache),
285
290
  cacheReadTokens: toNum(fields[cacheReadIdx]),
@@ -312,9 +317,18 @@ function normalizeCursorUsage(record) {
312
317
  output_tokens: outputTokens,
313
318
  reasoning_output_tokens: 0,
314
319
  total_tokens: totalTokens,
320
+ billable_total_tokens: isCursorBillableKind(record?.kind) ? totalTokens : 0,
315
321
  };
316
322
  }
317
323
 
324
+ function isCursorBillableKind(kind) {
325
+ const normalized = String(kind || "").trim().toLowerCase();
326
+ if (!normalized) return true;
327
+ if (normalized.includes("no charge")) return false;
328
+ if (normalized === "free") return false;
329
+ return true;
330
+ }
331
+
318
332
  // ── CSV helpers ──
319
333
 
320
334
  function parseCsvLine(line) {
@@ -364,5 +378,6 @@ module.exports = {
364
378
  fetchCursorUsageCsv,
365
379
  fetchCursorUsageSummary,
366
380
  parseCursorCsv,
381
+ isCursorBillableKind,
367
382
  normalizeCursorUsage,
368
383
  };
@@ -4,6 +4,12 @@ const path = require("node:path");
4
4
  const { spawn } = require("node:child_process");
5
5
  const crypto = require("node:crypto");
6
6
  const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
7
+ const {
8
+ filterRowsByUsageScope,
9
+ getSourceScope,
10
+ listExcludedSources,
11
+ normalizeUsageScope,
12
+ } = require("./source-metadata");
7
13
 
8
14
  const SYNC_TIMEOUT_MS = 120_000;
9
15
  const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
@@ -154,7 +160,7 @@ function aggregateByDay(rows, timeZoneContext = null) {
154
160
  }
155
161
  const a = byDay.get(day);
156
162
  a.total_tokens += row.total_tokens || 0;
157
- a.billable_total_tokens += row.total_tokens || 0;
163
+ a.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
158
164
  a.total_cost_usd += computeRowCost(row);
159
165
  a.input_tokens += row.input_tokens || 0;
160
166
  a.output_tokens += row.output_tokens || 0;
@@ -166,6 +172,22 @@ function aggregateByDay(rows, timeZoneContext = null) {
166
172
  return Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
167
173
  }
168
174
 
175
+ function getRequestedUsageScope(url) {
176
+ if (url.searchParams.get("include_account_level") === "1") return "all";
177
+ return normalizeUsageScope(url.searchParams.get("scope"));
178
+ }
179
+
180
+ function scopedQueueRows(queuePath, url) {
181
+ const scope = getRequestedUsageScope(url);
182
+ const allRows = readQueueData(queuePath);
183
+ return {
184
+ scope,
185
+ allRows,
186
+ rows: filterRowsByUsageScope(allRows, scope),
187
+ excludedSources: listExcludedSources(allRows, scope),
188
+ };
189
+ }
190
+
169
191
  function getTimeZoneContext(url) {
170
192
  const tz = String(url.searchParams.get("tz") || "").trim();
171
193
  const rawOffset = Number(url.searchParams.get("tz_offset_minutes"));
@@ -796,7 +818,7 @@ function createLocalApiHandler({ queuePath }) {
796
818
  const from = url.searchParams.get("from") || "";
797
819
  const to = url.searchParams.get("to") || "";
798
820
  const timeZoneContext = getTimeZoneContext(url);
799
- const rows = readQueueData(qp);
821
+ const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
800
822
  const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
801
823
  const totals = daily.reduce(
802
824
  (acc, r) => {
@@ -848,7 +870,7 @@ function createLocalApiHandler({ queuePath }) {
848
870
  const l30fromStr = shiftDay(todayStr, -29);
849
871
 
850
872
  json(res, {
851
- from, to, days: daily.length,
873
+ from, to, days: daily.length, scope, excluded_sources: excludedSources,
852
874
  totals: { ...totals, total_cost_usd: totalCost.toFixed(6) },
853
875
  rolling: {
854
876
  last_7d: { from: l7fromStr, to: todayStr, active_days: l7.length, totals: l7t },
@@ -863,9 +885,9 @@ function createLocalApiHandler({ queuePath }) {
863
885
  const from = url.searchParams.get("from") || "";
864
886
  const to = url.searchParams.get("to") || "";
865
887
  const timeZoneContext = getTimeZoneContext(url);
866
- const rows = readQueueData(qp);
888
+ const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
867
889
  const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
868
- json(res, { from, to, data: daily });
890
+ json(res, { from, to, scope, excluded_sources: excludedSources, data: daily });
869
891
  return true;
870
892
  }
871
893
 
@@ -873,7 +895,7 @@ function createLocalApiHandler({ queuePath }) {
873
895
  if (p === "/functions/tokentracker-usage-heatmap") {
874
896
  const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
875
897
  const timeZoneContext = getTimeZoneContext(url);
876
- const rows = readQueueData(qp);
898
+ const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
877
899
  const daily = aggregateByDay(rows, timeZoneContext);
878
900
  const todayParts = getZonedParts(new Date(), timeZoneContext);
879
901
  const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
@@ -910,7 +932,7 @@ function createLocalApiHandler({ queuePath }) {
910
932
  for (let i = 0; i < cells.length; i += 7) {
911
933
  weeksArr.push(cells.slice(i, i + 7));
912
934
  }
913
- json(res, { from, to, week_starts_on: "sun", active_days: cells.filter((c) => c.billable_total_tokens > 0).length, streak_days: 0, weeks: weeksArr });
935
+ json(res, { from, to, scope, excluded_sources: excludedSources, week_starts_on: "sun", active_days: cells.filter((c) => c.billable_total_tokens > 0).length, streak_days: 0, weeks: weeksArr });
914
936
  return true;
915
937
  }
916
938
 
@@ -919,7 +941,8 @@ function createLocalApiHandler({ queuePath }) {
919
941
  const from = url.searchParams.get("from") || "";
920
942
  const to = url.searchParams.get("to") || "";
921
943
  const timeZoneContext = getTimeZoneContext(url);
922
- const rows = readQueueData(qp).filter((r) => {
944
+ const { rows: scopedRows, scope, excludedSources } = scopedQueueRows(qp, url);
945
+ const rows = scopedRows.filter((r) => {
923
946
  if (!r.hour_start) return false;
924
947
  const d = rowDayKey(r, timeZoneContext);
925
948
  return d >= from && d <= to;
@@ -930,10 +953,10 @@ function createLocalApiHandler({ queuePath }) {
930
953
  const src = row.source || "unknown";
931
954
  const mdl = row.model || "unknown";
932
955
  if (!bySource.has(src))
933
- bySource.set(src, { source: src, totals: { 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, total_cost_usd: "0" }, models: new Map() });
956
+ bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { 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, total_cost_usd: "0" }, models: new Map() });
934
957
  const sa = bySource.get(src);
935
958
  sa.totals.total_tokens += row.total_tokens || 0;
936
- sa.totals.billable_total_tokens += row.total_tokens || 0;
959
+ sa.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
937
960
  sa.totals.input_tokens += row.input_tokens || 0;
938
961
  sa.totals.output_tokens += row.output_tokens || 0;
939
962
  sa.totals.cached_input_tokens += row.cached_input_tokens || 0;
@@ -943,7 +966,7 @@ function createLocalApiHandler({ queuePath }) {
943
966
  sa.models.set(mdl, { model: mdl, model_id: mdl, totals: { 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, total_cost_usd: "0" } });
944
967
  const ma = sa.models.get(mdl);
945
968
  ma.totals.total_tokens += row.total_tokens || 0;
946
- ma.totals.billable_total_tokens += row.total_tokens || 0;
969
+ ma.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
947
970
  ma.totals.input_tokens += row.input_tokens || 0;
948
971
  ma.totals.output_tokens += row.output_tokens || 0;
949
972
  ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
@@ -968,7 +991,7 @@ function createLocalApiHandler({ queuePath }) {
968
991
  });
969
992
 
970
993
  json(res, {
971
- from, to, days: 0, sources,
994
+ from, to, days: 0, scope, excluded_sources: excludedSources, sources,
972
995
  pricing: { model: "per-model", pricing_mode: "per_token_type", source: "litellm", effective_from: new Date().toISOString().slice(0, 10) },
973
996
  });
974
997
  return true;
@@ -1061,9 +1084,9 @@ function createLocalApiHandler({ queuePath }) {
1061
1084
  if (p === "/functions/tokentracker-usage-hourly") {
1062
1085
  const day = url.searchParams.get("day") || new Date().toISOString().slice(0, 10);
1063
1086
  const timeZoneContext = getTimeZoneContext(url);
1064
- const rows = readQueueData(qp);
1087
+ const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
1065
1088
  const data = aggregateHourlyByDay(rows, day, timeZoneContext);
1066
- json(res, { day, data });
1089
+ json(res, { day, scope, excluded_sources: excludedSources, data });
1067
1090
  return true;
1068
1091
  }
1069
1092
 
@@ -1072,7 +1095,7 @@ function createLocalApiHandler({ queuePath }) {
1072
1095
  const from = url.searchParams.get("from") || "";
1073
1096
  const to = url.searchParams.get("to") || "";
1074
1097
  const timeZoneContext = getTimeZoneContext(url);
1075
- const rows = readQueueData(qp);
1098
+ const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
1076
1099
  const byMonth = new Map();
1077
1100
  for (const row of rows) {
1078
1101
  if (!row.hour_start) continue;
@@ -1091,7 +1114,7 @@ function createLocalApiHandler({ queuePath }) {
1091
1114
  a.reasoning_output_tokens += row.reasoning_output_tokens || 0;
1092
1115
  a.conversation_count += row.conversation_count || 0;
1093
1116
  }
1094
- json(res, { from, to, data: Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)) });
1117
+ json(res, { from, to, scope, excluded_sources: excludedSources, data: Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)) });
1095
1118
  return true;
1096
1119
  }
1097
1120