tokentracker-cli 0.5.98 → 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.
@@ -0,0 +1,81 @@
1
+ const cp = require("node:child_process");
2
+
3
+ function hasProxyEnv(env = process.env) {
4
+ return Boolean(
5
+ env.HTTPS_PROXY ||
6
+ env.https_proxy ||
7
+ env.HTTP_PROXY ||
8
+ env.http_proxy ||
9
+ env.ALL_PROXY ||
10
+ env.all_proxy,
11
+ );
12
+ }
13
+
14
+ function parseMacProxyOutput(output) {
15
+ const values = {};
16
+ for (const line of String(output || "").split(/\r?\n/)) {
17
+ const match = line.match(/^\s*([A-Za-z]+)\s*:\s*(.+?)\s*$/);
18
+ if (match) values[match[1]] = match[2];
19
+ }
20
+ if (values.HTTPSEnable !== "1" || !values.HTTPSProxy || !values.HTTPSPort) return null;
21
+ return `http://${values.HTTPSProxy}:${values.HTTPSPort}`;
22
+ }
23
+
24
+ function resolveSystemProxyEnv({ env = process.env, platform = process.platform, commandRunner = cp.spawnSync } = {}) {
25
+ const out = {};
26
+ if (hasProxyEnv(env)) {
27
+ out.NODE_USE_ENV_PROXY = env.NODE_USE_ENV_PROXY || "1";
28
+ return out;
29
+ }
30
+
31
+ if (platform !== "darwin") return null;
32
+ const result = commandRunner("scutil", ["--proxy"], {
33
+ encoding: "utf8",
34
+ timeout: 2000,
35
+ });
36
+ if (result?.error || result?.status !== 0) return null;
37
+ const proxyUrl = parseMacProxyOutput(result.stdout);
38
+ if (!proxyUrl) return null;
39
+
40
+ return {
41
+ NODE_USE_ENV_PROXY: "1",
42
+ HTTPS_PROXY: proxyUrl,
43
+ HTTP_PROXY: proxyUrl,
44
+ };
45
+ }
46
+
47
+ function shouldRelaunchForProxy(argv, env = process.env) {
48
+ if (env.TOKENTRACKER_PROXY_ENV_APPLIED === "1") return false;
49
+ const command = Array.isArray(argv) ? argv[0] : null;
50
+ return !command || command === "serve";
51
+ }
52
+
53
+ function relaunchWithProxyEnvIfNeeded({
54
+ argv,
55
+ originalArgv,
56
+ env = process.env,
57
+ platform = process.platform,
58
+ commandRunner = cp.spawnSync,
59
+ nodePath = process.execPath,
60
+ } = {}) {
61
+ if (!shouldRelaunchForProxy(argv, env)) return null;
62
+ const proxyEnv = resolveSystemProxyEnv({ env, platform, commandRunner });
63
+ if (!proxyEnv || proxyEnv.NODE_USE_ENV_PROXY === env.NODE_USE_ENV_PROXY) return null;
64
+
65
+ const childEnv = {
66
+ ...env,
67
+ ...proxyEnv,
68
+ TOKENTRACKER_PROXY_ENV_APPLIED: "1",
69
+ };
70
+ return commandRunner(nodePath, originalArgv, {
71
+ stdio: "inherit",
72
+ env: childEnv,
73
+ });
74
+ }
75
+
76
+ module.exports = {
77
+ hasProxyEnv,
78
+ parseMacProxyOutput,
79
+ resolveSystemProxyEnv,
80
+ relaunchWithProxyEnvIfNeeded,
81
+ };
@@ -1244,6 +1244,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1244
1244
  output_tokens: zeroTotals.output_tokens,
1245
1245
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1246
1246
  total_tokens: zeroTotals.total_tokens,
1247
+ billable_total_tokens: zeroTotals.billable_total_tokens,
1247
1248
  conversation_count: zeroTotals.conversation_count,
1248
1249
  }),
1249
1250
  );
@@ -1265,6 +1266,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1265
1266
  output_tokens: zeroTotals.output_tokens,
1266
1267
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1267
1268
  total_tokens: zeroTotals.total_tokens,
1269
+ billable_total_tokens: zeroTotals.billable_total_tokens,
1268
1270
  conversation_count: zeroTotals.conversation_count,
1269
1271
  }),
1270
1272
  );
@@ -1292,6 +1294,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1292
1294
  output_tokens: totals.output_tokens,
1293
1295
  reasoning_output_tokens: totals.reasoning_output_tokens,
1294
1296
  total_tokens: totals.total_tokens,
1297
+ billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
1295
1298
  conversation_count: totals.conversation_count,
1296
1299
  }),
1297
1300
  );
@@ -1318,6 +1321,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1318
1321
  output_tokens: zeroTotals.output_tokens,
1319
1322
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1320
1323
  total_tokens: zeroTotals.total_tokens,
1324
+ billable_total_tokens: zeroTotals.billable_total_tokens,
1321
1325
  conversation_count: zeroTotals.conversation_count,
1322
1326
  }),
1323
1327
  );
@@ -1340,6 +1344,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1340
1344
  output_tokens: zeroTotals.output_tokens,
1341
1345
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1342
1346
  total_tokens: zeroTotals.total_tokens,
1347
+ billable_total_tokens: zeroTotals.billable_total_tokens,
1343
1348
  conversation_count: zeroTotals.conversation_count,
1344
1349
  }),
1345
1350
  );
@@ -1361,6 +1366,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1361
1366
  output_tokens: unknownBucket.totals.output_tokens,
1362
1367
  reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
1363
1368
  total_tokens: unknownBucket.totals.total_tokens,
1369
+ billable_total_tokens: unknownBucket.totals.billable_total_tokens ?? unknownBucket.totals.total_tokens,
1364
1370
  conversation_count: unknownBucket.totals.conversation_count,
1365
1371
  }),
1366
1372
  );
@@ -1407,6 +1413,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1407
1413
  output_tokens: group.totals.output_tokens,
1408
1414
  reasoning_output_tokens: group.totals.reasoning_output_tokens,
1409
1415
  total_tokens: group.totals.total_tokens,
1416
+ billable_total_tokens: group.totals.billable_total_tokens ?? group.totals.total_tokens,
1410
1417
  conversation_count: group.totals.conversation_count,
1411
1418
  }),
1412
1419
  );
@@ -1461,6 +1468,7 @@ async function enqueueTouchedProjectBuckets({
1461
1468
  output_tokens: totals.output_tokens,
1462
1469
  reasoning_output_tokens: totals.reasoning_output_tokens,
1463
1470
  total_tokens: totals.total_tokens,
1471
+ billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
1464
1472
  conversation_count: totals.conversation_count,
1465
1473
  }),
1466
1474
  );
@@ -1716,6 +1724,7 @@ function initTotals() {
1716
1724
  output_tokens: 0,
1717
1725
  reasoning_output_tokens: 0,
1718
1726
  total_tokens: 0,
1727
+ billable_total_tokens: 0,
1719
1728
  conversation_count: 0,
1720
1729
  };
1721
1730
  }
@@ -1727,6 +1736,7 @@ function addTotals(target, delta) {
1727
1736
  target.output_tokens += delta.output_tokens || 0;
1728
1737
  target.reasoning_output_tokens += delta.reasoning_output_tokens || 0;
1729
1738
  target.total_tokens += delta.total_tokens || 0;
1739
+ target.billable_total_tokens += delta.billable_total_tokens ?? delta.total_tokens ?? 0;
1730
1740
  target.conversation_count += delta.conversation_count || 0;
1731
1741
  }
1732
1742
 
@@ -1738,6 +1748,7 @@ function totalsKey(totals) {
1738
1748
  totals.output_tokens || 0,
1739
1749
  totals.reasoning_output_tokens || 0,
1740
1750
  totals.total_tokens || 0,
1751
+ totals.billable_total_tokens ?? totals.total_tokens ?? 0,
1741
1752
  totals.conversation_count || 0,
1742
1753
  ].join("|");
1743
1754
  }
@@ -2056,7 +2067,9 @@ function normalizeGeminiTokens(tokens) {
2056
2067
  const output = toNonNegativeInt(tokens.output);
2057
2068
  const tool = toNonNegativeInt(tokens.tool);
2058
2069
  const thoughts = toNonNegativeInt(tokens.thoughts);
2059
- const total = toNonNegativeInt(tokens.total);
2070
+ const reportedTotal = toNonNegativeInt(tokens.total);
2071
+ const computedTotal = input + cached + output + tool + thoughts;
2072
+ const total = Math.max(reportedTotal, computedTotal);
2060
2073
 
2061
2074
  return {
2062
2075
  input_tokens: input,
@@ -2150,26 +2163,18 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
2150
2163
  const hasTotal = isNonEmptyObject(totalUsage);
2151
2164
  const hasPrevTotals = isNonEmptyObject(prevTotals);
2152
2165
 
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
- if (!hasLast && hasTotal && hasPrevTotals && totalsReset(totalUsage, prevTotals)) {
2160
- const normalized = normalizeUsage(totalUsage);
2161
- return isAllZeroUsage(normalized) ? null : normalized;
2162
- }
2163
-
2164
- if (hasLast) {
2165
- return normalizeUsage(lastUsage);
2166
- }
2167
-
2168
2166
  if (hasTotal && hasPrevTotals) {
2167
+ if (totalsReset(totalUsage, prevTotals)) {
2168
+ const resetUsage = hasLast ? lastUsage : totalUsage;
2169
+ const normalized = normalizeUsage(resetUsage);
2170
+ return isAllZeroUsage(normalized) ? null : normalized;
2171
+ }
2172
+
2169
2173
  const delta = {};
2170
2174
  for (const k of [
2171
2175
  "input_tokens",
2172
2176
  "cached_input_tokens",
2177
+ "cache_creation_input_tokens",
2173
2178
  "output_tokens",
2174
2179
  "reasoning_output_tokens",
2175
2180
  "total_tokens",
@@ -2182,6 +2187,11 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
2182
2187
  return isAllZeroUsage(normalized) ? null : normalized;
2183
2188
  }
2184
2189
 
2190
+ if (hasLast) {
2191
+ const normalized = normalizeUsage(lastUsage);
2192
+ return isAllZeroUsage(normalized) ? null : normalized;
2193
+ }
2194
+
2185
2195
  if (hasTotal) {
2186
2196
  const normalized = normalizeUsage(totalUsage);
2187
2197
  return isAllZeroUsage(normalized) ? null : normalized;
@@ -2554,21 +2564,31 @@ async function parseCursorApiIncremental({
2554
2564
  const hourlyState = normalizeHourlyState(cursors?.hourly);
2555
2565
  const touchedBuckets = new Set();
2556
2566
 
2557
- // Incremental: skip records we already processed
2567
+ // Cursor's CSV is an account-level API export, not an append-only local log.
2568
+ // Treat the fetched CSV as authoritative so historical backfills and row
2569
+ // corrections replace prior local bucket totals instead of being skipped.
2558
2570
  const lastTs = cursors?.cursorApi?.lastRecordTimestamp || null;
2559
2571
  let latestTs = lastTs;
2560
2572
  let eventsAggregated = 0;
2561
2573
  const cb = typeof onProgress === "function" ? onProgress : null;
2562
2574
  const total = records.length;
2563
2575
 
2576
+ if (records.length > 0) {
2577
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
2578
+ const parsed = parseBucketKey(key);
2579
+ const sourceKey = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
2580
+ if (sourceKey !== defaultSource) continue;
2581
+ if (!bucket?.totals) continue;
2582
+ bucket.totals = initTotals();
2583
+ touchedBuckets.add(key);
2584
+ }
2585
+ }
2586
+
2564
2587
  for (let i = 0; i < records.length; i++) {
2565
2588
  const record = records[i];
2566
2589
  const recordDate = record.date;
2567
2590
  if (!recordDate) continue;
2568
2591
 
2569
- // Skip records we already processed (CSV is ordered newest-first)
2570
- if (lastTs && recordDate <= lastTs) continue;
2571
-
2572
2592
  const { normalizeCursorUsage } = require("./cursor-config");
2573
2593
  const delta = normalizeCursorUsage(record);
2574
2594
  if (isAllZeroUsage(delta)) continue;
@@ -0,0 +1,46 @@
1
+ const ACCOUNT_LEVEL_SOURCES = new Set(["cursor"]);
2
+
3
+ function normalizeSource(value) {
4
+ return String(value || "").trim().toLowerCase();
5
+ }
6
+
7
+ function getSourceScope(source) {
8
+ return ACCOUNT_LEVEL_SOURCES.has(normalizeSource(source)) ? "account" : "local";
9
+ }
10
+
11
+ function isAccountLevelSource(source) {
12
+ return getSourceScope(source) === "account";
13
+ }
14
+
15
+ function normalizeUsageScope(value) {
16
+ const raw = String(value || "").trim().toLowerCase();
17
+ return raw === "all" || raw === "raw" ? "all" : "personal";
18
+ }
19
+
20
+ function filterRowsByUsageScope(rows, scope = "personal") {
21
+ const normalizedScope = normalizeUsageScope(scope);
22
+ if (normalizedScope === "all") return Array.isArray(rows) ? rows : [];
23
+ return (Array.isArray(rows) ? rows : []).filter((row) => !isAccountLevelSource(row?.source));
24
+ }
25
+
26
+ function listExcludedSources(rows, scope = "personal") {
27
+ const normalizedScope = normalizeUsageScope(scope);
28
+ if (normalizedScope === "all") return [];
29
+ const seen = new Set();
30
+ const out = [];
31
+ for (const row of Array.isArray(rows) ? rows : []) {
32
+ const source = normalizeSource(row?.source);
33
+ if (!source || seen.has(source) || !isAccountLevelSource(source)) continue;
34
+ seen.add(source);
35
+ out.push({ source, source_scope: getSourceScope(source), reason: "account_level_source" });
36
+ }
37
+ return out.sort((a, b) => a.source.localeCompare(b.source));
38
+ }
39
+
40
+ module.exports = {
41
+ getSourceScope,
42
+ isAccountLevelSource,
43
+ normalizeUsageScope,
44
+ filterRowsByUsageScope,
45
+ listExcludedSources,
46
+ };