tokentracker-cli 0.5.98 → 0.5.100

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-io8wJG1t.js"></script>
138
+ <script type="module" crossorigin src="/assets/main-BYMjcXxR.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-io8wJG1t.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-BYMjcXxR.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
56
56
  </head>
57
57
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.5.98",
4
- "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw, Hermes, GitHub Copilot)",
3
+ "version": "0.5.100",
4
+ "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tokentracker-cli": "bin/tracker.js",
@@ -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",
@@ -425,6 +425,18 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
425
425
  }
426
426
  }
427
427
 
428
+ // oh-my-pi: passive reader — no hook installation needed.
429
+ // TokenTracker reads ~/.omp/agent/sessions/**/*.jsonl directly.
430
+ {
431
+ const ompHome = process.env.OMP_HOME ||
432
+ (process.env.PI_CONFIG_DIR ? path.join(home, process.env.PI_CONFIG_DIR) : path.join(home, ".omp"));
433
+ const ompAgentDir = process.env.PI_CODING_AGENT_DIR || path.join(ompHome, "agent");
434
+ const ompSessions = path.join(ompAgentDir, "sessions");
435
+ if (fssync.existsSync(ompSessions)) {
436
+ summary.push({ label: "oh-my-pi", status: "detected", detail: "Passive reader (no hook needed)" });
437
+ }
438
+ }
439
+
428
440
  // CodeBuddy: Claude-Code fork. Install the SessionEnd hook so finished
429
441
  // sessions trigger notify.cjs → tracker sync; passive scan still runs as a
430
442
  // safety net for sessions that don't fire SessionEnd cleanly.
@@ -879,7 +891,7 @@ function isRunnableCommand(command) {
879
891
  `;
880
892
  }
881
893
 
882
- module.exports = { cmdInit, buildNotifyHandler };
894
+ module.exports = { cmdInit, buildNotifyHandler, installLocalTrackerApp };
883
895
 
884
896
  async function probeFile(p) {
885
897
  try {
@@ -908,7 +920,9 @@ async function installLocalTrackerApp({ appDir }) {
908
920
  const packageRoot = path.resolve(__dirname, "../..");
909
921
  const srcFrom = path.join(packageRoot, "src");
910
922
  const binFrom = path.join(packageRoot, "bin", "tracker.js");
923
+ const packageJsonFrom = path.join(packageRoot, "package.json");
911
924
  const nodeModulesFrom = path.join(packageRoot, "node_modules");
925
+ const dashboardDistFrom = path.join(packageRoot, "dashboard", "dist");
912
926
 
913
927
  // When running from the installed local runtime (or when appDir is symlinked to this package),
914
928
  // source and destination resolve to the same place. Do not delete appDir in that case.
@@ -920,6 +934,7 @@ async function installLocalTrackerApp({ appDir }) {
920
934
  const binToDir = path.join(appDir, "bin");
921
935
  const binTo = path.join(binToDir, "tracker.js");
922
936
  const nodeModulesTo = path.join(appDir, "node_modules");
937
+ const dashboardDistTo = path.join(appDir, "dashboard", "dist");
923
938
 
924
939
  await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
925
940
  await ensureDir(appDir);
@@ -927,6 +942,10 @@ async function installLocalTrackerApp({ appDir }) {
927
942
  await ensureDir(binToDir);
928
943
  await fs.copyFile(binFrom, binTo);
929
944
  await fs.chmod(binTo, 0o755).catch(() => {});
945
+ await fs.copyFile(packageJsonFrom, path.join(appDir, "package.json")).catch(() => {});
946
+ if (await isDir(dashboardDistFrom)) {
947
+ await fs.cp(dashboardDistFrom, dashboardDistTo, { recursive: true });
948
+ }
930
949
  await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
931
950
  }
932
951
 
@@ -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");
@@ -16,19 +16,31 @@ const {
16
16
  isGeminiHookConfigured,
17
17
  buildGeminiHookCommand,
18
18
  } = require("../lib/gemini-config");
19
- const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("../lib/opencode-config");
19
+ const {
20
+ resolveOpencodeConfigDir,
21
+ isOpencodePluginInstalled,
22
+ } = require("../lib/opencode-config");
20
23
  const { collectLocalSubscriptions } = require("../lib/subscriptions");
21
- const { describeCopilotOtelStatus, readCopilotOauthToken } = require("../lib/usage-limits");
22
- const { normalizeState: normalizeUploadState } = require("../lib/upload-throttle");
24
+ const {
25
+ describeCopilotOtelStatus,
26
+ readCopilotOauthToken,
27
+ } = require("../lib/usage-limits");
28
+ const {
29
+ normalizeState: normalizeUploadState,
30
+ } = require("../lib/upload-throttle");
23
31
  const { collectTrackerDiagnostics } = require("../lib/diagnostics");
24
32
  const { probeOpenclawHookState } = require("../lib/openclaw-hook");
25
- const { probeOpenclawSessionPluginState } = require("../lib/openclaw-session-plugin");
33
+ const {
34
+ probeOpenclawSessionPluginState,
35
+ } = require("../lib/openclaw-session-plugin");
26
36
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
27
37
  const {
28
38
  resolveKimiWireFiles,
29
39
  resolveKiroCliDbPath,
30
40
  resolveCodebuddyHome,
31
41
  resolveCodebuddyProjectFiles,
42
+ resolveOmpSessionFiles,
43
+ resolveOmpAgentDir,
32
44
  } = require("../lib/rollout");
33
45
 
34
46
  async function cmdStatus(argv = []) {
@@ -60,8 +72,13 @@ async function cmdStatus(argv = []) {
60
72
  "settings.json",
61
73
  );
62
74
  const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
63
- const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
64
- const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
75
+ const geminiSettingsPath = resolveGeminiSettingsPath({
76
+ configDir: geminiConfigDir,
77
+ });
78
+ const opencodeConfigDir = resolveOpencodeConfigDir({
79
+ home,
80
+ env: process.env,
81
+ });
65
82
  const notifyPath = path.join(binDir, "notify.cjs");
66
83
  const claudeHookCommand = buildClaudeHookCommand(notifyPath);
67
84
  const codebuddyHookCommand = buildHookCommand(notifyPath, "codebuddy");
@@ -70,20 +87,26 @@ async function cmdStatus(argv = []) {
70
87
  const config = await readJson(configPath);
71
88
  const cursors = await readJson(cursorsPath);
72
89
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
73
- const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
90
+ const uploadThrottle = normalizeUploadState(
91
+ await readJson(uploadThrottlePath),
92
+ );
74
93
  const autoRetry = await readJson(autoRetryPath);
75
94
 
76
95
  const queueSize = await safeStatSize(queuePath);
77
96
  const pendingBytes = Math.max(0, queueSize - (queueState.offset || 0));
78
97
 
79
98
  const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
80
- const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
81
- const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
99
+ const lastOpenclawSync =
100
+ (await safeReadText(openclawSignalPath))?.trim() || null;
101
+ const lastNotifySpawn = parseEpochMsToIso(
102
+ (await safeReadText(throttlePath))?.trim() || null,
103
+ );
82
104
 
83
105
  const codexNotify = await readCodexNotify(codexConfigPath);
84
106
  const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
85
107
  const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
86
- const everyCodeConfigured = Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
108
+ const everyCodeConfigured =
109
+ Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
87
110
  const claudeHookConfigured = await isClaudeHookConfigured({
88
111
  settingsPath: claudeSettingsPath,
89
112
  hookCommand: claudeHookCommand,
@@ -104,7 +127,11 @@ async function cmdStatus(argv = []) {
104
127
  trackerDir,
105
128
  env: process.env,
106
129
  });
107
- const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
130
+ const openclawHookState = await probeOpenclawHookState({
131
+ home,
132
+ trackerDir,
133
+ env: process.env,
134
+ });
108
135
 
109
136
  const lastUpload = uploadThrottle.lastSuccessMs
110
137
  ? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
@@ -151,9 +178,17 @@ async function cmdStatus(argv = []) {
151
178
  ? resolveCodebuddyProjectFiles(process.env)
152
179
  : [];
153
180
 
181
+ // oh-my-pi — passive scan only (no hooks).
182
+ const ompAgentDir = resolveOmpAgentDir(process.env);
183
+ const ompInstalled = fssync.existsSync(path.join(ompAgentDir, "sessions"));
184
+ const ompFiles = ompInstalled ? resolveOmpSessionFiles(process.env) : [];
185
+
154
186
  const copilotToken = readCopilotOauthToken({ home });
155
187
  const copilotOtel = describeCopilotOtelStatus({ home, env: process.env });
156
- const copilotLines = formatCopilotLines({ token: copilotToken, otel: copilotOtel });
188
+ const copilotLines = formatCopilotLines({
189
+ token: copilotToken,
190
+ otel: copilotOtel,
191
+ });
157
192
 
158
193
  process.stdout.write(
159
194
  [
@@ -186,6 +221,9 @@ async function cmdStatus(argv = []) {
186
221
  codebuddyInstalled
187
222
  ? `- CodeBuddy hooks: ${codebuddyHookConfigured ? "set" : "unset"} (${codebuddyFiles.length} session jsonl file${codebuddyFiles.length !== 1 ? "s" : ""} found)`
188
223
  : null,
224
+ ompInstalled
225
+ ? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found)`
226
+ : null,
189
227
  ...copilotLines,
190
228
  ...subscriptionLines,
191
229
  "",
@@ -197,7 +235,9 @@ async function cmdStatus(argv = []) {
197
235
 
198
236
  function formatCopilotLines({ token, otel }) {
199
237
  if (!token && !otel.otel_has_files) return [];
200
- const limitsState = token ? "set (via GitHub OAuth)" : "unset (no Copilot OAuth token found)";
238
+ const limitsState = token
239
+ ? "set (via GitHub OAuth)"
240
+ : "unset (no Copilot OAuth token found)";
201
241
  const usageState = otel.otel_has_files
202
242
  ? `set (${otel.otel_path || otel.otel_default_dir})`
203
243
  : otel.otel_enabled
@@ -235,7 +275,11 @@ function formatSubscriptionLine(entry = {}) {
235
275
 
236
276
  if (!planType) return null;
237
277
 
238
- if (tool === "claude" && provider === "anthropic" && product === "subscription") {
278
+ if (
279
+ tool === "claude" &&
280
+ provider === "anthropic" &&
281
+ product === "subscription"
282
+ ) {
239
283
  const suffix = rateLimitTier ? ` (rate limit tier: ${rateLimitTier})` : "";
240
284
  return `- ${toolLabel} subscription: ${planType}${suffix}`;
241
285
  }
@@ -249,7 +293,11 @@ function formatSubscriptionLine(entry = {}) {
249
293
  }
250
294
 
251
295
  function parseArgs(argv) {
252
- const out = { diagnostics: false, probeKeychain: false, probeKeychainDetails: false };
296
+ const out = {
297
+ diagnostics: false,
298
+ probeKeychain: false,
299
+ probeKeychainDetails: false,
300
+ };
253
301
 
254
302
  for (let i = 0; i < argv.length; i++) {
255
303
  const a = argv[i];
@@ -27,6 +27,8 @@ const {
27
27
  parseCopilotIncremental,
28
28
  resolveKimiWireFiles,
29
29
  parseKimiIncremental,
30
+ resolveOmpSessionFiles,
31
+ parseOmpIncremental,
30
32
  resolveCodebuddyProjectFiles,
31
33
  parseCodebuddyIncremental,
32
34
  resolveKiroCliSessionFiles,
@@ -52,6 +54,7 @@ const { resolveTrackerPaths } = require("../lib/tracker-paths");
52
54
  const { resolveRuntimeConfig } = require("../lib/runtime-config");
53
55
 
54
56
  const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
57
+ const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
55
58
 
56
59
  async function cmdSync(argv) {
57
60
  const opts = parseArgs(argv);
@@ -114,6 +117,8 @@ async function cmdSync(argv) {
114
117
  }
115
118
  }
116
119
 
120
+ await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
121
+
117
122
  const openclawFiles = openclawSignal?.sessionFile
118
123
  ? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
119
124
  : [];
@@ -444,6 +449,28 @@ async function cmdSync(argv) {
444
449
  });
445
450
  }
446
451
 
452
+ // ── oh-my-pi (passive ~/.omp/agent/sessions/**/*.jsonl reader) ──
453
+ let ompResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
454
+ const ompFiles = resolveOmpSessionFiles(process.env);
455
+ if (ompFiles.length > 0) {
456
+ if (progress?.enabled) {
457
+ progress.start(`Parsing oh-my-pi ${renderBar(0)} | buckets 0`);
458
+ }
459
+ ompResult = await parseOmpIncremental({
460
+ sessionFiles: ompFiles,
461
+ cursors,
462
+ queuePath,
463
+ env: process.env,
464
+ onProgress: (p) => {
465
+ if (!progress?.enabled) return;
466
+ const pct = p.total > 0 ? p.index / p.total : 1;
467
+ progress.update(
468
+ `Parsing oh-my-pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
469
+ );
470
+ },
471
+ });
472
+ }
473
+
447
474
  // ── GitHub Copilot CLI (OTEL JSONL files) ──
448
475
  let copilotResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
449
476
  const copilotPaths = resolveCopilotOtelPaths(process.env);
@@ -563,6 +590,7 @@ async function cmdSync(argv) {
563
590
  hermesResult.recordsProcessed +
564
591
  kimiResult.recordsProcessed +
565
592
  codebuddyResult.recordsProcessed +
593
+ ompResult.recordsProcessed +
566
594
  copilotResult.recordsProcessed;
567
595
  const totalBuckets =
568
596
  parseResult.bucketsQueued +
@@ -576,6 +604,7 @@ async function cmdSync(argv) {
576
604
  hermesResult.bucketsQueued +
577
605
  kimiResult.bucketsQueued +
578
606
  codebuddyResult.bucketsQueued +
607
+ ompResult.bucketsQueued +
579
608
  copilotResult.bucketsQueued;
580
609
  process.stdout.write(
581
610
  [
@@ -621,7 +650,13 @@ function parseArgs(argv) {
621
650
  return out;
622
651
  }
623
652
 
624
- module.exports = { cmdSync, migrateCursorUnknownBuckets, CURSOR_UNKNOWN_MIGRATION_KEY };
653
+ module.exports = {
654
+ cmdSync,
655
+ migrateCursorUnknownBuckets,
656
+ migrateRolloutCumulativeDeltaBuckets,
657
+ CURSOR_UNKNOWN_MIGRATION_KEY,
658
+ ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
659
+ };
625
660
 
626
661
  function normalizeString(value) {
627
662
  if (typeof value !== "string") return null;
@@ -1034,3 +1069,67 @@ async function migrateCursorUnknownBuckets({ cursors, queuePath }) {
1034
1069
 
1035
1070
  cursors.migrations[CURSOR_UNKNOWN_MIGRATION_KEY] = new Date().toISOString();
1036
1071
  }
1072
+
1073
+ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles }) {
1074
+ if (!cursors || typeof cursors !== "object") return;
1075
+ cursors.migrations = cursors.migrations || {};
1076
+ if (cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY]) return;
1077
+
1078
+ const rolloutPathSources = new Map();
1079
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
1080
+ const filePath = typeof entry === "string" ? entry : entry?.path;
1081
+ const source = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
1082
+ if (!filePath) continue;
1083
+ if (source === "codex" || source === "every-code") {
1084
+ rolloutPathSources.set(filePath, source);
1085
+ }
1086
+ }
1087
+
1088
+ if (cursors.files && typeof cursors.files === "object") {
1089
+ for (const filePath of rolloutPathSources.keys()) {
1090
+ delete cursors.files[filePath];
1091
+ }
1092
+ }
1093
+
1094
+ const buckets = cursors.hourly?.buckets;
1095
+ const retractions = [];
1096
+ if (buckets && typeof buckets === "object") {
1097
+ for (const key of Object.keys(buckets)) {
1098
+ const [source, model, ...hourParts] = key.split("|");
1099
+ if (source !== "codex" && source !== "every-code") continue;
1100
+ const hourStart = hourParts.join("|");
1101
+ retractions.push(
1102
+ JSON.stringify({
1103
+ source,
1104
+ model: model || "unknown",
1105
+ hour_start: hourStart,
1106
+ input_tokens: 0,
1107
+ cached_input_tokens: 0,
1108
+ cache_creation_input_tokens: 0,
1109
+ output_tokens: 0,
1110
+ reasoning_output_tokens: 0,
1111
+ total_tokens: 0,
1112
+ billable_total_tokens: 0,
1113
+ conversation_count: 0,
1114
+ }),
1115
+ );
1116
+ delete buckets[key];
1117
+ }
1118
+ }
1119
+
1120
+ const groupQueued = cursors.hourly?.groupQueued;
1121
+ if (groupQueued && typeof groupQueued === "object") {
1122
+ for (const key of Object.keys(groupQueued)) {
1123
+ if (key.startsWith("codex|") || key.startsWith("every-code|")) {
1124
+ delete groupQueued[key];
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ if (retractions.length > 0) {
1130
+ await ensureDir(path.dirname(queuePath));
1131
+ await fs.appendFile(queuePath, retractions.join("\n") + "\n");
1132
+ }
1133
+
1134
+ cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
1135
+ }
@@ -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