vibeusage 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +306 -173
  2. package/README.old.md +324 -0
  3. package/README.zh-CN.md +304 -188
  4. package/package.json +32 -30
  5. package/src/cli.js +41 -37
  6. package/src/commands/activate-if-needed.js +41 -0
  7. package/src/commands/diagnostics.js +8 -9
  8. package/src/commands/doctor.js +31 -26
  9. package/src/commands/init.js +324 -208
  10. package/src/commands/status.js +86 -80
  11. package/src/commands/sync.js +182 -130
  12. package/src/commands/uninstall.js +69 -58
  13. package/src/lib/activation-check.js +290 -0
  14. package/src/lib/browser-auth.js +52 -54
  15. package/src/lib/claude-config.js +25 -25
  16. package/src/lib/cli-ui.js +35 -35
  17. package/src/lib/codex-config.js +40 -36
  18. package/src/lib/debug-flags.js +2 -2
  19. package/src/lib/diagnostics.js +73 -55
  20. package/src/lib/doctor.js +139 -132
  21. package/src/lib/fs.js +17 -17
  22. package/src/lib/gemini-config.js +44 -40
  23. package/src/lib/init-flow.js +16 -22
  24. package/src/lib/insforge-client.js +10 -10
  25. package/src/lib/insforge.js +9 -3
  26. package/src/lib/openclaw-hook.js +91 -67
  27. package/src/lib/openclaw-session-plugin.js +520 -0
  28. package/src/lib/opencode-config.js +31 -32
  29. package/src/lib/opencode-usage-audit.js +34 -31
  30. package/src/lib/progress.js +12 -13
  31. package/src/lib/project-usage-purge.js +23 -17
  32. package/src/lib/prompt.js +8 -4
  33. package/src/lib/rollout.js +342 -241
  34. package/src/lib/runtime-config.js +34 -22
  35. package/src/lib/subscriptions.js +94 -92
  36. package/src/lib/tracker-paths.js +6 -6
  37. package/src/lib/upload-throttle.js +35 -16
  38. package/src/lib/uploader.js +33 -29
  39. package/src/lib/vibeusage-api.js +72 -56
  40. package/src/lib/vibeusage-public-repo.js +41 -24
@@ -1,14 +1,14 @@
1
- const fs = require('node:fs/promises');
2
- const fssync = require('node:fs');
3
- const path = require('node:path');
4
- const readline = require('node:readline');
1
+ const fs = require("node:fs/promises");
2
+ const fssync = require("node:fs");
3
+ const path = require("node:path");
4
+ const readline = require("node:readline");
5
5
 
6
- const { ensureDir } = require('./fs');
7
- const { hashRepoRoot, resolveGitHubPublicStatus } = require('./vibeusage-public-repo');
6
+ const { ensureDir } = require("./fs");
7
+ const { hashRepoRoot, resolveGitHubPublicStatus } = require("./vibeusage-public-repo");
8
8
 
9
- const DEFAULT_SOURCE = 'codex';
10
- const DEFAULT_MODEL = 'unknown';
11
- const BUCKET_SEPARATOR = '|';
9
+ const DEFAULT_SOURCE = "codex";
10
+ const DEFAULT_MODEL = "unknown";
11
+ const BUCKET_SEPARATOR = "|";
12
12
 
13
13
  async function listRolloutFiles(sessionsDir) {
14
14
  const out = [];
@@ -27,7 +27,7 @@ async function listRolloutFiles(sessionsDir) {
27
27
  const files = await safeReadDir(dayDir);
28
28
  for (const f of files) {
29
29
  if (!f.isFile()) continue;
30
- if (!f.name.startsWith('rollout-') || !f.name.endsWith('.jsonl')) continue;
30
+ if (!f.name.startsWith("rollout-") || !f.name.endsWith(".jsonl")) continue;
31
31
  out.push(path.join(dayDir, f.name));
32
32
  }
33
33
  }
@@ -50,11 +50,11 @@ async function listGeminiSessionFiles(tmpDir) {
50
50
  const roots = await safeReadDir(tmpDir);
51
51
  for (const root of roots) {
52
52
  if (!root.isDirectory()) continue;
53
- const chatsDir = path.join(tmpDir, root.name, 'chats');
53
+ const chatsDir = path.join(tmpDir, root.name, "chats");
54
54
  const chats = await safeReadDir(chatsDir);
55
55
  for (const entry of chats) {
56
56
  if (!entry.isFile()) continue;
57
- if (!entry.name.startsWith('session-') || !entry.name.endsWith('.json')) continue;
57
+ if (!entry.name.startsWith("session-") || !entry.name.endsWith(".json")) continue;
58
58
  out.push(path.join(chatsDir, entry.name));
59
59
  }
60
60
  }
@@ -64,7 +64,7 @@ async function listGeminiSessionFiles(tmpDir) {
64
64
 
65
65
  async function listOpencodeMessageFiles(storageDir) {
66
66
  const out = [];
67
- const messageDir = path.join(storageDir, 'message');
67
+ const messageDir = path.join(storageDir, "message");
68
68
  await walkOpencodeMessages(messageDir, out);
69
69
  out.sort((a, b) => a.localeCompare(b));
70
70
  return out;
@@ -77,16 +77,16 @@ async function parseRolloutIncremental({
77
77
  projectQueuePath,
78
78
  onProgress,
79
79
  source,
80
- publicRepoResolver
80
+ publicRepoResolver,
81
81
  }) {
82
82
  await ensureDir(path.dirname(queuePath));
83
83
  let filesProcessed = 0;
84
84
  let eventsAggregated = 0;
85
85
 
86
- const cb = typeof onProgress === 'function' ? onProgress : null;
86
+ const cb = typeof onProgress === "function" ? onProgress : null;
87
87
  const totalFiles = Array.isArray(rolloutFiles) ? rolloutFiles.length : 0;
88
88
  const hourlyState = normalizeHourlyState(cursors?.hourly);
89
- const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
89
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
90
90
  const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
91
91
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
92
92
  const projectMetaCache = projectEnabled ? new Map() : null;
@@ -94,16 +94,18 @@ async function parseRolloutIncremental({
94
94
  const touchedBuckets = new Set();
95
95
  const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
96
96
 
97
- if (!cursors.files || typeof cursors.files !== 'object') {
97
+ if (!cursors.files || typeof cursors.files !== "object") {
98
98
  cursors.files = {};
99
99
  }
100
100
 
101
101
  for (let idx = 0; idx < rolloutFiles.length; idx++) {
102
102
  const entry = rolloutFiles[idx];
103
- const filePath = typeof entry === 'string' ? entry : entry?.path;
103
+ const filePath = typeof entry === "string" ? entry : entry?.path;
104
104
  if (!filePath) continue;
105
105
  const fileSource =
106
- typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
106
+ typeof entry === "string"
107
+ ? defaultSource
108
+ : normalizeSourceInput(entry?.source) || defaultSource;
107
109
  const st = await fs.stat(filePath).catch(() => null);
108
110
  if (!st || !st.isFile()) continue;
109
111
 
@@ -120,7 +122,7 @@ async function parseRolloutIncremental({
120
122
  projectMetaCache,
121
123
  publicRepoCache,
122
124
  publicRepoResolver,
123
- projectState
125
+ projectState,
124
126
  })
125
127
  : null;
126
128
  const projectRef = projectContext?.projectRef || null;
@@ -140,7 +142,7 @@ async function parseRolloutIncremental({
140
142
  projectKey,
141
143
  projectMetaCache,
142
144
  publicRepoCache,
143
- publicRepoResolver
145
+ publicRepoResolver,
144
146
  });
145
147
 
146
148
  cursors.files[key] = {
@@ -148,7 +150,7 @@ async function parseRolloutIncremental({
148
150
  offset: result.endOffset,
149
151
  lastTotal: result.lastTotal,
150
152
  lastModel: result.lastModel,
151
- updatedAt: new Date().toISOString()
153
+ updatedAt: new Date().toISOString(),
152
154
  };
153
155
 
154
156
  filesProcessed += 1;
@@ -161,7 +163,7 @@ async function parseRolloutIncremental({
161
163
  filePath,
162
164
  filesProcessed,
163
165
  eventsAggregated,
164
- bucketsQueued: touchedBuckets.size
166
+ bucketsQueued: touchedBuckets.size,
165
167
  });
166
168
  }
167
169
  }
@@ -187,34 +189,36 @@ async function parseClaudeIncremental({
187
189
  projectQueuePath,
188
190
  onProgress,
189
191
  source,
190
- publicRepoResolver
192
+ publicRepoResolver,
191
193
  }) {
192
194
  await ensureDir(path.dirname(queuePath));
193
195
  let filesProcessed = 0;
194
196
  let eventsAggregated = 0;
195
197
 
196
- const cb = typeof onProgress === 'function' ? onProgress : null;
198
+ const cb = typeof onProgress === "function" ? onProgress : null;
197
199
  const files = Array.isArray(projectFiles) ? projectFiles : [];
198
200
  const totalFiles = files.length;
199
201
  const hourlyState = normalizeHourlyState(cursors?.hourly);
200
- const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
202
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
201
203
  const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
202
204
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
203
205
  const projectMetaCache = projectEnabled ? new Map() : null;
204
206
  const publicRepoCache = projectEnabled ? new Map() : null;
205
207
  const touchedBuckets = new Set();
206
- const defaultSource = normalizeSourceInput(source) || 'claude';
208
+ const defaultSource = normalizeSourceInput(source) || "claude";
207
209
 
208
- if (!cursors.files || typeof cursors.files !== 'object') {
210
+ if (!cursors.files || typeof cursors.files !== "object") {
209
211
  cursors.files = {};
210
212
  }
211
213
 
212
214
  for (let idx = 0; idx < files.length; idx++) {
213
215
  const entry = files[idx];
214
- const filePath = typeof entry === 'string' ? entry : entry?.path;
216
+ const filePath = typeof entry === "string" ? entry : entry?.path;
215
217
  if (!filePath) continue;
216
218
  const fileSource =
217
- typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
219
+ typeof entry === "string"
220
+ ? defaultSource
221
+ : normalizeSourceInput(entry?.source) || defaultSource;
218
222
  const st = await fs.stat(filePath).catch(() => null);
219
223
  if (!st || !st.isFile()) continue;
220
224
 
@@ -229,7 +233,7 @@ async function parseClaudeIncremental({
229
233
  projectMetaCache,
230
234
  publicRepoCache,
231
235
  publicRepoResolver,
232
- projectState
236
+ projectState,
233
237
  })
234
238
  : null;
235
239
  const projectRef = projectContext?.projectRef || null;
@@ -244,13 +248,13 @@ async function parseClaudeIncremental({
244
248
  projectState,
245
249
  projectTouchedBuckets,
246
250
  projectRef,
247
- projectKey
251
+ projectKey,
248
252
  });
249
253
 
250
254
  cursors.files[key] = {
251
255
  inode,
252
256
  offset: result.endOffset,
253
- updatedAt: new Date().toISOString()
257
+ updatedAt: new Date().toISOString(),
254
258
  };
255
259
 
256
260
  filesProcessed += 1;
@@ -263,7 +267,7 @@ async function parseClaudeIncremental({
263
267
  filePath,
264
268
  filesProcessed,
265
269
  eventsAggregated,
266
- bucketsQueued: touchedBuckets.size
270
+ bucketsQueued: touchedBuckets.size,
267
271
  });
268
272
  }
269
273
  }
@@ -289,34 +293,36 @@ async function parseGeminiIncremental({
289
293
  projectQueuePath,
290
294
  onProgress,
291
295
  source,
292
- publicRepoResolver
296
+ publicRepoResolver,
293
297
  }) {
294
298
  await ensureDir(path.dirname(queuePath));
295
299
  let filesProcessed = 0;
296
300
  let eventsAggregated = 0;
297
301
 
298
- const cb = typeof onProgress === 'function' ? onProgress : null;
302
+ const cb = typeof onProgress === "function" ? onProgress : null;
299
303
  const files = Array.isArray(sessionFiles) ? sessionFiles : [];
300
304
  const totalFiles = files.length;
301
305
  const hourlyState = normalizeHourlyState(cursors?.hourly);
302
- const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
306
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
303
307
  const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
304
308
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
305
309
  const projectMetaCache = projectEnabled ? new Map() : null;
306
310
  const publicRepoCache = projectEnabled ? new Map() : null;
307
311
  const touchedBuckets = new Set();
308
- const defaultSource = normalizeSourceInput(source) || 'gemini';
312
+ const defaultSource = normalizeSourceInput(source) || "gemini";
309
313
 
310
- if (!cursors.files || typeof cursors.files !== 'object') {
314
+ if (!cursors.files || typeof cursors.files !== "object") {
311
315
  cursors.files = {};
312
316
  }
313
317
 
314
318
  for (let idx = 0; idx < files.length; idx++) {
315
319
  const entry = files[idx];
316
- const filePath = typeof entry === 'string' ? entry : entry?.path;
320
+ const filePath = typeof entry === "string" ? entry : entry?.path;
317
321
  if (!filePath) continue;
318
322
  const fileSource =
319
- typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
323
+ typeof entry === "string"
324
+ ? defaultSource
325
+ : normalizeSourceInput(entry?.source) || defaultSource;
320
326
  const st = await fs.stat(filePath).catch(() => null);
321
327
  if (!st || !st.isFile()) continue;
322
328
 
@@ -333,7 +339,7 @@ async function parseGeminiIncremental({
333
339
  projectMetaCache,
334
340
  publicRepoCache,
335
341
  publicRepoResolver,
336
- projectState
342
+ projectState,
337
343
  })
338
344
  : null;
339
345
  const projectRef = projectContext?.projectRef || null;
@@ -350,7 +356,7 @@ async function parseGeminiIncremental({
350
356
  projectState,
351
357
  projectTouchedBuckets,
352
358
  projectRef,
353
- projectKey
359
+ projectKey,
354
360
  });
355
361
 
356
362
  cursors.files[key] = {
@@ -358,7 +364,7 @@ async function parseGeminiIncremental({
358
364
  lastIndex: result.lastIndex,
359
365
  lastTotals: result.lastTotals,
360
366
  lastModel: result.lastModel,
361
- updatedAt: new Date().toISOString()
367
+ updatedAt: new Date().toISOString(),
362
368
  };
363
369
 
364
370
  filesProcessed += 1;
@@ -371,7 +377,7 @@ async function parseGeminiIncremental({
371
377
  filePath,
372
378
  filesProcessed,
373
379
  eventsAggregated,
374
- bucketsQueued: touchedBuckets.size
380
+ bucketsQueued: touchedBuckets.size,
375
381
  });
376
382
  }
377
383
  }
@@ -397,17 +403,17 @@ async function parseOpencodeIncremental({
397
403
  projectQueuePath,
398
404
  onProgress,
399
405
  source,
400
- publicRepoResolver
406
+ publicRepoResolver,
401
407
  }) {
402
408
  await ensureDir(path.dirname(queuePath));
403
409
  let filesProcessed = 0;
404
410
  let eventsAggregated = 0;
405
411
 
406
- const cb = typeof onProgress === 'function' ? onProgress : null;
412
+ const cb = typeof onProgress === "function" ? onProgress : null;
407
413
  const files = Array.isArray(messageFiles) ? messageFiles : [];
408
414
  const totalFiles = files.length;
409
415
  const hourlyState = normalizeHourlyState(cursors?.hourly);
410
- const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
416
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
411
417
  const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
412
418
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
413
419
  const projectMetaCache = projectEnabled ? new Map() : null;
@@ -415,18 +421,20 @@ async function parseOpencodeIncremental({
415
421
  const opencodeState = normalizeOpencodeState(cursors?.opencode);
416
422
  const messageIndex = opencodeState.messages;
417
423
  const touchedBuckets = new Set();
418
- const defaultSource = normalizeSourceInput(source) || 'opencode';
424
+ const defaultSource = normalizeSourceInput(source) || "opencode";
419
425
 
420
- if (!cursors.files || typeof cursors.files !== 'object') {
426
+ if (!cursors.files || typeof cursors.files !== "object") {
421
427
  cursors.files = {};
422
428
  }
423
429
 
424
430
  for (let idx = 0; idx < files.length; idx++) {
425
431
  const entry = files[idx];
426
- const filePath = typeof entry === 'string' ? entry : entry?.path;
432
+ const filePath = typeof entry === "string" ? entry : entry?.path;
427
433
  if (!filePath) continue;
428
434
  const fileSource =
429
- typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
435
+ typeof entry === "string"
436
+ ? defaultSource
437
+ : normalizeSourceInput(entry?.source) || defaultSource;
430
438
  const st = await fs.stat(filePath).catch(() => null);
431
439
  if (!st || !st.isFile()) continue;
432
440
 
@@ -435,7 +443,8 @@ async function parseOpencodeIncremental({
435
443
  const inode = st.ino || 0;
436
444
  const size = Number.isFinite(st.size) ? st.size : 0;
437
445
  const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
438
- const unchanged = prev && prev.inode === inode && prev.size === size && prev.mtimeMs === mtimeMs;
446
+ const unchanged =
447
+ prev && prev.inode === inode && prev.size === size && prev.mtimeMs === mtimeMs;
439
448
  if (unchanged) {
440
449
  filesProcessed += 1;
441
450
  if (cb) {
@@ -445,22 +454,24 @@ async function parseOpencodeIncremental({
445
454
  filePath,
446
455
  filesProcessed,
447
456
  eventsAggregated,
448
- bucketsQueued: touchedBuckets.size
457
+ bucketsQueued: touchedBuckets.size,
449
458
  });
450
459
  }
451
460
  continue;
452
461
  }
453
462
 
454
- const fallbackTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
463
+ const fallbackTotals = prev && typeof prev.lastTotals === "object" ? prev.lastTotals : null;
455
464
  const fallbackMessageKey =
456
- prev && typeof prev.messageKey === 'string' && prev.messageKey.trim() ? prev.messageKey.trim() : null;
465
+ prev && typeof prev.messageKey === "string" && prev.messageKey.trim()
466
+ ? prev.messageKey.trim()
467
+ : null;
457
468
  const projectContext = projectEnabled
458
469
  ? await resolveProjectContextForFile({
459
470
  filePath,
460
471
  projectMetaCache,
461
472
  publicRepoCache,
462
473
  publicRepoResolver,
463
- projectState
474
+ projectState,
464
475
  })
465
476
  : null;
466
477
  const projectRef = projectContext?.projectRef || null;
@@ -477,7 +488,7 @@ async function parseOpencodeIncremental({
477
488
  projectState,
478
489
  projectTouchedBuckets,
479
490
  projectRef,
480
- projectKey
491
+ projectKey,
481
492
  });
482
493
 
483
494
  cursors.files[key] = {
@@ -486,7 +497,7 @@ async function parseOpencodeIncremental({
486
497
  mtimeMs,
487
498
  lastTotals: result.lastTotals,
488
499
  messageKey: result.messageKey || null,
489
- updatedAt: new Date().toISOString()
500
+ updatedAt: new Date().toISOString(),
490
501
  };
491
502
 
492
503
  filesProcessed += 1;
@@ -495,7 +506,7 @@ async function parseOpencodeIncremental({
495
506
  if (result.messageKey && result.shouldUpdate) {
496
507
  messageIndex[result.messageKey] = {
497
508
  lastTotals: result.lastTotals,
498
- updatedAt: new Date().toISOString()
509
+ updatedAt: new Date().toISOString(),
499
510
  };
500
511
  }
501
512
 
@@ -506,7 +517,7 @@ async function parseOpencodeIncremental({
506
517
  filePath,
507
518
  filesProcessed,
508
519
  eventsAggregated,
509
- bucketsQueued: touchedBuckets.size
520
+ bucketsQueued: touchedBuckets.size,
510
521
  });
511
522
  }
512
523
  }
@@ -533,32 +544,34 @@ async function parseOpenclawIncremental({
533
544
  queuePath,
534
545
  projectQueuePath,
535
546
  onProgress,
536
- source
547
+ source,
537
548
  }) {
538
549
  await ensureDir(path.dirname(queuePath));
539
550
  let filesProcessed = 0;
540
551
  let eventsAggregated = 0;
541
552
 
542
- const cb = typeof onProgress === 'function' ? onProgress : null;
553
+ const cb = typeof onProgress === "function" ? onProgress : null;
543
554
  const files = Array.isArray(sessionFiles) ? sessionFiles : [];
544
555
  const totalFiles = files.length;
545
556
  const hourlyState = normalizeHourlyState(cursors?.hourly);
546
- const projectEnabled = typeof projectQueuePath === 'string' && projectQueuePath.length > 0;
557
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
547
558
  const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
548
559
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
549
560
  const touchedBuckets = new Set();
550
- const defaultSource = normalizeSourceInput(source) || 'openclaw';
561
+ const defaultSource = normalizeSourceInput(source) || "openclaw";
551
562
 
552
- if (!cursors.files || typeof cursors.files !== 'object') {
563
+ if (!cursors.files || typeof cursors.files !== "object") {
553
564
  cursors.files = {};
554
565
  }
555
566
 
556
567
  for (let idx = 0; idx < files.length; idx++) {
557
568
  const entry = files[idx];
558
- const filePath = typeof entry === 'string' ? entry : entry?.path;
569
+ const filePath = typeof entry === "string" ? entry : entry?.path;
559
570
  if (!filePath) continue;
560
571
  const fileSource =
561
- typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
572
+ typeof entry === "string"
573
+ ? defaultSource
574
+ : normalizeSourceInput(entry?.source) || defaultSource;
562
575
  const st = await fs.stat(filePath).catch(() => null);
563
576
  if (!st || !st.isFile()) continue;
564
577
 
@@ -574,13 +587,13 @@ async function parseOpenclawIncremental({
574
587
  touchedBuckets,
575
588
  source: fileSource,
576
589
  projectState,
577
- projectTouchedBuckets
590
+ projectTouchedBuckets,
578
591
  });
579
592
 
580
593
  cursors.files[key] = {
581
594
  inode,
582
595
  offset: result.endOffset,
583
- updatedAt: new Date().toISOString()
596
+ updatedAt: new Date().toISOString(),
584
597
  };
585
598
 
586
599
  filesProcessed += 1;
@@ -593,7 +606,7 @@ async function parseOpenclawIncremental({
593
606
  filePath,
594
607
  filesProcessed,
595
608
  eventsAggregated,
596
- bucketsQueued: touchedBuckets.size
609
+ bucketsQueued: touchedBuckets.size,
597
610
  });
598
611
  }
599
612
  }
@@ -619,20 +632,20 @@ async function parseOpenclawSessionFile({
619
632
  touchedBuckets,
620
633
  source,
621
634
  projectState,
622
- projectTouchedBuckets
635
+ projectTouchedBuckets,
623
636
  }) {
624
637
  const st = await fs.stat(filePath);
625
638
  const endOffset = st.size;
626
639
  if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
627
640
 
628
- const stream = fssync.createReadStream(filePath, { encoding: 'utf8', start: startOffset });
641
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
629
642
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
630
643
 
631
644
  let eventsAggregated = 0;
632
645
  for await (const line of rl) {
633
646
  if (!line) continue;
634
647
  // Fast-path filter: OpenClaw assistant messages include message.usage.totalTokens.
635
- if (!line.includes('"usage"') || !line.includes('totalTokens')) continue;
648
+ if (!line.includes('"usage"') || !line.includes("totalTokens")) continue;
636
649
 
637
650
  let obj;
638
651
  try {
@@ -641,14 +654,14 @@ async function parseOpenclawSessionFile({
641
654
  continue;
642
655
  }
643
656
 
644
- if (obj?.type !== 'message') continue;
657
+ if (obj?.type !== "message") continue;
645
658
  const msg = obj?.message;
646
- if (!msg || typeof msg !== 'object') continue;
659
+ if (!msg || typeof msg !== "object") continue;
647
660
 
648
661
  const usage = msg.usage;
649
- if (!usage || typeof usage !== 'object') continue;
662
+ if (!usage || typeof usage !== "object") continue;
650
663
 
651
- const tokenTimestamp = typeof obj?.timestamp === 'string' ? obj.timestamp : null;
664
+ const tokenTimestamp = typeof obj?.timestamp === "string" ? obj.timestamp : null;
652
665
  if (!tokenTimestamp) continue;
653
666
 
654
667
  const model = normalizeModelInput(msg.model) || DEFAULT_MODEL;
@@ -658,7 +671,7 @@ async function parseOpenclawSessionFile({
658
671
  cached_input_tokens: Number((usage.cacheRead || 0) + (usage.cacheWrite || 0)),
659
672
  output_tokens: Number(usage.output || 0),
660
673
  reasoning_output_tokens: 0,
661
- total_tokens: Number(usage.totalTokens || 0)
674
+ total_tokens: Number(usage.totalTokens || 0),
662
675
  };
663
676
 
664
677
  if (isAllZeroUsage(delta)) continue;
@@ -694,7 +707,7 @@ async function parseRolloutFile({
694
707
  projectKey,
695
708
  projectMetaCache,
696
709
  publicRepoCache,
697
- publicRepoResolver
710
+ publicRepoResolver,
698
711
  }) {
699
712
  const st = await fs.stat(filePath);
700
713
  const endOffset = st.size;
@@ -702,11 +715,11 @@ async function parseRolloutFile({
702
715
  return { endOffset, lastTotal, lastModel, eventsAggregated: 0 };
703
716
  }
704
717
 
705
- const stream = fssync.createReadStream(filePath, { encoding: 'utf8', start: startOffset });
718
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
706
719
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
707
720
 
708
- let model = typeof lastModel === 'string' ? lastModel : null;
709
- let totals = lastTotal && typeof lastTotal === 'object' ? lastTotal : null;
721
+ let model = typeof lastModel === "string" ? lastModel : null;
722
+ let totals = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
710
723
  let currentCwd = null;
711
724
  let currentProjectRef = projectRef || null;
712
725
  let currentProjectKey = projectKey || null;
@@ -729,14 +742,14 @@ async function parseRolloutFile({
729
742
  }
730
743
 
731
744
  if (
732
- (obj?.type === 'turn_context' || obj?.type === 'session_meta') &&
745
+ (obj?.type === "turn_context" || obj?.type === "session_meta") &&
733
746
  obj?.payload &&
734
- typeof obj.payload === 'object'
747
+ typeof obj.payload === "object"
735
748
  ) {
736
- if (typeof obj.payload.model === 'string') {
749
+ if (typeof obj.payload.model === "string") {
737
750
  model = obj.payload.model;
738
751
  }
739
- if (projectState && typeof obj.payload.cwd === 'string') {
752
+ if (projectState && typeof obj.payload.cwd === "string") {
740
753
  const nextCwd = obj.payload.cwd.trim();
741
754
  if (nextCwd && nextCwd !== currentCwd) {
742
755
  const context = await resolveProjectContextForPath({
@@ -744,7 +757,7 @@ async function parseRolloutFile({
744
757
  projectMetaCache,
745
758
  publicRepoCache,
746
759
  publicRepoResolver,
747
- projectState
760
+ projectState,
748
761
  });
749
762
  currentCwd = nextCwd;
750
763
  currentProjectRef = context?.projectRef || null;
@@ -758,9 +771,9 @@ async function parseRolloutFile({
758
771
  if (!token) continue;
759
772
 
760
773
  const info = token.info;
761
- if (!info || typeof info !== 'object') continue;
774
+ if (!info || typeof info !== "object") continue;
762
775
 
763
- const tokenTimestamp = typeof token.timestamp === 'string' ? token.timestamp : null;
776
+ const tokenTimestamp = typeof token.timestamp === "string" ? token.timestamp : null;
764
777
  if (!tokenTimestamp) continue;
765
778
 
766
779
  const lastUsage = info.last_token_usage;
@@ -769,7 +782,7 @@ async function parseRolloutFile({
769
782
  const delta = pickDelta(lastUsage, totalUsage, totals);
770
783
  if (!delta) continue;
771
784
 
772
- if (totalUsage && typeof totalUsage === 'object') {
785
+ if (totalUsage && typeof totalUsage === "object") {
773
786
  totals = totalUsage;
774
787
  }
775
788
 
@@ -785,7 +798,7 @@ async function parseRolloutFile({
785
798
  currentProjectKey,
786
799
  source,
787
800
  bucketStart,
788
- currentProjectRef
801
+ currentProjectRef,
789
802
  );
790
803
  addTotals(projectBucket.totals, delta);
791
804
  projectTouchedBuckets.add(projectBucketKey(currentProjectKey, source, bucketStart));
@@ -805,7 +818,7 @@ async function parseClaudeFile({
805
818
  projectState,
806
819
  projectTouchedBuckets,
807
820
  projectRef,
808
- projectKey
821
+ projectKey,
809
822
  }) {
810
823
  const st = await fs.stat(filePath).catch(() => null);
811
824
  if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
@@ -813,7 +826,7 @@ async function parseClaudeFile({
813
826
  const endOffset = st.size;
814
827
  if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
815
828
 
816
- const stream = fssync.createReadStream(filePath, { encoding: 'utf8', start: startOffset });
829
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
817
830
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
818
831
 
819
832
  let eventsAggregated = 0;
@@ -827,10 +840,10 @@ async function parseClaudeFile({
827
840
  }
828
841
 
829
842
  const usage = obj?.message?.usage || obj?.usage;
830
- if (!usage || typeof usage !== 'object') continue;
843
+ if (!usage || typeof usage !== "object") continue;
831
844
 
832
845
  const model = normalizeModelInput(obj?.message?.model || obj?.model) || DEFAULT_MODEL;
833
- const tokenTimestamp = typeof obj?.timestamp === 'string' ? obj.timestamp : null;
846
+ const tokenTimestamp = typeof obj?.timestamp === "string" ? obj.timestamp : null;
834
847
  if (!tokenTimestamp) continue;
835
848
 
836
849
  const delta = normalizeClaudeUsage(usage);
@@ -843,7 +856,13 @@ async function parseClaudeFile({
843
856
  addTotals(bucket.totals, delta);
844
857
  touchedBuckets.add(bucketKey(source, model, bucketStart));
845
858
  if (projectKey && projectState && projectTouchedBuckets) {
846
- const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
859
+ const projectBucket = getProjectBucket(
860
+ projectState,
861
+ projectKey,
862
+ source,
863
+ bucketStart,
864
+ projectRef,
865
+ );
847
866
  addTotals(projectBucket.totals, delta);
848
867
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
849
868
  }
@@ -866,9 +885,9 @@ async function parseGeminiFile({
866
885
  projectState,
867
886
  projectTouchedBuckets,
868
887
  projectRef,
869
- projectKey
888
+ projectKey,
870
889
  }) {
871
- const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
890
+ const raw = await fs.readFile(filePath, "utf8").catch(() => "");
872
891
  if (!raw.trim()) return { lastIndex: startIndex, lastTotals, lastModel, eventsAggregated: 0 };
873
892
 
874
893
  let session;
@@ -886,18 +905,18 @@ async function parseGeminiFile({
886
905
  }
887
906
 
888
907
  let eventsAggregated = 0;
889
- let model = typeof lastModel === 'string' ? lastModel : null;
890
- let totals = lastTotals && typeof lastTotals === 'object' ? lastTotals : null;
908
+ let model = typeof lastModel === "string" ? lastModel : null;
909
+ let totals = lastTotals && typeof lastTotals === "object" ? lastTotals : null;
891
910
  const begin = Number.isFinite(startIndex) ? startIndex + 1 : 0;
892
911
 
893
912
  for (let idx = begin; idx < messages.length; idx++) {
894
913
  const msg = messages[idx];
895
- if (!msg || typeof msg !== 'object') continue;
914
+ if (!msg || typeof msg !== "object") continue;
896
915
 
897
916
  const normalizedModel = normalizeModelInput(msg.model);
898
917
  if (normalizedModel) model = normalizedModel;
899
918
 
900
- const timestamp = typeof msg.timestamp === 'string' ? msg.timestamp : null;
919
+ const timestamp = typeof msg.timestamp === "string" ? msg.timestamp : null;
901
920
  const currentTotals = normalizeGeminiTokens(msg.tokens);
902
921
  if (!timestamp || !currentTotals) {
903
922
  totals = currentTotals || totals;
@@ -920,7 +939,13 @@ async function parseGeminiFile({
920
939
  addTotals(bucket.totals, delta);
921
940
  touchedBuckets.add(bucketKey(source, model, bucketStart));
922
941
  if (projectKey && projectState && projectTouchedBuckets) {
923
- const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
942
+ const projectBucket = getProjectBucket(
943
+ projectState,
944
+ projectKey,
945
+ source,
946
+ bucketStart,
947
+ projectRef,
948
+ );
924
949
  addTotals(projectBucket.totals, delta);
925
950
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
926
951
  }
@@ -932,7 +957,7 @@ async function parseGeminiFile({
932
957
  lastIndex: messages.length - 1,
933
958
  lastTotals: totals,
934
959
  lastModel: model,
935
- eventsAggregated
960
+ eventsAggregated,
936
961
  };
937
962
  }
938
963
 
@@ -947,22 +972,26 @@ async function parseOpencodeMessageFile({
947
972
  projectState,
948
973
  projectTouchedBuckets,
949
974
  projectRef,
950
- projectKey
975
+ projectKey,
951
976
  }) {
952
977
  const fallbackKey =
953
- typeof fallbackMessageKey === 'string' && fallbackMessageKey.trim() ? fallbackMessageKey.trim() : null;
954
- const legacyTotals = fallbackTotals && typeof fallbackTotals === 'object' ? fallbackTotals : null;
978
+ typeof fallbackMessageKey === "string" && fallbackMessageKey.trim()
979
+ ? fallbackMessageKey.trim()
980
+ : null;
981
+ const legacyTotals = fallbackTotals && typeof fallbackTotals === "object" ? fallbackTotals : null;
955
982
  const fallbackEntry = messageIndex && fallbackKey ? messageIndex[fallbackKey] : null;
956
983
  const fallbackLastTotals =
957
- fallbackEntry && typeof fallbackEntry.lastTotals === 'object' ? fallbackEntry.lastTotals : legacyTotals;
984
+ fallbackEntry && typeof fallbackEntry.lastTotals === "object"
985
+ ? fallbackEntry.lastTotals
986
+ : legacyTotals;
958
987
 
959
- const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
988
+ const raw = await fs.readFile(filePath, "utf8").catch(() => "");
960
989
  if (!raw.trim()) {
961
990
  return {
962
991
  messageKey: fallbackKey,
963
992
  lastTotals: fallbackLastTotals,
964
993
  eventsAggregated: 0,
965
- shouldUpdate: false
994
+ shouldUpdate: false,
966
995
  };
967
996
  }
968
997
 
@@ -974,13 +1003,13 @@ async function parseOpencodeMessageFile({
974
1003
  messageKey: fallbackKey,
975
1004
  lastTotals: fallbackLastTotals,
976
1005
  eventsAggregated: 0,
977
- shouldUpdate: false
1006
+ shouldUpdate: false,
978
1007
  };
979
1008
  }
980
1009
 
981
1010
  const messageKey = deriveOpencodeMessageKey(msg, filePath);
982
1011
  const prev = messageIndex && messageKey ? messageIndex[messageKey] : null;
983
- const indexTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
1012
+ const indexTotals = prev && typeof prev.lastTotals === "object" ? prev.lastTotals : null;
984
1013
  const fallbackMatch = !fallbackKey || fallbackKey === messageKey;
985
1014
  const lastTotals = indexTotals || (fallbackMatch ? fallbackLastTotals : null);
986
1015
 
@@ -1000,7 +1029,7 @@ async function parseOpencodeMessageFile({
1000
1029
  messageKey,
1001
1030
  lastTotals,
1002
1031
  eventsAggregated: 0,
1003
- shouldUpdate: Boolean(lastTotals)
1032
+ shouldUpdate: Boolean(lastTotals),
1004
1033
  };
1005
1034
  }
1006
1035
 
@@ -1011,7 +1040,7 @@ async function parseOpencodeMessageFile({
1011
1040
  messageKey,
1012
1041
  lastTotals,
1013
1042
  eventsAggregated: 0,
1014
- shouldUpdate: Boolean(lastTotals)
1043
+ shouldUpdate: Boolean(lastTotals),
1015
1044
  };
1016
1045
  }
1017
1046
 
@@ -1020,7 +1049,13 @@ async function parseOpencodeMessageFile({
1020
1049
  addTotals(bucket.totals, delta);
1021
1050
  touchedBuckets.add(bucketKey(source, model, bucketStart));
1022
1051
  if (projectKey && projectState && projectTouchedBuckets) {
1023
- const projectBucket = getProjectBucket(projectState, projectKey, source, bucketStart, projectRef);
1052
+ const projectBucket = getProjectBucket(
1053
+ projectState,
1054
+ projectKey,
1055
+ source,
1056
+ bucketStart,
1057
+ projectRef,
1058
+ );
1024
1059
  addTotals(projectBucket.totals, delta);
1025
1060
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
1026
1061
  }
@@ -1039,7 +1074,10 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1039
1074
  }
1040
1075
  if (touchedGroups.size === 0) return 0;
1041
1076
 
1042
- const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
1077
+ const groupQueued =
1078
+ hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
1079
+ ? hourlyState.groupQueued
1080
+ : {};
1043
1081
  let codexTouched = false;
1044
1082
  const legacyGroups = new Set();
1045
1083
  for (const groupKey of touchedGroups) {
@@ -1068,7 +1106,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1068
1106
  groupedBuckets.set(groupKey, group);
1069
1107
  }
1070
1108
 
1071
- if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
1109
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== "string") {
1072
1110
  bucket.queuedKey = null;
1073
1111
  }
1074
1112
  group.buckets.set(model, bucket);
@@ -1082,7 +1120,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1082
1120
  const hourStart = parsed.hourStart;
1083
1121
  if (!hourStart) continue;
1084
1122
  const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
1085
- if (source !== 'every-code') continue;
1123
+ if (source !== "every-code") continue;
1086
1124
  const groupKey = groupBucketKey(source, hourStart);
1087
1125
  if (legacyGroups.has(groupKey) || groupedBuckets.has(groupKey)) continue;
1088
1126
  const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
@@ -1104,7 +1142,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1104
1142
  group = { source, hourStart, buckets: new Map() };
1105
1143
  groupedBuckets.set(groupKey, group);
1106
1144
  }
1107
- if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
1145
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== "string") {
1108
1146
  bucket.queuedKey = null;
1109
1147
  }
1110
1148
  const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
@@ -1138,11 +1176,16 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1138
1176
  cached_input_tokens: zeroTotals.cached_input_tokens,
1139
1177
  output_tokens: zeroTotals.output_tokens,
1140
1178
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1141
- total_tokens: zeroTotals.total_tokens
1142
- })
1179
+ total_tokens: zeroTotals.total_tokens,
1180
+ }),
1143
1181
  );
1144
1182
  }
1145
- if (unknownBucket && !alignedModel && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
1183
+ if (
1184
+ unknownBucket &&
1185
+ !alignedModel &&
1186
+ unknownBucket.queuedKey &&
1187
+ unknownBucket.queuedKey !== zeroKey
1188
+ ) {
1146
1189
  if (unknownBucket.retractedUnknownKey !== zeroKey) {
1147
1190
  toAppend.push(
1148
1191
  JSON.stringify({
@@ -1153,8 +1196,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1153
1196
  cached_input_tokens: zeroTotals.cached_input_tokens,
1154
1197
  output_tokens: zeroTotals.output_tokens,
1155
1198
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1156
- total_tokens: zeroTotals.total_tokens
1157
- })
1199
+ total_tokens: zeroTotals.total_tokens,
1200
+ }),
1158
1201
  );
1159
1202
  unknownBucket.retractedUnknownKey = zeroKey;
1160
1203
  }
@@ -1178,8 +1221,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1178
1221
  cached_input_tokens: totals.cached_input_tokens,
1179
1222
  output_tokens: totals.output_tokens,
1180
1223
  reasoning_output_tokens: totals.reasoning_output_tokens,
1181
- total_tokens: totals.total_tokens
1182
- })
1224
+ total_tokens: totals.total_tokens,
1225
+ }),
1183
1226
  );
1184
1227
  bucket.queuedKey = key;
1185
1228
  }
@@ -1188,7 +1231,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1188
1231
 
1189
1232
  if (!unknownBucket?.totals) continue;
1190
1233
  let outputModel = DEFAULT_MODEL;
1191
- if (group.source === 'every-code') {
1234
+ if (group.source === "every-code") {
1192
1235
  const aligned = findNearestCodexModel(group.hourStart, codexDominants);
1193
1236
  if (aligned) outputModel = aligned;
1194
1237
  }
@@ -1203,11 +1246,16 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1203
1246
  cached_input_tokens: zeroTotals.cached_input_tokens,
1204
1247
  output_tokens: zeroTotals.output_tokens,
1205
1248
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1206
- total_tokens: zeroTotals.total_tokens
1207
- })
1249
+ total_tokens: zeroTotals.total_tokens,
1250
+ }),
1208
1251
  );
1209
1252
  }
1210
- if (!alignedModel && nextAligned && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
1253
+ if (
1254
+ !alignedModel &&
1255
+ nextAligned &&
1256
+ unknownBucket.queuedKey &&
1257
+ unknownBucket.queuedKey !== zeroKey
1258
+ ) {
1211
1259
  if (unknownBucket.retractedUnknownKey !== zeroKey) {
1212
1260
  toAppend.push(
1213
1261
  JSON.stringify({
@@ -1218,8 +1266,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1218
1266
  cached_input_tokens: zeroTotals.cached_input_tokens,
1219
1267
  output_tokens: zeroTotals.output_tokens,
1220
1268
  reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
1221
- total_tokens: zeroTotals.total_tokens
1222
- })
1269
+ total_tokens: zeroTotals.total_tokens,
1270
+ }),
1223
1271
  );
1224
1272
  unknownBucket.retractedUnknownKey = zeroKey;
1225
1273
  }
@@ -1237,8 +1285,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1237
1285
  cached_input_tokens: unknownBucket.totals.cached_input_tokens,
1238
1286
  output_tokens: unknownBucket.totals.output_tokens,
1239
1287
  reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
1240
- total_tokens: unknownBucket.totals.total_tokens
1241
- })
1288
+ total_tokens: unknownBucket.totals.total_tokens,
1289
+ }),
1242
1290
  );
1243
1291
  unknownBucket.queuedKey = outputKey;
1244
1292
  }
@@ -1259,7 +1307,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1259
1307
  source: normalizeSourceInput(parsed.source) || DEFAULT_SOURCE,
1260
1308
  hourStart,
1261
1309
  models: new Set(),
1262
- totals: initTotals()
1310
+ totals: initTotals(),
1263
1311
  };
1264
1312
  grouped.set(groupKey, group);
1265
1313
  }
@@ -1281,8 +1329,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1281
1329
  cached_input_tokens: group.totals.cached_input_tokens,
1282
1330
  output_tokens: group.totals.output_tokens,
1283
1331
  reasoning_output_tokens: group.totals.reasoning_output_tokens,
1284
- total_tokens: group.totals.total_tokens
1285
- })
1332
+ total_tokens: group.totals.total_tokens,
1333
+ }),
1286
1334
  );
1287
1335
  groupQueued[groupKey] = key;
1288
1336
  }
@@ -1291,14 +1339,24 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
1291
1339
  hourlyState.groupQueued = groupQueued;
1292
1340
 
1293
1341
  if (toAppend.length > 0) {
1294
- await fs.appendFile(queuePath, toAppend.join('\n') + '\n', 'utf8');
1342
+ await fs.appendFile(queuePath, toAppend.join("\n") + "\n", "utf8");
1295
1343
  }
1296
1344
 
1297
1345
  return toAppend.length;
1298
1346
  }
1299
1347
 
1300
- async function enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets }) {
1301
- if (!projectQueuePath || !projectState || !projectTouchedBuckets || projectTouchedBuckets.size === 0) return 0;
1348
+ async function enqueueTouchedProjectBuckets({
1349
+ projectQueuePath,
1350
+ projectState,
1351
+ projectTouchedBuckets,
1352
+ }) {
1353
+ if (
1354
+ !projectQueuePath ||
1355
+ !projectState ||
1356
+ !projectTouchedBuckets ||
1357
+ projectTouchedBuckets.size === 0
1358
+ )
1359
+ return 0;
1302
1360
 
1303
1361
  await ensureDir(path.dirname(projectQueuePath));
1304
1362
 
@@ -1309,8 +1367,8 @@ async function enqueueTouchedProjectBuckets({ projectQueuePath, projectState, pr
1309
1367
  const totals = bucket.totals;
1310
1368
  const queuedKey = totalsKey(totals);
1311
1369
  if (bucket.queuedKey === queuedKey) continue;
1312
- const projectRef = typeof bucket.project_ref === 'string' ? bucket.project_ref : null;
1313
- const projectKey = typeof bucket.project_key === 'string' ? bucket.project_key : null;
1370
+ const projectRef = typeof bucket.project_ref === "string" ? bucket.project_ref : null;
1371
+ const projectKey = typeof bucket.project_key === "string" ? bucket.project_key : null;
1314
1372
  if (!projectRef || !projectKey) continue;
1315
1373
 
1316
1374
  toAppend.push(
@@ -1323,14 +1381,14 @@ async function enqueueTouchedProjectBuckets({ projectQueuePath, projectState, pr
1323
1381
  cached_input_tokens: totals.cached_input_tokens,
1324
1382
  output_tokens: totals.output_tokens,
1325
1383
  reasoning_output_tokens: totals.reasoning_output_tokens,
1326
- total_tokens: totals.total_tokens
1327
- })
1384
+ total_tokens: totals.total_tokens,
1385
+ }),
1328
1386
  );
1329
1387
  bucket.queuedKey = queuedKey;
1330
1388
  }
1331
1389
 
1332
1390
  if (toAppend.length > 0) {
1333
- await fs.appendFile(projectQueuePath, toAppend.join('\n') + '\n', 'utf8');
1391
+ await fs.appendFile(projectQueuePath, toAppend.join("\n") + "\n", "utf8");
1334
1392
  }
1335
1393
 
1336
1394
  return toAppend.length;
@@ -1422,9 +1480,9 @@ function findNearestCodexModel(hourStart, dominants) {
1422
1480
  }
1423
1481
 
1424
1482
  function normalizeHourlyState(raw) {
1425
- const state = raw && typeof raw === 'object' ? raw : {};
1483
+ const state = raw && typeof raw === "object" ? raw : {};
1426
1484
  const version = Number(state.version || 1);
1427
- const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
1485
+ const rawBuckets = state.buckets && typeof state.buckets === "object" ? state.buckets : {};
1428
1486
  const buckets = {};
1429
1487
  const groupQueued = {};
1430
1488
 
@@ -1444,7 +1502,7 @@ function normalizeHourlyState(raw) {
1444
1502
  version: 3,
1445
1503
  buckets,
1446
1504
  groupQueued,
1447
- updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
1505
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
1448
1506
  };
1449
1507
  }
1450
1508
 
@@ -1457,21 +1515,21 @@ function normalizeHourlyState(raw) {
1457
1515
  }
1458
1516
 
1459
1517
  const existingGroupQueued =
1460
- state.groupQueued && typeof state.groupQueued === 'object' ? state.groupQueued : {};
1518
+ state.groupQueued && typeof state.groupQueued === "object" ? state.groupQueued : {};
1461
1519
 
1462
1520
  return {
1463
1521
  version: 3,
1464
1522
  buckets,
1465
1523
  groupQueued: version >= 3 ? existingGroupQueued : {},
1466
- updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
1524
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
1467
1525
  };
1468
1526
  }
1469
1527
 
1470
1528
  function normalizeProjectState(raw) {
1471
- const state = raw && typeof raw === 'object' ? raw : {};
1472
- const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
1529
+ const state = raw && typeof raw === "object" ? raw : {};
1530
+ const rawBuckets = state.buckets && typeof state.buckets === "object" ? state.buckets : {};
1473
1531
  const buckets = {};
1474
- const rawProjects = state.projects && typeof state.projects === 'object' ? state.projects : {};
1532
+ const rawProjects = state.projects && typeof state.projects === "object" ? state.projects : {};
1475
1533
  const projects = {};
1476
1534
 
1477
1535
  for (const [key, value] of Object.entries(rawBuckets)) {
@@ -1480,7 +1538,7 @@ function normalizeProjectState(raw) {
1480
1538
  }
1481
1539
 
1482
1540
  for (const [key, value] of Object.entries(rawProjects)) {
1483
- if (!key || !value || typeof value !== 'object') continue;
1541
+ if (!key || !value || typeof value !== "object") continue;
1484
1542
  projects[key] = { ...value };
1485
1543
  }
1486
1544
 
@@ -1488,21 +1546,21 @@ function normalizeProjectState(raw) {
1488
1546
  version: 2,
1489
1547
  buckets,
1490
1548
  projects,
1491
- updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
1549
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
1492
1550
  };
1493
1551
  }
1494
1552
 
1495
1553
  function normalizeOpencodeState(raw) {
1496
- const state = raw && typeof raw === 'object' ? raw : {};
1497
- const messages = state.messages && typeof state.messages === 'object' ? state.messages : {};
1554
+ const state = raw && typeof raw === "object" ? raw : {};
1555
+ const messages = state.messages && typeof state.messages === "object" ? state.messages : {};
1498
1556
  return {
1499
1557
  messages,
1500
- updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
1558
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
1501
1559
  };
1502
1560
  }
1503
1561
 
1504
1562
  function normalizeMessageKeyPart(value) {
1505
- if (typeof value !== 'string') return '';
1563
+ if (typeof value !== "string") return "";
1506
1564
  return value.trim();
1507
1565
  }
1508
1566
 
@@ -1519,17 +1577,17 @@ function getHourlyBucket(state, source, model, hourStart) {
1519
1577
  const normalizedModel = normalizeModelInput(model) || DEFAULT_MODEL;
1520
1578
  const key = bucketKey(normalizedSource, normalizedModel, hourStart);
1521
1579
  let bucket = buckets[key];
1522
- if (!bucket || typeof bucket !== 'object') {
1580
+ if (!bucket || typeof bucket !== "object") {
1523
1581
  bucket = { totals: initTotals(), queuedKey: null };
1524
1582
  buckets[key] = bucket;
1525
1583
  return bucket;
1526
1584
  }
1527
1585
 
1528
- if (!bucket.totals || typeof bucket.totals !== 'object') {
1586
+ if (!bucket.totals || typeof bucket.totals !== "object") {
1529
1587
  bucket.totals = initTotals();
1530
1588
  }
1531
1589
 
1532
- if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
1590
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== "string") {
1533
1591
  bucket.queuedKey = null;
1534
1592
  }
1535
1593
 
@@ -1541,24 +1599,24 @@ function getProjectBucket(state, projectKey, source, hourStart, projectRef) {
1541
1599
  const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
1542
1600
  const key = projectBucketKey(projectKey, normalizedSource, hourStart);
1543
1601
  let bucket = buckets[key];
1544
- if (!bucket || typeof bucket !== 'object') {
1602
+ if (!bucket || typeof bucket !== "object") {
1545
1603
  bucket = {
1546
1604
  totals: initTotals(),
1547
1605
  queuedKey: null,
1548
1606
  project_key: projectKey,
1549
1607
  project_ref: projectRef,
1550
1608
  source: normalizedSource,
1551
- hour_start: hourStart
1609
+ hour_start: hourStart,
1552
1610
  };
1553
1611
  buckets[key] = bucket;
1554
1612
  return bucket;
1555
1613
  }
1556
1614
 
1557
- if (!bucket.totals || typeof bucket.totals !== 'object') {
1615
+ if (!bucket.totals || typeof bucket.totals !== "object") {
1558
1616
  bucket.totals = initTotals();
1559
1617
  }
1560
1618
 
1561
- if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
1619
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== "string") {
1562
1620
  bucket.queuedKey = null;
1563
1621
  }
1564
1622
 
@@ -1576,7 +1634,7 @@ function initTotals() {
1576
1634
  cached_input_tokens: 0,
1577
1635
  output_tokens: 0,
1578
1636
  reasoning_output_tokens: 0,
1579
- total_tokens: 0
1637
+ total_tokens: 0,
1580
1638
  };
1581
1639
  }
1582
1640
 
@@ -1594,8 +1652,8 @@ function totalsKey(totals) {
1594
1652
  totals.cached_input_tokens || 0,
1595
1653
  totals.output_tokens || 0,
1596
1654
  totals.reasoning_output_tokens || 0,
1597
- totals.total_tokens || 0
1598
- ].join('|');
1655
+ totals.total_tokens || 0,
1656
+ ].join("|");
1599
1657
  }
1600
1658
 
1601
1659
  function toUtcHalfHourStart(ts) {
@@ -1611,8 +1669,8 @@ function toUtcHalfHourStart(ts) {
1611
1669
  dt.getUTCHours(),
1612
1670
  halfMinute,
1613
1671
  0,
1614
- 0
1615
- )
1672
+ 0,
1673
+ ),
1616
1674
  );
1617
1675
  return bucketStart.toISOString();
1618
1676
  }
@@ -1634,7 +1692,8 @@ function groupBucketKey(source, hourStart) {
1634
1692
  }
1635
1693
 
1636
1694
  function parseBucketKey(key) {
1637
- if (typeof key !== 'string') return { source: DEFAULT_SOURCE, model: DEFAULT_MODEL, hourStart: '' };
1695
+ if (typeof key !== "string")
1696
+ return { source: DEFAULT_SOURCE, model: DEFAULT_MODEL, hourStart: "" };
1638
1697
  const first = key.indexOf(BUCKET_SEPARATOR);
1639
1698
  if (first <= 0) return { source: DEFAULT_SOURCE, model: DEFAULT_MODEL, hourStart: key };
1640
1699
  const second = key.indexOf(BUCKET_SEPARATOR, first + 1);
@@ -1644,24 +1703,24 @@ function parseBucketKey(key) {
1644
1703
  return {
1645
1704
  source: key.slice(0, first),
1646
1705
  model: key.slice(first + 1, second),
1647
- hourStart: key.slice(second + 1)
1706
+ hourStart: key.slice(second + 1),
1648
1707
  };
1649
1708
  }
1650
1709
 
1651
1710
  function normalizeSourceInput(value) {
1652
- if (typeof value !== 'string') return null;
1711
+ if (typeof value !== "string") return null;
1653
1712
  const trimmed = value.trim().toLowerCase();
1654
1713
  return trimmed.length > 0 ? trimmed : null;
1655
1714
  }
1656
1715
 
1657
1716
  function normalizeModelInput(value) {
1658
- if (typeof value !== 'string') return null;
1717
+ if (typeof value !== "string") return null;
1659
1718
  const trimmed = value.trim();
1660
1719
  return trimmed.length > 0 ? trimmed : null;
1661
1720
  }
1662
1721
 
1663
1722
  async function resolveProjectMetaForPath(startDir, cache) {
1664
- if (!startDir || typeof startDir !== 'string') return null;
1723
+ if (!startDir || typeof startDir !== "string") return null;
1665
1724
  if (cache && cache.has(startDir)) return cache.get(startDir);
1666
1725
 
1667
1726
  const visited = [];
@@ -1701,11 +1760,11 @@ async function resolveProjectMetaForPath(startDir, cache) {
1701
1760
  async function defaultPublicRepoResolver({ projectRef, repoRoot, cache }) {
1702
1761
  if (!projectRef) {
1703
1762
  return {
1704
- status: 'blocked',
1763
+ status: "blocked",
1705
1764
  projectKey: null,
1706
1765
  projectRef: null,
1707
1766
  repoRootHash: repoRoot ? hashRepoRoot(repoRoot) : null,
1708
- reason: 'missing_ref'
1767
+ reason: "missing_ref",
1709
1768
  };
1710
1769
  }
1711
1770
 
@@ -1714,15 +1773,20 @@ async function defaultPublicRepoResolver({ projectRef, repoRoot, cache }) {
1714
1773
  if (cache && !cached) cache.set(projectRef, base);
1715
1774
  return {
1716
1775
  ...base,
1717
- repoRootHash: repoRoot ? hashRepoRoot(repoRoot) : null
1776
+ repoRootHash: repoRoot ? hashRepoRoot(repoRoot) : null,
1718
1777
  };
1719
1778
  }
1720
1779
 
1721
1780
  function recordProjectMeta(projectState, meta) {
1722
- if (!projectState || !meta || typeof meta !== 'object') return;
1723
- const repoRootHash = typeof meta.repoRootHash === 'string' ? meta.repoRootHash : null;
1724
- let projectKey = typeof meta.projectKey === 'string' ? meta.projectKey : null;
1725
- if (!projectKey && repoRootHash && projectState.projects && typeof projectState.projects === 'object') {
1781
+ if (!projectState || !meta || typeof meta !== "object") return;
1782
+ const repoRootHash = typeof meta.repoRootHash === "string" ? meta.repoRootHash : null;
1783
+ let projectKey = typeof meta.projectKey === "string" ? meta.projectKey : null;
1784
+ if (
1785
+ !projectKey &&
1786
+ repoRootHash &&
1787
+ projectState.projects &&
1788
+ typeof projectState.projects === "object"
1789
+ ) {
1726
1790
  for (const [key, entry] of Object.entries(projectState.projects)) {
1727
1791
  if (entry && entry.repo_root_hash === repoRootHash) {
1728
1792
  projectKey = key;
@@ -1731,22 +1795,22 @@ function recordProjectMeta(projectState, meta) {
1731
1795
  }
1732
1796
  }
1733
1797
  if (!projectKey) return;
1734
- if (!projectState.projects || typeof projectState.projects !== 'object') {
1798
+ if (!projectState.projects || typeof projectState.projects !== "object") {
1735
1799
  projectState.projects = {};
1736
1800
  }
1737
1801
  const prev = projectState.projects[projectKey] || {};
1738
- const status = typeof meta.status === 'string' ? meta.status : null;
1739
- const projectRef = typeof meta.projectRef === 'string' ? meta.projectRef : null;
1802
+ const status = typeof meta.status === "string" ? meta.status : null;
1803
+ const projectRef = typeof meta.projectRef === "string" ? meta.projectRef : null;
1740
1804
  const next = {
1741
1805
  ...prev,
1742
1806
  project_ref: projectRef || prev.project_ref || null,
1743
1807
  status: status || prev.status || null,
1744
1808
  repo_root_hash: repoRootHash || prev.repo_root_hash || null,
1745
- updated_at: new Date().toISOString()
1809
+ updated_at: new Date().toISOString(),
1746
1810
  };
1747
- if (status === 'blocked' && prev.status !== 'blocked') {
1811
+ if (status === "blocked" && prev.status !== "blocked") {
1748
1812
  next.purge_pending = true;
1749
- } else if (status && status !== 'blocked') {
1813
+ } else if (status && status !== "blocked") {
1750
1814
  next.purge_pending = false;
1751
1815
  }
1752
1816
  projectState.projects[projectKey] = next;
@@ -1757,7 +1821,7 @@ async function resolveProjectContextForFile({
1757
1821
  projectMetaCache,
1758
1822
  publicRepoCache,
1759
1823
  publicRepoResolver,
1760
- projectState
1824
+ projectState,
1761
1825
  }) {
1762
1826
  if (!filePath) return null;
1763
1827
  return resolveProjectContextForPath({
@@ -1765,7 +1829,7 @@ async function resolveProjectContextForFile({
1765
1829
  projectMetaCache,
1766
1830
  publicRepoCache,
1767
1831
  publicRepoResolver,
1768
- projectState
1832
+ projectState,
1769
1833
  });
1770
1834
  }
1771
1835
 
@@ -1774,43 +1838,48 @@ async function resolveProjectContextForPath({
1774
1838
  projectMetaCache,
1775
1839
  publicRepoCache,
1776
1840
  publicRepoResolver,
1777
- projectState
1841
+ projectState,
1778
1842
  }) {
1779
1843
  if (!startDir) return null;
1780
1844
  const projectMeta = await resolveProjectMetaForPath(startDir, projectMetaCache);
1781
1845
  if (!projectMeta) return null;
1782
- const resolver = typeof publicRepoResolver === 'function' ? publicRepoResolver : defaultPublicRepoResolver;
1846
+ const resolver =
1847
+ typeof publicRepoResolver === "function" ? publicRepoResolver : defaultPublicRepoResolver;
1783
1848
  const meta = await resolver({
1784
1849
  projectRef: projectMeta.projectRef,
1785
1850
  repoRoot: projectMeta.repoRoot,
1786
- cache: publicRepoCache
1851
+ cache: publicRepoCache,
1787
1852
  });
1788
1853
  const repoRootHash = projectMeta.repoRoot ? hashRepoRoot(projectMeta.repoRoot) : null;
1789
1854
  const normalized = {
1790
1855
  ...(meta || {}),
1791
1856
  projectRef: meta?.projectRef || projectMeta.projectRef,
1792
1857
  projectKey: meta?.projectKey || null,
1793
- status: meta?.status || 'blocked',
1794
- repoRootHash: meta?.repoRootHash || repoRootHash
1858
+ status: meta?.status || "blocked",
1859
+ repoRootHash: meta?.repoRootHash || repoRootHash,
1795
1860
  };
1796
1861
  recordProjectMeta(projectState, normalized);
1797
- if (normalized.status !== 'public_verified') {
1862
+ if (normalized.status !== "public_verified") {
1798
1863
  return { projectRef: normalized.projectRef, projectKey: null, status: normalized.status };
1799
1864
  }
1800
- return { projectRef: normalized.projectRef, projectKey: normalized.projectKey, status: normalized.status };
1865
+ return {
1866
+ projectRef: normalized.projectRef,
1867
+ projectKey: normalized.projectKey,
1868
+ status: normalized.status,
1869
+ };
1801
1870
  }
1802
1871
 
1803
1872
  async function resolveGitConfigPath(rootDir) {
1804
- const gitPath = path.join(rootDir, '.git');
1873
+ const gitPath = path.join(rootDir, ".git");
1805
1874
  const st = await fs.stat(gitPath).catch(() => null);
1806
1875
  if (!st) return null;
1807
1876
  if (st.isDirectory()) {
1808
- const configPath = path.join(gitPath, 'config');
1877
+ const configPath = path.join(gitPath, "config");
1809
1878
  const cfg = await fs.stat(configPath).catch(() => null);
1810
1879
  return cfg && cfg.isFile() ? configPath : null;
1811
1880
  }
1812
1881
  if (st.isFile()) {
1813
- const content = await fs.readFile(gitPath, 'utf8').catch(() => '');
1882
+ const content = await fs.readFile(gitPath, "utf8").catch(() => "");
1814
1883
  const match = content.match(/gitdir:\s*(.+)/i);
1815
1884
  if (!match) return null;
1816
1885
  let gitDir = match[1].trim();
@@ -1818,18 +1887,18 @@ async function resolveGitConfigPath(rootDir) {
1818
1887
  if (!path.isAbsolute(gitDir)) {
1819
1888
  gitDir = path.resolve(rootDir, gitDir);
1820
1889
  }
1821
- const configPath = path.join(gitDir, 'config');
1890
+ const configPath = path.join(gitDir, "config");
1822
1891
  const cfg = await fs.stat(configPath).catch(() => null);
1823
1892
  if (cfg && cfg.isFile()) return configPath;
1824
1893
 
1825
- const commonDirRaw = await fs.readFile(path.join(gitDir, 'commondir'), 'utf8').catch(() => '');
1894
+ const commonDirRaw = await fs.readFile(path.join(gitDir, "commondir"), "utf8").catch(() => "");
1826
1895
  const commonDirRel = commonDirRaw.trim();
1827
1896
  if (!commonDirRel) return null;
1828
1897
  let commonDir = commonDirRel;
1829
1898
  if (!path.isAbsolute(commonDir)) {
1830
1899
  commonDir = path.resolve(gitDir, commonDir);
1831
1900
  }
1832
- const commonConfigPath = path.join(commonDir, 'config');
1901
+ const commonConfigPath = path.join(commonDir, "config");
1833
1902
  const commonCfg = await fs.stat(commonConfigPath).catch(() => null);
1834
1903
  return commonCfg && commonCfg.isFile() ? commonConfigPath : null;
1835
1904
  }
@@ -1837,7 +1906,7 @@ async function resolveGitConfigPath(rootDir) {
1837
1906
  }
1838
1907
 
1839
1908
  async function readGitRemoteUrl(configPath) {
1840
- const raw = await fs.readFile(configPath, 'utf8').catch(() => '');
1909
+ const raw = await fs.readFile(configPath, "utf8").catch(() => "");
1841
1910
  if (!raw.trim()) return null;
1842
1911
 
1843
1912
  const remotes = new Map();
@@ -1856,34 +1925,34 @@ async function readGitRemoteUrl(configPath) {
1856
1925
  }
1857
1926
  }
1858
1927
 
1859
- if (remotes.has('origin')) return remotes.get('origin');
1928
+ if (remotes.has("origin")) return remotes.get("origin");
1860
1929
  const first = remotes.values().next();
1861
1930
  return first.done ? null : first.value;
1862
1931
  }
1863
1932
 
1864
1933
  function canonicalizeProjectRef(remoteUrl) {
1865
- if (typeof remoteUrl !== 'string') return null;
1934
+ if (typeof remoteUrl !== "string") return null;
1866
1935
  let ref = remoteUrl.trim();
1867
1936
  if (!ref) return null;
1868
1937
 
1869
- if (ref.startsWith('file://')) return null;
1938
+ if (ref.startsWith("file://")) return null;
1870
1939
  if (path.isAbsolute(ref) || path.win32.isAbsolute(ref)) return null;
1871
1940
 
1872
1941
  const gitAtMatch = ref.match(/^git@([^:]+):(.+)$/i);
1873
1942
  if (gitAtMatch) {
1874
1943
  ref = `https://${gitAtMatch[1]}/${gitAtMatch[2]}`;
1875
- } else if (ref.startsWith('ssh://')) {
1944
+ } else if (ref.startsWith("ssh://")) {
1876
1945
  try {
1877
1946
  const parsed = new URL(ref);
1878
1947
  ref = `https://${parsed.hostname}${parsed.pathname}`;
1879
1948
  } catch (_e) {
1880
1949
  return null;
1881
1950
  }
1882
- } else if (ref.startsWith('git://')) {
1883
- ref = `https://${ref.slice('git://'.length)}`;
1884
- } else if (ref.startsWith('http://')) {
1885
- ref = `https://${ref.slice('http://'.length)}`;
1886
- } else if (!ref.startsWith('https://')) {
1951
+ } else if (ref.startsWith("git://")) {
1952
+ ref = `https://${ref.slice("git://".length)}`;
1953
+ } else if (ref.startsWith("http://")) {
1954
+ ref = `https://${ref.slice("http://".length)}`;
1955
+ } else if (!ref.startsWith("https://")) {
1887
1956
  return null;
1888
1957
  }
1889
1958
 
@@ -1895,13 +1964,13 @@ function canonicalizeProjectRef(remoteUrl) {
1895
1964
  return null;
1896
1965
  }
1897
1966
 
1898
- ref = ref.replace(/\.git$/i, '');
1899
- ref = ref.replace(/\/+$/, '');
1967
+ ref = ref.replace(/\.git$/i, "");
1968
+ ref = ref.replace(/\/+$/, "");
1900
1969
  return ref || null;
1901
1970
  }
1902
1971
 
1903
1972
  function normalizeGeminiTokens(tokens) {
1904
- if (!tokens || typeof tokens !== 'object') return null;
1973
+ if (!tokens || typeof tokens !== "object") return null;
1905
1974
  const input = toNonNegativeInt(tokens.input);
1906
1975
  const cached = toNonNegativeInt(tokens.cached);
1907
1976
  const output = toNonNegativeInt(tokens.output);
@@ -1914,12 +1983,12 @@ function normalizeGeminiTokens(tokens) {
1914
1983
  cached_input_tokens: cached,
1915
1984
  output_tokens: output + tool,
1916
1985
  reasoning_output_tokens: thoughts,
1917
- total_tokens: total
1986
+ total_tokens: total,
1918
1987
  };
1919
1988
  }
1920
1989
 
1921
1990
  function normalizeOpencodeTokens(tokens) {
1922
- if (!tokens || typeof tokens !== 'object') return null;
1991
+ if (!tokens || typeof tokens !== "object") return null;
1923
1992
  const input = toNonNegativeInt(tokens.input);
1924
1993
  const output = toNonNegativeInt(tokens.output);
1925
1994
  const reasoning = toNonNegativeInt(tokens.reasoning);
@@ -1933,7 +2002,7 @@ function normalizeOpencodeTokens(tokens) {
1933
2002
  cached_input_tokens: cached,
1934
2003
  output_tokens: output,
1935
2004
  reasoning_output_tokens: reasoning,
1936
- total_tokens: total
2005
+ total_tokens: total,
1937
2006
  };
1938
2007
  }
1939
2008
 
@@ -1949,8 +2018,8 @@ function sameGeminiTotals(a, b) {
1949
2018
  }
1950
2019
 
1951
2020
  function diffGeminiTotals(current, previous) {
1952
- if (!current || typeof current !== 'object') return null;
1953
- if (!previous || typeof previous !== 'object') return current;
2021
+ if (!current || typeof current !== "object") return null;
2022
+ if (!previous || typeof previous !== "object") return current;
1954
2023
  if (sameGeminiTotals(current, previous)) return null;
1955
2024
 
1956
2025
  const totalReset = (current.total_tokens || 0) < (previous.total_tokens || 0);
@@ -1958,10 +2027,16 @@ function diffGeminiTotals(current, previous) {
1958
2027
 
1959
2028
  const delta = {
1960
2029
  input_tokens: Math.max(0, (current.input_tokens || 0) - (previous.input_tokens || 0)),
1961
- cached_input_tokens: Math.max(0, (current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0)),
2030
+ cached_input_tokens: Math.max(
2031
+ 0,
2032
+ (current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0),
2033
+ ),
1962
2034
  output_tokens: Math.max(0, (current.output_tokens || 0) - (previous.output_tokens || 0)),
1963
- reasoning_output_tokens: Math.max(0, (current.reasoning_output_tokens || 0) - (previous.reasoning_output_tokens || 0)),
1964
- total_tokens: Math.max(0, (current.total_tokens || 0) - (previous.total_tokens || 0))
2035
+ reasoning_output_tokens: Math.max(
2036
+ 0,
2037
+ (current.reasoning_output_tokens || 0) - (previous.reasoning_output_tokens || 0),
2038
+ ),
2039
+ total_tokens: Math.max(0, (current.total_tokens || 0) - (previous.total_tokens || 0)),
1965
2040
  };
1966
2041
 
1967
2042
  return isAllZeroUsage(delta) ? null : delta;
@@ -1970,11 +2045,11 @@ function diffGeminiTotals(current, previous) {
1970
2045
  function extractTokenCount(obj) {
1971
2046
  const payload = obj?.payload;
1972
2047
  if (!payload) return null;
1973
- if (payload.type === 'token_count') {
2048
+ if (payload.type === "token_count") {
1974
2049
  return { info: payload.info, timestamp: obj?.timestamp || null };
1975
2050
  }
1976
2051
  const msg = payload.msg;
1977
- if (msg && msg.type === 'token_count') {
2052
+ if (msg && msg.type === "token_count") {
1978
2053
  return { info: msg.info, timestamp: obj?.timestamp || null };
1979
2054
  }
1980
2055
  return null;
@@ -2002,7 +2077,13 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
2002
2077
 
2003
2078
  if (hasTotal && hasPrevTotals) {
2004
2079
  const delta = {};
2005
- for (const k of ['input_tokens', 'cached_input_tokens', 'output_tokens', 'reasoning_output_tokens', 'total_tokens']) {
2080
+ for (const k of [
2081
+ "input_tokens",
2082
+ "cached_input_tokens",
2083
+ "output_tokens",
2084
+ "reasoning_output_tokens",
2085
+ "total_tokens",
2086
+ ]) {
2006
2087
  const a = Number(totalUsage[k]);
2007
2088
  const b = Number(prevTotals[k]);
2008
2089
  if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
@@ -2021,7 +2102,13 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
2021
2102
 
2022
2103
  function normalizeUsage(u) {
2023
2104
  const out = {};
2024
- for (const k of ['input_tokens', 'cached_input_tokens', 'output_tokens', 'reasoning_output_tokens', 'total_tokens']) {
2105
+ for (const k of [
2106
+ "input_tokens",
2107
+ "cached_input_tokens",
2108
+ "output_tokens",
2109
+ "reasoning_output_tokens",
2110
+ "total_tokens",
2111
+ ]) {
2025
2112
  const n = Number(u[k] || 0);
2026
2113
  out[k] = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
2027
2114
  }
@@ -2029,33 +2116,46 @@ function normalizeUsage(u) {
2029
2116
  }
2030
2117
 
2031
2118
  function normalizeClaudeUsage(u) {
2032
- const inputTokens = toNonNegativeInt(u?.input_tokens) + toNonNegativeInt(u?.cache_creation_input_tokens);
2119
+ const inputTokens =
2120
+ toNonNegativeInt(u?.input_tokens) + toNonNegativeInt(u?.cache_creation_input_tokens);
2033
2121
  const outputTokens = toNonNegativeInt(u?.output_tokens);
2034
- const hasTotal = u && Object.prototype.hasOwnProperty.call(u, 'total_tokens');
2122
+ const hasTotal = u && Object.prototype.hasOwnProperty.call(u, "total_tokens");
2035
2123
  const totalTokens = hasTotal ? toNonNegativeInt(u?.total_tokens) : inputTokens + outputTokens;
2036
2124
  return {
2037
2125
  input_tokens: inputTokens,
2038
2126
  cached_input_tokens: toNonNegativeInt(u?.cache_read_input_tokens),
2039
2127
  output_tokens: outputTokens,
2040
2128
  reasoning_output_tokens: 0,
2041
- total_tokens: totalTokens
2129
+ total_tokens: totalTokens,
2042
2130
  };
2043
2131
  }
2044
2132
 
2045
2133
  function isNonEmptyObject(v) {
2046
- return Boolean(v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0);
2134
+ return Boolean(v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0);
2047
2135
  }
2048
2136
 
2049
2137
  function isAllZeroUsage(u) {
2050
- if (!u || typeof u !== 'object') return true;
2051
- for (const k of ['input_tokens', 'cached_input_tokens', 'output_tokens', 'reasoning_output_tokens', 'total_tokens']) {
2138
+ if (!u || typeof u !== "object") return true;
2139
+ for (const k of [
2140
+ "input_tokens",
2141
+ "cached_input_tokens",
2142
+ "output_tokens",
2143
+ "reasoning_output_tokens",
2144
+ "total_tokens",
2145
+ ]) {
2052
2146
  if (Number(u[k] || 0) !== 0) return false;
2053
2147
  }
2054
2148
  return true;
2055
2149
  }
2056
2150
 
2057
2151
  function sameUsage(a, b) {
2058
- for (const k of ['input_tokens', 'cached_input_tokens', 'output_tokens', 'reasoning_output_tokens', 'total_tokens']) {
2152
+ for (const k of [
2153
+ "input_tokens",
2154
+ "cached_input_tokens",
2155
+ "output_tokens",
2156
+ "reasoning_output_tokens",
2157
+ "total_tokens",
2158
+ ]) {
2059
2159
  if (toNonNegativeInt(a?.[k]) !== toNonNegativeInt(b?.[k])) return false;
2060
2160
  }
2061
2161
  return true;
@@ -2069,7 +2169,7 @@ function totalsReset(curr, prev) {
2069
2169
  }
2070
2170
 
2071
2171
  function isFiniteNumber(v) {
2072
- return typeof v === 'number' && Number.isFinite(v);
2172
+ return typeof v === "number" && Number.isFinite(v);
2073
2173
  }
2074
2174
 
2075
2175
  function toNonNegativeInt(v) {
@@ -2101,7 +2201,7 @@ async function walkClaudeProjects(dir, out) {
2101
2201
  await walkClaudeProjects(fullPath, out);
2102
2202
  continue;
2103
2203
  }
2104
- if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(fullPath);
2204
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(fullPath);
2105
2205
  }
2106
2206
  }
2107
2207
 
@@ -2113,7 +2213,8 @@ async function walkOpencodeMessages(dir, out) {
2113
2213
  await walkOpencodeMessages(fullPath, out);
2114
2214
  continue;
2115
2215
  }
2116
- if (entry.isFile() && entry.name.startsWith('msg_') && entry.name.endsWith('.json')) out.push(fullPath);
2216
+ if (entry.isFile() && entry.name.startsWith("msg_") && entry.name.endsWith(".json"))
2217
+ out.push(fullPath);
2117
2218
  }
2118
2219
  }
2119
2220
 
@@ -2126,5 +2227,5 @@ module.exports = {
2126
2227
  parseClaudeIncremental,
2127
2228
  parseGeminiIncremental,
2128
2229
  parseOpencodeIncremental,
2129
- parseOpenclawIncremental
2230
+ parseOpenclawIncremental,
2130
2231
  };