@vibescore/tracker 0.0.8 → 0.0.9

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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ _Real-time AI Analytics for Codex CLI_
13
13
 
14
14
  [**English**](README.md) • [**中文说明**](README.zh-CN.md)
15
15
 
16
- [**Documentation**](docs/) • [**Dashboard**](dashboard/) • [**Backend API**](BACKEND_API.md) • [**Dashboard API**](docs/dashboard/api.md)
16
+ [**Documentation**](docs/) • [**Dashboard**](dashboard/) • [**Backend API**](BACKEND_API.md)
17
17
 
18
18
  <br/>
19
19
 
package/README.zh-CN.md CHANGED
@@ -13,7 +13,7 @@ _Codex CLI 实时 AI 分析工具_
13
13
 
14
14
  [**English**](README.md) • [**中文说明**](README.zh-CN.md)
15
15
 
16
- [**文档**](docs/) • [**控制台**](dashboard/) • [**后端接口**](BACKEND_API.md) • [**Dashboard API**](docs/dashboard/api.md)
16
+ [**文档**](docs/) • [**控制台**](dashboard/) • [**后端接口**](BACKEND_API.md)
17
17
 
18
18
  <br/>
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -387,6 +387,10 @@ function spawnInitSync({ trackerBinPath, packageName }) {
387
387
  stdio: 'ignore',
388
388
  env: process.env
389
389
  });
390
+ child.on('error', (err) => {
391
+ const msg = err && err.message ? err.message : 'unknown error';
392
+ process.stderr.write(`Initial sync spawn failed: ${msg}\n`);
393
+ });
390
394
  child.unref();
391
395
  }
392
396
 
@@ -299,32 +299,62 @@ async function parseClaudeFile({ filePath, startOffset, hourlyState, touchedBuck
299
299
  async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets }) {
300
300
  if (!touchedBuckets || touchedBuckets.size === 0) return 0;
301
301
 
302
- const toAppend = [];
302
+ const touchedGroups = new Set();
303
303
  for (const bucketStart of touchedBuckets) {
304
- const parsedKey = parseBucketKey(bucketStart);
305
- const source = parsedKey.source || DEFAULT_SOURCE;
306
- const model = parsedKey.model || DEFAULT_MODEL;
307
- const hourStart = parsedKey.hourStart;
308
- const bucket =
309
- hourlyState.buckets[bucketKey(source, model, hourStart)] || hourlyState.buckets[bucketStart];
304
+ const parsed = parseBucketKey(bucketStart);
305
+ const hourStart = parsed.hourStart;
306
+ if (!hourStart) continue;
307
+ touchedGroups.add(groupBucketKey(parsed.source, hourStart));
308
+ }
309
+ if (touchedGroups.size === 0) return 0;
310
+
311
+ const grouped = new Map();
312
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
310
313
  if (!bucket || !bucket.totals) continue;
311
- const key = totalsKey(bucket.totals);
312
- if (bucket.queuedKey === key) continue;
314
+ const parsed = parseBucketKey(key);
315
+ const hourStart = parsed.hourStart;
316
+ if (!hourStart) continue;
317
+ const groupKey = groupBucketKey(parsed.source, hourStart);
318
+ if (!touchedGroups.has(groupKey)) continue;
319
+
320
+ let group = grouped.get(groupKey);
321
+ if (!group) {
322
+ group = {
323
+ source: normalizeSourceInput(parsed.source) || DEFAULT_SOURCE,
324
+ hourStart,
325
+ models: new Set(),
326
+ totals: initTotals()
327
+ };
328
+ grouped.set(groupKey, group);
329
+ }
330
+ group.models.add(parsed.model || DEFAULT_MODEL);
331
+ addTotals(group.totals, bucket.totals);
332
+ }
333
+
334
+ const toAppend = [];
335
+ const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
336
+ for (const group of grouped.values()) {
337
+ const model = group.models.size === 1 ? [...group.models][0] : DEFAULT_MODEL;
338
+ const key = totalsKey(group.totals);
339
+ const groupKey = groupBucketKey(group.source, group.hourStart);
340
+ if (groupQueued[groupKey] === key) continue;
313
341
  toAppend.push(
314
342
  JSON.stringify({
315
- source,
343
+ source: group.source,
316
344
  model,
317
- hour_start: hourStart,
318
- input_tokens: bucket.totals.input_tokens,
319
- cached_input_tokens: bucket.totals.cached_input_tokens,
320
- output_tokens: bucket.totals.output_tokens,
321
- reasoning_output_tokens: bucket.totals.reasoning_output_tokens,
322
- total_tokens: bucket.totals.total_tokens
345
+ hour_start: group.hourStart,
346
+ input_tokens: group.totals.input_tokens,
347
+ cached_input_tokens: group.totals.cached_input_tokens,
348
+ output_tokens: group.totals.output_tokens,
349
+ reasoning_output_tokens: group.totals.reasoning_output_tokens,
350
+ total_tokens: group.totals.total_tokens
323
351
  })
324
352
  );
325
- bucket.queuedKey = key;
353
+ groupQueued[groupKey] = key;
326
354
  }
327
355
 
356
+ hourlyState.groupQueued = groupQueued;
357
+
328
358
  if (toAppend.length > 0) {
329
359
  await fs.appendFile(queuePath, toAppend.join('\n') + '\n', 'utf8');
330
360
  }
@@ -335,15 +365,30 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
335
365
  function normalizeHourlyState(raw) {
336
366
  const state = raw && typeof raw === 'object' ? raw : {};
337
367
  const version = Number(state.version || 1);
368
+ const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
369
+ const buckets = {};
370
+ const groupQueued = {};
371
+
338
372
  if (!Number.isFinite(version) || version < 2) {
373
+ for (const [key, value] of Object.entries(rawBuckets)) {
374
+ const parsed = parseBucketKey(key);
375
+ const hourStart = parsed.hourStart;
376
+ if (!hourStart) continue;
377
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
378
+ const normalizedKey = bucketKey(source, DEFAULT_MODEL, hourStart);
379
+ buckets[normalizedKey] = value;
380
+ if (value?.queuedKey) {
381
+ groupQueued[groupBucketKey(source, hourStart)] = value.queuedKey;
382
+ }
383
+ }
339
384
  return {
340
- version: 2,
341
- buckets: {},
342
- updatedAt: null
385
+ version: 3,
386
+ buckets,
387
+ groupQueued,
388
+ updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
343
389
  };
344
390
  }
345
- const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
346
- const buckets = {};
391
+
347
392
  for (const [key, value] of Object.entries(rawBuckets)) {
348
393
  const parsed = parseBucketKey(key);
349
394
  const hourStart = parsed.hourStart;
@@ -351,9 +396,14 @@ function normalizeHourlyState(raw) {
351
396
  const normalizedKey = bucketKey(parsed.source, parsed.model, hourStart);
352
397
  buckets[normalizedKey] = value;
353
398
  }
399
+
400
+ const existingGroupQueued =
401
+ state.groupQueued && typeof state.groupQueued === 'object' ? state.groupQueued : {};
402
+
354
403
  return {
355
- version: 2,
404
+ version: 3,
356
405
  buckets,
406
+ groupQueued: version >= 3 ? existingGroupQueued : {},
357
407
  updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
358
408
  };
359
409
  }
@@ -434,6 +484,11 @@ function bucketKey(source, model, hourStart) {
434
484
  return `${safeSource}${BUCKET_SEPARATOR}${safeModel}${BUCKET_SEPARATOR}${hourStart}`;
435
485
  }
436
486
 
487
+ function groupBucketKey(source, hourStart) {
488
+ const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
489
+ return `${safeSource}${BUCKET_SEPARATOR}${hourStart}`;
490
+ }
491
+
437
492
  function parseBucketKey(key) {
438
493
  if (typeof key !== 'string') return { source: DEFAULT_SOURCE, model: DEFAULT_MODEL, hourStart: '' };
439
494
  const first = key.indexOf(BUCKET_SEPARATOR);
@@ -523,12 +578,16 @@ function normalizeUsage(u) {
523
578
  }
524
579
 
525
580
  function normalizeClaudeUsage(u) {
581
+ const inputTokens = toNonNegativeInt(u?.input_tokens);
582
+ const outputTokens = toNonNegativeInt(u?.output_tokens);
583
+ const hasTotal = u && Object.prototype.hasOwnProperty.call(u, 'total_tokens');
584
+ const totalTokens = hasTotal ? toNonNegativeInt(u?.total_tokens) : inputTokens + outputTokens;
526
585
  return {
527
- input_tokens: toNonNegativeInt(u?.input_tokens),
586
+ input_tokens: inputTokens,
528
587
  cached_input_tokens: toNonNegativeInt(u?.cache_read_input_tokens),
529
- output_tokens: toNonNegativeInt(u?.output_tokens),
588
+ output_tokens: outputTokens,
530
589
  reasoning_output_tokens: 0,
531
- total_tokens: toNonNegativeInt(u?.input_tokens) + toNonNegativeInt(u?.output_tokens)
590
+ total_tokens: totalTokens
532
591
  };
533
592
  }
534
593
 
@@ -78,7 +78,7 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
78
78
  const model = normalizeModel(bucket?.model) || DEFAULT_MODEL;
79
79
  bucket.source = source;
80
80
  bucket.model = model;
81
- bucketMap.set(bucketKey(source, model, hourStart), bucket);
81
+ bucketMap.set(bucketKey(source, hourStart), bucket);
82
82
  linesRead += 1;
83
83
  if (linesRead >= maxBuckets) break;
84
84
  }
@@ -97,8 +97,8 @@ async function safeFileSize(p) {
97
97
  }
98
98
  }
99
99
 
100
- function bucketKey(source, model, hourStart) {
101
- return `${source}${BUCKET_SEPARATOR}${model}${BUCKET_SEPARATOR}${hourStart}`;
100
+ function bucketKey(source, hourStart) {
101
+ return `${source}${BUCKET_SEPARATOR}${hourStart}`;
102
102
  }
103
103
 
104
104
  function normalizeSource(value) {