@vibe-cafe/vibe-usage 0.10.11 → 0.10.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.10.11",
3
+ "version": "0.10.12",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -195,7 +195,7 @@ async function scanProjectCandidate(candidate) {
195
195
  if (usageScore === 0) return;
196
196
 
197
197
  entries.push({
198
- uuid: typeof obj.uuid === 'string' && obj.uuid ? obj.uuid : null,
198
+ dedupeKey: usageDedupeKey(obj),
199
199
  usageScore,
200
200
  source: 'claude-code',
201
201
  model,
@@ -239,22 +239,36 @@ async function scanBestCandidate(candidates, scanner, ctx) {
239
239
  return null;
240
240
  }
241
241
 
242
+ // One API call is written as several assistant lines - one per content block -
243
+ // that share `message.id`/`requestId` and repeat the same `usage` object, so a
244
+ // per-line key counts the same call once per block. Streaming also emits an
245
+ // early partial line (lower `output_tokens`) before the final one under that
246
+ // same id. Keying on the call identity collapses both, and the existing
247
+ // highest-usageScore wins rule then keeps the final, complete payload.
248
+ // Records without either id (older logs) fall back to the line uuid.
249
+ function usageDedupeKey(obj) {
250
+ const messageId = typeof obj.message?.id === 'string' ? obj.message.id.trim() : '';
251
+ const requestId = typeof obj.requestId === 'string' ? obj.requestId.trim() : '';
252
+ if (messageId || requestId) return `call:${messageId}\u0000${requestId}`;
253
+ return typeof obj.uuid === 'string' && obj.uuid ? obj.uuid : null;
254
+ }
255
+
242
256
  function mergeUsageEntry(ctx, entry) {
243
- if (!entry.uuid) {
257
+ if (!entry.dedupeKey) {
244
258
  ctx.anonymousEntries.push(entry);
245
259
  return;
246
260
  }
247
- const current = ctx.entriesByUuid.get(entry.uuid);
248
- // Claude sometimes copies the same UUID into another session with zeroed
261
+ const current = ctx.entriesByKey.get(entry.dedupeKey);
262
+ // Claude sometimes copies the same record into another session with zeroed
249
263
  // usage. Keep the most complete payload, independent of directory order.
250
264
  if (!current || entry.usageScore > current.usageScore) {
251
- ctx.entriesByUuid.set(entry.uuid, entry);
265
+ ctx.entriesByKey.set(entry.dedupeKey, entry);
252
266
  }
253
267
  }
254
268
 
255
269
  export async function parse() {
256
270
  const ctx = {
257
- entriesByUuid: new Map(),
271
+ entriesByKey: new Map(),
258
272
  anonymousEntries: [],
259
273
  sessionEvents: [],
260
274
  warnings: [],
@@ -283,8 +297,8 @@ export async function parse() {
283
297
 
284
298
  const entries = [
285
299
  ...ctx.anonymousEntries,
286
- ...ctx.entriesByUuid.values(),
287
- ].map(({ uuid: _uuid, usageScore: _usageScore, ...entry }) => entry);
300
+ ...ctx.entriesByKey.values(),
301
+ ].map(({ dedupeKey: _dedupeKey, usageScore: _usageScore, ...entry }) => entry);
288
302
 
289
303
  return {
290
304
  buckets: aggregateToBuckets(entries),