tokentracker-cli 0.2.26 → 0.3.0

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.
@@ -12,14 +12,7 @@ const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
12
12
 
13
13
  function resolveQueuePath() {
14
14
  const home = os.homedir();
15
- const candidates = [
16
- path.join(home, ".tokentracker", "tracker", "queue.jsonl"),
17
- path.join(home, ".vibeusage", "tracker", "queue.jsonl"),
18
- ];
19
- for (const p of candidates) {
20
- if (fs.existsSync(p)) return p;
21
- }
22
- return candidates[0];
15
+ return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
23
16
  }
24
17
 
25
18
  function readQueueData(queuePath) {
@@ -46,6 +39,7 @@ function aggregateByDay(rows) {
46
39
  input_tokens: 0,
47
40
  output_tokens: 0,
48
41
  cached_input_tokens: 0,
42
+ cache_creation_input_tokens: 0,
49
43
  reasoning_output_tokens: 0,
50
44
  conversation_count: 0,
51
45
  });
@@ -56,12 +50,118 @@ function aggregateByDay(rows) {
56
50
  a.input_tokens += row.input_tokens || 0;
57
51
  a.output_tokens += row.output_tokens || 0;
58
52
  a.cached_input_tokens += row.cached_input_tokens || 0;
53
+ a.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
59
54
  a.reasoning_output_tokens += row.reasoning_output_tokens || 0;
60
55
  a.conversation_count += row.conversation_count || 0;
61
56
  }
62
57
  return Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
63
58
  }
64
59
 
60
+ function getTimeZoneContext(url) {
61
+ const tz = String(url.searchParams.get("tz") || "").trim();
62
+ const rawOffset = Number(url.searchParams.get("tz_offset_minutes"));
63
+ return {
64
+ timeZone: tz || null,
65
+ offsetMinutes: Number.isFinite(rawOffset) ? Math.trunc(rawOffset) : null,
66
+ };
67
+ }
68
+
69
+ function getZonedParts(date, { timeZone, offsetMinutes } = {}) {
70
+ const dt = date instanceof Date ? date : new Date(date);
71
+ if (!Number.isFinite(dt.getTime())) return null;
72
+
73
+ if (timeZone && typeof Intl !== "undefined" && Intl.DateTimeFormat) {
74
+ try {
75
+ const formatter = new Intl.DateTimeFormat("en-CA", {
76
+ timeZone,
77
+ year: "numeric",
78
+ month: "2-digit",
79
+ day: "2-digit",
80
+ hour: "2-digit",
81
+ minute: "2-digit",
82
+ second: "2-digit",
83
+ hourCycle: "h23",
84
+ });
85
+ const parts = formatter.formatToParts(dt);
86
+ const values = parts.reduce((acc, part) => {
87
+ if (part.type && part.value) acc[part.type] = part.value;
88
+ return acc;
89
+ }, {});
90
+ const year = Number(values.year);
91
+ const month = Number(values.month);
92
+ const day = Number(values.day);
93
+ const hour = Number(values.hour);
94
+ const minute = Number(values.minute);
95
+ const second = Number(values.second);
96
+ if ([year, month, day, hour, minute, second].every(Number.isFinite)) {
97
+ return { year, month, day, hour, minute, second };
98
+ }
99
+ } catch (_e) {
100
+ // fall through
101
+ }
102
+ }
103
+
104
+ if (Number.isFinite(offsetMinutes)) {
105
+ const shifted = new Date(dt.getTime() + offsetMinutes * 60 * 1000);
106
+ return {
107
+ year: shifted.getUTCFullYear(),
108
+ month: shifted.getUTCMonth() + 1,
109
+ day: shifted.getUTCDate(),
110
+ hour: shifted.getUTCHours(),
111
+ minute: shifted.getUTCMinutes(),
112
+ second: shifted.getUTCSeconds(),
113
+ };
114
+ }
115
+
116
+ return {
117
+ year: dt.getFullYear(),
118
+ month: dt.getMonth() + 1,
119
+ day: dt.getDate(),
120
+ hour: dt.getHours(),
121
+ minute: dt.getMinutes(),
122
+ second: dt.getSeconds(),
123
+ };
124
+ }
125
+
126
+ function formatPartsDayKey(parts) {
127
+ if (!parts) return "";
128
+ return `${parts.year}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
129
+ }
130
+
131
+ function aggregateHourlyByDay(rows, dayKey, timeZoneContext) {
132
+ const byHour = new Map();
133
+ for (const row of rows) {
134
+ if (!row.hour_start) continue;
135
+ const parts = getZonedParts(new Date(row.hour_start), timeZoneContext);
136
+ if (!parts) continue;
137
+ if (formatPartsDayKey(parts) !== dayKey) continue;
138
+ const hourKey = `${dayKey}T${String(parts.hour).padStart(2, "0")}:00:00`;
139
+ if (!byHour.has(hourKey)) {
140
+ byHour.set(hourKey, {
141
+ hour: hourKey,
142
+ total_tokens: 0,
143
+ billable_total_tokens: 0,
144
+ input_tokens: 0,
145
+ output_tokens: 0,
146
+ cached_input_tokens: 0,
147
+ cache_creation_input_tokens: 0,
148
+ reasoning_output_tokens: 0,
149
+ conversation_count: 0,
150
+ });
151
+ }
152
+ const bucket = byHour.get(hourKey);
153
+ bucket.total_tokens += row.total_tokens || 0;
154
+ bucket.billable_total_tokens += row.total_tokens || 0;
155
+ bucket.input_tokens += row.input_tokens || 0;
156
+ bucket.output_tokens += row.output_tokens || 0;
157
+ bucket.cached_input_tokens += row.cached_input_tokens || 0;
158
+ bucket.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
159
+ bucket.reasoning_output_tokens += row.reasoning_output_tokens || 0;
160
+ bucket.conversation_count += row.conversation_count || 0;
161
+ }
162
+ return Array.from(byHour.values()).sort((a, b) => a.hour.localeCompare(b.hour));
163
+ }
164
+
65
165
  // ---------------------------------------------------------------------------
66
166
  // Sync helper
67
167
  // ---------------------------------------------------------------------------
@@ -261,11 +361,12 @@ function createLocalApiHandler({ queuePath }) {
261
361
  acc.input_tokens += r.input_tokens;
262
362
  acc.output_tokens += r.output_tokens;
263
363
  acc.cached_input_tokens += r.cached_input_tokens;
364
+ acc.cache_creation_input_tokens += r.cache_creation_input_tokens;
264
365
  acc.reasoning_output_tokens += r.reasoning_output_tokens;
265
366
  acc.conversation_count += r.conversation_count;
266
367
  return acc;
267
368
  },
268
- { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0 },
369
+ { 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 },
269
370
  );
270
371
  const totalCost = (totals.total_tokens * 1.75) / 1_000_000;
271
372
 
@@ -379,22 +480,24 @@ function createLocalApiHandler({ queuePath }) {
379
480
  const src = row.source || "unknown";
380
481
  const mdl = row.model || "unknown";
381
482
  if (!bySource.has(src))
382
- bySource.set(src, { source: src, totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" }, models: new Map() });
483
+ 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() });
383
484
  const sa = bySource.get(src);
384
485
  sa.totals.total_tokens += row.total_tokens || 0;
385
486
  sa.totals.billable_total_tokens += row.total_tokens || 0;
386
487
  sa.totals.input_tokens += row.input_tokens || 0;
387
488
  sa.totals.output_tokens += row.output_tokens || 0;
388
489
  sa.totals.cached_input_tokens += row.cached_input_tokens || 0;
490
+ sa.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
389
491
  sa.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
390
492
  if (!sa.models.has(mdl))
391
- 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, reasoning_output_tokens: 0, total_cost_usd: "0" } });
493
+ 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" } });
392
494
  const ma = sa.models.get(mdl);
393
495
  ma.totals.total_tokens += row.total_tokens || 0;
394
496
  ma.totals.billable_total_tokens += row.total_tokens || 0;
395
497
  ma.totals.input_tokens += row.input_tokens || 0;
396
498
  ma.totals.output_tokens += row.output_tokens || 0;
397
499
  ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
500
+ ma.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
398
501
  ma.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
399
502
  }
400
503
 
@@ -463,17 +566,9 @@ function createLocalApiHandler({ queuePath }) {
463
566
  // --- usage-hourly (stub for day-view) ---
464
567
  if (p === "/functions/vibeusage-usage-hourly") {
465
568
  const day = url.searchParams.get("day") || new Date().toISOString().slice(0, 10);
466
- const rows = readQueueData(qp).filter((r) => r.hour_start && r.hour_start.startsWith(day));
467
- const data = rows.map((r) => ({
468
- hour: r.hour_start,
469
- total_tokens: r.total_tokens || 0,
470
- billable_total_tokens: r.total_tokens || 0,
471
- input_tokens: r.input_tokens || 0,
472
- output_tokens: r.output_tokens || 0,
473
- cached_input_tokens: r.cached_input_tokens || 0,
474
- reasoning_output_tokens: r.reasoning_output_tokens || 0,
475
- conversation_count: r.conversation_count || 0,
476
- }));
569
+ const timeZoneContext = getTimeZoneContext(url);
570
+ const rows = readQueueData(qp);
571
+ const data = aggregateHourlyByDay(rows, day, timeZoneContext);
477
572
  json(res, { day, data });
478
573
  return true;
479
574
  }
@@ -490,13 +585,14 @@ function createLocalApiHandler({ queuePath }) {
490
585
  if (day < from || day > to) continue;
491
586
  const month = day.slice(0, 7);
492
587
  if (!byMonth.has(month))
493
- byMonth.set(month, { month, total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0 });
588
+ 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 });
494
589
  const a = byMonth.get(month);
495
590
  a.total_tokens += row.total_tokens || 0;
496
591
  a.billable_total_tokens += row.total_tokens || 0;
497
592
  a.input_tokens += row.input_tokens || 0;
498
593
  a.output_tokens += row.output_tokens || 0;
499
594
  a.cached_input_tokens += row.cached_input_tokens || 0;
595
+ a.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
500
596
  a.reasoning_output_tokens += row.reasoning_output_tokens || 0;
501
597
  a.conversation_count += row.conversation_count || 0;
502
598
  }
@@ -206,6 +206,7 @@ async function parseClaudeIncremental({
206
206
  const projectMetaCache = projectEnabled ? new Map() : null;
207
207
  const publicRepoCache = projectEnabled ? new Map() : null;
208
208
  const touchedBuckets = new Set();
209
+ const seenMessageHashes = new Set();
209
210
  const defaultSource = normalizeSourceInput(source) || "claude";
210
211
 
211
212
  if (!cursors.files || typeof cursors.files !== "object") {
@@ -250,6 +251,7 @@ async function parseClaudeIncremental({
250
251
  projectTouchedBuckets,
251
252
  projectRef,
252
253
  projectKey,
254
+ seenMessageHashes,
253
255
  });
254
256
 
255
257
  cursors.files[key] = {
@@ -822,6 +824,7 @@ async function parseClaudeFile({
822
824
  projectTouchedBuckets,
823
825
  projectRef,
824
826
  projectKey,
827
+ seenMessageHashes,
825
828
  }) {
826
829
  const st = await fs.stat(filePath).catch(() => null);
827
830
  if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
@@ -833,8 +836,39 @@ async function parseClaudeFile({
833
836
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
834
837
 
835
838
  let eventsAggregated = 0;
839
+ const isMainSession = !filePath.includes("/subagents/");
836
840
  for await (const line of rl) {
837
- if (!line || !line.includes('\"usage\"')) continue;
841
+ if (!line) continue;
842
+
843
+ // Count user-typed messages as conversations (main sessions only).
844
+ // Exclude tool_result messages — those are auto-generated by tool calls,
845
+ // not manually typed by the user. Only count messages with a "text" block.
846
+ if (isMainSession && line.includes('"type":"user"')) {
847
+ let userObj;
848
+ try {
849
+ userObj = JSON.parse(line);
850
+ } catch (_e) {
851
+ /* skip */
852
+ }
853
+ if (userObj?.type === "user") {
854
+ const content = userObj?.message?.content;
855
+ const hasText =
856
+ typeof content === "string" ||
857
+ (Array.isArray(content) && content.some((b) => b?.type === "text"));
858
+ if (hasText) {
859
+ const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
860
+ const userBucketStart = userTs ? toUtcHalfHourStart(userTs) : null;
861
+ if (userBucketStart) {
862
+ const userModel = DEFAULT_MODEL;
863
+ const userBucket = getHourlyBucket(hourlyState, source, userModel, userBucketStart);
864
+ userBucket.totals.conversation_count += 1;
865
+ touchedBuckets.add(bucketKey(source, userModel, userBucketStart));
866
+ }
867
+ }
868
+ }
869
+ }
870
+
871
+ if (!line.includes('"usage"')) continue;
838
872
  let obj;
839
873
  try {
840
874
  obj = JSON.parse(line);
@@ -845,13 +879,23 @@ async function parseClaudeFile({
845
879
  const usage = obj?.message?.usage || obj?.usage;
846
880
  if (!usage || typeof usage !== "object") continue;
847
881
 
882
+ if (seenMessageHashes) {
883
+ const msgId = obj?.message?.id;
884
+ const reqId = obj?.requestId;
885
+ if (msgId && reqId) {
886
+ const hash = `${msgId}:${reqId}`;
887
+ if (seenMessageHashes.has(hash)) continue;
888
+ seenMessageHashes.add(hash);
889
+ }
890
+ }
891
+
848
892
  const model = normalizeModelInput(obj?.message?.model || obj?.model) || DEFAULT_MODEL;
849
893
  const tokenTimestamp = typeof obj?.timestamp === "string" ? obj.timestamp : null;
850
894
  if (!tokenTimestamp) continue;
851
895
 
852
896
  const delta = normalizeClaudeUsage(usage);
853
897
  if (!delta || isAllZeroUsage(delta)) continue;
854
- delta.conversation_count = 1;
898
+ delta.conversation_count = 0;
855
899
 
856
900
  const bucketStart = toUtcHalfHourStart(tokenTimestamp);
857
901
  if (!bucketStart) continue;
@@ -1180,6 +1224,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1180
1224
  hour_start: group.hourStart,
1181
1225
  input_tokens: zeroTotals.input_tokens,
1182
1226
  cached_input_tokens: zeroTotals.cached_input_tokens,
1227
+ cache_creation_input_tokens: zeroTotals.cache_creation_input_tokens,
1183
1228
  output_tokens: zeroTotals.output_tokens,
1184
1229
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1185
1230
  total_tokens: zeroTotals.total_tokens,
@@ -1227,6 +1272,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1227
1272
  hour_start: group.hourStart,
1228
1273
  input_tokens: totals.input_tokens,
1229
1274
  cached_input_tokens: totals.cached_input_tokens,
1275
+ cache_creation_input_tokens: totals.cache_creation_input_tokens,
1230
1276
  output_tokens: totals.output_tokens,
1231
1277
  reasoning_output_tokens: totals.reasoning_output_tokens,
1232
1278
  total_tokens: totals.total_tokens,
@@ -1274,6 +1320,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1274
1320
  hour_start: group.hourStart,
1275
1321
  input_tokens: zeroTotals.input_tokens,
1276
1322
  cached_input_tokens: zeroTotals.cached_input_tokens,
1323
+ cache_creation_input_tokens: zeroTotals.cache_creation_input_tokens,
1277
1324
  output_tokens: zeroTotals.output_tokens,
1278
1325
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1279
1326
  total_tokens: zeroTotals.total_tokens,
@@ -1294,6 +1341,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1294
1341
  hour_start: group.hourStart,
1295
1342
  input_tokens: unknownBucket.totals.input_tokens,
1296
1343
  cached_input_tokens: unknownBucket.totals.cached_input_tokens,
1344
+ cache_creation_input_tokens: unknownBucket.totals.cache_creation_input_tokens,
1297
1345
  output_tokens: unknownBucket.totals.output_tokens,
1298
1346
  reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
1299
1347
  total_tokens: unknownBucket.totals.total_tokens,
@@ -1339,6 +1387,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1339
1387
  hour_start: group.hourStart,
1340
1388
  input_tokens: group.totals.input_tokens,
1341
1389
  cached_input_tokens: group.totals.cached_input_tokens,
1390
+ cache_creation_input_tokens: group.totals.cache_creation_input_tokens,
1342
1391
  output_tokens: group.totals.output_tokens,
1343
1392
  reasoning_output_tokens: group.totals.reasoning_output_tokens,
1344
1393
  total_tokens: group.totals.total_tokens,
@@ -1392,6 +1441,7 @@ async function enqueueTouchedProjectBuckets({
1392
1441
  hour_start: bucket.hour_start,
1393
1442
  input_tokens: totals.input_tokens,
1394
1443
  cached_input_tokens: totals.cached_input_tokens,
1444
+ cache_creation_input_tokens: totals.cache_creation_input_tokens,
1395
1445
  output_tokens: totals.output_tokens,
1396
1446
  reasoning_output_tokens: totals.reasoning_output_tokens,
1397
1447
  total_tokens: totals.total_tokens,
@@ -1646,6 +1696,7 @@ function initTotals() {
1646
1696
  return {
1647
1697
  input_tokens: 0,
1648
1698
  cached_input_tokens: 0,
1699
+ cache_creation_input_tokens: 0,
1649
1700
  output_tokens: 0,
1650
1701
  reasoning_output_tokens: 0,
1651
1702
  total_tokens: 0,
@@ -1656,6 +1707,7 @@ function initTotals() {
1656
1707
  function addTotals(target, delta) {
1657
1708
  target.input_tokens += delta.input_tokens || 0;
1658
1709
  target.cached_input_tokens += delta.cached_input_tokens || 0;
1710
+ target.cache_creation_input_tokens += delta.cache_creation_input_tokens || 0;
1659
1711
  target.output_tokens += delta.output_tokens || 0;
1660
1712
  target.reasoning_output_tokens += delta.reasoning_output_tokens || 0;
1661
1713
  target.total_tokens += delta.total_tokens || 0;
@@ -1666,6 +1718,7 @@ function totalsKey(totals) {
1666
1718
  return [
1667
1719
  totals.input_tokens || 0,
1668
1720
  totals.cached_input_tokens || 0,
1721
+ totals.cache_creation_input_tokens || 0,
1669
1722
  totals.output_tokens || 0,
1670
1723
  totals.reasoning_output_tokens || 0,
1671
1724
  totals.total_tokens || 0,
@@ -1998,6 +2051,7 @@ function normalizeGeminiTokens(tokens) {
1998
2051
  return {
1999
2052
  input_tokens: input,
2000
2053
  cached_input_tokens: cached,
2054
+ cache_creation_input_tokens: 0,
2001
2055
  output_tokens: output + tool,
2002
2056
  reasoning_output_tokens: thoughts,
2003
2057
  total_tokens: total,
@@ -2011,12 +2065,12 @@ function normalizeOpencodeTokens(tokens) {
2011
2065
  const reasoning = toNonNegativeInt(tokens.reasoning);
2012
2066
  const cached = toNonNegativeInt(tokens.cache?.read);
2013
2067
  const cacheWrite = toNonNegativeInt(tokens.cache?.write);
2014
- // cache tokens (read/write) excluded from input_tokens and total_tokens
2015
- const total = input + output + reasoning;
2068
+ const total = input + output + reasoning + cached + cacheWrite;
2016
2069
 
2017
2070
  return {
2018
2071
  input_tokens: input,
2019
2072
  cached_input_tokens: cached,
2073
+ cache_creation_input_tokens: cacheWrite,
2020
2074
  output_tokens: output,
2021
2075
  reasoning_output_tokens: reasoning,
2022
2076
  total_tokens: total,
@@ -2122,6 +2176,7 @@ function normalizeUsage(u) {
2122
2176
  for (const k of [
2123
2177
  "input_tokens",
2124
2178
  "cached_input_tokens",
2179
+ "cache_creation_input_tokens",
2125
2180
  "output_tokens",
2126
2181
  "reasoning_output_tokens",
2127
2182
  "total_tokens",
@@ -2135,13 +2190,13 @@ function normalizeUsage(u) {
2135
2190
  function normalizeClaudeUsage(u) {
2136
2191
  const inputTokens = toNonNegativeInt(u?.input_tokens);
2137
2192
  const outputTokens = toNonNegativeInt(u?.output_tokens);
2138
- const hasTotal = u && Object.prototype.hasOwnProperty.call(u, "total_tokens");
2139
- const totalTokens = hasTotal
2140
- ? toNonNegativeInt(u?.total_tokens)
2141
- : inputTokens + outputTokens;
2193
+ const cacheCreation = toNonNegativeInt(u?.cache_creation_input_tokens);
2194
+ const cacheRead = toNonNegativeInt(u?.cache_read_input_tokens);
2195
+ const totalTokens = inputTokens + outputTokens + cacheCreation + cacheRead;
2142
2196
  return {
2143
2197
  input_tokens: inputTokens,
2144
- cached_input_tokens: toNonNegativeInt(u?.cache_read_input_tokens),
2198
+ cached_input_tokens: cacheRead,
2199
+ cache_creation_input_tokens: cacheCreation,
2145
2200
  output_tokens: outputTokens,
2146
2201
  reasoning_output_tokens: 0,
2147
2202
  total_tokens: totalTokens,
@@ -2157,6 +2212,7 @@ function isAllZeroUsage(u) {
2157
2212
  for (const k of [
2158
2213
  "input_tokens",
2159
2214
  "cached_input_tokens",
2215
+ "cache_creation_input_tokens",
2160
2216
  "output_tokens",
2161
2217
  "reasoning_output_tokens",
2162
2218
  "total_tokens",
@@ -2170,6 +2226,7 @@ function sameUsage(a, b) {
2170
2226
  for (const k of [
2171
2227
  "input_tokens",
2172
2228
  "cached_input_tokens",
2229
+ "cache_creation_input_tokens",
2173
2230
  "output_tokens",
2174
2231
  "reasoning_output_tokens",
2175
2232
  "total_tokens",
@@ -2457,6 +2514,88 @@ async function resolveProjectContextForDb({
2457
2514
  });
2458
2515
  }
2459
2516
 
2517
+ // ── Cursor (API-based) ──
2518
+
2519
+ /**
2520
+ * Incremental parser for Cursor usage data fetched via API.
2521
+ *
2522
+ * Unlike other parsers that read local files, this one receives pre-parsed
2523
+ * CSV records from cursor-config.js and aggregates them into 30-min buckets.
2524
+ *
2525
+ * Incremental state is tracked in `cursors.cursorApi.lastRecordTimestamp`.
2526
+ */
2527
+ async function parseCursorApiIncremental({
2528
+ records,
2529
+ cursors,
2530
+ queuePath,
2531
+ onProgress,
2532
+ source,
2533
+ }) {
2534
+ await ensureDir(path.dirname(queuePath));
2535
+ const defaultSource = normalizeSourceInput(source) || "cursor";
2536
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
2537
+ const touchedBuckets = new Set();
2538
+
2539
+ // Incremental: skip records we already processed
2540
+ const lastTs = cursors?.cursorApi?.lastRecordTimestamp || null;
2541
+ let latestTs = lastTs;
2542
+ let eventsAggregated = 0;
2543
+ const cb = typeof onProgress === "function" ? onProgress : null;
2544
+ const total = records.length;
2545
+
2546
+ for (let i = 0; i < records.length; i++) {
2547
+ const record = records[i];
2548
+ const recordDate = record.date;
2549
+ if (!recordDate) continue;
2550
+
2551
+ // Skip records we already processed (CSV is ordered newest-first)
2552
+ if (lastTs && recordDate <= lastTs) continue;
2553
+
2554
+ const { normalizeCursorUsage } = require("./cursor-config");
2555
+ const delta = normalizeCursorUsage(record);
2556
+ if (isAllZeroUsage(delta)) continue;
2557
+
2558
+ delta.conversation_count = 1;
2559
+
2560
+ const bucketStart = toUtcHalfHourStart(recordDate);
2561
+ if (!bucketStart) continue;
2562
+
2563
+ const model = normalizeModelInput(record.model) || DEFAULT_MODEL;
2564
+ const bucket = getHourlyBucket(hourlyState, defaultSource, model, bucketStart);
2565
+ addTotals(bucket.totals, delta);
2566
+ touchedBuckets.add(bucketKey(defaultSource, model, bucketStart));
2567
+
2568
+ eventsAggregated += 1;
2569
+
2570
+ // Track latest timestamp
2571
+ if (!latestTs || recordDate > latestTs) {
2572
+ latestTs = recordDate;
2573
+ }
2574
+
2575
+ if (cb && (i % 200 === 0 || i === records.length - 1)) {
2576
+ cb({
2577
+ index: i + 1,
2578
+ total,
2579
+ eventsAggregated,
2580
+ bucketsQueued: touchedBuckets.size,
2581
+ });
2582
+ }
2583
+ }
2584
+
2585
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
2586
+ hourlyState.updatedAt = new Date().toISOString();
2587
+ cursors.hourly = hourlyState;
2588
+
2589
+ // Update cursor state
2590
+ if (!cursors.cursorApi) cursors.cursorApi = {};
2591
+ if (latestTs && latestTs !== lastTs) {
2592
+ cursors.cursorApi.lastRecordTimestamp = latestTs;
2593
+ }
2594
+ cursors.cursorApi.updatedAt = new Date().toISOString();
2595
+
2596
+ return { recordsProcessed: total, eventsAggregated, bucketsQueued };
2597
+ }
2598
+
2460
2599
  module.exports = {
2461
2600
  listRolloutFiles,
2462
2601
  listClaudeProjectFiles,
@@ -2469,4 +2608,5 @@ module.exports = {
2469
2608
  parseOpencodeIncremental,
2470
2609
  parseOpencodeDbIncremental,
2471
2610
  parseOpenclawIncremental,
2611
+ parseCursorApiIncremental,
2472
2612
  };
@@ -1 +0,0 @@
1
- import{b as k,ad as Z,d as ue,ae as z,af as L,ag as le,ah as de,ai as fe,aj as me,ak as pe,al as ge,am as ke,_ as ye,an as he,ao as Te,ap as we,m}from"./main-B206gspN.js";const xe="Backend runtime unavailable (InsForge). Please retry later.",p={usageSummary:"vibeusage-usage-summary",usageDaily:"vibeusage-usage-daily",usageHourly:"vibeusage-usage-hourly",usageMonthly:"vibeusage-usage-monthly",usageHeatmap:"vibeusage-usage-heatmap",usageModelBreakdown:"vibeusage-usage-model-breakdown",projectUsageSummary:"vibeusage-project-usage-summary",leaderboard:"vibeusage-leaderboard",leaderboardProfile:"vibeusage-leaderboard-profile",userStatus:"vibeusage-user-status",linkCodeInit:"vibeusage-link-code-init",publicViewProfile:"vibeusage-public-view-profile",publicVisibility:"vibeusage-public-visibility"},J="/functions",be="/api/functions",h={business:"business",probe:"probe"};let D=null;async function f(e){return await ue(e)}async function Ee({baseUrl:e,accessToken:s,signal:t}={}){const a=await f(s),n=we(new Date);return await y({baseUrl:e,accessToken:a,slug:p.usageSummary,params:{from:n,to:n},fetchOptions:{cache:"no-store",signal:t},retry:!1,requestKind:h.probe}),{status:200}}async function Ce({baseUrl:e,accessToken:s,from:t,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:i,rolling:c=!1}={}){const u=await f(s);if(k())return de({from:t,to:a,seed:u,rolling:c});const o=M({timeZone:l,tzOffsetMinutes:i}),d=P({source:n,model:r}),v=c?{rolling:"1"}:{};return y({baseUrl:e,accessToken:u,slug:p.usageSummary,params:{from:t,to:a,...d,...o,...v}})}async function je({baseUrl:e,accessToken:s,from:t,to:a,source:n,limit:r,timeZone:l,tzOffsetMinutes:i}={}){const c=await f(s);if(k())return me({seed:c,limit:r});const u=M({timeZone:l,tzOffsetMinutes:i}),d={...P({source:n}),...u};return t&&(d.from=t),a&&(d.to=a),r!=null&&(d.limit=String(r)),y({baseUrl:e,accessToken:c,slug:p.projectUsageSummary,params:d})}async function Ie({baseUrl:e,accessToken:s,period:t,metric:a,limit:n,offset:r}={}){const l=await f(s);if(k())return Z({seed:l,period:t,metric:a,limit:n,offset:r});const c=(typeof t=="string"?t:"week").trim().toLowerCase(),o={period:c==="month"||c==="total"||c==="week"?c:"week"};return a&&(o.metric=String(a)),n!=null&&(o.limit=String(n)),r!=null&&(o.offset=String(r)),y({baseUrl:e,accessToken:l,slug:p.leaderboard,params:o})}async function Ne({baseUrl:e,accessToken:s}={}){const t=await f(s);return k()?{enabled:!1,updated_at:null,share_token:null}:y({baseUrl:e,accessToken:t,slug:p.publicVisibility})}async function De({baseUrl:e,accessToken:s,enabled:t}={}){const a=await f(s);return k()?{enabled:!!t,updated_at:new Date().toISOString(),share_token:t?"pv1-mock-token":null}:X({baseUrl:e,accessToken:a,slug:p.publicVisibility,body:{enabled:!!t}})}async function Le({baseUrl:e,accessToken:s,userId:t,period:a}={}){const n=await f(s);if(k()){const r=Z({seed:n,period:a,metric:"all",limit:250,offset:0}),i=(Array.isArray(r?.entries)?r.entries:[]).find(c=>c?.user_id===t)||null;return{period:r?.period??"week",from:r?.from??null,to:r?.to??null,generated_at:r?.generated_at??new Date().toISOString(),entry:i?{user_id:i.user_id??null,display_name:i.display_name??null,avatar_url:i.avatar_url??null,rank:i.rank??null,gpt_tokens:i.gpt_tokens??"0",claude_tokens:i.claude_tokens??"0",other_tokens:i.other_tokens??"0",total_tokens:i.total_tokens??"0"}:null}}return y({baseUrl:e,accessToken:n,slug:p.leaderboardProfile,params:{user_id:String(t||""),period:String(a||"")}})}async function He({baseUrl:e,accessToken:s}={}){const t=await f(s);if(k()){const a=new Date().toISOString();return{user_id:"mock-user",created_at:a,pro:{active:!0,sources:["mock"],expires_at:null,partial:!1,as_of:a},subscriptions:{partial:!1,as_of:a,items:[{tool:"codex",provider:"openai",product:"chatgpt",plan_type:"pro",updated_at:a},{tool:"claude",provider:"anthropic",product:"subscription",plan_type:"max",rate_limit_tier:"default_claude_max_5x",updated_at:a}]},install:{partial:!1,as_of:a,has_active_device_token:!1,has_active_device:!1,active_device_tokens:0,active_devices:0,latest_token_activity_at:null,latest_device_seen_at:null}}}return y({baseUrl:e,accessToken:t,slug:p.userStatus})}async function ze({signal:e}={}){const s=await fetch("/functions/vibeusage-local-sync",{method:"POST",headers:{Accept:"application/json"},cache:"no-store",signal:e}),t=await s.json().catch(()=>({ok:!1,error:`Local sync request failed with HTTP ${s.status}`}));if(!s.ok||t?.ok===!1){const a=t?.error||t?.message||`Local sync request failed with HTTP ${s.status}`,n=new Error(a);throw n.status=s.status,n.statusCode=s.status,n.payload=t,n}return t}async function Ue({baseUrl:e,accessToken:s,from:t,to:a,source:n,timeZone:r,tzOffsetMinutes:l}={}){const i=await f(s);if(k())return fe({from:t,to:a,seed:i});const c=M({timeZone:r,tzOffsetMinutes:l}),u=P({source:n});return y({baseUrl:e,accessToken:i,slug:p.usageModelBreakdown,params:{from:t,to:a,...u,...c}})}async function Be({baseUrl:e,accessToken:s,from:t,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:i}={}){const c=await f(s);if(k())return le({from:t,to:a,seed:c});const u=M({timeZone:l,tzOffsetMinutes:i}),o=P({source:n,model:r});return y({baseUrl:e,accessToken:c,slug:p.usageDaily,params:{from:t,to:a,...o,...u}})}async function Oe({baseUrl:e,accessToken:s,day:t,source:a,model:n,timeZone:r,tzOffsetMinutes:l}={}){const i=await f(s);if(k())return pe({day:t,seed:i});const c=M({timeZone:r,tzOffsetMinutes:l}),u=P({source:a,model:n});return y({baseUrl:e,accessToken:i,slug:p.usageHourly,params:t?{day:t,...u,...c}:{...u,...c}})}async function Fe({baseUrl:e,accessToken:s,months:t,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:i}={}){const c=await f(s);if(k())return ge({months:t,to:a,seed:c});const u=M({timeZone:l,tzOffsetMinutes:i}),o=P({source:n,model:r});return y({baseUrl:e,accessToken:c,slug:p.usageMonthly,params:{...t?{months:String(t)}:{},...a?{to:a}:{},...o,...u}})}async function qe({baseUrl:e,accessToken:s,weeks:t,to:a,weekStartsOn:n,source:r,model:l,timeZone:i,tzOffsetMinutes:c}={}){const u=await f(s);if(k())return ke({weeks:t,to:a,weekStartsOn:n,seed:u});const o=M({timeZone:i,tzOffsetMinutes:c}),d=P({source:r,model:l});return y({baseUrl:e,accessToken:u,slug:p.usageHeatmap,params:{weeks:String(t),to:a,week_starts_on:n,...d,...o}})}async function Ve({baseUrl:e,accessToken:s}={}){const t=await f(s);return k()?{link_code:"mock_link_code",expires_at:new Date(Date.now()+10*6e4).toISOString()}:X({baseUrl:e,accessToken:t,slug:p.linkCodeInit,body:{}})}async function $e({baseUrl:e,accessToken:s}={}){const t=await f(s);return y({baseUrl:e,accessToken:t,slug:p.publicViewProfile})}function M({timeZone:e,tzOffsetMinutes:s}={}){const t={},a=typeof e=="string"?e.trim():"";return a&&(t.tz=a),Number.isFinite(s)&&(t.tz_offset_minutes=String(Math.trunc(s))),t}function P({source:e,model:s}={}){const t={},a=typeof e=="string"?e.trim().toLowerCase():"";a&&(t.source=a);const n=typeof s=="string"?s.trim():"";return n&&(t.model=n),t}async function y({baseUrl:e,accessToken:s,slug:t,params:a,fetchOptions:n,errorPrefix:r,retry:l,requestKind:i=h.business,skipSessionExpiry:c=!1,allowRefresh:u=!0}={}){let o=await f(s),d=E(o),v=z({baseUrl:e,accessToken:o??void 0}).getHttpClient();const C=ae(l,"GET"),b=c?h.probe:i;let A=0;const{primaryPath:j,fallbackPath:I}=Q(t);for(;;)try{const S=await G({http:v,primaryPath:j,fallbackPath:I,params:a,fetchOptions:n});return U({hadAccessToken:d,accessToken:o}),S}catch(S){const T=S;if(T?.name==="AbortError")throw S;let w=null;const B=T?.statusCode??T?.status;if(u&&se({status:B,requestKind:b,hadAccessToken:d,accessToken:o})){const g=(await te())?.accessToken??null;if(E(g)){const N=z({baseUrl:e,accessToken:g}).getHttpClient();o=g,d=!0,v=N;try{const x=await G({http:N,primaryPath:j,fallbackPath:I,params:a,fetchOptions:n});return U({hadAccessToken:!0,accessToken:g}),x}catch(x){const F=x?.statusCode??x?.status;$({status:F,hadAccessToken:!0,accessToken:g,skipSessionExpiry:b===h.probe})&&L(),w=R(x,{errorPrefix:r,hadAccessToken:!0,accessToken:g,skipSessionExpiry:!0})}}else H({hadAccessToken:d,accessToken:o,skipSessionExpiry:b===h.probe})&&L();w??=R(T,{errorPrefix:r,hadAccessToken:d,accessToken:o,skipSessionExpiry:!0})}if(w??=R(T,{errorPrefix:r,hadAccessToken:d,accessToken:o,skipSessionExpiry:b===h.probe}),!ne({err:w,attempt:A,retryOptions:C}))throw w;const O=re({retryOptions:C,attempt:A});await ie(O),A+=1}}async function X({baseUrl:e,accessToken:s,slug:t,body:a,fetchOptions:n,errorPrefix:r,retry:l,requestKind:i=h.business,skipSessionExpiry:c=!1,allowRefresh:u=!0}={}){let o=await f(s),d=E(o),v=z({baseUrl:e,accessToken:o??void 0}).getHttpClient();const C=ae(l,"POST"),b=c?h.probe:i;let A=0;const{primaryPath:j,fallbackPath:I}=Q(t);for(;;)try{const S=await K({http:v,primaryPath:j,fallbackPath:I,body:a,fetchOptions:n});return U({hadAccessToken:d,accessToken:o}),S}catch(S){const T=S;if(T?.name==="AbortError")throw S;let w=null;const B=T?.statusCode??T?.status;if(u&&se({status:B,requestKind:b,hadAccessToken:d,accessToken:o})){const g=(await te())?.accessToken??null;if(E(g)){const N=z({baseUrl:e,accessToken:g}).getHttpClient();o=g,d=!0,v=N;try{const x=await K({http:N,primaryPath:j,fallbackPath:I,body:a,fetchOptions:n});return U({hadAccessToken:!0,accessToken:g}),x}catch(x){const F=x?.statusCode??x?.status;$({status:F,hadAccessToken:!0,accessToken:g,skipSessionExpiry:b===h.probe})&&L(),w=R(x,{errorPrefix:r,hadAccessToken:!0,accessToken:g,skipSessionExpiry:!0})}}else H({hadAccessToken:d,accessToken:o,skipSessionExpiry:b===h.probe})&&L();w??=R(T,{errorPrefix:r,hadAccessToken:d,accessToken:o,skipSessionExpiry:!0})}if(w??=R(T,{errorPrefix:r,hadAccessToken:d,accessToken:o,skipSessionExpiry:b===h.probe}),!ne({err:w,attempt:A,retryOptions:C}))throw w;const O=re({retryOptions:C,attempt:A});await ie(O),A+=1}}function Q(e){const s=Se(e),t=`${V(J)}/${s}`,a=`${V(be)}/${s}`;return{primaryPath:t,fallbackPath:a}}function Se(e){return(typeof e=="string"?e.trim():"").replace(/^\/+/,"")}function V(e){const s=typeof e=="string"?e.trim():"";return s.endsWith("/")?s.slice(0,-1):s}async function G({http:e,primaryPath:s,fallbackPath:t,params:a,fetchOptions:n}={}){try{return await e.get(s,{params:a,...n||{}})}catch(r){if(!Y(r,s))throw r;return await e.get(t,{params:a,...n||{}})}}async function K({http:e,primaryPath:s,fallbackPath:t,body:a,fetchOptions:n}={}){try{return await W({http:e,path:s,body:a,fetchOptions:n})}catch(r){if(!Y(r,s))throw r;return await W({http:e,path:t,body:a,fetchOptions:n})}}async function W({http:e,path:s,body:t,fetchOptions:a}={}){return await e.post(s,t,{...a||{}})}function Y(e,s){return!s||!s.startsWith(`${V(J)}/`)?!1:(e?.statusCode??e?.status)===404}function R(e,{errorPrefix:s,hadAccessToken:t,accessToken:a,skipSessionExpiry:n}={}){const r=typeof e?.message=="string"?e.message.trim():"",l=typeof e?.error=="string"?e.error.trim():"",i=r||l||String(e||"Unknown error"),c=ve(i),u=new Error(s?`${s}: ${c}`:c);u.cause=e;const o=e?.statusCode??e?.status;return $({status:o,hadAccessToken:t,accessToken:a,skipSessionExpiry:n})&&L(),typeof o=="number"&&(u.status=o,u.statusCode=o),u.retryable=Ae(o)||Me(i),c!==i&&(u.originalMessage=i),e?.nextActions&&(u.nextActions=e.nextActions),e?.error&&(u.error=e.error),u}function H({hadAccessToken:e,accessToken:s,skipSessionExpiry:t}={}){return t||!e||!E(s)?!1:Pe(s)}function $({status:e,hadAccessToken:s,accessToken:t,skipSessionExpiry:a}={}){return e!==401?!1:H({hadAccessToken:s,accessToken:t,skipSessionExpiry:a})}function _e({hadAccessToken:e,accessToken:s}={}){return H({hadAccessToken:e,accessToken:s})}function U({hadAccessToken:e,accessToken:s}={}){_e({hadAccessToken:e,accessToken:s})&&he()}function ve(e){return ee(e)?xe:String(e||"Unknown error")}function ee(e){const s=String(e||"").toLowerCase();return s?!!(s.includes("deno:")||s.includes("deno")||s.includes("econnreset")||s.includes("econnrefused")||s.includes("etimedout")||s.includes("timeout")&&s.includes("request")||s.includes("upstream")&&(s.includes("deno")||s.includes("connect"))):!1}function se({status:e,requestKind:s,hadAccessToken:t,accessToken:a}={}){return e!==401||s!==h.business?!1:H({hadAccessToken:t,accessToken:a})}async function te(){return D||(D=Te.auth.getCurrentSession().then(({data:e})=>e?.session??null).catch(()=>null).finally(()=>{D=null}),D)}function Ae(e){return e===502||e===503||e===504}function Me(e){const s=String(e||"").toLowerCase();return s?!!(ee(s)||s.includes("econnreset")||s.includes("econnrefused")||s.includes("etimedout")||s.includes("timeout")||s.includes("networkerror")||s.includes("failed to fetch")||s.includes("socket hang up")||s.includes("connection reset")):!1}function ae(e,s){const a=(s||"GET").toUpperCase()==="GET"?{maxRetries:2,baseDelayMs:300,maxDelayMs:1500,jitterRatio:.2}:{maxRetries:0,baseDelayMs:0,maxDelayMs:0,jitterRatio:0};if(e==null)return a;if(e===!1)return{maxRetries:0,baseDelayMs:0,maxDelayMs:0,jitterRatio:0};const n=q(e.maxRetries??a.maxRetries,0,10),r=q(e.baseDelayMs??a.baseDelayMs,50,6e4),l=q(e.maxDelayMs??a.maxDelayMs,r,12e4),i=typeof e.jitterRatio=="number"?Math.max(0,Math.min(.5,e.jitterRatio)):a.jitterRatio;return{maxRetries:n,baseDelayMs:r,maxDelayMs:l,jitterRatio:i}}function E(e){return!!ye(e)}function Pe(e){if(!E(e))return!1;const s=e.split(".");return s.length!==3?!1:s.every(t=>/^[A-Za-z0-9_-]+$/.test(t))}function ne({err:e,attempt:s,retryOptions:t}={}){return!t||t.maxRetries<=0||s>=t.maxRetries?!1:!!(e&&e.retryable)}function re({retryOptions:e,attempt:s}={}){if(!e||e.maxRetries<=0)return 0;const t=e.baseDelayMs*Math.pow(2,s),a=Math.min(e.maxDelayMs,t),n=a*e.jitterRatio*Math.random();return Math.round(a+n)}function q(e,s,t){const a=Number(e);return Number.isFinite(a)?Math.min(t,Math.max(s,Math.floor(a))):s}function ie(e){return!e||e<=0?Promise.resolve():new Promise(s=>setTimeout(s,e))}const _={TOP_LEFT:"┌",TOP_RIGHT:"┐",BOTTOM_LEFT:"└",BOTTOM_RIGHT:"┘",HORIZONTAL:"─",VERTICAL:"│"};function Ge({title:e,subtitle:s,children:t,className:a="",bodyClassName:n=""}){return m.jsxs("div",{className:`relative flex flex-col matrix-panel ${a}`,children:[m.jsxs("div",{className:"flex items-center leading-none",children:[m.jsx("span",{className:"shrink-0 text-matrix-dim",children:_.TOP_LEFT}),m.jsx("span",{className:"mx-3 shrink-0 text-heading uppercase text-matrix-primary px-2 py-1 bg-matrix-panelStrong border border-matrix-ghost",children:e}),s?m.jsxs("span",{className:"text-caption text-matrix-muted mr-2 uppercase",children:["[",s,"]"]}):null,m.jsx("span",{className:"flex-1 overflow-hidden whitespace-nowrap text-matrix-ghost",children:_.HORIZONTAL.repeat(100)}),m.jsx("span",{className:"shrink-0 text-matrix-dim",children:_.TOP_RIGHT})]}),m.jsxs("div",{className:"flex flex-1",children:[m.jsx("div",{className:"shrink-0 w-3 flex justify-center text-matrix-ghost",children:_.VERTICAL}),m.jsx("div",{className:`flex-1 min-w-0 py-5 px-4 relative z-10 ${n}`,children:t}),m.jsx("div",{className:"shrink-0 w-3 flex justify-center text-matrix-ghost",children:_.VERTICAL})]}),m.jsxs("div",{className:"flex items-center leading-none text-matrix-ghost",children:[m.jsx("span",{className:"shrink-0",children:_.BOTTOM_LEFT}),m.jsx("span",{className:"flex-1 overflow-hidden whitespace-nowrap",children:_.HORIZONTAL.repeat(100)}),m.jsx("span",{className:"shrink-0",children:_.BOTTOM_RIGHT})]})]})}export{Ge as A,Be as a,je as b,Oe as c,Fe as d,Ce as e,Ue as f,qe as g,Ne as h,$e as i,He as j,Ie as k,Le as l,Ee as p,Ve as r,De as s,ze as t};