@ipv9/tokentracker-cli 0.39.47 → 0.39.49

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 (29) hide show
  1. package/README.md +11 -9
  2. package/dashboard/dist/assets/{Card-BMWFZ5k5.js → Card-CQ0G1GlR.js} +1 -1
  3. package/dashboard/dist/assets/{DashboardPage-HjWNRKq7.js → DashboardPage-B5aBShIc.js} +1 -1
  4. package/dashboard/dist/assets/{FadeIn-BgLVBi0I.js → FadeIn-BG37Pb1J.js} +1 -1
  5. package/dashboard/dist/assets/{IpCheckPage-CsS9rEbM.js → IpCheckPage-DD2nObWJ.js} +1 -1
  6. package/dashboard/dist/assets/{LimitsPage-BifpRDJ6.js → LimitsPage-L55ZcbUp.js} +1 -1
  7. package/dashboard/dist/assets/{LocalOnlyNotice-D7tdrheS.js → LocalOnlyNotice-3-SG-oKl.js} +1 -1
  8. package/dashboard/dist/assets/{PopoverPopup-DjMB9uLV.js → PopoverPopup--Imnw4O7.js} +1 -1
  9. package/dashboard/dist/assets/{Select-DeCgT4YH.js → Select-BNQ73B60.js} +1 -1
  10. package/dashboard/dist/assets/{SelectItemText-DwZRVpPE.js → SelectItemText-Dlv4NEpN.js} +1 -1
  11. package/dashboard/dist/assets/{SettingsPage-B1l6jasL.js → SettingsPage-BDo6aFgs.js} +1 -1
  12. package/dashboard/dist/assets/{SkillsPage-DP5bdeUE.js → SkillsPage-CA2IaWI5.js} +1 -1
  13. package/dashboard/dist/assets/{WidgetsPage-BVOQlozx.js → WidgetsPage-DTVOLZEy.js} +1 -1
  14. package/dashboard/dist/assets/{WrappedPage-ixZYG73C.js → WrappedPage-CYwBaHuQ.js} +1 -1
  15. package/dashboard/dist/assets/{arrow-up-right-D74FAyJC.js → arrow-up-right-3x5AJIbb.js} +1 -1
  16. package/dashboard/dist/assets/{download-sKFNy5AQ.js → download-DSegD216.js} +1 -1
  17. package/dashboard/dist/assets/{format-Bl1W1y2T.js → format-yTFTff0D.js} +1 -1
  18. package/dashboard/dist/assets/{limitDisplay-C2rw2OOb.js → limitDisplay-DxvqWNgp.js} +1 -1
  19. package/dashboard/dist/assets/{main-NjvOmlYA.js → main-DBDTVhz8.js} +3 -3
  20. package/dashboard/dist/assets/{mock-data-DJ2XGAUP.js → mock-data-oSiCcgVh.js} +1 -1
  21. package/dashboard/dist/assets/{use-limits-display-prefs-C7BBDoNV.js → use-limits-display-prefs-C2F0IlsU.js} +1 -1
  22. package/dashboard/dist/assets/{use-native-settings-BYIme-b6.js → use-native-settings-DtQFof4g.js} +1 -1
  23. package/dashboard/dist/assets/{useCurrency-CJzAqnSI.js → useCurrency-5LxVaA8B.js} +1 -1
  24. package/dashboard/dist/index.html +1 -1
  25. package/package.json +1 -1
  26. package/src/cli.js +1 -1
  27. package/src/commands/init.js +5 -7
  28. package/src/lib/pricing/seed-snapshot.json +1 -1
  29. package/src/lib/rollout.js +90 -17
@@ -3294,12 +3294,6 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
3294
3294
  const forceIncludeSql = forceIds.length > 0
3295
3295
  ? ` OR id IN (${forceIds.map(sqliteStringLiteral).join(",")})`
3296
3296
  : "";
3297
- // Fetch sessions that started at/after the cursor, sessions that are still
3298
- // in-progress (ended_at IS NULL), OR sessions that were previously observed
3299
- // unfinished. Hermes updates token counts in real-time, including a final
3300
- // delta when an active session later gets ended_at set.
3301
- const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
3302
-
3303
3297
  let snapshot = null;
3304
3298
  let effectiveDbPath = dbPath;
3305
3299
  if (isUncPath(dbPath)) {
@@ -3311,6 +3305,65 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
3311
3305
  // the non-locked case (e.g. permissions, transient I/O).
3312
3306
  }
3313
3307
  }
3308
+ const eligible = `(started_at >= ${since} OR ended_at IS NULL${forceIncludeSql})`;
3309
+ const modelUsageColumns = new Set(
3310
+ readSqliteJsonRows(
3311
+ effectiveDbPath,
3312
+ "PRAGMA table_info(session_model_usage)",
3313
+ { label: "Hermes", maxBuffer: 1024 * 1024, timeout: 5_000, ...sqliteOptions },
3314
+ ).map((column) => column?.name).filter(Boolean),
3315
+ );
3316
+ const hasModelUsage = [
3317
+ "session_id",
3318
+ "model",
3319
+ "api_call_count",
3320
+ "input_tokens",
3321
+ "output_tokens",
3322
+ "cache_read_tokens",
3323
+ "cache_write_tokens",
3324
+ "reasoning_tokens",
3325
+ "first_seen",
3326
+ "last_seen",
3327
+ ].every((column) => modelUsageColumns.has(column));
3328
+ const sql = hasModelUsage
3329
+ ? `WITH eligible_sessions AS (
3330
+ SELECT * FROM sessions WHERE ${eligible}
3331
+ )
3332
+ SELECT s.id, TRIM(u.model) AS model, s.model AS session_model, s.started_at, s.ended_at,
3333
+ SUM(u.input_tokens) AS input_tokens,
3334
+ SUM(u.output_tokens) AS output_tokens,
3335
+ SUM(u.cache_read_tokens) AS cache_read_tokens,
3336
+ SUM(u.cache_write_tokens) AS cache_write_tokens,
3337
+ SUM(u.reasoning_tokens) AS reasoning_tokens,
3338
+ SUM(u.api_call_count) AS message_count,
3339
+ MIN(u.first_seen) AS first_seen,
3340
+ MAX(u.last_seen) AS last_seen,
3341
+ 1 AS per_model
3342
+ FROM eligible_sessions s
3343
+ JOIN session_model_usage u ON u.session_id = s.id
3344
+ GROUP BY s.id, TRIM(u.model)
3345
+ HAVING SUM(u.input_tokens) > 0 OR SUM(u.output_tokens) > 0 OR SUM(u.cache_read_tokens) > 0 OR SUM(u.cache_write_tokens) > 0 OR SUM(u.reasoning_tokens) > 0
3346
+ UNION ALL
3347
+ SELECT s.id, s.model, s.model AS session_model, s.started_at, s.ended_at,
3348
+ s.input_tokens, s.output_tokens, s.cache_read_tokens,
3349
+ s.cache_write_tokens, s.reasoning_tokens, s.message_count,
3350
+ NULL AS first_seen, NULL AS last_seen, 0 AS per_model
3351
+ FROM eligible_sessions s
3352
+ WHERE NOT EXISTS (
3353
+ SELECT 1 FROM session_model_usage u
3354
+ WHERE u.session_id = s.id
3355
+ AND (u.input_tokens > 0 OR u.output_tokens > 0 OR u.cache_read_tokens > 0 OR u.cache_write_tokens > 0 OR u.reasoning_tokens > 0)
3356
+ )
3357
+ AND (s.input_tokens > 0 OR s.output_tokens > 0 OR s.cache_read_tokens > 0 OR s.cache_write_tokens > 0 OR s.reasoning_tokens > 0)
3358
+ ORDER BY 4 ASC`
3359
+ : `SELECT id, model, model AS session_model, started_at, ended_at,
3360
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
3361
+ reasoning_tokens, message_count, NULL AS first_seen,
3362
+ NULL AS last_seen, 0 AS per_model
3363
+ FROM sessions
3364
+ WHERE ${eligible}
3365
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_write_tokens > 0 OR reasoning_tokens > 0)
3366
+ ORDER BY started_at ASC`;
3314
3367
 
3315
3368
  try {
3316
3369
  return readSqliteJsonRows(effectiveDbPath, sql, {
@@ -3368,6 +3421,11 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3368
3421
  // Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
3369
3422
  const prevSnapshots = (dbState.snapshots && typeof dbState.snapshots === "object")
3370
3423
  ? dbState.snapshots : {};
3424
+ const hasPerModelRows = rows.some((row) => Number(row.per_model) === 1);
3425
+ const adoptingPerModelState =
3426
+ hasPerModelRows &&
3427
+ dbState.modelUsageVersion !== 1 &&
3428
+ Object.keys(prevSnapshots).length > 0;
3371
3429
 
3372
3430
  // Only advance past sessions that have fully ended. Active sessions
3373
3431
  // (ended_at IS NULL) must be re-read every sync because Hermes updates
@@ -3388,10 +3446,22 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3388
3446
  const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
3389
3447
  const reasoning = toNonNegativeInt(row.reasoning_tokens);
3390
3448
  const messageCount = toNonNegativeInt(row.message_count);
3391
- if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
3449
+ if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) continue;
3392
3450
 
3393
- // Save current snapshot for next sync
3394
- nextSnapshots[row.id] = { in: inputTokens, out: outputTokens, cacheRead, cacheWrite, reasoning, message_count: messageCount };
3451
+ const perModel = Number(row.per_model) === 1;
3452
+ const model = normalizeModelInput(row.model) || "hermes-agent";
3453
+ const snapshotKey = perModel ? JSON.stringify([String(row.id || ""), model]) : row.id;
3454
+ const currentSnapshot = {
3455
+ in: inputTokens,
3456
+ out: outputTokens,
3457
+ cacheRead,
3458
+ cacheWrite,
3459
+ reasoning,
3460
+ message_count: messageCount,
3461
+ };
3462
+ // Save current snapshot for next sync. Mixed-model sessions are keyed by
3463
+ // both session and model so later growth is attributed independently.
3464
+ nextSnapshots[snapshotKey] = currentSnapshot;
3395
3465
 
3396
3466
  const startedAt = Number(row.started_at);
3397
3467
  const endedAt = row.ended_at == null ? null : Number(row.ended_at);
@@ -3407,7 +3477,10 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3407
3477
  // Compute delta from previous snapshot (if any) so that we only count
3408
3478
  // new usage since the last sync. First time we see a session the
3409
3479
  // previous snapshot is absent, so the full amount is the delta.
3410
- const prev = prevSnapshots[row.id];
3480
+ const legacySessionSnapshot = prevSnapshots[row.id];
3481
+ const prev = adoptingPerModelState && perModel && legacySessionSnapshot
3482
+ ? currentSnapshot
3483
+ : prevSnapshots[snapshotKey];
3411
3484
  let dInput = inputTokens;
3412
3485
  let dOutput = outputTokens;
3413
3486
  let dCacheRead = cacheRead;
@@ -3425,18 +3498,17 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3425
3498
  // Skip if delta is zero (session unchanged since last sync)
3426
3499
  if (dInput === 0 && dOutput === 0 && dCacheRead === 0 && dCacheWrite === 0 && dReasoning === 0) continue;
3427
3500
 
3428
- // A first observation has only the session start as a usable timestamp.
3429
- // Attribute later active-session deltas to this sync so cross-day usage
3430
- // does not keep growing the day on which the session originally started.
3431
- // Once Hermes records completion, ended_at is authoritative for the final delta.
3432
- const epochSec = endedAt ?? (prev ? Date.parse(updatedAt) / 1000 : startedAt);
3501
+ // Per-model rows carry the authoritative last API-call timestamp. Older
3502
+ // Hermes schemas fall back to the session-level start/end policy.
3503
+ const lastSeen = row.last_seen == null ? null : Number(row.last_seen);
3504
+ const epochSec = perModel && Number.isFinite(lastSeen) && lastSeen > 0
3505
+ ? lastSeen
3506
+ : endedAt ?? (prev ? Date.parse(updatedAt) / 1000 : startedAt);
3433
3507
  if (!epochSec || !Number.isFinite(epochSec)) continue;
3434
3508
  const tsIso = new Date(epochSec * 1000).toISOString();
3435
3509
  const bucketStart = toUtcHalfHourStart(tsIso);
3436
3510
  if (!bucketStart) continue;
3437
3511
 
3438
- const model = normalizeModelInput(row.model) || "hermes-agent";
3439
-
3440
3512
  const delta = {
3441
3513
  input_tokens: dInput,
3442
3514
  cached_input_tokens: dCacheRead,
@@ -3472,6 +3544,7 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3472
3544
  lastCompletedStartedAt: nextLastCompletedStartedAt,
3473
3545
  unfinishedSessionIds: Array.from(nextUnfinishedSessionIds),
3474
3546
  snapshots: nextSnapshots,
3547
+ ...(hasPerModelRows ? { modelUsageVersion: 1 } : {}),
3475
3548
  updatedAt,
3476
3549
  });
3477
3550
  }