tokenleak 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +94 -5
  2. package/package.json +1 -1
  3. package/tokenleak +2060 -1198
package/tokenleak CHANGED
@@ -415,7 +415,7 @@ async function runMain(cmd, opts = {}) {
415
415
  }
416
416
 
417
417
  // packages/cli/src/cli.ts
418
- import { writeFileSync } from "fs";
418
+ import { writeFileSync as writeFileSync2 } from "fs";
419
419
 
420
420
  // packages/core/dist/constants.js
421
421
  var DEFAULT_DAYS = 90;
@@ -3170,21 +3170,306 @@ class CodexProvider {
3170
3170
  };
3171
3171
  }
3172
3172
  }
3173
- // packages/registry/dist/providers/open-code.js
3174
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync } from "fs";
3173
+ // packages/registry/dist/providers/cursor.js
3174
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync, statSync as statSync3 } from "fs";
3175
3175
  import { join as join3 } from "path";
3176
3176
  import { homedir as homedir3 } from "os";
3177
+ var PROVIDER_NAME = "cursor";
3178
+ var DISPLAY_NAME = "Cursor";
3179
+ var CURSOR_COLORS = {
3180
+ primary: "#22c55e",
3181
+ secondary: "#86efac",
3182
+ gradient: ["#22c55e", "#86efac"]
3183
+ };
3184
+ var DASHED_DATE_SUFFIX2 = /-(\d{4})-(\d{2})-(\d{2})$/;
3185
+ function resolveCacheDir(baseDir) {
3186
+ return baseDir ?? join3(process.env["TOKENLEAK_CURSOR_DIR"] ?? join3(homedir3(), ".config", "tokenleak"), "cursor-cache");
3187
+ }
3188
+ function isCursorUsageFile(name) {
3189
+ if (name === "usage.csv") {
3190
+ return true;
3191
+ }
3192
+ if (!name.startsWith("usage.") || !name.endsWith(".csv")) {
3193
+ return false;
3194
+ }
3195
+ const stem = name.slice("usage.".length, -".csv".length);
3196
+ return stem.length > 0;
3197
+ }
3198
+ function collectUsageFiles(dir) {
3199
+ if (!existsSync3(dir)) {
3200
+ return [];
3201
+ }
3202
+ const files = [];
3203
+ for (const entry of readdirSync3(dir)) {
3204
+ if (entry === "archive") {
3205
+ continue;
3206
+ }
3207
+ const fullPath = join3(dir, entry);
3208
+ const stats = statSync3(fullPath);
3209
+ if (stats.isFile() && isCursorUsageFile(entry)) {
3210
+ files.push(fullPath);
3211
+ }
3212
+ }
3213
+ return files.sort((left, right) => left.localeCompare(right));
3214
+ }
3215
+ function parseCsvLine(line) {
3216
+ const fields = [];
3217
+ let current = "";
3218
+ let inQuotes = false;
3219
+ for (let index = 0;index < line.length; index += 1) {
3220
+ const char = line[index];
3221
+ if (char === '"') {
3222
+ if (inQuotes && line[index + 1] === '"') {
3223
+ current += '"';
3224
+ index += 1;
3225
+ } else {
3226
+ inQuotes = !inQuotes;
3227
+ }
3228
+ continue;
3229
+ }
3230
+ if (char === "," && !inQuotes) {
3231
+ fields.push(current);
3232
+ current = "";
3233
+ continue;
3234
+ }
3235
+ current += char;
3236
+ }
3237
+ fields.push(current);
3238
+ return fields;
3239
+ }
3240
+ function extractDate2(timestamp) {
3241
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
3242
+ return match ? match[1] : null;
3243
+ }
3244
+ function toIsoTimestamp(value) {
3245
+ const trimmed = value.trim();
3246
+ if (!trimmed) {
3247
+ return null;
3248
+ }
3249
+ const dateOnlyMatch = /^(\d{4}-\d{2}-\d{2})$/.exec(trimmed);
3250
+ if (dateOnlyMatch) {
3251
+ return `${dateOnlyMatch[1]}T12:00:00.000Z`;
3252
+ }
3253
+ const millis = Date.parse(trimmed);
3254
+ if (!Number.isFinite(millis)) {
3255
+ return null;
3256
+ }
3257
+ return new Date(millis).toISOString();
3258
+ }
3259
+ function parseCost(value) {
3260
+ const cleaned = value.replaceAll("$", "").replaceAll(",", "").trim();
3261
+ if (!cleaned || cleaned.toLowerCase() === "nan") {
3262
+ return;
3263
+ }
3264
+ const parsed = Number(cleaned);
3265
+ return Number.isFinite(parsed) ? parsed : undefined;
3266
+ }
3267
+ function compactModelDateSuffix2(model) {
3268
+ return model.replace(DASHED_DATE_SUFFIX2, "-$1$2$3");
3269
+ }
3270
+ function toCachePricing3(pricing) {
3271
+ if (!pricing) {
3272
+ return;
3273
+ }
3274
+ return {
3275
+ input: pricing.input,
3276
+ cacheRead: pricing.cacheRead,
3277
+ cacheWrite: pricing.cacheWrite
3278
+ };
3279
+ }
3280
+ function parseUsageFile(filePath) {
3281
+ let raw;
3282
+ try {
3283
+ raw = readFileSync(filePath, "utf8");
3284
+ } catch {
3285
+ return [];
3286
+ }
3287
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
3288
+ if (lines.length <= 1) {
3289
+ return [];
3290
+ }
3291
+ const header = parseCsvLine(lines[0]);
3292
+ const hasKindColumn = header.includes("Kind");
3293
+ const modelIndex = hasKindColumn ? 2 : 1;
3294
+ const inputWithCacheWriteIndex = hasKindColumn ? 4 : 2;
3295
+ const inputWithoutCacheWriteIndex = hasKindColumn ? 5 : 3;
3296
+ const cacheReadIndex = hasKindColumn ? 6 : 4;
3297
+ const outputIndex = hasKindColumn ? 7 : 5;
3298
+ const costIndex = hasKindColumn ? 9 : 7;
3299
+ const accountId = filePath.endsWith("/usage.csv") || filePath.endsWith("\\usage.csv") ? "active" : filePath.split(/[/\\]/).pop()?.replace(/^usage\./, "").replace(/\.csv$/, "") || "unknown";
3300
+ const records = [];
3301
+ for (const line of lines.slice(1)) {
3302
+ const fields = parseCsvLine(line);
3303
+ if (fields.length <= costIndex) {
3304
+ continue;
3305
+ }
3306
+ const timestamp = toIsoTimestamp(fields[0] ?? "");
3307
+ if (!timestamp) {
3308
+ continue;
3309
+ }
3310
+ const date = extractDate2(timestamp);
3311
+ if (!date) {
3312
+ continue;
3313
+ }
3314
+ const rawModel = (fields[modelIndex] ?? "").trim();
3315
+ if (!rawModel) {
3316
+ continue;
3317
+ }
3318
+ const inputWithCacheWrite = Number((fields[inputWithCacheWriteIndex] ?? "").trim());
3319
+ const inputWithoutCacheWrite = Number((fields[inputWithoutCacheWriteIndex] ?? "").trim());
3320
+ const cacheReadTokens = Number((fields[cacheReadIndex] ?? "").trim());
3321
+ const outputTokens = Number((fields[outputIndex] ?? "").trim());
3322
+ if (!Number.isFinite(inputWithCacheWrite) || !Number.isFinite(inputWithoutCacheWrite) || !Number.isFinite(cacheReadTokens) || !Number.isFinite(outputTokens)) {
3323
+ continue;
3324
+ }
3325
+ const inputTokens = Math.max(0, inputWithoutCacheWrite);
3326
+ const cacheWriteTokens = Math.max(0, inputWithCacheWrite - inputWithoutCacheWrite);
3327
+ const totalTokens = inputTokens + outputTokens + Math.max(0, cacheReadTokens) + cacheWriteTokens;
3328
+ if (totalTokens === 0) {
3329
+ continue;
3330
+ }
3331
+ const model = compactModelDateSuffix2(rawModel);
3332
+ records.push({
3333
+ date,
3334
+ timestamp,
3335
+ model,
3336
+ normalizedModel: normalizeModelName2(model),
3337
+ inputTokens,
3338
+ outputTokens: Math.max(0, outputTokens),
3339
+ cacheReadTokens: Math.max(0, cacheReadTokens),
3340
+ cacheWriteTokens,
3341
+ explicitCost: parseCost(fields[costIndex] ?? ""),
3342
+ sessionId: `cursor-${accountId}-${timestamp}`
3343
+ });
3344
+ }
3345
+ return records;
3346
+ }
3347
+ function getRecordCost(record) {
3348
+ if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
3349
+ return record.explicitCost;
3350
+ }
3351
+ return estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).totalCost;
3352
+ }
3353
+ function toUsageEvent(record) {
3354
+ const pricing = estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).pricing;
3355
+ return {
3356
+ provider: PROVIDER_NAME,
3357
+ timestamp: record.timestamp,
3358
+ date: record.date,
3359
+ model: record.normalizedModel,
3360
+ inputTokens: record.inputTokens,
3361
+ outputTokens: record.outputTokens,
3362
+ cacheReadTokens: record.cacheReadTokens,
3363
+ cacheWriteTokens: record.cacheWriteTokens,
3364
+ totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
3365
+ cost: getRecordCost(record),
3366
+ pricing: toCachePricing3(pricing),
3367
+ sessionId: record.sessionId
3368
+ };
3369
+ }
3370
+ function buildProviderData(records) {
3371
+ const byDate = new Map;
3372
+ const events = records.map(toUsageEvent);
3373
+ for (const event of events) {
3374
+ let dateMap = byDate.get(event.date);
3375
+ if (!dateMap) {
3376
+ dateMap = new Map;
3377
+ byDate.set(event.date, dateMap);
3378
+ }
3379
+ const existing = dateMap.get(event.model);
3380
+ if (existing) {
3381
+ existing.inputTokens += event.inputTokens;
3382
+ existing.outputTokens += event.outputTokens;
3383
+ existing.cacheReadTokens += event.cacheReadTokens;
3384
+ existing.cacheWriteTokens += event.cacheWriteTokens;
3385
+ existing.totalTokens += event.totalTokens;
3386
+ existing.cost += event.cost;
3387
+ if (!existing.pricing && event.pricing) {
3388
+ existing.pricing = event.pricing;
3389
+ }
3390
+ continue;
3391
+ }
3392
+ dateMap.set(event.model, {
3393
+ model: event.model,
3394
+ inputTokens: event.inputTokens,
3395
+ outputTokens: event.outputTokens,
3396
+ cacheReadTokens: event.cacheReadTokens,
3397
+ cacheWriteTokens: event.cacheWriteTokens,
3398
+ totalTokens: event.totalTokens,
3399
+ cost: event.cost,
3400
+ pricing: event.pricing
3401
+ });
3402
+ }
3403
+ let totalTokens = 0;
3404
+ let totalCost = 0;
3405
+ const daily = [...byDate.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([date, modelMap]) => {
3406
+ const models = [...modelMap.values()].sort((left, right) => left.model.localeCompare(right.model));
3407
+ const inputTokens = models.reduce((sum, model) => sum + model.inputTokens, 0);
3408
+ const outputTokens = models.reduce((sum, model) => sum + model.outputTokens, 0);
3409
+ const cacheReadTokens = models.reduce((sum, model) => sum + model.cacheReadTokens, 0);
3410
+ const cacheWriteTokens = models.reduce((sum, model) => sum + model.cacheWriteTokens, 0);
3411
+ const dayTotal = models.reduce((sum, model) => sum + model.totalTokens, 0);
3412
+ const dayCost = models.reduce((sum, model) => sum + model.cost, 0);
3413
+ totalTokens += dayTotal;
3414
+ totalCost += dayCost;
3415
+ return {
3416
+ date,
3417
+ inputTokens,
3418
+ outputTokens,
3419
+ cacheReadTokens,
3420
+ cacheWriteTokens,
3421
+ totalTokens: dayTotal,
3422
+ cost: dayCost,
3423
+ models
3424
+ };
3425
+ });
3426
+ return {
3427
+ provider: PROVIDER_NAME,
3428
+ displayName: DISPLAY_NAME,
3429
+ daily,
3430
+ totalTokens,
3431
+ totalCost,
3432
+ colors: CURSOR_COLORS,
3433
+ events
3434
+ };
3435
+ }
3436
+
3437
+ class CursorProvider {
3438
+ name = PROVIDER_NAME;
3439
+ displayName = DISPLAY_NAME;
3440
+ colors = CURSOR_COLORS;
3441
+ cacheDir;
3442
+ constructor(baseDir) {
3443
+ this.cacheDir = resolveCacheDir(baseDir);
3444
+ }
3445
+ async isAvailable() {
3446
+ try {
3447
+ return collectUsageFiles(this.cacheDir).length > 0;
3448
+ } catch {
3449
+ return false;
3450
+ }
3451
+ }
3452
+ async load(range) {
3453
+ const files = collectUsageFiles(this.cacheDir);
3454
+ const records = files.flatMap((filePath) => parseUsageFile(filePath)).filter((record) => isInRange(record.date, range));
3455
+ return buildProviderData(records);
3456
+ }
3457
+ }
3458
+ // packages/registry/dist/providers/open-code.js
3459
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync2 } from "fs";
3460
+ import { join as join4 } from "path";
3461
+ import { homedir as homedir4 } from "os";
3177
3462
  import { Database } from "bun:sqlite";
3178
- var PROVIDER_NAME = "open-code";
3179
- var DISPLAY_NAME = "OpenCode";
3463
+ var PROVIDER_NAME2 = "open-code";
3464
+ var DISPLAY_NAME2 = "OpenCode";
3180
3465
  var COLORS = {
3181
3466
  primary: "#6366f1",
3182
3467
  secondary: "#a78bfa",
3183
3468
  gradient: ["#6366f1", "#a78bfa"]
3184
3469
  };
3185
- var CURRENT_DEFAULT_BASE_DIR = join3(homedir3(), ".local", "share", "opencode");
3186
- var LEGACY_DEFAULT_BASE_DIR = join3(homedir3(), ".opencode");
3187
- var CONFIG_DEFAULT_BASE_DIR = join3(homedir3(), ".config", "opencode");
3470
+ var CURRENT_DEFAULT_BASE_DIR = join4(homedir4(), ".local", "share", "opencode");
3471
+ var LEGACY_DEFAULT_BASE_DIR = join4(homedir4(), ".opencode");
3472
+ var CONFIG_DEFAULT_BASE_DIR = join4(homedir4(), ".config", "opencode");
3188
3473
  function resolveBaseDir2(baseDir) {
3189
3474
  if (baseDir) {
3190
3475
  return baseDir;
@@ -3194,13 +3479,13 @@ function resolveBaseDir2(baseDir) {
3194
3479
  LEGACY_DEFAULT_BASE_DIR,
3195
3480
  CONFIG_DEFAULT_BASE_DIR
3196
3481
  ]) {
3197
- if (existsSync3(candidate)) {
3482
+ if (existsSync4(candidate)) {
3198
3483
  return candidate;
3199
3484
  }
3200
3485
  }
3201
3486
  return CURRENT_DEFAULT_BASE_DIR;
3202
3487
  }
3203
- function extractDate2(createdAt) {
3488
+ function extractDate3(createdAt) {
3204
3489
  const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
3205
3490
  if (!Number.isFinite(timestamp)) {
3206
3491
  return null;
@@ -3220,7 +3505,7 @@ function toTimestampMillis(createdAt) {
3220
3505
  const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
3221
3506
  return Number.isFinite(millis) ? millis : null;
3222
3507
  }
3223
- function toIsoTimestamp(createdAt) {
3508
+ function toIsoTimestamp2(createdAt) {
3224
3509
  const millis = toTimestampMillis(createdAt);
3225
3510
  if (millis === null) {
3226
3511
  return null;
@@ -3228,7 +3513,7 @@ function toIsoTimestamp(createdAt) {
3228
3513
  const date = new Date(millis);
3229
3514
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
3230
3515
  }
3231
- function toCachePricing3(pricing) {
3516
+ function toCachePricing4(pricing) {
3232
3517
  if (!pricing) {
3233
3518
  return;
3234
3519
  }
@@ -3241,17 +3526,17 @@ function toCachePricing3(pricing) {
3241
3526
  function getEstimatedCostBreakdown(record) {
3242
3527
  return estimateCostBreakdown(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
3243
3528
  }
3244
- function getRecordCost(record) {
3529
+ function getRecordCost2(record) {
3245
3530
  if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
3246
3531
  return record.explicitCost;
3247
3532
  }
3248
3533
  return getEstimatedCostBreakdown(record).totalCost;
3249
3534
  }
3250
- function toUsageEvent(record) {
3535
+ function toUsageEvent2(record) {
3251
3536
  const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
3252
3537
  const estimated = getEstimatedCostBreakdown(record);
3253
3538
  return {
3254
- provider: PROVIDER_NAME,
3539
+ provider: PROVIDER_NAME2,
3255
3540
  timestamp: record.timestamp,
3256
3541
  date: record.date,
3257
3542
  model: normalizeModelName2(record.model),
@@ -3260,14 +3545,14 @@ function toUsageEvent(record) {
3260
3545
  cacheReadTokens: record.cacheReadTokens,
3261
3546
  cacheWriteTokens: record.cacheWriteTokens,
3262
3547
  totalTokens,
3263
- cost: getRecordCost(record),
3264
- pricing: toCachePricing3(estimated.pricing),
3548
+ cost: getRecordCost2(record),
3549
+ pricing: toCachePricing4(estimated.pricing),
3265
3550
  sessionId: record.sessionId,
3266
3551
  projectId: record.projectId,
3267
3552
  durationMs: record.durationMs
3268
3553
  };
3269
3554
  }
3270
- function buildProviderData(records) {
3555
+ function buildProviderData2(records) {
3271
3556
  const byDate = new Map;
3272
3557
  for (const record of records) {
3273
3558
  let dateMap = byDate.get(record.date);
@@ -3279,7 +3564,7 @@ function buildProviderData(records) {
3279
3564
  const existing = dateMap.get(normalized);
3280
3565
  const estimated = getEstimatedCostBreakdown(record);
3281
3566
  const recordCost = typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost) ? record.explicitCost : estimated.totalCost;
3282
- const pricing = toCachePricing3(estimated.pricing);
3567
+ const pricing = toCachePricing4(estimated.pricing);
3283
3568
  if (existing) {
3284
3569
  existing.inputTokens += record.inputTokens;
3285
3570
  existing.outputTokens += record.outputTokens;
@@ -3327,13 +3612,13 @@ function buildProviderData(records) {
3327
3612
  };
3328
3613
  });
3329
3614
  return {
3330
- provider: PROVIDER_NAME,
3331
- displayName: DISPLAY_NAME,
3615
+ provider: PROVIDER_NAME2,
3616
+ displayName: DISPLAY_NAME2,
3332
3617
  daily,
3333
3618
  totalTokens,
3334
3619
  totalCost,
3335
3620
  colors: COLORS,
3336
- events: records.map(toUsageEvent)
3621
+ events: records.map(toUsageEvent2)
3337
3622
  };
3338
3623
  }
3339
3624
  function loadFromSqlite(dbPath, range) {
@@ -3351,8 +3636,8 @@ function loadFromSqlite(dbPath, range) {
3351
3636
  const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
3352
3637
  const records = [];
3353
3638
  for (const row of rows) {
3354
- const date = extractDate2(row.created_at);
3355
- const timestamp = toIsoTimestamp(row.created_at);
3639
+ const date = extractDate3(row.created_at);
3640
+ const timestamp = toIsoTimestamp2(row.created_at);
3356
3641
  if (date && timestamp && isInRange(date, range)) {
3357
3642
  records.push({
3358
3643
  date,
@@ -3374,11 +3659,11 @@ function loadFromSqlite(dbPath, range) {
3374
3659
  }
3375
3660
  }
3376
3661
  function loadFromLegacyJson(sessionsDir, range) {
3377
- const files = readdirSync3(sessionsDir).filter((file) => file.endsWith(".json"));
3662
+ const files = readdirSync4(sessionsDir).filter((file) => file.endsWith(".json"));
3378
3663
  const records = [];
3379
3664
  for (const file of files) {
3380
3665
  try {
3381
- const content = readFileSync(join3(sessionsDir, file), "utf-8");
3666
+ const content = readFileSync2(join4(sessionsDir, file), "utf-8");
3382
3667
  const session = JSON.parse(content);
3383
3668
  if (!Array.isArray(session.messages)) {
3384
3669
  continue;
@@ -3387,8 +3672,8 @@ function loadFromLegacyJson(sessionsDir, range) {
3387
3672
  if (msg.role !== "assistant" || !msg.usage) {
3388
3673
  continue;
3389
3674
  }
3390
- const date = extractDate2(msg.created_at);
3391
- const timestamp = toIsoTimestamp(msg.created_at);
3675
+ const date = extractDate3(msg.created_at);
3676
+ const timestamp = toIsoTimestamp2(msg.created_at);
3392
3677
  if (date && timestamp && isInRange(date, range)) {
3393
3678
  records.push({
3394
3679
  date,
@@ -3409,23 +3694,23 @@ function loadFromLegacyJson(sessionsDir, range) {
3409
3694
  return records;
3410
3695
  }
3411
3696
  function loadFromCurrentStorage(baseDir, range) {
3412
- const messagesRoot = join3(baseDir, "storage", "message");
3413
- if (!existsSync3(messagesRoot)) {
3697
+ const messagesRoot = join4(baseDir, "storage", "message");
3698
+ if (!existsSync4(messagesRoot)) {
3414
3699
  return [];
3415
3700
  }
3416
3701
  const recordsById = new Map;
3417
3702
  const recordsWithoutId = [];
3418
- for (const sessionDir of readdirSync3(messagesRoot)) {
3419
- const sessionPath = join3(messagesRoot, sessionDir);
3703
+ for (const sessionDir of readdirSync4(messagesRoot)) {
3704
+ const sessionPath = join4(messagesRoot, sessionDir);
3420
3705
  let messageFiles;
3421
3706
  try {
3422
- messageFiles = readdirSync3(sessionPath).filter((file) => file.endsWith(".json"));
3707
+ messageFiles = readdirSync4(sessionPath).filter((file) => file.endsWith(".json"));
3423
3708
  } catch {
3424
3709
  continue;
3425
3710
  }
3426
3711
  for (const file of messageFiles) {
3427
3712
  try {
3428
- const content = readFileSync(join3(sessionPath, file), "utf-8");
3713
+ const content = readFileSync2(join4(sessionPath, file), "utf-8");
3429
3714
  const message = JSON.parse(content);
3430
3715
  if (message.role !== "assistant") {
3431
3716
  continue;
@@ -3435,8 +3720,8 @@ function loadFromCurrentStorage(baseDir, range) {
3435
3720
  if (typeof model !== "string" || typeof createdAt !== "string" && typeof createdAt !== "number") {
3436
3721
  continue;
3437
3722
  }
3438
- const date = extractDate2(createdAt);
3439
- const timestamp = toIsoTimestamp(createdAt);
3723
+ const date = extractDate3(createdAt);
3724
+ const timestamp = toIsoTimestamp2(createdAt);
3440
3725
  if (!date || !timestamp || !isInRange(date, range)) {
3441
3726
  continue;
3442
3727
  }
@@ -3479,8 +3764,8 @@ function loadFromCurrentStorage(baseDir, range) {
3479
3764
  }
3480
3765
 
3481
3766
  class OpenCodeProvider {
3482
- name = PROVIDER_NAME;
3483
- displayName = DISPLAY_NAME;
3767
+ name = PROVIDER_NAME2;
3768
+ displayName = DISPLAY_NAME2;
3484
3769
  colors = COLORS;
3485
3770
  baseDir;
3486
3771
  constructor(baseDir) {
@@ -3488,51 +3773,51 @@ class OpenCodeProvider {
3488
3773
  }
3489
3774
  async isAvailable() {
3490
3775
  try {
3491
- if (!existsSync3(this.baseDir)) {
3776
+ if (!existsSync4(this.baseDir)) {
3492
3777
  return false;
3493
3778
  }
3494
- const hasCurrentStorage = existsSync3(join3(this.baseDir, "storage", "message"));
3495
- const hasLegacyDb = existsSync3(join3(this.baseDir, "opencode.db")) || existsSync3(join3(this.baseDir, "sessions.db"));
3496
- const hasLegacySessionsDir = existsSync3(join3(this.baseDir, "sessions"));
3779
+ const hasCurrentStorage = existsSync4(join4(this.baseDir, "storage", "message"));
3780
+ const hasLegacyDb = existsSync4(join4(this.baseDir, "opencode.db")) || existsSync4(join4(this.baseDir, "sessions.db"));
3781
+ const hasLegacySessionsDir = existsSync4(join4(this.baseDir, "sessions"));
3497
3782
  return hasCurrentStorage || hasLegacyDb || hasLegacySessionsDir;
3498
3783
  } catch {
3499
3784
  return false;
3500
3785
  }
3501
3786
  }
3502
3787
  async load(range) {
3503
- const currentMessagesRoot = join3(this.baseDir, "storage", "message");
3504
- if (existsSync3(currentMessagesRoot)) {
3788
+ const currentMessagesRoot = join4(this.baseDir, "storage", "message");
3789
+ if (existsSync4(currentMessagesRoot)) {
3505
3790
  const currentRecords = loadFromCurrentStorage(this.baseDir, range);
3506
- return buildProviderData(currentRecords);
3791
+ return buildProviderData2(currentRecords);
3507
3792
  }
3508
- const opencodeDbPath = join3(this.baseDir, "opencode.db");
3509
- const sessionsDbPath = join3(this.baseDir, "sessions.db");
3510
- const sessionsDir = join3(this.baseDir, "sessions");
3793
+ const opencodeDbPath = join4(this.baseDir, "opencode.db");
3794
+ const sessionsDbPath = join4(this.baseDir, "sessions.db");
3795
+ const sessionsDir = join4(this.baseDir, "sessions");
3511
3796
  let records = [];
3512
- if (existsSync3(opencodeDbPath)) {
3797
+ if (existsSync4(opencodeDbPath)) {
3513
3798
  records = loadFromSqlite(opencodeDbPath, range);
3514
- } else if (existsSync3(sessionsDbPath)) {
3799
+ } else if (existsSync4(sessionsDbPath)) {
3515
3800
  records = loadFromSqlite(sessionsDbPath, range);
3516
- } else if (existsSync3(sessionsDir)) {
3801
+ } else if (existsSync4(sessionsDir)) {
3517
3802
  records = loadFromLegacyJson(sessionsDir, range);
3518
3803
  }
3519
- return buildProviderData(records);
3804
+ return buildProviderData2(records);
3520
3805
  }
3521
3806
  }
3522
3807
  // packages/registry/dist/providers/pi.js
3523
- import { existsSync as existsSync4 } from "fs";
3808
+ import { existsSync as existsSync5 } from "fs";
3524
3809
  import { readdir } from "fs/promises";
3525
- import { homedir as homedir4 } from "os";
3526
- import { join as join4, relative as relative4, sep as sep3 } from "path";
3527
- var PROVIDER_NAME2 = "pi";
3528
- var DISPLAY_NAME2 = "Pi";
3529
- var DEFAULT_AGENT_DIR = join4(homedir4(), ".pi", "agent");
3810
+ import { homedir as homedir5 } from "os";
3811
+ import { join as join5, relative as relative4, sep as sep3 } from "path";
3812
+ var PROVIDER_NAME3 = "pi";
3813
+ var DISPLAY_NAME3 = "Pi";
3814
+ var DEFAULT_AGENT_DIR = join5(homedir5(), ".pi", "agent");
3530
3815
  var PI_COLORS = {
3531
3816
  primary: "#0ea5e9",
3532
3817
  secondary: "#67e8f9",
3533
3818
  gradient: ["#0ea5e9", "#67e8f9"]
3534
3819
  };
3535
- var DASHED_DATE_SUFFIX2 = /-(\d{4})-(\d{2})-(\d{2})$/;
3820
+ var DASHED_DATE_SUFFIX3 = /-(\d{4})-(\d{2})-(\d{2})$/;
3536
3821
  function resolveAgentDir(baseDir) {
3537
3822
  if (baseDir) {
3538
3823
  return baseDir;
@@ -3540,15 +3825,15 @@ function resolveAgentDir(baseDir) {
3540
3825
  return process.env["PI_CODING_AGENT_DIR"] ?? DEFAULT_AGENT_DIR;
3541
3826
  }
3542
3827
  function getSessionsDir(agentDir) {
3543
- return join4(agentDir, "sessions");
3828
+ return join5(agentDir, "sessions");
3544
3829
  }
3545
3830
  async function collectJsonlFiles3(dir) {
3546
- if (!existsSync4(dir)) {
3831
+ if (!existsSync5(dir)) {
3547
3832
  return [];
3548
3833
  }
3549
3834
  const files = [];
3550
3835
  for (const entry of await readdir(dir, { withFileTypes: true })) {
3551
- const fullPath = join4(dir, entry.name);
3836
+ const fullPath = join5(dir, entry.name);
3552
3837
  if (entry.isDirectory()) {
3553
3838
  files.push(...await collectJsonlFiles3(fullPath));
3554
3839
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -3557,16 +3842,16 @@ async function collectJsonlFiles3(dir) {
3557
3842
  }
3558
3843
  return files;
3559
3844
  }
3560
- function extractDate3(timestamp) {
3845
+ function extractDate4(timestamp) {
3561
3846
  const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
3562
3847
  return match ? match[1] : null;
3563
3848
  }
3564
- function compactModelDateSuffix2(model) {
3565
- return model.replace(DASHED_DATE_SUFFIX2, "-$1$2$3");
3849
+ function compactModelDateSuffix3(model) {
3850
+ return model.replace(DASHED_DATE_SUFFIX3, "-$1$2$3");
3566
3851
  }
3567
- function toIsoTimestamp2(value) {
3852
+ function toIsoTimestamp3(value) {
3568
3853
  if (typeof value === "string") {
3569
- return extractDate3(value) ? value : null;
3854
+ return extractDate4(value) ? value : null;
3570
3855
  }
3571
3856
  if (typeof value === "number") {
3572
3857
  const date = new Date(value);
@@ -3574,7 +3859,7 @@ function toIsoTimestamp2(value) {
3574
3859
  }
3575
3860
  return null;
3576
3861
  }
3577
- function toCachePricing4(pricing) {
3862
+ function toCachePricing5(pricing) {
3578
3863
  if (!pricing) {
3579
3864
  return;
3580
3865
  }
@@ -3598,7 +3883,7 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
3598
3883
  if (obj["type"] !== "message") {
3599
3884
  return null;
3600
3885
  }
3601
- const entryTimestamp = toIsoTimestamp2(obj["timestamp"]);
3886
+ const entryTimestamp = toIsoTimestamp3(obj["timestamp"]);
3602
3887
  const message = obj["message"];
3603
3888
  if (typeof message !== "object" || message === null) {
3604
3889
  return null;
@@ -3624,16 +3909,16 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
3624
3909
  if (totalTokens === 0) {
3625
3910
  return null;
3626
3911
  }
3627
- const timestamp = entryTimestamp ?? toIsoTimestamp2(msg["timestamp"]);
3912
+ const timestamp = entryTimestamp ?? toIsoTimestamp3(msg["timestamp"]);
3628
3913
  if (!timestamp) {
3629
3914
  return null;
3630
3915
  }
3631
- const date = extractDate3(timestamp);
3916
+ const date = extractDate4(timestamp);
3632
3917
  if (!date) {
3633
3918
  return null;
3634
3919
  }
3635
3920
  const cost = typeof usageObj["cost"] === "object" && usageObj["cost"] !== null ? usageObj["cost"]["total"] : undefined;
3636
- const normalizedModel = normalizeModelName2(compactModelDateSuffix2(model));
3921
+ const normalizedModel = normalizeModelName2(compactModelDateSuffix3(model));
3637
3922
  return {
3638
3923
  date,
3639
3924
  timestamp,
@@ -3648,17 +3933,17 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
3648
3933
  projectId: fallbackProjectId
3649
3934
  };
3650
3935
  }
3651
- function getRecordCost2(record) {
3936
+ function getRecordCost3(record) {
3652
3937
  if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
3653
3938
  return record.explicitCost;
3654
3939
  }
3655
3940
  return estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).totalCost;
3656
3941
  }
3657
- function toUsageEvent2(record) {
3942
+ function toUsageEvent3(record) {
3658
3943
  const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
3659
- const pricing = toCachePricing4(estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).pricing);
3944
+ const pricing = toCachePricing5(estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).pricing);
3660
3945
  return {
3661
- provider: PROVIDER_NAME2,
3946
+ provider: PROVIDER_NAME3,
3662
3947
  timestamp: record.timestamp,
3663
3948
  date: record.date,
3664
3949
  model: record.normalizedModel,
@@ -3667,15 +3952,15 @@ function toUsageEvent2(record) {
3667
3952
  cacheReadTokens: record.cacheReadTokens,
3668
3953
  cacheWriteTokens: record.cacheWriteTokens,
3669
3954
  totalTokens,
3670
- cost: getRecordCost2(record),
3955
+ cost: getRecordCost3(record),
3671
3956
  pricing,
3672
3957
  sessionId: record.sessionId,
3673
3958
  projectId: record.projectId
3674
3959
  };
3675
3960
  }
3676
- function buildProviderData2(records) {
3961
+ function buildProviderData3(records) {
3677
3962
  const byDate = new Map;
3678
- const events = records.map(toUsageEvent2);
3963
+ const events = records.map(toUsageEvent3);
3679
3964
  for (const event of events) {
3680
3965
  let dateMap = byDate.get(event.date);
3681
3966
  if (!dateMap) {
@@ -3730,8 +4015,8 @@ function buildProviderData2(records) {
3730
4015
  };
3731
4016
  });
3732
4017
  return {
3733
- provider: PROVIDER_NAME2,
3734
- displayName: DISPLAY_NAME2,
4018
+ provider: PROVIDER_NAME3,
4019
+ displayName: DISPLAY_NAME3,
3735
4020
  daily,
3736
4021
  totalTokens,
3737
4022
  totalCost,
@@ -3741,8 +4026,8 @@ function buildProviderData2(records) {
3741
4026
  }
3742
4027
 
3743
4028
  class PiProvider {
3744
- name = PROVIDER_NAME2;
3745
- displayName = DISPLAY_NAME2;
4029
+ name = PROVIDER_NAME3;
4030
+ displayName = DISPLAY_NAME3;
3746
4031
  colors = PI_COLORS;
3747
4032
  agentDir;
3748
4033
  constructor(baseDir) {
@@ -3750,7 +4035,7 @@ class PiProvider {
3750
4035
  }
3751
4036
  async isAvailable() {
3752
4037
  try {
3753
- return existsSync4(getSessionsDir(this.agentDir));
4038
+ return existsSync5(getSessionsDir(this.agentDir));
3754
4039
  } catch {
3755
4040
  return false;
3756
4041
  }
@@ -3773,7 +4058,7 @@ class PiProvider {
3773
4058
  }
3774
4059
  }
3775
4060
  }
3776
- return buildProviderData2(records);
4061
+ return buildProviderData3(records);
3777
4062
  }
3778
4063
  }
3779
4064
  // packages/renderers/dist/json/json-renderer.js
@@ -4430,198 +4715,7 @@ class SvgRenderer {
4430
4715
  });
4431
4716
  }
4432
4717
  }
4433
- // packages/renderers/dist/svg/theme.js
4434
- var DARK_THEME = {
4435
- background: "#0d1117",
4436
- foreground: "#e6edf3",
4437
- muted: "#7d8590",
4438
- border: "#30363d",
4439
- cardBackground: "#161b22",
4440
- heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
4441
- accent: "#58a6ff",
4442
- accentSecondary: "#bc8cff",
4443
- barFill: "#3b82f6",
4444
- barBackground: "#21262d"
4445
- };
4446
- var LIGHT_THEME = {
4447
- background: "#ffffff",
4448
- foreground: "#1a1a2e",
4449
- muted: "#8b8fa3",
4450
- border: "#e5e7eb",
4451
- cardBackground: "#f8f9fc",
4452
- heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
4453
- accent: "#3b5bdb",
4454
- accentSecondary: "#7048e8",
4455
- barFill: "#5b6abf",
4456
- barBackground: "#ebedf0"
4457
- };
4458
- function getTheme(mode) {
4459
- return mode === "dark" ? DARK_THEME : LIGHT_THEME;
4460
- }
4461
4718
  // packages/renderers/dist/svg/wrapped-slides.js
4462
- var WIDTH = 1200;
4463
- var PAD = 80;
4464
- var DISPLAY_FONT = "'SF Pro Display', 'Helvetica Neue', 'Segoe UI', -apple-system, sans-serif";
4465
- var MONO_FONT = "'SF Mono', 'Menlo', 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace";
4466
- var BODY_FONT = "'SF Pro Text', 'Helvetica Neue', 'Segoe UI', -apple-system, sans-serif";
4467
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
4468
- var DAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
4469
- var MONTH_NAMES2 = [
4470
- "January",
4471
- "February",
4472
- "March",
4473
- "April",
4474
- "May",
4475
- "June",
4476
- "July",
4477
- "August",
4478
- "September",
4479
- "October",
4480
- "November",
4481
- "December"
4482
- ];
4483
- function getWrappedTheme(mode) {
4484
- const base = getTheme(mode);
4485
- if (mode === "dark") {
4486
- return {
4487
- base,
4488
- mode,
4489
- sectionBgs: [
4490
- ["#08080c", "#0c0c14"],
4491
- ["#0a0a12", "#0e0e18"],
4492
- ["#0c0a08", "#100e0c"],
4493
- ["#080c14", "#0c1018"],
4494
- ["#0a0a0e", "#0e0e12"],
4495
- ["#080c14", "#0c1018"],
4496
- ["#0c0814", "#100c18"],
4497
- ["#080e0c", "#0c1210"],
4498
- ["#0c0a06", "#100e0a"],
4499
- ["#08080c", "#0c0c14"],
4500
- ["#0a0c10", "#0e1014"],
4501
- ["#060608", "#060608"]
4502
- ],
4503
- heroAccent: "#a78bfa",
4504
- warmAccent: "#fb923c",
4505
- coolAccent: "#38bdf8",
4506
- greenAccent: "#4ade80",
4507
- goldAccent: "#fbbf24",
4508
- purpleAccent: "#c084fc",
4509
- narrativeColor: "#d1d5db",
4510
- subtitleColor: "#6b7280"
4511
- };
4512
- }
4513
- return {
4514
- base,
4515
- mode,
4516
- sectionBgs: [
4517
- ["#fafaf9", "#f5f5f4"],
4518
- ["#f5f5f4", "#fafaf9"],
4519
- ["#fefce8", "#fef9c3"],
4520
- ["#eff6ff", "#dbeafe"],
4521
- ["#fafaf9", "#f5f5f4"],
4522
- ["#eff6ff", "#dbeafe"],
4523
- ["#faf5ff", "#f3e8ff"],
4524
- ["#ecfdf5", "#d1fae5"],
4525
- ["#fffbeb", "#fef3c7"],
4526
- ["#faf5ff", "#f3e8ff"],
4527
- ["#eff6ff", "#dbeafe"],
4528
- ["#fafaf9", "#fafaf9"]
4529
- ],
4530
- heroAccent: "#7c3aed",
4531
- warmAccent: "#ea580c",
4532
- coolAccent: "#2563eb",
4533
- greenAccent: "#16a34a",
4534
- goldAccent: "#ca8a04",
4535
- purpleAccent: "#7c3aed",
4536
- narrativeColor: "#1f2937",
4537
- subtitleColor: "#6b7280"
4538
- };
4539
- }
4540
- function svgIconFire(x, y, size, color) {
4541
- const s = size / 24;
4542
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2C6 8 4 12 4 15.5C4 19.09 7.58 22 12 22C16.42 22 20 19.09 20 15.5C20 12 18 8 12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `<path d="M12 8C9 12 8 14 8 15.5C8 17.71 9.79 19.5 12 19.5C14.21 19.5 16 17.71 16 15.5C16 14 15 12 12 8Z" ` + `fill="${escapeXml(color)}" opacity="0.5"/>` + `</g>`;
4543
- }
4544
- function svgIconStar(x, y, size, color) {
4545
- const s = size / 24;
4546
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4547
- }
4548
- function svgIconCircle(x, y, size, color) {
4549
- const r = size / 2;
4550
- return `<circle cx="${x + r}" cy="${y + r}" r="${r}" fill="${escapeXml(color)}" opacity="0.85"/>`;
4551
- }
4552
- function svgIconDiamond(x, y, size, color) {
4553
- const s = size / 2;
4554
- const cx = x + s;
4555
- const cy = y + s;
4556
- return `<path d="M${cx} ${cy - s} L${cx + s} ${cy} L${cx} ${cy + s} L${cx - s} ${cy} Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>`;
4557
- }
4558
- function svgIconBolt(x, y, size, color) {
4559
- const s = size / 24;
4560
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4561
- }
4562
- function svgIconTrophy(x, y, size, color) {
4563
- const s = size / 24;
4564
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M7 4V2H17V4H20V8C20 9.1 19.1 10 18 10H16.76C16.34 11.8 14.84 13.17 13 13.44V16H16V18H8V16H11V13.44C9.16 13.17 7.66 11.8 7.24 10H6C4.9 10 4 9.1 4 8V4H7Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4565
- }
4566
- function svgIconTarget(x, y, size, color) {
4567
- const cx = x + size / 2;
4568
- const cy = y + size / 2;
4569
- const r = size / 2;
4570
- return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${escapeXml(color)}" stroke-width="2" opacity="0.6"/>` + `<circle cx="${cx}" cy="${cy}" r="${r * 0.6}" fill="none" stroke="${escapeXml(color)}" stroke-width="2" opacity="0.7"/>` + `<circle cx="${cx}" cy="${cy}" r="${r * 0.25}" fill="${escapeXml(color)}" opacity="0.85"/>`;
4571
- }
4572
- function svgIconMountain(x, y, size, color) {
4573
- const s = size / 24;
4574
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M14 6L20 18H4L10 8L13 12.5L14 6Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4575
- }
4576
- function svgIconPalette(x, y, size, color) {
4577
- const s = size / 24;
4578
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C12.83 22 13.5 21.33 13.5 20.5C13.5 20.12 13.37 19.78 13.15 19.52C12.93 19.26 12.82 18.93 12.82 18.57C12.82 17.75 13.5 17.07 14.32 17.07H16.5C19.54 17.07 22 14.61 22 11.57C22 6.28 17.51 2 12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4579
- }
4580
- function svgIconCalendar(x, y, size, color) {
4581
- const s = size / 24;
4582
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M19 4H18V2H16V4H8V2H6V4H5C3.89 4 3 4.9 3 6V20C3 21.1 3.89 22 5 22H19C20.1 22 21 21.1 21 20V6C21 4.9 20.1 4 19 4ZM19 20H5V10H19V20ZM19 8H5V6H19V8Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4583
- }
4584
- function svgIconMoon(x, y, size, color) {
4585
- const s = size / 24;
4586
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
4587
- }
4588
- function svgIconSun(x, y, size, color) {
4589
- const cx = x + size / 2;
4590
- const cy = y + size / 2;
4591
- const r = size * 0.3;
4592
- let svg = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${escapeXml(color)}" opacity="0.85"/>`;
4593
- for (let i = 0;i < 8; i++) {
4594
- const angle = i * 45 * Math.PI / 180;
4595
- const x1 = cx + r * 1.4 * Math.cos(angle);
4596
- const y1 = cy + r * 1.4 * Math.sin(angle);
4597
- const x2 = cx + r * 2 * Math.cos(angle);
4598
- const y2 = cy + r * 2 * Math.sin(angle);
4599
- svg += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${escapeXml(color)}" stroke-width="2" stroke-linecap="round" opacity="0.7"/>`;
4600
- }
4601
- return svg;
4602
- }
4603
- function svgIconRocket(x, y, size, color) {
4604
- const s = size / 24;
4605
- return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2.5C12 2.5 7 8 7 13.5C7 16.81 9.24 19.5 12 19.5C14.76 19.5 17 16.81 17 13.5C17 8 12 2.5 12 2.5Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `<circle cx="12" cy="13" r="2" fill="${escapeXml(color)}" opacity="0.4"/>` + `</g>`;
4606
- }
4607
- function renderIcon(name, x, y, size, color) {
4608
- const fns = {
4609
- fire: svgIconFire,
4610
- star: svgIconStar,
4611
- circle: svgIconCircle,
4612
- diamond: svgIconDiamond,
4613
- bolt: svgIconBolt,
4614
- trophy: svgIconTrophy,
4615
- target: svgIconTarget,
4616
- mountain: svgIconMountain,
4617
- palette: svgIconPalette,
4618
- calendar: svgIconCalendar,
4619
- moon: svgIconMoon,
4620
- sun: svgIconSun,
4621
- rocket: svgIconRocket
4622
- };
4623
- return fns[name](x, y, size, color);
4624
- }
4625
4719
  function computeAchievements(output) {
4626
4720
  const stats = output.aggregated;
4627
4721
  const more = output.more;
@@ -4684,22 +4778,83 @@ function computeAchievements(output) {
4684
4778
  }
4685
4779
  return all.slice(0, 6);
4686
4780
  }
4687
- function sectionBg(y, height, gradColors, gradId) {
4688
- return [
4689
- `<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="100%">`,
4690
- `<stop offset="0%" stop-color="${escapeXml(gradColors[0])}"/>`,
4691
- `<stop offset="100%" stop-color="${escapeXml(gradColors[1])}"/>`,
4692
- `</linearGradient></defs>`,
4693
- `<rect x="0" y="${y}" width="${WIDTH}" height="${height}" fill="url(#${escapeXml(gradId)})"/>`
4694
- ].join("");
4695
- }
4696
- function svgText(x, y, content, opts = {}) {
4781
+ // packages/renderers/dist/svg/wrapped-single-page.js
4782
+ var WIDTH = 1200;
4783
+ var PAD = 56;
4784
+ var INNER = WIDTH - PAD * 2;
4785
+ var DISPLAY = "'Bricolage Grotesque', 'SF Pro Display', 'Helvetica Neue', sans-serif";
4786
+ var MONO = "'Space Mono', 'SF Mono', 'Menlo', monospace";
4787
+ var BODY = "'Space Grotesk', 'SF Pro Text', 'Helvetica Neue', sans-serif";
4788
+ var DARK = {
4789
+ bg: "#09090b",
4790
+ surface: "#111114",
4791
+ surface2: "#16161a",
4792
+ border: "rgba(255,255,255,0.07)",
4793
+ borderHi: "rgba(212,175,95,0.24)",
4794
+ gold: "#d4af5f",
4795
+ goldDim: "#a08040",
4796
+ ivory: "#f0ead6",
4797
+ ivoryDim: "rgba(240,234,214,0.52)",
4798
+ text: "#e8e2cc",
4799
+ muted: "rgba(232,226,204,0.38)",
4800
+ muted2: "rgba(232,226,204,0.18)",
4801
+ providerDefault: "#555555",
4802
+ trackStroke: "rgba(255,255,255,0.05)",
4803
+ barTrack: "rgba(255,255,255,0.09)",
4804
+ dotInactive: "#16161a",
4805
+ dotInactiveBorder: "rgba(255,255,255,0.07)"
4806
+ };
4807
+ var LIGHT = {
4808
+ bg: "#fafaf9",
4809
+ surface: "#f0efed",
4810
+ surface2: "#e8e6e3",
4811
+ border: "rgba(0,0,0,0.08)",
4812
+ borderHi: "rgba(160,128,64,0.3)",
4813
+ gold: "#9a7b3a",
4814
+ goldDim: "#b8985a",
4815
+ ivory: "#1a1a18",
4816
+ ivoryDim: "rgba(26,26,24,0.6)",
4817
+ text: "#2c2c28",
4818
+ muted: "rgba(44,44,40,0.42)",
4819
+ muted2: "rgba(44,44,40,0.22)",
4820
+ providerDefault: "#888888",
4821
+ trackStroke: "rgba(0,0,0,0.06)",
4822
+ barTrack: "rgba(0,0,0,0.08)",
4823
+ dotInactive: "#e8e6e3",
4824
+ dotInactiveBorder: "rgba(0,0,0,0.08)"
4825
+ };
4826
+ var PROVIDER_COLORS = {
4827
+ anthropic: { dark: "#d4af5f", light: "#9a7b3a" },
4828
+ "claude-code": { dark: "#d4af5f", light: "#9a7b3a" },
4829
+ openai: { dark: "#3a5070", light: "#4a6a90" },
4830
+ codex: { dark: "#3a5070", light: "#4a6a90" },
4831
+ google: { dark: "#6a2535", light: "#8a3548" },
4832
+ cursor: { dark: "#7c5cbf", light: "#6a4aaa" },
4833
+ pi: { dark: "#5a4a70", light: "#706088" }
4834
+ };
4835
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
4836
+ var DAY_NAMES_FULL = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
4837
+ var MONTH_NAMES2 = [
4838
+ "January",
4839
+ "February",
4840
+ "March",
4841
+ "April",
4842
+ "May",
4843
+ "June",
4844
+ "July",
4845
+ "August",
4846
+ "September",
4847
+ "October",
4848
+ "November",
4849
+ "December"
4850
+ ];
4851
+ function txt(x, y, content, opts = {}) {
4697
4852
  const attrs = [
4698
4853
  `x="${x}"`,
4699
4854
  `y="${y}"`,
4700
- `fill="${escapeXml(opts.fill ?? "#ffffff")}"`,
4855
+ `fill="${escapeXml(opts.fill ?? "#e8e2cc")}"`,
4701
4856
  `font-size="${opts.size ?? 14}"`,
4702
- `font-family="${escapeXml(opts.family ?? BODY_FONT)}"`,
4857
+ `font-family="${escapeXml(opts.family ?? BODY)}"`,
4703
4858
  `font-weight="${opts.weight ?? 400}"`
4704
4859
  ];
4705
4860
  if (opts.anchor)
@@ -4708,9 +4863,11 @@ function svgText(x, y, content, opts = {}) {
4708
4863
  attrs.push(`letter-spacing="${opts.spacing}"`);
4709
4864
  if (opts.opacity !== undefined)
4710
4865
  attrs.push(`opacity="${opts.opacity}"`);
4866
+ if (opts.dominantBaseline)
4867
+ attrs.push(`dominant-baseline="${opts.dominantBaseline}"`);
4711
4868
  return `<text ${attrs.join(" ")}>${escapeXml(content)}</text>`;
4712
4869
  }
4713
- function rect2(x, y, w, h, fill, rx = 4, opts = {}) {
4870
+ function box(x, y, w, h, fill, rx = 2, opts = {}) {
4714
4871
  const extra = [];
4715
4872
  if (opts.opacity !== undefined)
4716
4873
  extra.push(`opacity="${opts.opacity}"`);
@@ -4718,781 +4875,658 @@ function rect2(x, y, w, h, fill, rx = 4, opts = {}) {
4718
4875
  extra.push(`stroke="${escapeXml(opts.stroke)}" stroke-width="${opts.strokeWidth ?? 1}"`);
4719
4876
  return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${escapeXml(fill)}" ${extra.join(" ")}/>`;
4720
4877
  }
4721
- function describeArc(cx, cy, radius, startAngle, endAngle) {
4722
- const start = polarToCartesian(cx, cy, radius, endAngle);
4723
- const end = polarToCartesian(cx, cy, radius, startAngle);
4724
- const largeArc = endAngle - startAngle <= 180 ? "0" : "1";
4725
- return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArc} 0 ${end.x} ${end.y}`;
4726
- }
4727
- function polarToCartesian(cx, cy, radius, angleDeg) {
4728
- const rad = (angleDeg - 90) * Math.PI / 180;
4729
- return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) };
4730
- }
4731
- function cornerMark(x, y, size, color, corner) {
4732
- const s = size;
4733
- const paths = {
4734
- tl: `M${x} ${y + s} L${x} ${y} L${x + s} ${y}`,
4735
- tr: `M${x - s} ${y} L${x} ${y} L${x} ${y + s}`,
4736
- bl: `M${x} ${y - s} L${x} ${y} L${x + s} ${y}`,
4737
- br: `M${x - s} ${y} L${x} ${y} L${x} ${y - s}`
4738
- };
4739
- return `<path d="${paths[corner]}" fill="none" stroke="${escapeXml(color)}" stroke-width="1.5" opacity="0.3"/>`;
4740
- }
4741
- function sectionLabel(x, y, text2, color, accent) {
4742
- return [
4743
- `<line x1="${x}" y1="${y - 4}" x2="${x + 24}" y2="${y - 4}" stroke="${escapeXml(accent)}" stroke-width="2" opacity="0.6"/>`,
4744
- svgText(x + 32, y, text2, { fill: color, size: 11, weight: 600, spacing: 3, family: MONO_FONT })
4745
- ].join(`
4746
- `);
4878
+ function line(x1, y1, x2, y2, color, width = 1, opacity = 1) {
4879
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${escapeXml(color)}" stroke-width="${width}" opacity="${opacity}"/>`;
4747
4880
  }
4748
- function rule(x, y, width, color, opacity = 0.1) {
4749
- return `<line x1="${x}" y1="${y}" x2="${x + width}" y2="${y}" stroke="${escapeXml(color)}" stroke-width="1" opacity="${opacity}"/>`;
4750
- }
4751
- function dotGrid(x, y, w, h, color, spacing = 24, radius = 1) {
4752
- const dots = [];
4753
- for (let gx = x;gx <= x + w; gx += spacing) {
4754
- for (let gy = y;gy <= y + h; gy += spacing) {
4755
- dots.push(`<circle cx="${gx}" cy="${gy}" r="${radius}" fill="${escapeXml(color)}" opacity="0.06"/>`);
4756
- }
4757
- }
4758
- return dots.join(`
4759
- `);
4881
+ function formatDateShort(dateStr) {
4882
+ const d = new Date(dateStr + "T00:00:00Z");
4883
+ return `${MONTH_NAMES2[d.getUTCMonth()]?.slice(0, 3)} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
4760
4884
  }
4761
4885
  function formatDateLong(dateStr) {
4762
4886
  const d = new Date(dateStr + "T00:00:00Z");
4763
- if (Number.isNaN(d.getTime()))
4764
- return dateStr;
4765
- const month = MONTH_NAMES2[d.getUTCMonth()] ?? "";
4766
- return `${month} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
4887
+ return `${MONTH_NAMES2[d.getUTCMonth()] ?? ""} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
4888
+ }
4889
+ function getProviderColor(provider, mode) {
4890
+ const entry = PROVIDER_COLORS[provider.toLowerCase()];
4891
+ if (entry)
4892
+ return entry[mode];
4893
+ return mode === "dark" ? "#555555" : "#888888";
4894
+ }
4895
+ function describeArc(cx, cy, r, startDeg, endDeg) {
4896
+ const toRad = (deg) => (deg - 90) * Math.PI / 180;
4897
+ const start = { x: cx + r * Math.cos(toRad(endDeg)), y: cy + r * Math.sin(toRad(endDeg)) };
4898
+ const end = { x: cx + r * Math.cos(toRad(startDeg)), y: cy + r * Math.sin(toRad(startDeg)) };
4899
+ const large = endDeg - startDeg <= 180 ? "0" : "1";
4900
+ return `M ${start.x} ${start.y} A ${r} ${r} 0 ${large} 0 ${end.x} ${end.y}`;
4901
+ }
4902
+ function sectionTag(x, y, label, C) {
4903
+ return txt(x, y, label, {
4904
+ fill: C.muted,
4905
+ size: 10,
4906
+ weight: 400,
4907
+ family: MONO,
4908
+ spacing: 2.5
4909
+ });
4767
4910
  }
4768
- function globalDefs(isDark) {
4769
- const noiseOpacity = isDark ? 0.035 : 0.025;
4911
+ function noiseDef(isDark) {
4912
+ const opacity = isDark ? 0.03 : 0.02;
4770
4913
  return [
4771
4914
  "<defs>",
4772
4915
  '<filter id="grain" x="0" y="0" width="100%" height="100%">',
4773
- '<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" stitchTiles="stitch" result="noise"/>',
4916
+ '<feTurbulence type="fractalNoise" baseFrequency="0.78" numOctaves="4" stitchTiles="stitch" result="noise"/>',
4774
4917
  '<feColorMatrix type="saturate" values="0" in="noise" result="mono"/>',
4775
- `<feBlend in="SourceGraphic" in2="mono" mode="multiply"/>`,
4918
+ '<feBlend in="SourceGraphic" in2="mono" mode="multiply"/>',
4776
4919
  "</filter>",
4777
4920
  "</defs>",
4778
- `<rect width="${WIDTH}" height="99999" fill="transparent" filter="url(#grain)" opacity="${noiseOpacity}" pointer-events="none"/>`
4921
+ `<!-- noise opacity: ${opacity} -->`
4779
4922
  ].join(`
4780
4923
  `);
4781
4924
  }
4782
- function renderTitleSlide(output, theme) {
4783
- const height = 300;
4784
- const p = [];
4785
- const gc = theme.sectionBgs[0] ?? ["#08080c", "#0c0c14"];
4786
- p.push(sectionBg(0, height, gc, "title-bg"));
4787
- const gridColor = theme.mode === "dark" ? "#ffffff" : "#000000";
4788
- p.push(dotGrid(WIDTH - 300, 30, 220, 120, gridColor, 20, 1.2));
4789
- p.push(cornerMark(PAD - 16, 40, 20, theme.heroAccent, "tl"));
4790
- p.push(cornerMark(WIDTH - PAD + 16, height - 40, 20, theme.heroAccent, "br"));
4791
- const titleColor = theme.mode === "dark" ? "#e5e7eb" : "#1f2937";
4792
- p.push(svgText(PAD, 100, "Your AI Coding", {
4793
- fill: titleColor,
4794
- size: 36,
4795
- weight: 300,
4796
- family: DISPLAY_FONT,
4797
- spacing: -0.5
4925
+ function renderWrappedSinglePageSvg(output, options = { theme: "dark" }) {
4926
+ const stats = output.aggregated;
4927
+ const more = output.more;
4928
+ const providers = output.providers;
4929
+ const { since, until } = output.dateRange;
4930
+ const achievements = computeAchievements(output);
4931
+ const isDark = options.theme === "dark";
4932
+ const C = isDark ? DARK : LIGHT;
4933
+ const parts = [];
4934
+ let y = 0;
4935
+ const headerH = 200;
4936
+ parts.push(box(0, y, WIDTH, headerH, C.bg));
4937
+ if (isDark) {
4938
+ parts.push(`<defs><radialGradient id="amb1" cx="10%" cy="10%" r="60%"><stop offset="0%" stop-color="rgba(44,70,120,0.07)"/><stop offset="100%" stop-color="transparent"/></radialGradient></defs>`);
4939
+ parts.push(`<rect x="0" y="${y}" width="${WIDTH}" height="${headerH}" fill="url(#amb1)"/>`);
4940
+ }
4941
+ parts.push(line(0, y, WIDTH, y, C.gold, 1, 0.5));
4942
+ const year = until.slice(0, 4);
4943
+ parts.push(txt(PAD, y + 68, "AI Wrapped", {
4944
+ fill: C.ivory,
4945
+ size: 68,
4946
+ weight: 800,
4947
+ family: DISPLAY,
4948
+ spacing: -3
4798
4949
  }));
4799
- p.push(svgText(PAD, 160, "Wrapped", {
4800
- fill: theme.heroAccent,
4801
- size: 80,
4950
+ parts.push(txt(PAD + 460, y + 68, `'${year.slice(2)}`, {
4951
+ fill: C.gold,
4952
+ size: 68,
4802
4953
  weight: 800,
4803
- family: DISPLAY_FONT,
4954
+ family: DISPLAY,
4804
4955
  spacing: -3
4805
4956
  }));
4806
- p.push(`<line x1="${PAD}" y1="${178}" x2="${PAD + 120}" y2="${178}" stroke="${escapeXml(theme.heroAccent)}" stroke-width="2" opacity="0.4"/>`);
4807
- const { since, until } = output.dateRange;
4808
- const rangeText = `${formatDateLong(since)} \u2014 ${formatDateLong(until)}`;
4809
- p.push(svgText(PAD, 210, rangeText, {
4810
- fill: theme.subtitleColor,
4811
- size: 14,
4957
+ parts.push(txt(PAD, y + 96, `${formatDateShort(since)} \u2014 ${formatDateShort(until)}`, {
4958
+ fill: C.muted,
4959
+ size: 13,
4812
4960
  weight: 400,
4813
- family: MONO_FONT
4961
+ family: MONO,
4962
+ spacing: 1
4814
4963
  }));
4815
- p.push(svgText(PAD, 248, "tokenleak", {
4816
- fill: theme.heroAccent,
4817
- size: 13,
4818
- weight: 500,
4819
- family: MONO_FONT,
4820
- opacity: 0.4
4964
+ parts.push(txt(PAD, y + 126, "Every prompt has a price. Here's yours.", {
4965
+ fill: C.ivoryDim,
4966
+ size: 15,
4967
+ weight: 400,
4968
+ family: BODY
4821
4969
  }));
4822
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
4823
- return { svg: p.join(`
4824
- `), height };
4825
- }
4826
- function renderBigNumbersSlide(output, theme) {
4827
- const height = 260;
4828
- const p = [];
4829
- const stats = output.aggregated;
4830
- const gc = theme.sectionBgs[1] ?? ["#0a0a12", "#0e0e18"];
4831
- p.push(sectionBg(0, height, gc, "bignums-bg"));
4832
- p.push(sectionLabel(PAD, 40, "THE BIG NUMBERS", theme.subtitleColor, theme.heroAccent));
4833
- const numColor = theme.mode === "dark" ? "#f9fafb" : "#111827";
4834
- p.push(svgText(PAD, 130, formatNumber(stats.totalTokens), {
4835
- fill: numColor,
4836
- size: 96,
4970
+ const stampW = 140;
4971
+ const stampX = WIDTH - PAD - stampW;
4972
+ parts.push(box(stampX, y + 78, stampW, 36, "transparent", 2, { stroke: C.borderHi, strokeWidth: 1 }));
4973
+ parts.push(txt(stampX + stampW / 2, y + 101, "TokenLeak", {
4974
+ fill: C.gold,
4975
+ size: 16,
4837
4976
  weight: 800,
4838
- family: MONO_FONT,
4839
- spacing: -4
4977
+ family: DISPLAY,
4978
+ spacing: -0.5,
4979
+ anchor: "middle"
4840
4980
  }));
4841
- p.push(svgText(PAD, 158, "total tokens", {
4842
- fill: theme.subtitleColor,
4843
- size: 14,
4844
- weight: 500,
4845
- family: BODY_FONT,
4846
- spacing: 1
4981
+ const totalDays = Math.round((new Date(until + "T00:00:00Z").getTime() - new Date(since + "T00:00:00Z").getTime()) / (1000 * 60 * 60 * 24)) + 1;
4982
+ parts.push(`<circle cx="${PAD + 4}" cy="${y + 156}" r="3" fill="${C.gold}"/>`);
4983
+ parts.push(txt(PAD + 14, y + 160, `${totalDays} DAYS OF DATA`, {
4984
+ fill: C.muted,
4985
+ size: 9,
4986
+ weight: 400,
4987
+ family: MONO,
4988
+ spacing: 2
4847
4989
  }));
4848
- const statsY = 200;
4849
- const colW = (WIDTH - PAD * 2) / 3;
4850
- const supportStats = [
4851
- { value: formatCost2(stats.totalCost), label: "TOTAL COST", accent: true },
4852
- { value: `${stats.activeDays}`, label: "ACTIVE DAYS", accent: false },
4853
- { value: `${stats.totalDays}`, label: "TOTAL DAYS", accent: false }
4990
+ parts.push(line(PAD, y + headerH - 1, WIDTH - PAD, y + headerH - 1, C.gold, 1, 0.15));
4991
+ y += headerH;
4992
+ const bigNumH = 140;
4993
+ parts.push(box(0, y, WIDTH, bigNumH, C.surface));
4994
+ parts.push(sectionTag(PAD, y + 24, "THE BIG NUMBERS", C));
4995
+ const cardW = (INNER - 30) / 4;
4996
+ const cardH = 82;
4997
+ const cardY = y + 38;
4998
+ const bigStats = [
4999
+ { label: "TOTAL TOKENS", value: formatNumber(stats.totalTokens), unit: "TOKENS", gold: false },
5000
+ { label: "TOTAL COST", value: formatCost2(stats.totalCost), unit: "USD", gold: true },
5001
+ { label: "ACTIVE DAYS", value: `${stats.activeDays}`, unit: `OF ${stats.totalDays}`, gold: false },
5002
+ { label: "AVG / DAY", value: formatNumber(stats.averageDailyTokens), unit: "TOKENS", gold: false }
4854
5003
  ];
4855
- for (let i = 0;i < supportStats.length; i++) {
4856
- const sx = PAD + i * colW;
4857
- const stat = supportStats[i];
4858
- if (i > 0) {
4859
- p.push(`<line x1="${sx}" y1="${statsY - 10}" x2="${sx}" y2="${statsY + 32}" stroke="${theme.mode === "dark" ? "#ffffff" : "#000000"}" stroke-width="1" opacity="0.08"/>`);
4860
- }
4861
- p.push(svgText(sx + (i > 0 ? 20 : 0), statsY + 4, stat.value, {
4862
- fill: stat.accent ? theme.greenAccent : numColor,
4863
- size: 28,
4864
- weight: 700,
4865
- family: MONO_FONT
5004
+ for (let i = 0;i < bigStats.length; i++) {
5005
+ const stat = bigStats[i];
5006
+ const cx = PAD + i * (cardW + 10);
5007
+ parts.push(box(cx, cardY, cardW, cardH, C.surface2, 2, { stroke: C.border, strokeWidth: 1 }));
5008
+ parts.push(txt(cx + 16, cardY + 20, stat.label, {
5009
+ fill: C.muted,
5010
+ size: 9,
5011
+ weight: 400,
5012
+ family: MONO,
5013
+ spacing: 2
4866
5014
  }));
4867
- p.push(svgText(sx + (i > 0 ? 20 : 0), statsY + 28, stat.label, {
4868
- fill: theme.subtitleColor,
4869
- size: 10,
4870
- weight: 600,
4871
- spacing: 2,
4872
- family: MONO_FONT
5015
+ parts.push(txt(cx + 16, cardY + 52, stat.value, {
5016
+ fill: stat.gold ? C.gold : C.ivory,
5017
+ size: 30,
5018
+ weight: 800,
5019
+ family: DISPLAY,
5020
+ spacing: -1
5021
+ }));
5022
+ parts.push(txt(cx + 16, cardY + 68, stat.unit, {
5023
+ fill: C.gold,
5024
+ size: 9,
5025
+ weight: 400,
5026
+ family: MONO,
5027
+ spacing: 1.5
4873
5028
  }));
4874
5029
  }
4875
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
4876
- return { svg: p.join(`
4877
- `), height };
4878
- }
4879
- function renderStreakSlide(output, theme) {
4880
- const height = 250;
4881
- const p = [];
4882
- const stats = output.aggregated;
4883
- const gc = theme.sectionBgs[2] ?? ["#0c0a08", "#100e0c"];
4884
- p.push(sectionBg(0, height, gc, "streak-bg"));
4885
- p.push(sectionLabel(PAD, 40, "STREAK STORY", theme.subtitleColor, theme.warmAccent));
4886
- const narrative = stats.longestStreak > 0 ? `Your longest coding streak was ${stats.longestStreak} days` : "Start your first coding streak!";
4887
- p.push(svgText(PAD, 85, narrative, {
4888
- fill: theme.narrativeColor,
4889
- size: 20,
4890
- weight: 500,
4891
- family: DISPLAY_FONT
4892
- }));
4893
- p.push(svgText(PAD, 170, `${stats.longestStreak}`, {
4894
- fill: theme.warmAccent,
4895
- size: 88,
4896
- weight: 800,
4897
- family: MONO_FONT,
4898
- spacing: -4
4899
- }));
4900
- const numWidth = Math.max(60, `${stats.longestStreak}`.length * 50);
4901
- p.push(svgText(PAD + numWidth + 8, 170, "days", {
4902
- fill: theme.subtitleColor,
4903
- size: 20,
4904
- weight: 400,
4905
- family: DISPLAY_FONT
4906
- }));
4907
- p.push(svgIconFire(PAD + numWidth + 8, 120, 40, theme.warmAccent));
4908
- p.push(svgText(WIDTH - PAD, 170, `Current: ${stats.currentStreak}`, {
4909
- fill: theme.subtitleColor,
4910
- size: 14,
4911
- weight: 500,
4912
- family: MONO_FONT,
4913
- anchor: "end"
4914
- }));
4915
- const barY = 198;
4916
- const barW = WIDTH - PAD * 2;
4917
- const barH = 6;
4918
- const streakRatio = Math.min(stats.longestStreak / 30, 1);
4919
- const trackColor = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
4920
- p.push(rect2(PAD, barY, barW, barH, trackColor, 3));
4921
- if (streakRatio > 0) {
4922
- p.push(rect2(PAD, barY, Math.max(8, streakRatio * barW), barH, theme.warmAccent, 3, { opacity: 0.7 }));
4923
- }
4924
- for (let i = 0;i <= 30 && i <= stats.longestStreak; i += 5) {
4925
- if (i === 0)
4926
- continue;
4927
- const tx = PAD + i / 30 * barW;
4928
- p.push(`<line x1="${tx}" y1="${barY + barH + 2}" x2="${tx}" y2="${barY + barH + 6}" stroke="${escapeXml(theme.subtitleColor)}" stroke-width="1" opacity="0.3"/>`);
4929
- }
4930
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
4931
- return { svg: p.join(`
4932
- `), height };
4933
- }
4934
- function renderTopModelSlide(output, theme) {
4935
- const height = 300;
4936
- const p = [];
4937
- const stats = output.aggregated;
5030
+ y += bigNumH;
5031
+ const colW = (INNER - 24) / 2;
5032
+ const leftX = PAD;
5033
+ const rightX = PAD + colW + 24;
4938
5034
  const topModels2 = stats.topModels.slice(0, 3);
4939
- const gc = theme.sectionBgs[3] ?? ["#080c14", "#0c1018"];
4940
- p.push(sectionBg(0, height, gc, "model-bg"));
4941
- p.push(sectionLabel(PAD, 40, "YOUR TOP MODEL", theme.subtitleColor, theme.coolAccent));
4942
- if (topModels2.length === 0) {
4943
- p.push(svgText(PAD, 160, "No model data available", {
4944
- fill: theme.subtitleColor,
4945
- size: 18,
4946
- weight: 500
4947
- }));
4948
- return { svg: p.join(`
4949
- `), height };
5035
+ const totalProviderTokens = providers.reduce((s, p) => s + p.totalTokens, 0);
5036
+ const providerMix = providers.map((p) => ({
5037
+ name: p.displayName,
5038
+ pct: totalProviderTokens > 0 ? p.totalTokens / totalProviderTokens * 100 : 0,
5039
+ color: getProviderColor(p.provider, options.theme)
5040
+ })).sort((a, b) => b.pct - a.pct);
5041
+ const modelSectionH = 28 + 22 + topModels2.length * 42;
5042
+ const providerSectionH = 18 + 22 + Math.max(150, providerMix.length * 30 + 50);
5043
+ const leftColH = modelSectionH + providerSectionH;
5044
+ const dow = stats.dayOfWeek;
5045
+ const dowH = dow.length > 0 ? 28 + 22 + 26 + 100 + 28 : 60;
5046
+ const todH = 20 + 22 + 65 + 8 + 65;
5047
+ const rightColH = dowH + todH;
5048
+ const midH = Math.max(leftColH, rightColH) + 20;
5049
+ parts.push(box(0, y, WIDTH, midH, C.bg));
5050
+ let ly = y + 28;
5051
+ parts.push(sectionTag(leftX, ly, "YOUR TOP MODELS", C));
5052
+ ly += 22;
5053
+ if (topModels2.length > 0) {
5054
+ const maxPct = Math.max(...topModels2.map((m) => m.percentage), 1);
5055
+ for (let i = 0;i < topModels2.length; i++) {
5056
+ const m = topModels2[i];
5057
+ const barMaxW = colW - 80;
5058
+ const barW = Math.max(4, m.percentage / maxPct * barMaxW);
5059
+ const opacity = i === 0 ? 1 : i === 1 ? 0.6 : 0.35;
5060
+ parts.push(txt(leftX, ly + 14, m.model, {
5061
+ fill: C.text,
5062
+ size: 14,
5063
+ weight: 500,
5064
+ family: BODY
5065
+ }));
5066
+ parts.push(txt(leftX + colW, ly + 14, `${m.percentage.toFixed(1)}%`, {
5067
+ fill: C.gold,
5068
+ size: 13,
5069
+ weight: 700,
5070
+ family: MONO,
5071
+ anchor: "end"
5072
+ }));
5073
+ parts.push(line(leftX, ly + 26, leftX + colW, ly + 26, C.barTrack, 1));
5074
+ parts.push(line(leftX, ly + 26, leftX + barW, ly + 26, C.gold, 1, opacity));
5075
+ ly += 42;
5076
+ }
5077
+ }
5078
+ ly += 18;
5079
+ parts.push(sectionTag(leftX, ly, "PROVIDER MIX", C));
5080
+ ly += 22;
5081
+ const donutCx = leftX + 70;
5082
+ const donutCy = ly + 70;
5083
+ const donutR = 50;
5084
+ const donutStroke = 14;
5085
+ parts.push(`<circle cx="${donutCx}" cy="${donutCy}" r="${donutR}" fill="none" stroke="${C.trackStroke}" stroke-width="${donutStroke}"/>`);
5086
+ let startAngle = 0;
5087
+ for (const p of providerMix) {
5088
+ const sweep = p.pct / 100 * 360;
5089
+ if (sweep < 0.1)
5090
+ continue;
5091
+ const endAngle = Math.min(startAngle + sweep, 360);
5092
+ if (sweep >= 359.9) {
5093
+ parts.push(`<circle cx="${donutCx}" cy="${donutCy}" r="${donutR}" fill="none" stroke="${escapeXml(p.color)}" stroke-width="${donutStroke}"/>`);
5094
+ } else {
5095
+ const arc = describeArc(donutCx, donutCy, donutR, startAngle, endAngle);
5096
+ parts.push(`<path d="${arc}" fill="none" stroke="${escapeXml(p.color)}" stroke-width="${donutStroke}" stroke-linecap="butt"/>`);
5097
+ }
5098
+ startAngle = endAngle;
4950
5099
  }
4951
- const topModel = topModels2[0];
4952
- const arcColors = [theme.coolAccent, theme.purpleAccent, theme.greenAccent, theme.warmAccent];
4953
- p.push(svgText(PAD, 100, topModel.model, {
4954
- fill: theme.coolAccent,
4955
- size: 32,
4956
- weight: 700,
4957
- family: MONO_FONT,
4958
- spacing: -1
5100
+ parts.push(txt(donutCx, donutCy + 5, `${providers.length}`, {
5101
+ fill: C.ivory,
5102
+ size: 22,
5103
+ weight: 800,
5104
+ family: DISPLAY,
5105
+ anchor: "middle"
4959
5106
  }));
4960
- p.push(svgText(PAD, 128, `${topModel.percentage.toFixed(0)}% of all tokens`, {
4961
- fill: theme.subtitleColor,
4962
- size: 14,
4963
- weight: 500
5107
+ parts.push(txt(donutCx, donutCy + 20, "providers", {
5108
+ fill: C.muted,
5109
+ size: 8,
5110
+ weight: 400,
5111
+ family: MONO,
5112
+ anchor: "middle",
5113
+ spacing: 1
4964
5114
  }));
4965
- const segBarY = 150;
4966
- const segBarH = 20;
4967
- const segBarW = WIDTH - PAD * 2;
4968
- const trackBg = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
4969
- p.push(rect2(PAD, segBarY, segBarW, segBarH, trackBg, 4));
4970
- let segX = PAD;
4971
- for (let i = 0;i < topModels2.length; i++) {
4972
- const model = topModels2[i];
4973
- const w = Math.max(4, model.percentage / 100 * segBarW);
4974
- const gap = i > 0 ? 2 : 0;
4975
- p.push(rect2(segX + gap, segBarY, w - gap, segBarH, arcColors[i % arcColors.length], 4, { opacity: 0.85 }));
4976
- segX += w;
4977
- }
4978
- const listY = 195;
4979
- for (let i = 0;i < topModels2.length; i++) {
4980
- const model = topModels2[i];
4981
- const my = listY + i * 32;
4982
- p.push(`<rect x="${PAD}" y="${my + 2}" width="4" height="16" rx="2" fill="${escapeXml(arcColors[i % arcColors.length])}" opacity="0.9"/>`);
4983
- p.push(svgText(PAD + 16, my + 14, `${model.model}`, {
4984
- fill: theme.mode === "dark" ? "#e5e7eb" : "#1f2937",
4985
- size: 14,
4986
- weight: 600,
4987
- family: MONO_FONT
5115
+ const legendX = leftX + 155;
5116
+ for (let i = 0;i < providerMix.length; i++) {
5117
+ const p = providerMix[i];
5118
+ const py = ly + 35 + i * 28;
5119
+ parts.push(box(legendX, py, 8, 8, p.color, 1));
5120
+ parts.push(txt(legendX + 16, py + 8, p.name, {
5121
+ fill: C.text,
5122
+ size: 13,
5123
+ weight: 500,
5124
+ family: BODY
4988
5125
  }));
4989
- p.push(svgText(WIDTH - PAD, my + 14, `${model.percentage.toFixed(0)}%`, {
4990
- fill: arcColors[i % arcColors.length],
4991
- size: 14,
5126
+ const pctLabel = p.pct > 0 && p.pct < 1 ? "<1%" : `${p.pct.toFixed(0)}%`;
5127
+ parts.push(txt(leftX + colW, py + 8, pctLabel, {
5128
+ fill: C.gold,
5129
+ size: 12,
4992
5130
  weight: 700,
4993
- family: MONO_FONT,
5131
+ family: MONO,
4994
5132
  anchor: "end"
4995
5133
  }));
4996
- const barW = Math.max(4, model.percentage / 100 * (segBarW - 200));
4997
- p.push(rect2(PAD + 16, my + 20, barW, 4, arcColors[i % arcColors.length], 2, { opacity: 0.4 }));
4998
5134
  }
4999
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5000
- return { svg: p.join(`
5001
- `), height };
5002
- }
5003
- function renderProviderMixSlide(output, theme) {
5004
- const providers = output.providers;
5005
- const perRow = 52;
5006
- const height = Math.max(180, 100 + providers.length * perRow);
5007
- const p = [];
5008
- const gc = theme.sectionBgs[4] ?? ["#0a0a0e", "#0e0e12"];
5009
- p.push(sectionBg(0, height, gc, "provider-bg"));
5010
- p.push(sectionLabel(PAD, 40, "PROVIDER MIX", theme.subtitleColor, theme.purpleAccent));
5011
- if (providers.length === 0) {
5012
- p.push(svgText(PAD, 110, "No provider data", { fill: theme.subtitleColor, size: 16, weight: 500 }));
5013
- return { svg: p.join(`
5014
- `), height: 180 };
5015
- }
5016
- const totalTokens = providers.reduce((s, pv) => s + pv.totalTokens, 0);
5017
- const topProvider = providers.reduce((a, b) => a.totalTokens >= b.totalTokens ? a : b);
5018
- const topPct = totalTokens > 0 ? (topProvider.totalTokens / totalTokens * 100).toFixed(0) : "0";
5019
- p.push(svgText(PAD, 80, `${topProvider.displayName} \u2014 ${topPct}%`, {
5020
- fill: theme.narrativeColor,
5021
- size: 20,
5022
- weight: 600,
5023
- family: DISPLAY_FONT
5024
- }));
5025
- const barMaxWidth = WIDTH - PAD * 2 - 160;
5026
- for (let i = 0;i < providers.length; i++) {
5027
- const pv = providers[i];
5028
- const py = 105 + i * perRow;
5029
- const pct = totalTokens > 0 ? pv.totalTokens / totalTokens * 100 : 0;
5030
- p.push(`<rect x="${PAD}" y="${py + 4}" width="4" height="14" rx="2" fill="${escapeXml(pv.colors.primary)}"/>`);
5031
- p.push(svgText(PAD + 16, py + 15, pv.displayName, {
5032
- fill: theme.mode === "dark" ? "#e5e7eb" : "#1f2937",
5033
- size: 14,
5034
- weight: 600,
5035
- family: MONO_FONT
5036
- }));
5037
- p.push(svgText(WIDTH - PAD, py + 15, `${pct.toFixed(0)}%`, {
5038
- fill: theme.subtitleColor,
5039
- size: 13,
5135
+ let ry = y + 28;
5136
+ parts.push(sectionTag(rightX, ry, "CODING DAYS", C));
5137
+ ry += 22;
5138
+ const dowOrder = [1, 2, 3, 4, 5, 6, 0];
5139
+ const dowEntries = dowOrder.map((dayNum) => {
5140
+ const entry = dow.find((e) => e.day === dayNum);
5141
+ return { label: DAY_NAMES[dayNum] ?? "", tokens: entry?.tokens ?? 0 };
5142
+ });
5143
+ const maxDowTokens = Math.max(...dowEntries.map((e) => e.tokens), 1);
5144
+ if (dow.length > 0) {
5145
+ const peakDow = dowEntries.reduce((a, b) => b.tokens > a.tokens ? b : a);
5146
+ const peakDowFull = DAY_NAMES_FULL[dowOrder[dowEntries.indexOf(peakDow)] ?? 0] ?? "";
5147
+ parts.push(txt(rightX, ry + 14, `${peakDowFull} is your peak`, {
5148
+ fill: C.ivory,
5149
+ size: 18,
5040
5150
  weight: 700,
5041
- anchor: "end",
5042
- family: MONO_FONT
5043
- }));
5044
- const trackBg = theme.mode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.04)";
5045
- p.push(rect2(PAD + 16, py + 24, barMaxWidth, 8, trackBg, 4));
5046
- const barW = Math.max(4, pct / 100 * barMaxWidth);
5047
- p.push(rect2(PAD + 16, py + 24, barW, 8, pv.colors.primary, 4, { opacity: 0.75 }));
5048
- }
5049
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5050
- return { svg: p.join(`
5051
- `), height };
5052
- }
5053
- function renderDayOfWeekSlide(output, theme) {
5054
- const height = 340;
5055
- const p = [];
5056
- const dow = output.aggregated.dayOfWeek;
5057
- const gc = theme.sectionBgs[5] ?? ["#080c14", "#0c1018"];
5058
- p.push(sectionBg(0, height, gc, "dow-bg"));
5059
- p.push(sectionLabel(PAD, 40, "CODING DAYS", theme.subtitleColor, theme.coolAccent));
5060
- if (dow.length === 0) {
5061
- p.push(svgText(PAD, 170, "No day-of-week data", { fill: theme.subtitleColor, size: 16, weight: 500 }));
5062
- return { svg: p.join(`
5063
- `), height };
5064
- }
5065
- const peak = dow.reduce((a, b) => a.tokens >= b.tokens ? a : b);
5066
- const peakName = DAY_NAMES[peak.day] ?? "Unknown";
5067
- const maxTokens = Math.max(...dow.map((d) => d.tokens), 1);
5068
- p.push(svgText(PAD, 80, `${peakName}s are your power day`, {
5069
- fill: theme.narrativeColor,
5070
- size: 20,
5071
- weight: 600,
5072
- family: DISPLAY_FONT
5073
- }));
5074
- const chartX = PAD;
5075
- const chartY = 110;
5076
- const barAreaWidth = WIDTH - PAD * 2;
5077
- const gapSize = 12;
5078
- const barWidth = Math.floor((barAreaWidth - 6 * gapSize) / 7);
5079
- const barMaxHeight = 170;
5080
- for (let i = 0;i < 7 && i < dow.length; i++) {
5081
- const entry = dow[i];
5082
- const bx = chartX + i * (barWidth + gapSize);
5083
- const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
5084
- const barH = Math.max(4, ratio * barMaxHeight);
5085
- const by = chartY + barMaxHeight - barH;
5086
- const isPeak = entry.day === peak.day;
5087
- const barColor = isPeak ? theme.coolAccent : theme.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)";
5088
- p.push(rect2(bx, by, barWidth, barH, barColor, 4, { opacity: isPeak ? 1 : 0.7 }));
5089
- if (isPeak) {
5090
- p.push(`<line x1="${bx}" y1="${by}" x2="${bx + barWidth}" y2="${by}" stroke="${escapeXml(theme.coolAccent)}" stroke-width="3" opacity="0.8"/>`);
5091
- }
5092
- const dayLabel = DAY_SHORT[i] ?? "";
5093
- p.push(svgText(bx + barWidth / 2, chartY + barMaxHeight + 20, dayLabel, {
5094
- fill: isPeak ? theme.coolAccent : theme.subtitleColor,
5095
- size: 11,
5096
- weight: isPeak ? 700 : 500,
5097
- anchor: "middle",
5098
- family: MONO_FONT
5151
+ family: DISPLAY,
5152
+ spacing: -0.5
5099
5153
  }));
5100
- if (isPeak) {
5101
- p.push(svgText(bx + barWidth / 2, by - 10, formatNumber(entry.tokens), {
5102
- fill: theme.coolAccent,
5103
- size: 11,
5104
- weight: 700,
5154
+ ry += 30;
5155
+ const barGap = 8;
5156
+ const barW = (colW - 6 * barGap) / 7;
5157
+ const barMaxH = 100;
5158
+ for (let i = 0;i < 7; i++) {
5159
+ const entry = dowEntries[i];
5160
+ const bx = rightX + i * (barW + barGap);
5161
+ const ratio = entry.tokens / maxDowTokens;
5162
+ const barH = Math.max(3, ratio * barMaxH);
5163
+ const by = ry + barMaxH - barH;
5164
+ const isPeak = entry.tokens === maxDowTokens;
5165
+ const opacity = ratio < 0.35 ? 0.35 : ratio < 0.55 ? 0.6 : 1;
5166
+ parts.push(box(bx, by, barW, barH, C.gold, 1, { opacity }));
5167
+ parts.push(txt(bx + barW / 2, ry + barMaxH + 16, entry.label, {
5168
+ fill: isPeak ? C.gold : C.muted,
5169
+ size: 9,
5170
+ weight: isPeak ? 700 : 400,
5171
+ family: MONO,
5105
5172
  anchor: "middle",
5106
- family: MONO_FONT
5173
+ spacing: 0.5
5107
5174
  }));
5108
5175
  }
5109
- }
5110
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5111
- return { svg: p.join(`
5112
- `), height };
5113
- }
5114
- function renderTimeOfDaySlide(output, theme) {
5115
- const more = output.more;
5116
- if (!more?.hourOfDay)
5117
- return { svg: "", height: 0 };
5118
- const hourOfDay = more.hourOfDay;
5119
- const totalTokens = hourOfDay.reduce((s, e) => s + e.tokens, 0);
5120
- if (totalTokens === 0)
5121
- return { svg: "", height: 0 };
5122
- const height = 320;
5123
- const p = [];
5124
- const gc = theme.sectionBgs[6] ?? ["#0c0814", "#100c18"];
5125
- p.push(sectionBg(0, height, gc, "tod-bg"));
5126
- p.push(sectionLabel(PAD, 40, "WHEN YOU CODE", theme.subtitleColor, theme.purpleAccent));
5127
- const morning = hourOfDay.filter((e) => e.hour >= 6 && e.hour < 12).reduce((s, e) => s + e.tokens, 0);
5128
- const afternoon = hourOfDay.filter((e) => e.hour >= 12 && e.hour < 18).reduce((s, e) => s + e.tokens, 0);
5129
- const evening = hourOfDay.filter((e) => e.hour >= 18 && e.hour < 22).reduce((s, e) => s + e.tokens, 0);
5130
- const night = hourOfDay.filter((e) => e.hour >= 22 || e.hour < 6).reduce((s, e) => s + e.tokens, 0);
5131
- const periods = [
5132
- { label: "Morning", icon: "sun", tokens: morning, color: theme.warmAccent, hours: "6am\u201312pm" },
5133
- { label: "Afternoon", icon: "star", tokens: afternoon, color: theme.goldAccent, hours: "12\u20136pm" },
5134
- { label: "Evening", icon: "fire", tokens: evening, color: theme.purpleAccent, hours: "6\u201310pm" },
5135
- { label: "Night", icon: "moon", tokens: night, color: theme.coolAccent, hours: "10pm\u20136am" }
5176
+ ry += barMaxH + 28;
5177
+ }
5178
+ ry += 20;
5179
+ parts.push(sectionTag(rightX, ry, "WHEN YOU CODE", C));
5180
+ ry += 22;
5181
+ const hourOfDay = more?.hourOfDay;
5182
+ let todPeriods = [
5183
+ { label: "Morning", range: "6am-12pm", pct: 0 },
5184
+ { label: "Afternoon", range: "12-6pm", pct: 0 },
5185
+ { label: "Evening", range: "6-10pm", pct: 0 },
5186
+ { label: "Night", range: "10pm-6am", pct: 0 }
5136
5187
  ];
5137
- const dominant = periods.reduce((a, b) => a.tokens >= b.tokens ? a : b);
5138
- const dominantPct = totalTokens > 0 ? (dominant.tokens / totalTokens * 100).toFixed(0) : "0";
5139
- let narrativeText = "";
5140
- if (dominant.label === "Night")
5141
- narrativeText = `You're a night owl -- ${dominantPct}% of tokens between 10pm-6am`;
5142
- else if (dominant.label === "Evening")
5143
- narrativeText = `You're an evening coder -- ${dominantPct}% between 6-10pm`;
5144
- else if (dominant.label === "Morning")
5145
- narrativeText = `You're an early bird -- ${dominantPct}% before noon`;
5146
- else
5147
- narrativeText = `Afternoons are your peak -- ${dominantPct}% from 12-6pm`;
5148
- p.push(svgText(PAD, 80, narrativeText, {
5149
- fill: theme.narrativeColor,
5150
- size: 18,
5151
- weight: 500,
5152
- family: DISPLAY_FONT
5153
- }));
5154
- const cardGap = 16;
5155
- const cardWidth = (WIDTH - PAD * 2 - 3 * cardGap) / 4;
5156
- const cardsY = 110;
5157
- const cardH = 170;
5158
- for (let i = 0;i < periods.length; i++) {
5159
- const period = periods[i];
5160
- const cx = PAD + i * (cardWidth + cardGap);
5161
- const pct = totalTokens > 0 ? period.tokens / totalTokens * 100 : 0;
5162
- const isDominant = period.label === dominant.label;
5163
- const cardBg = theme.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)";
5164
- const borderColor = isDominant ? period.color : "transparent";
5165
- p.push(rect2(cx, cardsY, cardWidth, cardH, cardBg, 6, {
5166
- stroke: borderColor,
5167
- strokeWidth: isDominant ? 1.5 : 0,
5168
- opacity: 1
5188
+ if (hourOfDay) {
5189
+ const total = hourOfDay.reduce((s, e) => s + e.tokens, 0);
5190
+ if (total > 0) {
5191
+ const morning = hourOfDay.filter((e) => e.hour >= 6 && e.hour < 12).reduce((s, e) => s + e.tokens, 0);
5192
+ const afternoon = hourOfDay.filter((e) => e.hour >= 12 && e.hour < 18).reduce((s, e) => s + e.tokens, 0);
5193
+ const evening = hourOfDay.filter((e) => e.hour >= 18 && e.hour < 22).reduce((s, e) => s + e.tokens, 0);
5194
+ const morningPct = Math.round(morning / total * 100);
5195
+ const afternoonPct = Math.round(afternoon / total * 100);
5196
+ const eveningPct = Math.round(evening / total * 100);
5197
+ const nightPct = Math.max(0, 100 - morningPct - afternoonPct - eveningPct);
5198
+ todPeriods = [
5199
+ { label: "Morning", range: "6am-12pm", pct: morningPct },
5200
+ { label: "Afternoon", range: "12-6pm", pct: afternoonPct },
5201
+ { label: "Evening", range: "6-10pm", pct: eveningPct },
5202
+ { label: "Night", range: "10pm-6am", pct: nightPct }
5203
+ ];
5204
+ }
5205
+ }
5206
+ const peakPeriod = todPeriods.reduce((a, b) => b.pct > a.pct ? b : a);
5207
+ const todCellW = (colW - 8) / 2;
5208
+ const todCellH = 65;
5209
+ for (let i = 0;i < 4; i++) {
5210
+ const period = todPeriods[i];
5211
+ const col = i % 2;
5212
+ const row = Math.floor(i / 2);
5213
+ const cx = rightX + col * (todCellW + 8);
5214
+ const cy = ry + row * (todCellH + 8);
5215
+ const isPeak = period.label === peakPeriod.label;
5216
+ parts.push(box(cx, cy, todCellW, todCellH, C.surface2, 2, {
5217
+ stroke: isPeak ? C.borderHi : C.border,
5218
+ strokeWidth: isPeak ? 1.5 : 1
5169
5219
  }));
5170
- if (isDominant) {
5171
- p.push(`<line x1="${cx}" y1="${cardsY}" x2="${cx + cardWidth}" y2="${cardsY}" stroke="${escapeXml(period.color)}" stroke-width="2" opacity="0.8"/>`);
5220
+ if (isPeak) {
5221
+ parts.push(line(cx + 2, cy, cx + todCellW - 2, cy, C.gold, 2, 0.6));
5172
5222
  }
5173
- p.push(renderIcon(period.icon, cx + cardWidth / 2 - 12, cardsY + 20, 24, period.color));
5174
- p.push(svgText(cx + cardWidth / 2, cardsY + 65, period.label, {
5175
- fill: theme.subtitleColor,
5176
- size: 11,
5177
- weight: 600,
5178
- anchor: "middle",
5179
- family: MONO_FONT,
5180
- spacing: 1
5223
+ parts.push(txt(cx + 14, cy + 20, period.label.toUpperCase(), {
5224
+ fill: isPeak ? C.gold : C.muted,
5225
+ size: 8,
5226
+ weight: 400,
5227
+ family: MONO,
5228
+ spacing: 1.5
5181
5229
  }));
5182
- p.push(svgText(cx + cardWidth / 2, cardsY + 110, `${pct.toFixed(0)}%`, {
5183
- fill: period.color,
5184
- size: 36,
5230
+ parts.push(txt(cx + 14, cy + 44, `${period.pct}%`, {
5231
+ fill: isPeak ? C.gold : C.ivory,
5232
+ size: 22,
5185
5233
  weight: 800,
5186
- anchor: "middle",
5187
- family: MONO_FONT
5188
- }));
5189
- p.push(svgText(cx + cardWidth / 2, cardsY + 135, formatNumber(period.tokens), {
5190
- fill: theme.subtitleColor,
5191
- size: 11,
5192
- weight: 400,
5193
- anchor: "middle",
5194
- family: MONO_FONT
5234
+ family: DISPLAY,
5235
+ spacing: -0.5
5195
5236
  }));
5196
- p.push(svgText(cx + cardWidth / 2, cardsY + 155, period.hours, {
5197
- fill: theme.subtitleColor,
5198
- size: 10,
5237
+ parts.push(txt(cx + todCellW - 12, cy + 44, period.range, {
5238
+ fill: C.muted,
5239
+ size: 9,
5199
5240
  weight: 400,
5200
- anchor: "middle",
5201
- family: MONO_FONT,
5202
- opacity: 0.5
5241
+ family: MONO,
5242
+ anchor: "end"
5203
5243
  }));
5204
5244
  }
5205
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5206
- return { svg: p.join(`
5207
- `), height };
5208
- }
5209
- function renderCacheSlide(output, theme) {
5210
- const height = 280;
5211
- const p = [];
5212
- const stats = output.aggregated;
5213
- const gc = theme.sectionBgs[7] ?? ["#080e0c", "#0c1210"];
5214
- p.push(sectionBg(0, height, gc, "cache-bg"));
5215
- p.push(sectionLabel(PAD, 40, "CACHE EFFICIENCY", theme.subtitleColor, theme.greenAccent));
5245
+ y += midH;
5246
+ const bottomH = 260;
5247
+ parts.push(box(0, y, WIDTH, bottomH, C.surface));
5248
+ parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
5249
+ const col3W = (INNER - 32) / 3;
5250
+ const c1x = PAD;
5251
+ let c1y = y + 24;
5252
+ parts.push(sectionTag(c1x, c1y, "STREAK", C));
5253
+ c1y += 20;
5254
+ parts.push(txt(c1x, c1y + 42, `${stats.longestStreak}`, {
5255
+ fill: C.gold,
5256
+ size: 60,
5257
+ weight: 800,
5258
+ family: DISPLAY,
5259
+ spacing: -2
5260
+ }));
5261
+ parts.push(txt(c1x, c1y + 62, "DAY LONGEST STREAK", {
5262
+ fill: C.muted,
5263
+ size: 9,
5264
+ weight: 400,
5265
+ family: MONO,
5266
+ spacing: 2
5267
+ }));
5268
+ parts.push(txt(c1x, c1y + 86, `Current: ${stats.currentStreak} days`, {
5269
+ fill: C.muted,
5270
+ size: 11,
5271
+ weight: 400,
5272
+ family: MONO,
5273
+ spacing: 0.5
5274
+ }));
5275
+ c1y += 102;
5276
+ const activeDates = new Set;
5277
+ for (const prov of providers) {
5278
+ for (const d of prov.daily) {
5279
+ if (d.totalTokens > 0)
5280
+ activeDates.add(d.date);
5281
+ }
5282
+ }
5283
+ const dotSize = 10;
5284
+ const dotGap = 4;
5285
+ const dotsPerRow = Math.floor(col3W / (dotSize + dotGap));
5286
+ for (let i = 0;i < 30; i++) {
5287
+ const dDate = new Date(new Date(until + "T00:00:00Z").getTime() - (29 - i) * 86400000);
5288
+ const dateStr = dDate.toISOString().slice(0, 10);
5289
+ const isActive = activeDates.has(dateStr);
5290
+ const isCurrentStreak = i >= 30 - stats.currentStreak;
5291
+ const col = i % dotsPerRow;
5292
+ const row = Math.floor(i / dotsPerRow);
5293
+ const dx = c1x + col * (dotSize + dotGap);
5294
+ const dy = c1y + row * (dotSize + dotGap);
5295
+ let dotColor = C.dotInactive;
5296
+ let dotBorder = C.dotInactiveBorder;
5297
+ if (isCurrentStreak) {
5298
+ dotColor = C.gold;
5299
+ dotBorder = C.gold;
5300
+ } else if (isActive) {
5301
+ dotColor = C.goldDim;
5302
+ dotBorder = C.goldDim;
5303
+ }
5304
+ parts.push(box(dx, dy, dotSize, dotSize, dotColor, 2, { stroke: dotBorder, strokeWidth: 1 }));
5305
+ }
5306
+ const c2x = PAD + col3W + 16;
5307
+ let c2y = y + 24;
5308
+ parts.push(sectionTag(c2x, c2y, "CACHE", C));
5309
+ c2y += 20;
5216
5310
  const hitRate = stats.cacheHitRate;
5217
- const hitPct = (hitRate * 100).toFixed(1);
5218
- const gaugeCx = WIDTH - 220;
5219
- const gaugeCy = 160;
5220
- const gaugeR = 80;
5221
- const gaugeWidth = 14;
5222
- const trackColor = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
5223
- const bgArc = describeArc(gaugeCx, gaugeCy, gaugeR, -90, 270);
5224
- p.push(`<path d="${bgArc}" fill="none" stroke="${trackColor}" stroke-width="${gaugeWidth}" stroke-linecap="round"/>`);
5311
+ const hitPct = Math.round(hitRate * 100);
5312
+ const ringCx = c2x + col3W / 2;
5313
+ const ringCy = c2y + 68;
5314
+ const ringR = 48;
5315
+ const ringStroke = 10;
5316
+ parts.push(`<circle cx="${ringCx}" cy="${ringCy}" r="${ringR}" fill="none" stroke="${C.trackStroke}" stroke-width="${ringStroke}"/>`);
5225
5317
  if (hitRate > 0) {
5226
- const sweepAngle = Math.min(hitRate * 360, 359);
5227
- const fillArc = describeArc(gaugeCx, gaugeCy, gaugeR, -90, -90 + sweepAngle);
5228
- p.push(`<path d="${fillArc}" fill="none" stroke="${escapeXml(theme.greenAccent)}" stroke-width="${gaugeWidth}" stroke-linecap="round"/>`);
5318
+ const sweep = Math.min(hitRate * 360, 359);
5319
+ const arc = describeArc(ringCx, ringCy, ringR, 0, sweep);
5320
+ parts.push(`<path d="${arc}" fill="none" stroke="${C.gold}" stroke-width="${ringStroke}" stroke-linecap="butt"/>`);
5229
5321
  }
5230
- p.push(svgText(gaugeCx, gaugeCy + 8, `${hitPct}%`, {
5231
- fill: theme.greenAccent,
5322
+ parts.push(txt(ringCx, ringCy + 5, `${hitPct}%`, {
5323
+ fill: C.gold,
5232
5324
  size: 28,
5233
5325
  weight: 800,
5234
- anchor: "middle",
5235
- family: MONO_FONT
5326
+ family: DISPLAY,
5327
+ anchor: "middle"
5236
5328
  }));
5237
- p.push(svgText(gaugeCx, gaugeCy + 28, "hit rate", {
5238
- fill: theme.subtitleColor,
5239
- size: 11,
5240
- weight: 500,
5329
+ parts.push(txt(ringCx, ringCy + 20, "HIT RATE", {
5330
+ fill: C.muted,
5331
+ size: 8,
5332
+ weight: 400,
5333
+ family: MONO,
5241
5334
  anchor: "middle",
5242
- family: MONO_FONT
5335
+ spacing: 1.5
5243
5336
  }));
5244
- p.push(svgText(PAD, 100, `${hitPct}% Cache Hit Rate`, {
5245
- fill: theme.narrativeColor,
5246
- size: 22,
5247
- weight: 600,
5248
- family: DISPLAY_FONT
5249
- }));
5250
- const cacheEcon = output.more?.cacheEconomics;
5337
+ c2y += 136;
5338
+ const cacheEcon = more?.cacheEconomics;
5251
5339
  if (cacheEcon) {
5252
- const statItems = [
5253
- { label: "Cache Reads", value: formatNumber(cacheEcon.readTokens) },
5254
- { label: "Cache Writes", value: formatNumber(cacheEcon.writeTokens) }
5340
+ const cacheStatItems = [
5341
+ { label: "READS", value: formatNumber(cacheEcon.readTokens), highlight: false },
5342
+ { label: "WRITES", value: formatNumber(cacheEcon.writeTokens), highlight: false }
5255
5343
  ];
5256
5344
  if (cacheEcon.reuseRatio !== null && Number.isFinite(cacheEcon.reuseRatio)) {
5257
- statItems.push({ label: "Reuse Ratio", value: `${cacheEcon.reuseRatio.toFixed(1)}x` });
5258
- }
5259
- for (let i = 0;i < statItems.length; i++) {
5260
- const item = statItems[i];
5261
- const iy = 135 + i * 34;
5262
- p.push(svgText(PAD, iy, item.value, {
5263
- fill: theme.mode === "dark" ? "#e5e7eb" : "#1f2937",
5264
- size: 18,
5265
- weight: 700,
5266
- family: MONO_FONT
5345
+ cacheStatItems.push({ label: "REUSE", value: `${cacheEcon.reuseRatio.toFixed(1)}x`, highlight: true });
5346
+ }
5347
+ const cacheCardW = (col3W - (cacheStatItems.length - 1) * 6) / cacheStatItems.length;
5348
+ const cacheCardH = 52;
5349
+ for (let i = 0;i < cacheStatItems.length; i++) {
5350
+ const item = cacheStatItems[i];
5351
+ const ix = c2x + i * (cacheCardW + 6);
5352
+ const iy = c2y;
5353
+ parts.push(box(ix, iy, cacheCardW, cacheCardH, C.surface2, 2, {
5354
+ stroke: item.highlight ? C.borderHi : C.border,
5355
+ strokeWidth: 1
5267
5356
  }));
5268
- p.push(svgText(PAD + 140, iy, item.label, {
5269
- fill: theme.subtitleColor,
5270
- size: 12,
5271
- weight: 500,
5272
- family: MONO_FONT
5357
+ parts.push(txt(ix + cacheCardW / 2, iy + 16, item.label, {
5358
+ fill: C.muted,
5359
+ size: 8,
5360
+ weight: 400,
5361
+ family: MONO,
5362
+ anchor: "middle",
5363
+ spacing: 1.5
5364
+ }));
5365
+ parts.push(txt(ix + cacheCardW / 2, iy + 38, item.value, {
5366
+ fill: item.highlight ? C.gold : C.ivory,
5367
+ size: 16,
5368
+ weight: 700,
5369
+ family: DISPLAY,
5370
+ anchor: "middle",
5371
+ spacing: -0.5
5273
5372
  }));
5274
5373
  }
5275
5374
  }
5276
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5277
- return { svg: p.join(`
5278
- `), height };
5279
- }
5280
- function renderPeakDaySlide(output, theme) {
5281
- const height = 240;
5282
- const p = [];
5283
- const stats = output.aggregated;
5284
- const gc = theme.sectionBgs[8] ?? ["#0c0a06", "#100e0a"];
5285
- p.push(sectionBg(0, height, gc, "peak-bg"));
5286
- p.push(sectionLabel(PAD, 40, "PEAK DAY SPOTLIGHT", theme.subtitleColor, theme.goldAccent));
5287
- if (!stats.peakDay) {
5288
- p.push(svgText(PAD, 130, "No usage data recorded yet", {
5289
- fill: theme.subtitleColor,
5290
- size: 16,
5291
- weight: 500
5292
- }));
5293
- return { svg: p.join(`
5294
- `), height };
5295
- }
5296
- const formattedDate = formatDateLong(stats.peakDay.date);
5297
- p.push(svgText(PAD, 85, `${formattedDate} was your biggest day`, {
5298
- fill: theme.narrativeColor,
5299
- size: 20,
5300
- weight: 500,
5301
- family: DISPLAY_FONT
5302
- }));
5303
- p.push(svgText(PAD, 160, formatNumber(stats.peakDay.tokens), {
5304
- fill: theme.goldAccent,
5305
- size: 72,
5306
- weight: 800,
5307
- family: MONO_FONT,
5308
- spacing: -3
5309
- }));
5310
- p.push(svgText(PAD, 190, "tokens in a single day", {
5311
- fill: theme.subtitleColor,
5312
- size: 14,
5313
- weight: 500
5314
- }));
5315
- const badgeCx = WIDTH - 180;
5316
- const badgeCy = 140;
5317
- p.push(rect2(badgeCx - 40, badgeCy - 36, 80, 72, theme.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)", 6));
5318
- p.push(cornerMark(badgeCx - 40, badgeCy - 36, 12, theme.goldAccent, "tl"));
5319
- p.push(cornerMark(badgeCx + 40, badgeCy + 36, 12, theme.goldAccent, "br"));
5320
- p.push(svgIconTrophy(badgeCx - 20, badgeCy - 20, 40, theme.goldAccent));
5321
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5322
- return { svg: p.join(`
5323
- `), height };
5324
- }
5325
- function renderAchievementsSlide(output, theme) {
5326
- const achievements = computeAchievements(output);
5327
- const rows = Math.ceil(achievements.length / 2);
5328
- const rowHeight = 80;
5329
- const height = Math.max(200, 90 + rows * rowHeight + 20);
5330
- const p = [];
5331
- const gc = theme.sectionBgs[9] ?? ["#08080c", "#0c0c14"];
5332
- p.push(sectionBg(0, height, gc, "achieve-bg"));
5333
- p.push(sectionLabel(PAD, 40, "ACHIEVEMENTS UNLOCKED", theme.subtitleColor, theme.purpleAccent));
5334
- if (achievements.length === 0) {
5335
- p.push(svgText(PAD, 120, "Keep coding to unlock achievements!", {
5336
- fill: theme.subtitleColor,
5337
- size: 16,
5338
- weight: 500
5339
- }));
5340
- return { svg: p.join(`
5341
- `), height: 200 };
5342
- }
5343
- const colWidth = (WIDTH - PAD * 2 - 20) / 2;
5344
- for (let i = 0;i < achievements.length; i++) {
5345
- const a = achievements[i];
5346
- const col = i % 2;
5347
- const row = Math.floor(i / 2);
5348
- const ax = PAD + col * (colWidth + 20);
5349
- const ay = 70 + row * rowHeight;
5350
- const cardBg = theme.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)";
5351
- p.push(rect2(ax, ay, colWidth, 66, cardBg, 6));
5352
- p.push(`<rect x="${ax}" y="${ay}" width="3" height="66" rx="1.5" fill="${escapeXml(a.color)}" opacity="0.7"/>`);
5353
- p.push(renderIcon(a.icon, ax + 16, ay + 17, 32, a.color));
5354
- p.push(svgText(ax + 60, ay + 28, a.title, {
5355
- fill: theme.mode === "dark" ? "#f3f4f6" : "#1f2937",
5356
- size: 15,
5357
- weight: 700,
5358
- family: DISPLAY_FONT
5375
+ const c3x = PAD + 2 * (col3W + 16);
5376
+ let c3y = y + 24;
5377
+ parts.push(sectionTag(c3x, c3y, "PEAK DAY", C));
5378
+ c3y += 20;
5379
+ if (stats.peakDay) {
5380
+ parts.push(box(c3x, c3y, col3W, 130, C.surface2, 2, { stroke: C.borderHi, strokeWidth: 1 }));
5381
+ parts.push(txt(c3x + col3W / 2, c3y + 24, formatDateLong(stats.peakDay.date).toUpperCase(), {
5382
+ fill: C.muted,
5383
+ size: 9,
5384
+ weight: 400,
5385
+ family: MONO,
5386
+ anchor: "middle",
5387
+ spacing: 2.5
5359
5388
  }));
5360
- p.push(svgText(ax + 60, ay + 48, a.subtitle, {
5361
- fill: theme.subtitleColor,
5362
- size: 12,
5363
- weight: 500,
5364
- family: MONO_FONT
5389
+ parts.push(txt(c3x + col3W / 2, c3y + 72, formatNumber(stats.peakDay.tokens), {
5390
+ fill: C.gold,
5391
+ size: 46,
5392
+ weight: 800,
5393
+ family: DISPLAY,
5394
+ anchor: "middle",
5395
+ spacing: -2
5365
5396
  }));
5366
- }
5367
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5368
- return { svg: p.join(`
5369
- `), height };
5370
- }
5371
- function renderMonthlyBurnSlide(output, theme) {
5372
- const height = 260;
5373
- const p = [];
5374
- const gc = theme.sectionBgs[10] ?? ["#0a0c10", "#0e1014"];
5375
- p.push(sectionBg(0, height, gc, "burn-bg"));
5376
- p.push(sectionLabel(PAD, 40, "MONTHLY PROJECTION", theme.subtitleColor, theme.coolAccent));
5377
- const burn = output.more?.monthlyBurn;
5378
- if (!burn) {
5379
- const avgDailyCost = output.aggregated.averageDailyCost;
5380
- const projected = avgDailyCost * 30;
5381
- p.push(svgText(PAD, 90, "At this rate, you will spend about", {
5382
- fill: theme.narrativeColor,
5383
- size: 18,
5384
- weight: 500,
5385
- family: DISPLAY_FONT
5397
+ parts.push(txt(c3x + col3W / 2, c3y + 94, "TOKENS IN ONE DAY", {
5398
+ fill: C.muted,
5399
+ size: 9,
5400
+ weight: 400,
5401
+ family: MONO,
5402
+ anchor: "middle",
5403
+ spacing: 2
5386
5404
  }));
5387
- p.push(svgText(PAD, 160, formatCost2(projected), {
5388
- fill: theme.coolAccent,
5389
- size: 64,
5390
- weight: 800,
5391
- family: MONO_FONT,
5392
- spacing: -3
5405
+ const multiplier = stats.averageDailyTokens > 0 ? (stats.peakDay.tokens / stats.averageDailyTokens).toFixed(1) : "0";
5406
+ parts.push(txt(c3x + col3W / 2, c3y + 118, `${multiplier}x your daily average`, {
5407
+ fill: C.muted,
5408
+ size: 11,
5409
+ weight: 400,
5410
+ family: BODY,
5411
+ anchor: "middle"
5393
5412
  }));
5394
- p.push(svgText(PAD, 190, "per month", {
5395
- fill: theme.subtitleColor,
5413
+ } else {
5414
+ parts.push(txt(c3x, c3y + 60, "No peak day data", {
5415
+ fill: C.muted,
5396
5416
  size: 14,
5397
- weight: 500
5417
+ weight: 400,
5418
+ family: BODY
5398
5419
  }));
5399
- return { svg: p.join(`
5400
- `), height };
5401
5420
  }
5402
- p.push(svgText(PAD, 90, "At this rate, you will spend", {
5403
- fill: theme.narrativeColor,
5404
- size: 18,
5405
- weight: 500,
5406
- family: DISPLAY_FONT
5421
+ c3y += 148;
5422
+ const projectedCost = more?.monthlyBurn?.projectedCost ?? stats.averageDailyCost * 30;
5423
+ parts.push(txt(c3x, c3y, "PROJECTED / MONTH", {
5424
+ fill: C.muted,
5425
+ size: 9,
5426
+ weight: 400,
5427
+ family: MONO,
5428
+ spacing: 2
5407
5429
  }));
5408
- p.push(svgText(PAD, 155, formatCost2(burn.projectedCost), {
5409
- fill: theme.coolAccent,
5410
- size: 64,
5430
+ parts.push(txt(c3x, c3y + 28, formatCost2(projectedCost), {
5431
+ fill: C.ivory,
5432
+ size: 30,
5411
5433
  weight: 800,
5412
- family: MONO_FONT,
5413
- spacing: -3
5414
- }));
5415
- p.push(svgText(PAD, 185, "this month", {
5416
- fill: theme.subtitleColor,
5417
- size: 14,
5418
- weight: 500
5434
+ family: DISPLAY,
5435
+ spacing: -1
5419
5436
  }));
5420
- const barY = 210;
5421
- const barWidth = WIDTH - PAD * 2;
5422
- const barHeight = 10;
5423
- const progress = burn.calendarDays > 0 ? burn.observedDays / burn.calendarDays : 0;
5424
- const trackBg = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
5425
- p.push(rect2(PAD, barY, barWidth, barHeight, trackBg, 5));
5426
- p.push(rect2(PAD, barY, Math.max(6, progress * barWidth), barHeight, theme.coolAccent, 5, { opacity: 0.7 }));
5427
- p.push(svgText(PAD, barY + 30, `Based on ${burn.observedDays} of ${burn.calendarDays} days`, {
5428
- fill: theme.subtitleColor,
5437
+ const avgDailyCostStr = stats.averageDailyCost >= 1 ? `$${stats.averageDailyCost.toFixed(2)}` : `$${stats.averageDailyCost.toFixed(4)}`;
5438
+ parts.push(txt(c3x, c3y + 44, `${avgDailyCostStr} avg/day`, {
5439
+ fill: C.muted,
5429
5440
  size: 11,
5430
5441
  weight: 400,
5431
- family: MONO_FONT
5432
- }));
5433
- p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
5434
- return { svg: p.join(`
5435
- `), height };
5436
- }
5437
- function renderFooterSlide(_output, theme) {
5438
- const height = 100;
5439
- const p = [];
5440
- const gc = theme.sectionBgs[11] ?? ["#060608", "#060608"];
5441
- p.push(sectionBg(0, height, gc, "footer-bg"));
5442
- p.push(cornerMark(PAD - 16, height - 16, 16, theme.heroAccent, "bl"));
5443
- p.push(svgText(PAD, 40, "Generated by tokenleak", {
5444
- fill: theme.subtitleColor,
5445
- size: 12,
5446
- weight: 500,
5447
- family: MONO_FONT
5442
+ family: MONO,
5443
+ spacing: 0.5
5444
+ }));
5445
+ y += bottomH;
5446
+ const achH = 180;
5447
+ parts.push(box(0, y, WIDTH, achH, C.bg));
5448
+ parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
5449
+ const ALL_BADGES = [
5450
+ { title: "Streak Master", sub: ">30d streak" },
5451
+ { title: "Night Owl", sub: ">40% night" },
5452
+ { title: "Big Spender", sub: ">$100 total" },
5453
+ { title: "Cache Master", sub: ">50% hit rate" },
5454
+ { title: "Daily Driver", sub: ">80% active" },
5455
+ { title: "Power User", sub: ">10k avg/day" },
5456
+ { title: "Summit Day", sub: "Peak >50k" },
5457
+ { title: "Multi-Tool", sub: "3+ providers" },
5458
+ { title: "Early Bird", sub: ">40% morning" },
5459
+ { title: "Model Hopper", sub: "4+ models" }
5460
+ ];
5461
+ const badgeTitleSet = new Set(ALL_BADGES.map((b) => b.title));
5462
+ const earnedTitles = new Set(achievements.filter((a) => badgeTitleSet.has(a.title)).map((a) => a.title));
5463
+ const earnedCount = earnedTitles.size;
5464
+ parts.push(sectionTag(PAD, y + 24, `ACHIEVEMENTS \xB7 ${earnedCount} UNLOCKED`, C));
5465
+ const badgeCols = 5;
5466
+ const badgeGapX = 10;
5467
+ const badgeGapY = 8;
5468
+ const badgeW = (INNER - (badgeCols - 1) * badgeGapX) / badgeCols;
5469
+ const badgeH = 54;
5470
+ const badgeStartY = y + 40;
5471
+ for (let i = 0;i < ALL_BADGES.length; i++) {
5472
+ const badge = ALL_BADGES[i];
5473
+ const isEarned = earnedTitles.has(badge.title);
5474
+ const col = i % badgeCols;
5475
+ const row = Math.floor(i / badgeCols);
5476
+ const bx = PAD + col * (badgeW + badgeGapX);
5477
+ const by = badgeStartY + row * (badgeH + badgeGapY);
5478
+ parts.push(`<g opacity="${isEarned ? 1 : 0.2}">`);
5479
+ parts.push(box(bx, by, badgeW, badgeH, C.surface2, 2, {
5480
+ stroke: isEarned ? C.borderHi : C.border,
5481
+ strokeWidth: 1
5482
+ }));
5483
+ parts.push(txt(bx + badgeW / 2, by + 24, badge.title, {
5484
+ fill: isEarned ? C.ivory : C.muted,
5485
+ size: 11,
5486
+ weight: 600,
5487
+ family: BODY,
5488
+ anchor: "middle"
5489
+ }));
5490
+ parts.push(txt(bx + badgeW / 2, by + 42, badge.sub, {
5491
+ fill: C.muted,
5492
+ size: 9,
5493
+ weight: 400,
5494
+ family: MONO,
5495
+ anchor: "middle",
5496
+ spacing: 0.5
5497
+ }));
5498
+ parts.push("</g>");
5499
+ }
5500
+ y += achH;
5501
+ const footH = 44;
5502
+ parts.push(box(0, y, WIDTH, footH, C.bg));
5503
+ parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
5504
+ const generatedTs = output.generated ? output.generated.replace("T", " ").slice(0, 19) + " UTC" : new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
5505
+ parts.push(txt(PAD, y + 28, `Generated ${generatedTs}`, {
5506
+ fill: C.muted2,
5507
+ size: 9,
5508
+ weight: 400,
5509
+ family: MONO,
5510
+ spacing: 1
5448
5511
  }));
5449
- const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
5450
- p.push(svgText(PAD, 60, now, {
5451
- fill: theme.subtitleColor,
5512
+ parts.push(txt(WIDTH - PAD, y + 28, "tokenleak.devaa.dev", {
5513
+ fill: C.gold,
5452
5514
  size: 11,
5453
- weight: 400,
5454
- family: MONO_FONT,
5455
- opacity: 0.5
5456
- }));
5457
- p.push(svgText(WIDTH - PAD, 50, "tokenleak", {
5458
- fill: theme.heroAccent,
5459
- size: 16,
5460
5515
  weight: 700,
5516
+ family: MONO,
5461
5517
  anchor: "end",
5462
- family: MONO_FONT,
5463
- opacity: 0.4
5518
+ opacity: 0.5,
5519
+ spacing: 0.5
5464
5520
  }));
5465
- return { svg: p.join(`
5466
- `), height };
5467
- }
5468
- function renderWrappedSlidesSvg(output, options) {
5469
- const theme = getWrappedTheme(options.theme);
5470
- const slides = [
5471
- renderTitleSlide(output, theme),
5472
- renderBigNumbersSlide(output, theme),
5473
- renderStreakSlide(output, theme),
5474
- renderTopModelSlide(output, theme),
5475
- renderProviderMixSlide(output, theme),
5476
- renderDayOfWeekSlide(output, theme),
5477
- renderTimeOfDaySlide(output, theme),
5478
- renderCacheSlide(output, theme),
5479
- renderPeakDaySlide(output, theme),
5480
- renderAchievementsSlide(output, theme),
5481
- renderMonthlyBurnSlide(output, theme),
5482
- renderFooterSlide(output, theme)
5483
- ].filter((s) => s.height > 0);
5484
- let totalHeight = 0;
5485
- const stackedSections = [];
5486
- for (const slide of slides) {
5487
- stackedSections.push(`<g transform="translate(0, ${totalHeight})">`, slide.svg, "</g>");
5488
- totalHeight += slide.height;
5489
- }
5490
- const bgColor = theme.mode === "dark" ? "#060608" : "#fafaf9";
5521
+ y += footH;
5522
+ const totalHeight = y;
5523
+ const noiseOpacity = isDark ? 0.03 : 0.02;
5491
5524
  return [
5492
- `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${totalHeight}" viewBox="0 0 ${WIDTH} ${totalHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility" color-rendering="optimizeQuality">`,
5493
- `<rect width="${WIDTH}" height="${totalHeight}" fill="${escapeXml(bgColor)}"/>`,
5494
- globalDefs(theme.mode === "dark"),
5495
- ...stackedSections,
5525
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${totalHeight}" viewBox="0 0 ${WIDTH} ${totalHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility">`,
5526
+ `<rect width="${WIDTH}" height="${totalHeight}" fill="${C.bg}"/>`,
5527
+ noiseDef(isDark),
5528
+ `<rect width="${WIDTH}" height="${totalHeight}" fill="transparent" filter="url(#grain)" opacity="${noiseOpacity}" pointer-events="none"/>`,
5529
+ ...parts,
5496
5530
  "</svg>"
5497
5531
  ].join(`
5498
5532
  `);
@@ -5519,7 +5553,7 @@ class PngRenderer {
5519
5553
  import sharp2 from "sharp";
5520
5554
  var PNG_DENSITY2 = 216;
5521
5555
  async function renderWrappedPng(output, options) {
5522
- const svgString = renderWrappedSlidesSvg(output, options);
5556
+ const svgString = renderWrappedSinglePageSvg(output, { theme: options.theme });
5523
5557
  const pngBuffer = await sharp2(Buffer.from(svgString), {
5524
5558
  density: PNG_DENSITY2
5525
5559
  }).png({
@@ -5687,7 +5721,7 @@ function boxedHeader(title, width, noColor2) {
5687
5721
  function renderSummaryParts(parts, width, noColor2) {
5688
5722
  const left = parts.filter((_, index) => index % 2 === 0).map((part) => colorize256(part, SEMANTIC.INPUT, noColor2));
5689
5723
  const right = parts.filter((_, index) => index % 2 === 1).map((part) => colorize256(part, SEMANTIC.OUTPUT, noColor2));
5690
- return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((line) => ` ${line}`).join(`
5724
+ return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((line2) => ` ${line2}`).join(`
5691
5725
  `);
5692
5726
  }
5693
5727
  function renderCompactDashboard(model, options) {
@@ -5723,7 +5757,7 @@ function renderCompactDashboard(model, options) {
5723
5757
  } else if (model.activeProviders.length === 0) {
5724
5758
  lines.push(" No provider activity in the selected range.");
5725
5759
  }
5726
- return lines.filter((line, index, array) => !(line === "" && array[index - 1] === "")).join(`
5760
+ return lines.filter((line2, index, array) => !(line2 === "" && array[index - 1] === "")).join(`
5727
5761
  `);
5728
5762
  }
5729
5763
 
@@ -6139,10 +6173,10 @@ function buildMonthHeader(model, visibleStartWeek, displayWeekCount, mode) {
6139
6173
  nextFreeIndex = startIndex + marker.label.length + 1;
6140
6174
  placedLabels += 1;
6141
6175
  }
6142
- const line = header.some((cell) => cell !== " ") ? `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}` : null;
6176
+ const line2 = header.some((cell) => cell !== " ") ? `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}` : null;
6143
6177
  const uniqueVisibleMonths = visibleMarkers.map((marker) => `${marker.label} ${String(marker.year)}`).filter((value, index, values) => values.indexOf(value) === index);
6144
6178
  const caption = placedLabels === 0 && uniqueVisibleMonths.length === 1 ? `${" ".repeat(DAY_LABEL_WIDTH2)}${uniqueVisibleMonths[0]}` : null;
6145
- return { caption, line };
6179
+ return { caption, line: line2 };
6146
6180
  }
6147
6181
  function buildIntensityLegend(family, mode, noColor2) {
6148
6182
  const cellWidth = getCellWidth(mode);
@@ -6191,12 +6225,12 @@ function renderTerminalHeatmap(daily, options) {
6191
6225
  const maxWeeks = Math.max(1, Math.floor(availableColumns / weekColWidth));
6192
6226
  const displayWeeks = model.weeks.slice(Math.max(0, model.weeks.length - maxWeeks));
6193
6227
  const visibleStartWeek = model.weeks.length - displayWeeks.length;
6194
- const { caption, line } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length, mode);
6228
+ const { caption, line: line2 } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length, mode);
6195
6229
  const lines = [];
6196
6230
  if (caption)
6197
6231
  lines.push(caption);
6198
- if (line)
6199
- lines.push(line);
6232
+ if (line2)
6233
+ lines.push(line2);
6200
6234
  const gap = getGap(mode);
6201
6235
  for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
6202
6236
  const label = LABELED_DAYS[dayIndex] ?? "";
@@ -6269,8 +6303,8 @@ function renderSummary(parts, width, noColor2) {
6269
6303
  if (parts.length === 0)
6270
6304
  return [];
6271
6305
  const colored = parts.map((part, index) => colorize256(part, index % 2 === 0 ? SEMANTIC.INPUT : SEMANTIC.OUTPUT, noColor2));
6272
- const line = colored.join(dim(" \xB7 ", noColor2));
6273
- return [truncateVisible(` ${line}`, width)];
6306
+ const line2 = colored.join(dim(" \xB7 ", noColor2));
6307
+ return [truncateVisible(` ${line2}`, width)];
6274
6308
  }
6275
6309
  function renderTrend(trend, width, noColor2) {
6276
6310
  if (!trend)
@@ -6312,8 +6346,8 @@ function renderPatternList(title, entries, width, noColor2) {
6312
6346
  const fillLength = Math.max(1, Math.round(entry.share * barWidth));
6313
6347
  const fill = colorize256(BAR_CHAR.repeat(fillLength), SEMANTIC.OUTPUT, noColor2);
6314
6348
  const track = TRACK_CHAR.repeat(Math.max(0, barWidth - fillLength));
6315
- const line = ` ${colorize256(truncateVisible(entry.label, nameWidth).padEnd(nameWidth), SEMANTIC.ACCENT, noColor2)} ${fill}${track} ${entry.value.padStart(valueWidth)}`;
6316
- lines.push(truncateVisible(line, width));
6349
+ const line2 = ` ${colorize256(truncateVisible(entry.label, nameWidth).padEnd(nameWidth), SEMANTIC.ACCENT, noColor2)} ${fill}${track} ${entry.value.padStart(valueWidth)}`;
6350
+ lines.push(truncateVisible(line2, width));
6317
6351
  }
6318
6352
  return lines;
6319
6353
  }
@@ -6321,7 +6355,7 @@ function renderPatternColumns(provider, width, noColor2) {
6321
6355
  const dayLines = renderPatternList("Day of Week", provider.dayOfWeek, Math.floor((width - 3) / 2), noColor2);
6322
6356
  const modelLines = renderPatternList("Top Models", provider.topModels, Math.floor((width - 3) / 2), noColor2);
6323
6357
  if (dayLines.length > 0 && modelLines.length > 0 && width >= 96) {
6324
- return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((line) => ` ${line}`);
6358
+ return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((line2) => ` ${line2}`);
6325
6359
  }
6326
6360
  return [...dayLines, ...dayLines.length > 0 && modelLines.length > 0 ? [""] : [], ...modelLines];
6327
6361
  }
@@ -7298,8 +7332,8 @@ function renderRecommendation(rec, width, noColor2) {
7298
7332
  const title = bold256(`${icon} ${rec.title}`, COLOR_TITLE, noColor2);
7299
7333
  lines.push(`${indent}${title}`);
7300
7334
  const descLines = wrapText(rec.description, contentWidth - 2);
7301
- for (const line of descLines) {
7302
- lines.push(`${indent} ${dim(line, noColor2)}`);
7335
+ for (const line2 of descLines) {
7336
+ lines.push(`${indent} ${dim(line2, noColor2)}`);
7303
7337
  }
7304
7338
  if (rec.currentCost > 0 || rec.projectedCost > 0) {
7305
7339
  const current = bold2(`${formatDollars(rec.currentCost)}/mo`, noColor2);
@@ -7850,7 +7884,7 @@ function guessProvider(model) {
7850
7884
  return "Google";
7851
7885
  return "Other";
7852
7886
  }
7853
- var PROVIDER_COLORS = {
7887
+ var PROVIDER_COLORS2 = {
7854
7888
  anthropic: "#d4af5f",
7855
7889
  "claude-code": "#d4af5f",
7856
7890
  openai: "#3a5070",
@@ -7858,8 +7892,8 @@ var PROVIDER_COLORS = {
7858
7892
  google: "#6a2535",
7859
7893
  pi: "#5a4a70"
7860
7894
  };
7861
- function getProviderColor(provider) {
7862
- return PROVIDER_COLORS[provider.toLowerCase()] ?? "#555555";
7895
+ function getProviderColor2(provider) {
7896
+ return PROVIDER_COLORS2[provider.toLowerCase()] ?? "#555555";
7863
7897
  }
7864
7898
  function costValue(cost) {
7865
7899
  const raw = formatCost2(cost);
@@ -7939,7 +7973,7 @@ function generateWrappedLiveHtml(output) {
7939
7973
  const providerMix = providers.map((p) => ({
7940
7974
  name: p.displayName,
7941
7975
  pct: totalProviderTokens > 0 ? Math.round(p.totalTokens / totalProviderTokens * 100) : 0,
7942
- color: getProviderColor(p.provider)
7976
+ color: getProviderColor2(p.provider)
7943
7977
  })).sort((a, b) => b.pct - a.pct);
7944
7978
  const pctSum = providerMix.reduce((s, p) => s + p.pct, 0);
7945
7979
  if (providerMix.length > 0 && pctSum !== 100) {
@@ -8523,14 +8557,14 @@ async function startWrappedLiveServer(output, options = {}) {
8523
8557
  throw new Error(`Could not find a free port after ${maxAttempts} attempts starting from ${startPort}`);
8524
8558
  }
8525
8559
  // packages/cli/src/config.ts
8526
- import { readFileSync as readFileSync2 } from "fs";
8527
- import { join as join5 } from "path";
8528
- import { homedir as homedir5 } from "os";
8560
+ import { readFileSync as readFileSync3 } from "fs";
8561
+ import { join as join6 } from "path";
8562
+ import { homedir as homedir6 } from "os";
8529
8563
  var CONFIG_FILENAME = ".tokenleakrc";
8530
8564
  function loadConfig() {
8531
8565
  try {
8532
- const configPath = join5(homedir5(), CONFIG_FILENAME);
8533
- const raw = readFileSync2(configPath, "utf-8");
8566
+ const configPath = join6(homedir6(), CONFIG_FILENAME);
8567
+ const raw = readFileSync3(configPath, "utf-8");
8534
8568
  const parsed = JSON.parse(raw);
8535
8569
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
8536
8570
  return parsed;
@@ -8578,107 +8612,754 @@ function resolveCompareRange(compareStr, currentRange) {
8578
8612
  if (compareStr === "auto" || compareStr === "true" || compareStr === "") {
8579
8613
  return computePreviousPeriod(currentRange);
8580
8614
  }
8581
- const parsed = parseCompareRange(compareStr);
8582
- if (!parsed) {
8583
- throw new TokenleakError(`Invalid --compare format: "${compareStr}". Use YYYY-MM-DD..YYYY-MM-DD or "auto".`);
8615
+ const parsed = parseCompareRange(compareStr);
8616
+ if (!parsed) {
8617
+ throw new TokenleakError(`Invalid --compare format: "${compareStr}". Use YYYY-MM-DD..YYYY-MM-DD or "auto".`);
8618
+ }
8619
+ return parsed;
8620
+ }
8621
+ async function loadAndAggregate(providers, range, allowEmpty = false) {
8622
+ const results = await Promise.all(providers.map(async (p) => {
8623
+ try {
8624
+ return await p.load(range);
8625
+ } catch {
8626
+ return null;
8627
+ }
8628
+ }));
8629
+ const providerDataList = results.filter((r) => r !== null);
8630
+ if (!allowEmpty && providerDataList.length === 0) {
8631
+ throw new TokenleakError("No provider data found");
8632
+ }
8633
+ const mergedDaily = mergeProviderData(providerDataList);
8634
+ const stats = aggregate(mergedDaily, range.until);
8635
+ return { data: providerDataList, stats };
8636
+ }
8637
+ async function loadCompareTokenleakData(providers, currentRange, compareStr) {
8638
+ const previousRange = resolveCompareRange(compareStr, currentRange);
8639
+ const [currentResult, previousResult] = await Promise.all([
8640
+ loadAndAggregate(providers, currentRange),
8641
+ loadAndAggregate(providers, previousRange, true)
8642
+ ]);
8643
+ const compareOutput = buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
8644
+ return {
8645
+ compareOutput,
8646
+ currentData: currentResult.data,
8647
+ previousData: previousResult.data,
8648
+ output: {
8649
+ schemaVersion: SCHEMA_VERSION,
8650
+ generated: new Date().toISOString(),
8651
+ dateRange: currentRange,
8652
+ providers: currentResult.data,
8653
+ aggregated: currentResult.stats,
8654
+ more: buildMoreStats(currentResult.data, currentRange, {
8655
+ previousRange,
8656
+ previousProviders: previousResult.data,
8657
+ previousStats: compareOutput.periodB.stats,
8658
+ deltas: compareOutput.deltas
8659
+ })
8660
+ }
8661
+ };
8662
+ }
8663
+
8664
+ // packages/cli/src/date-range.ts
8665
+ var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
8666
+ function isValidDate(dateStr) {
8667
+ if (!DATE_FORMAT.test(dateStr))
8668
+ return false;
8669
+ const d = new Date(dateStr + "T00:00:00Z");
8670
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
8671
+ }
8672
+ function computeDateRange(args) {
8673
+ const until = args.until ?? new Date().toISOString().slice(0, 10);
8674
+ if (args.until && !isValidDate(args.until)) {
8675
+ throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
8676
+ }
8677
+ if (args.since && !isValidDate(args.since)) {
8678
+ throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
8679
+ }
8680
+ let since;
8681
+ if (args.since) {
8682
+ since = args.since;
8683
+ } else {
8684
+ const daysBack = args.days ?? DEFAULT_DAYS;
8685
+ const d = new Date(until);
8686
+ d.setDate(d.getDate() - daysBack);
8687
+ since = d.toISOString().slice(0, 10);
8688
+ }
8689
+ if (since > until) {
8690
+ throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
8691
+ }
8692
+ return { since, until };
8693
+ }
8694
+
8695
+ // packages/cli/src/env.ts
8696
+ var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
8697
+ var VALID_THEMES = new Set(["dark", "light"]);
8698
+ function loadEnvOverrides() {
8699
+ const overrides = {};
8700
+ const format = process.env["TOKENLEAK_FORMAT"];
8701
+ if (format && VALID_FORMATS.has(format)) {
8702
+ overrides.format = format;
8703
+ }
8704
+ const theme = process.env["TOKENLEAK_THEME"];
8705
+ if (theme && VALID_THEMES.has(theme)) {
8706
+ overrides.theme = theme;
8707
+ }
8708
+ const days = process.env["TOKENLEAK_DAYS"];
8709
+ if (days !== undefined && days !== "") {
8710
+ const parsed = Number(days);
8711
+ if (Number.isFinite(parsed) && parsed > 0) {
8712
+ overrides.days = parsed;
8713
+ }
8714
+ }
8715
+ return overrides;
8716
+ }
8717
+
8718
+ // packages/cli/src/cursor.ts
8719
+ import { chmodSync, existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync5, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
8720
+ import { createHash } from "crypto";
8721
+ import { homedir as homedir7 } from "os";
8722
+ import { basename as basename2, dirname as dirname4, join as join7 } from "path";
8723
+ import { createInterface } from "readline/promises";
8724
+ import { stdin as input, stdout as output } from "process";
8725
+ var CURSOR_USAGE_CSV_ENDPOINT = "https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens";
8726
+ var CURSOR_USAGE_SUMMARY_ENDPOINT = "https://cursor.com/api/usage-summary";
8727
+ function getCursorRootDir() {
8728
+ return process.env["TOKENLEAK_CURSOR_DIR"] ?? join7(homedir7(), ".config", "tokenleak");
8729
+ }
8730
+ function getCursorCredentialsPath() {
8731
+ return join7(getCursorRootDir(), "cursor-credentials.json");
8732
+ }
8733
+ function getCursorCacheDir() {
8734
+ return join7(getCursorRootDir(), "cursor-cache");
8735
+ }
8736
+ function ensureDir(dirPath, mode) {
8737
+ if (!existsSync6(dirPath)) {
8738
+ mkdirSync(dirPath, { recursive: true });
8739
+ }
8740
+ if (mode !== undefined && process.platform !== "win32") {
8741
+ chmodSync(dirPath, mode);
8742
+ }
8743
+ }
8744
+ function atomicWriteFile(path, contents, mode) {
8745
+ const dir = dirname4(path);
8746
+ ensureDir(dir);
8747
+ const tempPath = join7(dir, `.tmp-${basename2(path)}-${process.pid}`);
8748
+ writeFileSync(tempPath, contents, "utf8");
8749
+ if (mode !== undefined && process.platform !== "win32") {
8750
+ chmodSync(tempPath, mode);
8751
+ }
8752
+ renameSync(tempPath, path);
8753
+ }
8754
+ function buildCursorHeaders(sessionToken) {
8755
+ return {
8756
+ Accept: "*/*",
8757
+ "Accept-Language": "en-US,en;q=0.9",
8758
+ Cookie: `WorkosCursorSessionToken=${sessionToken}`,
8759
+ Referer: "https://www.cursor.com/settings",
8760
+ "User-Agent": `tokenleak/${VERSION} (+https://github.com/ya-nsh/tokenleak)`
8761
+ };
8762
+ }
8763
+ function sanitizeAccountIdForFilename(accountId) {
8764
+ const sanitized = accountId.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
8765
+ return sanitized.length > 0 ? sanitized.slice(0, 80) : "account";
8766
+ }
8767
+ function extractUserIdFromSessionToken(token) {
8768
+ const trimmed = token.trim();
8769
+ if (trimmed.includes("%3A%3A")) {
8770
+ return trimmed.split("%3A%3A")[0]?.trim() || undefined;
8771
+ }
8772
+ if (trimmed.includes("::")) {
8773
+ return trimmed.split("::")[0]?.trim() || undefined;
8774
+ }
8775
+ return;
8776
+ }
8777
+ function deriveAccountId(sessionToken) {
8778
+ const userId = extractUserIdFromSessionToken(sessionToken);
8779
+ if (userId) {
8780
+ return userId;
8781
+ }
8782
+ const hash = createHash("sha256").update(sessionToken).digest("hex");
8783
+ return `anon-${hash.slice(0, 12)}`;
8784
+ }
8785
+ function countCursorCsvRows(csvText) {
8786
+ const rows = csvText.split(/\r?\n/).map((line2) => line2.trim()).filter((line2) => line2.length > 0);
8787
+ return rows.length > 0 ? rows.length - 1 : 0;
8788
+ }
8789
+ function archiveCacheFile(path, label) {
8790
+ if (!existsSync6(path)) {
8791
+ return;
8792
+ }
8793
+ const archiveDir = join7(getCursorCacheDir(), "archive");
8794
+ ensureDir(archiveDir, 448);
8795
+ const timestamp = new Date().toISOString().replaceAll(":", "-");
8796
+ renameSync(path, join7(archiveDir, `${sanitizeAccountIdForFilename(label)}-${timestamp}.csv`));
8797
+ }
8798
+ function resolveAccountId(store, nameOrId) {
8799
+ const needle = nameOrId.trim();
8800
+ if (!needle) {
8801
+ return;
8802
+ }
8803
+ if (store.accounts[needle]) {
8804
+ return needle;
8805
+ }
8806
+ const lowered = needle.toLowerCase();
8807
+ for (const [id, account] of Object.entries(store.accounts)) {
8808
+ if (account.label?.toLowerCase() === lowered) {
8809
+ return id;
8810
+ }
8811
+ }
8812
+ return;
8813
+ }
8814
+ async function readSecret(prompt) {
8815
+ if (!input.isTTY || !output.isTTY) {
8816
+ const rl = createInterface({ input, output });
8817
+ try {
8818
+ return (await rl.question(prompt)).trim();
8819
+ } finally {
8820
+ rl.close();
8821
+ }
8822
+ }
8823
+ output.write(prompt);
8824
+ input.resume();
8825
+ input.setEncoding("utf8");
8826
+ if (typeof input.setRawMode === "function") {
8827
+ input.setRawMode(true);
8828
+ }
8829
+ return await new Promise((resolve, reject) => {
8830
+ let value = "";
8831
+ const cleanup = () => {
8832
+ input.off("data", onData);
8833
+ if (typeof input.setRawMode === "function") {
8834
+ input.setRawMode(false);
8835
+ }
8836
+ input.pause();
8837
+ output.write(`
8838
+ `);
8839
+ };
8840
+ const onData = (chunk) => {
8841
+ const text2 = String(chunk);
8842
+ for (const char of text2) {
8843
+ if (char === "\x03") {
8844
+ cleanup();
8845
+ reject(new TokenleakError("Cancelled"));
8846
+ return;
8847
+ }
8848
+ if (char === "\r" || char === `
8849
+ `) {
8850
+ cleanup();
8851
+ resolve(value.trim());
8852
+ return;
8853
+ }
8854
+ if (char === "\b" || char === "\x7F") {
8855
+ value = value.slice(0, -1);
8856
+ continue;
8857
+ }
8858
+ value += char;
8859
+ }
8860
+ };
8861
+ input.on("data", onData);
8862
+ });
8863
+ }
8864
+ function loadCursorCredentialsStore() {
8865
+ const path = getCursorCredentialsPath();
8866
+ if (!existsSync6(path)) {
8867
+ return null;
8868
+ }
8869
+ try {
8870
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
8871
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.activeAccountId !== "string" || typeof parsed.accounts !== "object" || parsed.accounts === null) {
8872
+ return null;
8873
+ }
8874
+ return {
8875
+ version: typeof parsed.version === "number" ? parsed.version : 1,
8876
+ activeAccountId: parsed.activeAccountId,
8877
+ accounts: parsed.accounts
8878
+ };
8879
+ } catch {
8880
+ return null;
8881
+ }
8882
+ }
8883
+ function saveCursorCredentialsStore(store) {
8884
+ ensureDir(getCursorRootDir(), 448);
8885
+ atomicWriteFile(getCursorCredentialsPath(), `${JSON.stringify(store, null, 2)}
8886
+ `, process.platform === "win32" ? undefined : 384);
8887
+ }
8888
+ function listCursorAccounts() {
8889
+ const store = loadCursorCredentialsStore();
8890
+ if (!store) {
8891
+ return [];
8892
+ }
8893
+ return Object.entries(store.accounts).map(([id, account]) => ({
8894
+ id,
8895
+ label: account.label,
8896
+ userId: account.userId,
8897
+ createdAt: account.createdAt,
8898
+ isActive: id === store.activeAccountId
8899
+ })).sort((left, right) => {
8900
+ if (left.isActive !== right.isActive) {
8901
+ return left.isActive ? -1 : 1;
8902
+ }
8903
+ return (left.label ?? left.id).localeCompare(right.label ?? right.id);
8904
+ });
8905
+ }
8906
+ function hasCursorUsageCache() {
8907
+ const cacheDir = getCursorCacheDir();
8908
+ if (!existsSync6(cacheDir)) {
8909
+ return false;
8910
+ }
8911
+ return readdirSync5(cacheDir).some((entry) => {
8912
+ if (entry === "archive") {
8913
+ return false;
8914
+ }
8915
+ return entry === "usage.csv" || /^usage\.[^.].*\.csv$/.test(entry);
8916
+ });
8917
+ }
8918
+ function isCursorLoggedIn() {
8919
+ const store = loadCursorCredentialsStore();
8920
+ return store !== null && Object.keys(store.accounts).length > 0;
8921
+ }
8922
+ function saveCursorCredentials(token, label) {
8923
+ const accountId = deriveAccountId(token);
8924
+ const store = loadCursorCredentialsStore() ?? {
8925
+ version: 1,
8926
+ activeAccountId: accountId,
8927
+ accounts: {}
8928
+ };
8929
+ const normalizedLabel = label?.trim();
8930
+ if (normalizedLabel) {
8931
+ for (const [id, account] of Object.entries(store.accounts)) {
8932
+ if (id === accountId) {
8933
+ continue;
8934
+ }
8935
+ if (account.label?.trim().toLowerCase() === normalizedLabel.toLowerCase()) {
8936
+ throw new TokenleakError(`Cursor account label already exists: ${normalizedLabel}`);
8937
+ }
8938
+ }
8939
+ }
8940
+ store.accounts[accountId] = {
8941
+ sessionToken: token,
8942
+ userId: extractUserIdFromSessionToken(token),
8943
+ createdAt: new Date().toISOString(),
8944
+ label: normalizedLabel
8945
+ };
8946
+ store.activeAccountId = accountId;
8947
+ saveCursorCredentialsStore(store);
8948
+ return accountId;
8949
+ }
8950
+ function removeCursorAccount(nameOrId, purgeCache) {
8951
+ const store = loadCursorCredentialsStore();
8952
+ if (!store) {
8953
+ throw new TokenleakError("No saved Cursor accounts");
8954
+ }
8955
+ const accountId = resolveAccountId(store, nameOrId);
8956
+ if (!accountId) {
8957
+ throw new TokenleakError(`Account not found: ${nameOrId}`);
8958
+ }
8959
+ const wasActive = accountId === store.activeAccountId;
8960
+ const cacheDir = getCursorCacheDir();
8961
+ const accountCachePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
8962
+ const activeCachePath = join7(cacheDir, "usage.csv");
8963
+ if (existsSync6(accountCachePath)) {
8964
+ if (purgeCache) {
8965
+ unlinkSync(accountCachePath);
8966
+ } else {
8967
+ archiveCacheFile(accountCachePath, `usage.${accountId}`);
8968
+ }
8969
+ }
8970
+ if (wasActive && existsSync6(activeCachePath)) {
8971
+ if (purgeCache) {
8972
+ unlinkSync(activeCachePath);
8973
+ } else {
8974
+ archiveCacheFile(activeCachePath, `usage.active.${accountId}`);
8975
+ }
8976
+ }
8977
+ delete store.accounts[accountId];
8978
+ const remainingIds = Object.keys(store.accounts);
8979
+ if (remainingIds.length === 0) {
8980
+ if (existsSync6(getCursorCredentialsPath())) {
8981
+ unlinkSync(getCursorCredentialsPath());
8982
+ }
8983
+ return;
8984
+ }
8985
+ if (wasActive) {
8986
+ const nextAccountId = remainingIds[0];
8987
+ store.activeAccountId = nextAccountId;
8988
+ const nextCachePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(nextAccountId)}.csv`);
8989
+ if (existsSync6(nextCachePath)) {
8990
+ renameSync(nextCachePath, activeCachePath);
8991
+ }
8992
+ }
8993
+ saveCursorCredentialsStore(store);
8994
+ }
8995
+ function removeAllCursorAccounts(purgeCache) {
8996
+ const cacheDir = getCursorCacheDir();
8997
+ if (existsSync6(cacheDir)) {
8998
+ for (const entry of readdirSync5(cacheDir)) {
8999
+ if (entry === "archive") {
9000
+ continue;
9001
+ }
9002
+ if (entry === "usage.csv" || /^usage\.[^.].*\.csv$/.test(entry)) {
9003
+ const fullPath = join7(cacheDir, entry);
9004
+ if (purgeCache) {
9005
+ rmSync(fullPath, { force: true });
9006
+ } else {
9007
+ archiveCacheFile(fullPath, `usage.all.${entry}`);
9008
+ }
9009
+ }
9010
+ }
9011
+ }
9012
+ if (existsSync6(getCursorCredentialsPath())) {
9013
+ unlinkSync(getCursorCredentialsPath());
9014
+ }
9015
+ }
9016
+ function setActiveCursorAccount(nameOrId) {
9017
+ const store = loadCursorCredentialsStore();
9018
+ if (!store) {
9019
+ throw new TokenleakError("No saved Cursor accounts");
9020
+ }
9021
+ const accountId = resolveAccountId(store, nameOrId);
9022
+ if (!accountId) {
9023
+ throw new TokenleakError(`Account not found: ${nameOrId}`);
9024
+ }
9025
+ if (accountId === store.activeAccountId) {
9026
+ return;
9027
+ }
9028
+ const cacheDir = getCursorCacheDir();
9029
+ const activeCachePath = join7(cacheDir, "usage.csv");
9030
+ const oldActivePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(store.activeAccountId)}.csv`);
9031
+ const newActivePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
9032
+ if (existsSync6(activeCachePath)) {
9033
+ if (existsSync6(oldActivePath)) {
9034
+ archiveCacheFile(oldActivePath, `usage.${store.activeAccountId}`);
9035
+ }
9036
+ renameSync(activeCachePath, oldActivePath);
9037
+ }
9038
+ if (existsSync6(newActivePath)) {
9039
+ renameSync(newActivePath, activeCachePath);
9040
+ }
9041
+ store.activeAccountId = accountId;
9042
+ saveCursorCredentialsStore(store);
9043
+ }
9044
+ function getActiveCursorCredentials() {
9045
+ const store = loadCursorCredentialsStore();
9046
+ if (!store) {
9047
+ return null;
9048
+ }
9049
+ return store.accounts[store.activeAccountId] ?? null;
9050
+ }
9051
+ function getCursorCredentialsFor(nameOrId) {
9052
+ const store = loadCursorCredentialsStore();
9053
+ if (!store) {
9054
+ return null;
9055
+ }
9056
+ const accountId = resolveAccountId(store, nameOrId);
9057
+ return accountId ? store.accounts[accountId] ?? null : null;
9058
+ }
9059
+ async function validateCursorSession(sessionToken) {
9060
+ let response;
9061
+ try {
9062
+ response = await fetch(CURSOR_USAGE_SUMMARY_ENDPOINT, {
9063
+ headers: buildCursorHeaders(sessionToken)
9064
+ });
9065
+ } catch (error) {
9066
+ return {
9067
+ valid: false,
9068
+ error: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`
9069
+ };
9070
+ }
9071
+ if (response.status === 401 || response.status === 403) {
9072
+ return {
9073
+ valid: false,
9074
+ error: "Session token expired or invalid"
9075
+ };
9076
+ }
9077
+ if (!response.ok) {
9078
+ return {
9079
+ valid: false,
9080
+ error: `API returned status ${response.status}`
9081
+ };
9082
+ }
9083
+ try {
9084
+ const payload = await response.json();
9085
+ const billingCycleStart = payload["billingCycleStart"];
9086
+ const billingCycleEnd = payload["billingCycleEnd"];
9087
+ if (typeof billingCycleStart !== "string" || typeof billingCycleEnd !== "string") {
9088
+ return {
9089
+ valid: false,
9090
+ error: "Invalid response format"
9091
+ };
9092
+ }
9093
+ return {
9094
+ valid: true,
9095
+ membershipType: typeof payload["membershipType"] === "string" ? payload["membershipType"] : undefined
9096
+ };
9097
+ } catch (error) {
9098
+ return {
9099
+ valid: false,
9100
+ error: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`
9101
+ };
9102
+ }
9103
+ }
9104
+ async function fetchCursorUsageCsv(sessionToken) {
9105
+ const response = await fetch(CURSOR_USAGE_CSV_ENDPOINT, {
9106
+ headers: buildCursorHeaders(sessionToken)
9107
+ });
9108
+ if (response.status === 401 || response.status === 403) {
9109
+ throw new TokenleakError("Cursor session expired. Please run 'tokenleak cursor login' to re-authenticate.");
9110
+ }
9111
+ if (!response.ok) {
9112
+ throw new TokenleakError(`Cursor API returned status ${response.status}`);
9113
+ }
9114
+ const text2 = await response.text();
9115
+ if (!text2.startsWith("Date,")) {
9116
+ throw new TokenleakError("Invalid response from Cursor API - expected CSV format");
8584
9117
  }
8585
- return parsed;
9118
+ return text2;
8586
9119
  }
8587
- async function loadAndAggregate(providers, range, allowEmpty = false) {
8588
- const results = await Promise.all(providers.map(async (p) => {
9120
+ async function syncCursorCache() {
9121
+ const store = loadCursorCredentialsStore();
9122
+ if (!store || Object.keys(store.accounts).length === 0) {
9123
+ return { synced: false, rows: 0, error: "Not authenticated" };
9124
+ }
9125
+ const cacheDir = getCursorCacheDir();
9126
+ ensureDir(cacheDir, 448);
9127
+ let totalRows = 0;
9128
+ let successCount = 0;
9129
+ const errors = [];
9130
+ for (const [accountId, credentials] of Object.entries(store.accounts)) {
8589
9131
  try {
8590
- return await p.load(range);
8591
- } catch {
8592
- return null;
9132
+ const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
9133
+ const filePath = accountId === store.activeAccountId ? join7(cacheDir, "usage.csv") : join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
9134
+ atomicWriteFile(filePath, csvText, process.platform === "win32" ? undefined : 384);
9135
+ if (accountId === store.activeAccountId) {
9136
+ const activeDupPath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(store.activeAccountId)}.csv`);
9137
+ if (existsSync6(activeDupPath)) {
9138
+ unlinkSync(activeDupPath);
9139
+ }
9140
+ }
9141
+ successCount += 1;
9142
+ totalRows += countCursorCsvRows(csvText);
9143
+ } catch (error) {
9144
+ errors.push(`${accountId}: ${error instanceof Error ? error.message : String(error)}`);
8593
9145
  }
8594
- }));
8595
- const providerDataList = results.filter((r) => r !== null);
8596
- if (!allowEmpty && providerDataList.length === 0) {
8597
- throw new TokenleakError("No provider data found");
8598
9146
  }
8599
- const mergedDaily = mergeProviderData(providerDataList);
8600
- const stats = aggregate(mergedDaily, range.until);
8601
- return { data: providerDataList, stats };
9147
+ if (successCount === 0) {
9148
+ return {
9149
+ synced: false,
9150
+ rows: 0,
9151
+ error: errors[0] ?? "Cursor sync failed"
9152
+ };
9153
+ }
9154
+ return {
9155
+ synced: true,
9156
+ rows: totalRows,
9157
+ error: errors.length > 0 ? `Some accounts failed to sync (${errors.length}/${Object.keys(store.accounts).length})` : undefined
9158
+ };
8602
9159
  }
8603
- async function loadCompareTokenleakData(providers, currentRange, compareStr) {
8604
- const previousRange = resolveCompareRange(compareStr, currentRange);
8605
- const [currentResult, previousResult] = await Promise.all([
8606
- loadAndAggregate(providers, currentRange),
8607
- loadAndAggregate(providers, previousRange, true)
8608
- ]);
8609
- const compareOutput = buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
9160
+ async function shouldSyncCursorForRun(config) {
9161
+ const hasProviderFilter = Boolean(config.provider || config.cursor || config.claude || config.codex || config.pi || config.openCode);
9162
+ const requestedCursor = config.cursor || (config.provider?.split(",").some((token) => {
9163
+ const normalized = token.trim().toLowerCase().replace(/\s+/g, "-");
9164
+ return normalized === "cursor" || normalized === "cursor-ide" || normalized === "cursoride";
9165
+ }) ?? false);
9166
+ if (!isCursorLoggedIn()) {
9167
+ return { attempted: false };
9168
+ }
9169
+ if (!requestedCursor && hasProviderFilter && !config.allProviders) {
9170
+ return { attempted: false };
9171
+ }
9172
+ const result = await syncCursorCache();
8610
9173
  return {
8611
- compareOutput,
8612
- currentData: currentResult.data,
8613
- previousData: previousResult.data,
8614
- output: {
8615
- schemaVersion: SCHEMA_VERSION,
8616
- generated: new Date().toISOString(),
8617
- dateRange: currentRange,
8618
- providers: currentResult.data,
8619
- aggregated: currentResult.stats,
8620
- more: buildMoreStats(currentResult.data, currentRange, {
8621
- previousRange,
8622
- previousProviders: previousResult.data,
8623
- previousStats: compareOutput.periodB.stats,
8624
- deltas: compareOutput.deltas
8625
- })
8626
- }
9174
+ attempted: true,
9175
+ error: result.error
8627
9176
  };
8628
9177
  }
8629
-
8630
- // packages/cli/src/date-range.ts
8631
- var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
8632
- function isValidDate(dateStr) {
8633
- if (!DATE_FORMAT.test(dateStr))
8634
- return false;
8635
- const d = new Date(dateStr + "T00:00:00Z");
8636
- return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
9178
+ function buildCursorHelpText() {
9179
+ return [
9180
+ "tokenleak cursor",
9181
+ "Manage Cursor authentication and cache sync.",
9182
+ "",
9183
+ "Usage:",
9184
+ " tokenleak cursor login [--name <label>]",
9185
+ " tokenleak cursor status [--name <label>]",
9186
+ " tokenleak cursor accounts [--json]",
9187
+ " tokenleak cursor switch <name-or-id>",
9188
+ " tokenleak cursor logout [--name <label> | --all] [--purge-cache]",
9189
+ "",
9190
+ "Notes:",
9191
+ " Session tokens come from https://www.cursor.com/settings",
9192
+ " Session tokens are stored in plaintext with local-only file permissions.",
9193
+ ` Credentials: ${getCursorCredentialsPath()}`,
9194
+ ` Cache: ${getCursorCacheDir()}`,
9195
+ ""
9196
+ ].join(`
9197
+ `);
8637
9198
  }
8638
- function computeDateRange(args) {
8639
- const until = args.until ?? new Date().toISOString().slice(0, 10);
8640
- if (args.until && !isValidDate(args.until)) {
8641
- throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
9199
+ function printCursorAccounts(json) {
9200
+ const accounts = listCursorAccounts();
9201
+ if (json) {
9202
+ process.stdout.write(`${JSON.stringify({ accounts }, null, 2)}
9203
+ `);
9204
+ return;
8642
9205
  }
8643
- if (args.since && !isValidDate(args.since)) {
8644
- throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
9206
+ if (accounts.length === 0) {
9207
+ process.stdout.write(`No saved Cursor accounts.
9208
+ `);
9209
+ return;
8645
9210
  }
8646
- let since;
8647
- if (args.since) {
8648
- since = args.since;
8649
- } else {
8650
- const daysBack = args.days ?? DEFAULT_DAYS;
8651
- const d = new Date(until);
8652
- d.setDate(d.getDate() - daysBack);
8653
- since = d.toISOString().slice(0, 10);
9211
+ process.stdout.write(`Cursor accounts:
9212
+ `);
9213
+ for (const account of accounts) {
9214
+ const marker = account.isActive ? "*" : "-";
9215
+ const label = account.label ? `${account.label} (${account.id})` : account.id;
9216
+ process.stdout.write(` ${marker} ${label}
9217
+ `);
8654
9218
  }
8655
- if (since > until) {
8656
- throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
9219
+ }
9220
+ async function runCursorLogin(name) {
9221
+ if (name && listCursorAccounts().some((account) => account.label?.toLowerCase() === name.toLowerCase())) {
9222
+ throw new TokenleakError(`Cursor account label already exists: ${name}`);
8657
9223
  }
8658
- return { since, until };
9224
+ process.stdout.write(`Session tokens are stored in plaintext at ${getCursorCredentialsPath()} with local-only file permissions.
9225
+ `);
9226
+ const token = await readSecret("Enter Cursor session token: ");
9227
+ if (!token) {
9228
+ throw new TokenleakError("No token provided");
9229
+ }
9230
+ process.stdout.write(`Validating session token...
9231
+ `);
9232
+ const result = await validateCursorSession(token);
9233
+ if (!result.valid) {
9234
+ throw new TokenleakError(result.error ?? "Invalid session token");
9235
+ }
9236
+ const accountId = saveCursorCredentials(token, name);
9237
+ const display = name ?? accountId;
9238
+ process.stdout.write(`Saved Cursor account ${display}.
9239
+ `);
8659
9240
  }
8660
-
8661
- // packages/cli/src/env.ts
8662
- var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
8663
- var VALID_THEMES = new Set(["dark", "light"]);
8664
- function loadEnvOverrides() {
8665
- const overrides = {};
8666
- const format = process.env["TOKENLEAK_FORMAT"];
8667
- if (format && VALID_FORMATS.has(format)) {
8668
- overrides.format = format;
9241
+ async function runCursorStatus(name) {
9242
+ const credentials = name ? getCursorCredentialsFor(name) : getActiveCursorCredentials();
9243
+ if (!credentials) {
9244
+ throw new TokenleakError(name ? `Account not found: ${name}` : "No saved Cursor accounts");
8669
9245
  }
8670
- const theme = process.env["TOKENLEAK_THEME"];
8671
- if (theme && VALID_THEMES.has(theme)) {
8672
- overrides.theme = theme;
9246
+ const result = await validateCursorSession(credentials.sessionToken);
9247
+ if (!result.valid) {
9248
+ throw new TokenleakError(result.error ?? "Invalid / expired session");
8673
9249
  }
8674
- const days = process.env["TOKENLEAK_DAYS"];
8675
- if (days !== undefined && days !== "") {
8676
- const parsed = Number(days);
8677
- if (Number.isFinite(parsed) && parsed > 0) {
8678
- overrides.days = parsed;
9250
+ process.stdout.write(`Cursor session is valid.
9251
+ `);
9252
+ if (result.membershipType) {
9253
+ process.stdout.write(`Membership: ${result.membershipType}
9254
+ `);
9255
+ }
9256
+ }
9257
+ function runCursorLogout(name, all, purgeCache) {
9258
+ if (all) {
9259
+ removeAllCursorAccounts(purgeCache);
9260
+ process.stdout.write(`Logged out from all Cursor accounts.
9261
+ `);
9262
+ return;
9263
+ }
9264
+ if (name) {
9265
+ removeCursorAccount(name, purgeCache);
9266
+ process.stdout.write(`Logged out from Cursor account '${name}'.
9267
+ `);
9268
+ return;
9269
+ }
9270
+ const store = loadCursorCredentialsStore();
9271
+ if (!store) {
9272
+ throw new TokenleakError("No saved Cursor accounts");
9273
+ }
9274
+ removeCursorAccount(store.activeAccountId, purgeCache);
9275
+ process.stdout.write(`Logged out from Cursor account '${store.activeAccountId}'.
9276
+ `);
9277
+ }
9278
+ function parseNameFlag(argv, index) {
9279
+ const value = argv[index + 1];
9280
+ if (!value) {
9281
+ throw new TokenleakError(`${argv[index]} requires a value`);
9282
+ }
9283
+ return [value, index + 2];
9284
+ }
9285
+ async function runCursorCommand(argv) {
9286
+ const command = argv[0];
9287
+ if (!command || command === "--help" || command === "-h") {
9288
+ process.stdout.write(buildCursorHelpText());
9289
+ return;
9290
+ }
9291
+ if (command === "login") {
9292
+ let name;
9293
+ for (let index = 1;index < argv.length; ) {
9294
+ const arg = argv[index];
9295
+ if (arg === "--name") {
9296
+ [name, index] = parseNameFlag(argv, index);
9297
+ } else {
9298
+ throw new TokenleakError(`Unknown cursor flag "${arg}"`);
9299
+ }
8679
9300
  }
9301
+ await runCursorLogin(name);
9302
+ return;
8680
9303
  }
8681
- return overrides;
9304
+ if (command === "status") {
9305
+ let name;
9306
+ for (let index = 1;index < argv.length; ) {
9307
+ const arg = argv[index];
9308
+ if (arg === "--name") {
9309
+ [name, index] = parseNameFlag(argv, index);
9310
+ } else {
9311
+ throw new TokenleakError(`Unknown cursor flag "${arg}"`);
9312
+ }
9313
+ }
9314
+ await runCursorStatus(name);
9315
+ return;
9316
+ }
9317
+ if (command === "accounts") {
9318
+ if (argv.length > 2 || argv[1] && argv[1] !== "--json") {
9319
+ throw new TokenleakError(`Unknown cursor flag "${argv[1]}"`);
9320
+ }
9321
+ printCursorAccounts(argv.includes("--json"));
9322
+ return;
9323
+ }
9324
+ if (command === "switch") {
9325
+ const target = argv[1];
9326
+ if (!target) {
9327
+ throw new TokenleakError("tokenleak cursor switch requires a name or account id");
9328
+ }
9329
+ setActiveCursorAccount(target);
9330
+ process.stdout.write(`Active Cursor account set to ${target}.
9331
+ `);
9332
+ return;
9333
+ }
9334
+ if (command === "logout") {
9335
+ let name;
9336
+ let all = false;
9337
+ let purgeCache = false;
9338
+ for (let index = 1;index < argv.length; ) {
9339
+ const arg = argv[index];
9340
+ if (arg === "--name") {
9341
+ [name, index] = parseNameFlag(argv, index);
9342
+ continue;
9343
+ }
9344
+ if (arg === "--all") {
9345
+ all = true;
9346
+ index += 1;
9347
+ continue;
9348
+ }
9349
+ if (arg === "--purge-cache") {
9350
+ purgeCache = true;
9351
+ index += 1;
9352
+ continue;
9353
+ }
9354
+ throw new TokenleakError(`Unknown cursor flag "${arg}"`);
9355
+ }
9356
+ if (all && name) {
9357
+ throw new TokenleakError("tokenleak cursor logout cannot combine --all with --name");
9358
+ }
9359
+ runCursorLogout(name, all, purgeCache);
9360
+ return;
9361
+ }
9362
+ throw new TokenleakError(`Unknown cursor command "${command}"`);
8682
9363
  }
8683
9364
 
8684
9365
  // packages/cli/src/explain.ts
@@ -8718,7 +9399,7 @@ function renderExplainTerminal(report, width = 80) {
8718
9399
  `Explain ${report.date}`,
8719
9400
  report.headline,
8720
9401
  "",
8721
- ...report.summary.map((line) => `- ${line}`),
9402
+ ...report.summary.map((line2) => `- ${line2}`),
8722
9403
  "",
8723
9404
  ...renderEvidenceTable("Providers", report.topProviders, width),
8724
9405
  "",
@@ -8752,6 +9433,7 @@ function buildExplainHelpText() {
8752
9433
  " -p, --provider <list> Provider filter list, comma-separated",
8753
9434
  " --claude Only include Claude Code",
8754
9435
  " --codex Only include Codex",
9436
+ " --cursor Only include Cursor",
8755
9437
  " --pi Only include Pi",
8756
9438
  " --open-code Only include OpenCode",
8757
9439
  " --all-providers Ignore provider filters and use every available provider",
@@ -8781,6 +9463,7 @@ var CLI_FLAG_ORDER = [
8781
9463
  "upload",
8782
9464
  "claude",
8783
9465
  "codex",
9466
+ "cursor",
8784
9467
  "pi",
8785
9468
  "openCode",
8786
9469
  "allProviders",
@@ -8807,6 +9490,7 @@ var CLI_FLAG_NAMES = {
8807
9490
  upload: "--upload",
8808
9491
  claude: "--claude",
8809
9492
  codex: "--codex",
9493
+ cursor: "--cursor",
8810
9494
  pi: "--pi",
8811
9495
  openCode: "--open-code",
8812
9496
  allProviders: "--all-providers",
@@ -8844,7 +9528,7 @@ function buildCliPreview(cliArgs) {
8844
9528
 
8845
9529
  // packages/cli/src/interactive.ts
8846
9530
  import { emitKeypressEvents } from "readline";
8847
- import { createInterface } from "readline/promises";
9531
+ import { createInterface as createInterface2 } from "readline/promises";
8848
9532
  var INTERACTIVE_FLAG_LINES = [
8849
9533
  " explain <date> explain one day of usage",
8850
9534
  " focus rank deep-work sessions",
@@ -8858,6 +9542,7 @@ var INTERACTIVE_FLAG_LINES = [
8858
9542
  "-p, --provider <list> comma-separated providers",
8859
9543
  " --claude shortcut for Claude Code",
8860
9544
  " --codex shortcut for Codex",
9545
+ " --cursor shortcut for Cursor",
8861
9546
  " --pi shortcut for Pi",
8862
9547
  " --open-code shortcut for Open Code",
8863
9548
  " --all-providers ignore provider filters",
@@ -8958,7 +9643,7 @@ function renderRule(width) {
8958
9643
  return color("-".repeat(width), DIM);
8959
9644
  }
8960
9645
  function describeRequest(args) {
8961
- const output = typeof args["output"] === "string" ? args["output"] : null;
9646
+ const output2 = typeof args["output"] === "string" ? args["output"] : null;
8962
9647
  if (args["liveServer"]) {
8963
9648
  return {
8964
9649
  title: "Live Dashboard",
@@ -8987,7 +9672,7 @@ function describeRequest(args) {
8987
9672
  return {
8988
9673
  title: "Compare Report",
8989
9674
  loadingTitle: "Building compare report",
8990
- loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
9675
+ loadingDetail: output2 ? `Computing period deltas and writing the report to ${output2}.` : "Computing period deltas for the current and previous windows.",
8991
9676
  executionMode: "capture"
8992
9677
  };
8993
9678
  }
@@ -8996,21 +9681,21 @@ function describeRequest(args) {
8996
9681
  return {
8997
9682
  title: "JSON Export",
8998
9683
  loadingTitle: "Generating JSON report",
8999
- loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
9684
+ loadingDetail: output2 ? `Collecting token usage and writing JSON to ${output2}.` : "Collecting token usage and building structured JSON output.",
9000
9685
  executionMode: "capture"
9001
9686
  };
9002
9687
  case "svg":
9003
9688
  return {
9004
9689
  title: "SVG Export",
9005
9690
  loadingTitle: "Rendering SVG",
9006
- loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
9691
+ loadingDetail: output2 ? `Rendering a vector card and writing it to ${output2}.` : "Rendering a vector card from your usage data.",
9007
9692
  executionMode: "capture"
9008
9693
  };
9009
9694
  case "png":
9010
9695
  return {
9011
9696
  title: "PNG Export",
9012
9697
  loadingTitle: "Rendering PNG",
9013
- loadingDetail: output ? `Rendering the PNG card and writing it to ${output}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
9698
+ loadingDetail: output2 ? `Rendering the PNG card and writing it to ${output2}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
9014
9699
  executionMode: "capture"
9015
9700
  };
9016
9701
  default:
@@ -9086,9 +9771,18 @@ function renderFlagPanel() {
9086
9771
  color("All Flags", WHITE + BOLD),
9087
9772
  color("Every flag remains available while using the launcher.", DIM),
9088
9773
  "",
9089
- ...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
9774
+ ...INTERACTIVE_FLAG_LINES.map((line2) => color(line2, CYAN))
9090
9775
  ];
9091
9776
  }
9777
+ function sliceVisibleWindow(items, selectedIndex, maxVisible) {
9778
+ if (items.length <= maxVisible) {
9779
+ return [...items];
9780
+ }
9781
+ const visibleCount = Math.max(1, maxVisible);
9782
+ const maxStart = items.length - visibleCount;
9783
+ const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(visibleCount / 2)));
9784
+ return items.slice(start, start + visibleCount);
9785
+ }
9092
9786
  function renderMenuPanel(context, options, selectedIndex) {
9093
9787
  const selected = options[selectedIndex];
9094
9788
  return [
@@ -9109,7 +9803,55 @@ function renderMenuPanel(context, options, selectedIndex) {
9109
9803
  renderRule(44)
9110
9804
  ];
9111
9805
  }
9806
+ function buildCompactLauncherBody(context, options, selectedIndex, width, rows) {
9807
+ const selected = options[selectedIndex];
9808
+ const menuLines = renderMenu(options, selectedIndex);
9809
+ const compact = rows < 18;
9810
+ const header = compact ? [
9811
+ color("Tokenleak Interactive Launcher", WHITE + BOLD),
9812
+ color("Narrow pane detected. Press H for the full flag reference.", DIM)
9813
+ ] : [
9814
+ color("Tokenleak Interactive Launcher", WHITE + BOLD),
9815
+ `${color(`v${context.version}`, YELLOW)} ${color("adaptive launcher view", CYAN)}`,
9816
+ color("Narrow pane detected. Press H for the full flag reference.", DIM),
9817
+ ""
9818
+ ];
9819
+ const footer = compact ? [
9820
+ "",
9821
+ color(truncateVisible2(selected.preview, width), GREEN),
9822
+ `${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`
9823
+ ] : [
9824
+ "",
9825
+ color("Preview", WHITE + BOLD),
9826
+ color(truncateVisible2(selected.preview, width), GREEN),
9827
+ "",
9828
+ `${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
9829
+ renderRule(44)
9830
+ ];
9831
+ const meta = [
9832
+ color("Actions", WHITE + BOLD),
9833
+ color("Use arrow keys to move through the launcher menu.", DIM)
9834
+ ];
9835
+ const fixedLineCount = header.length + meta.length + footer.length;
9836
+ const menuViewport = Math.max(1, rows - fixedLineCount);
9837
+ const visibleMenu = sliceVisibleWindow(menuLines, selectedIndex, menuViewport);
9838
+ return [...header, ...meta, ...visibleMenu, ...footer].slice(0, Math.max(1, rows));
9839
+ }
9840
+ function buildLauncherBody(context, options, selectedIndex, width, rows) {
9841
+ const menuPanel = renderMenuPanel(context, options, selectedIndex);
9842
+ const flagPanel = renderFlagPanel();
9843
+ const wideBody = joinColumns(menuPanel, flagPanel, width);
9844
+ if (width >= 118 && wideBody.length <= rows) {
9845
+ return wideBody;
9846
+ }
9847
+ const stackedBody = [...menuPanel, "", ...flagPanel];
9848
+ if (stackedBody.length <= rows) {
9849
+ return stackedBody;
9850
+ }
9851
+ return buildCompactLauncherBody(context, options, selectedIndex, width, rows);
9852
+ }
9112
9853
  function renderHelpOverlay(helpText, width) {
9854
+ const rows = Math.max(6, process.stdout.rows ?? 40);
9113
9855
  const lines = helpText.trimEnd().split(`
9114
9856
  `);
9115
9857
  const header = [
@@ -9117,14 +9859,18 @@ function renderHelpOverlay(helpText, width) {
9117
9859
  color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
9118
9860
  ""
9119
9861
  ];
9120
- return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible2(line, width))].join(`
9862
+ const availableHeight = Math.max(1, rows - header.length - 1);
9863
+ const visibleLines = lines.slice(0, availableHeight).map((line2) => truncateVisible2(line2, width));
9864
+ const truncatedNotice = lines.length > visibleLines.length ? [
9865
+ color("Help truncated to fit this pane. Resize taller or run `tokenleak --help` for the full output.", DIM)
9866
+ ] : [];
9867
+ return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...visibleLines, ...truncatedNotice].join(`
9121
9868
  `)}`;
9122
9869
  }
9123
9870
  function renderLauncher(context, options, selectedIndex) {
9124
- const width = process.stdout.columns ?? 120;
9125
- const menuPanel = renderMenuPanel(context, options, selectedIndex);
9126
- const flagPanel = renderFlagPanel();
9127
- const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
9871
+ const width = Math.max(40, process.stdout.columns ?? 120);
9872
+ const rows = Math.max(8, process.stdout.rows ?? 40);
9873
+ const body = buildLauncherBody(context, options, selectedIndex, width, rows);
9128
9874
  return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
9129
9875
  `)}`;
9130
9876
  }
@@ -9171,12 +9917,8 @@ function buildOutputSectionLines(title, content, width) {
9171
9917
  if (!normalized)
9172
9918
  return [];
9173
9919
  const lines = normalized.split(`
9174
- `).map((line) => truncateVisible2(line, width));
9175
- return [
9176
- color(title, WHITE + BOLD),
9177
- ...lines,
9178
- ""
9179
- ];
9920
+ `).map((line2) => truncateVisible2(line2, width));
9921
+ return [color(title, WHITE + BOLD), ...lines, ""];
9180
9922
  }
9181
9923
  function renderResult(request, result, scrollOffset = 0) {
9182
9924
  const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
@@ -9205,13 +9947,7 @@ function renderResult(request, result, scrollOffset = 0) {
9205
9947
  const visibleBody = body.slice(effectiveOffset, effectiveOffset + viewportHeight);
9206
9948
  const padding = Array.from({ length: Math.max(0, viewportHeight - visibleBody.length) }, () => "");
9207
9949
  const scrollStatus = body.length > viewportHeight ? color(`Lines ${effectiveOffset + 1}-${Math.min(body.length, effectiveOffset + viewportHeight)} of ${body.length}`, DIM) : color("All command output is visible.", DIM);
9208
- const lines = [
9209
- ...header,
9210
- ...visibleBody,
9211
- ...padding,
9212
- scrollStatus,
9213
- ...footer
9214
- ];
9950
+ const lines = [...header, ...visibleBody, ...padding, scrollStatus, ...footer];
9215
9951
  return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
9216
9952
  `)}`;
9217
9953
  }
@@ -9248,6 +9984,7 @@ class InteractiveExitError extends Error {
9248
9984
  var PROVIDER_CHOICES = [
9249
9985
  { value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
9250
9986
  { value: "codex", label: "Codex", description: "OpenAI session logs" },
9987
+ { value: "cursor", label: "Cursor", description: "Cursor API cache exports" },
9251
9988
  { value: "pi", label: "Pi", description: "pi-mono local session logs" },
9252
9989
  { value: "open-code", label: "Open Code", description: "Open Code storage and database" }
9253
9990
  ];
@@ -9287,7 +10024,7 @@ function renderChoiceScreen(title, description, options, selectedIndex, selected
9287
10024
  `)}`;
9288
10025
  }
9289
10026
  async function ask(prompt, initialValue = "") {
9290
- const readline = createInterface({
10027
+ const readline = createInterface2({
9291
10028
  input: process.stdin,
9292
10029
  output: process.stdout
9293
10030
  });
@@ -9495,8 +10232,16 @@ Press Enter to try again.`);
9495
10232
  async function promptCompareSetting() {
9496
10233
  const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
9497
10234
  { value: "off", label: "No compare", description: "Render a standard single-period report" },
9498
- { value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
9499
- { value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
10235
+ {
10236
+ value: "auto",
10237
+ label: "Auto compare",
10238
+ description: "Split the selected window automatically"
10239
+ },
10240
+ {
10241
+ value: "custom",
10242
+ label: "Custom compare range",
10243
+ description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range"
10244
+ }
9500
10245
  ]);
9501
10246
  if (choice === "off")
9502
10247
  return null;
@@ -9629,13 +10374,13 @@ async function buildImagePreset(format) {
9629
10374
  const rangeArgs = await promptDateWindow();
9630
10375
  const providers = await promptProviderSelection("Provider Filter");
9631
10376
  const compare = await promptCompareSetting();
9632
- const output = await promptOutputPath(`tokenleak.${format}`);
10377
+ const output2 = await promptOutputPath(`tokenleak.${format}`);
9633
10378
  const shouldOpen = await askYesNo("Open the file when done", true);
9634
10379
  const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
9635
10380
  const args = {
9636
10381
  format,
9637
10382
  theme,
9638
- output,
10383
+ output: output2,
9639
10384
  open: shouldOpen,
9640
10385
  more,
9641
10386
  ...rangeArgs
@@ -9649,12 +10394,12 @@ async function buildWrappedPreset() {
9649
10394
  const theme = await promptTheme();
9650
10395
  const rangeArgs = await promptDateWindow();
9651
10396
  const providers = await promptProviderSelection("Provider Filter");
9652
- const output = await promptOutputPath("tokenleak-wrapped.png");
10397
+ const output2 = await promptOutputPath("tokenleak-wrapped.png");
9653
10398
  const shouldOpen = await askYesNo("Open the image when done", true);
9654
10399
  const args = {
9655
10400
  format: "wrapped",
9656
10401
  theme,
9657
- output,
10402
+ output: output2,
9658
10403
  open: shouldOpen,
9659
10404
  ...rangeArgs
9660
10405
  };
@@ -9665,8 +10410,16 @@ async function buildComparePreset() {
9665
10410
  const rangeArgs = await promptDateWindow();
9666
10411
  const providers = await promptProviderSelection();
9667
10412
  const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
9668
- { value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
9669
- { value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
10413
+ {
10414
+ value: "auto",
10415
+ label: "Auto compare",
10416
+ description: "Split the chosen window automatically"
10417
+ },
10418
+ {
10419
+ value: "custom",
10420
+ label: "Custom compare range",
10421
+ description: "Enter an explicit prior range manually"
10422
+ }
9670
10423
  ]);
9671
10424
  const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
9672
10425
  const saveToFile = await askYesNo("Write compare output to a file", false);
@@ -9684,20 +10437,24 @@ async function buildComparePreset() {
9684
10437
  async function buildExplainPreset() {
9685
10438
  const date = await ask("Date to explain (YYYY-MM-DD)");
9686
10439
  const format = await promptSingleChoice("Explain Format", "Choose how the explain report should be rendered.", [
9687
- { value: "terminal", label: "Terminal", description: "Narrative report in the current terminal" },
10440
+ {
10441
+ value: "terminal",
10442
+ label: "Terminal",
10443
+ description: "Narrative report in the current terminal"
10444
+ },
9688
10445
  { value: "json", label: "JSON", description: "Structured explain payload" }
9689
10446
  ]);
9690
10447
  const providers = await promptProviderSelection();
9691
10448
  const width = format === "terminal" ? await promptWidth() : null;
9692
- const output = await ask("Output file (blank keeps stdout)");
10449
+ const output2 = await ask("Output file (blank keeps stdout)");
9693
10450
  const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
9694
10451
  const args = {
9695
10452
  format
9696
10453
  };
9697
10454
  if (width)
9698
10455
  args["width"] = width;
9699
- if (output)
9700
- args["output"] = output;
10456
+ if (output2)
10457
+ args["output"] = output2;
9701
10458
  if (noColor2)
9702
10459
  args["noColor"] = true;
9703
10460
  applySelectedProviders(args, providers);
@@ -9709,13 +10466,17 @@ async function buildExplainPreset() {
9709
10466
  }
9710
10467
  async function buildFocusPreset() {
9711
10468
  const format = await promptSingleChoice("Focus Format", "Choose how the focus report should be rendered.", [
9712
- { value: "terminal", label: "Terminal", description: "Ranked session report in the current terminal" },
10469
+ {
10470
+ value: "terminal",
10471
+ label: "Terminal",
10472
+ description: "Ranked session report in the current terminal"
10473
+ },
9713
10474
  { value: "json", label: "JSON", description: "Structured focus payload" }
9714
10475
  ]);
9715
10476
  const rangeArgs = await promptDateWindow();
9716
10477
  const providers = await promptProviderSelection();
9717
10478
  const width = format === "terminal" ? await promptWidth() : null;
9718
- const output = await ask("Output file (blank keeps stdout)");
10479
+ const output2 = await ask("Output file (blank keeps stdout)");
9719
10480
  const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
9720
10481
  const args = {
9721
10482
  format,
@@ -9723,8 +10484,8 @@ async function buildFocusPreset() {
9723
10484
  };
9724
10485
  if (width)
9725
10486
  args["width"] = width;
9726
- if (output)
9727
- args["output"] = output;
10487
+ if (output2)
10488
+ args["output"] = output2;
9728
10489
  if (noColor2)
9729
10490
  args["noColor"] = true;
9730
10491
  applySelectedProviders(args, providers);
@@ -9736,20 +10497,24 @@ async function buildFocusPreset() {
9736
10497
  }
9737
10498
  async function buildAdvisorPreset() {
9738
10499
  const format = await promptSingleChoice("Advisor Format", "Choose how the advisor report should be rendered.", [
9739
- { value: "terminal", label: "Terminal", description: "Efficiency recommendations in the current terminal" },
10500
+ {
10501
+ value: "terminal",
10502
+ label: "Terminal",
10503
+ description: "Efficiency recommendations in the current terminal"
10504
+ },
9740
10505
  { value: "json", label: "JSON", description: "Structured advisor payload" }
9741
10506
  ]);
9742
10507
  const rangeArgs = await promptDateWindow();
9743
10508
  const providers = await promptProviderSelection();
9744
- const output = await ask("Output file (blank keeps stdout)");
10509
+ const output2 = await ask("Output file (blank keeps stdout)");
9745
10510
  const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
9746
10511
  const args = {
9747
10512
  format,
9748
10513
  advisor: true,
9749
10514
  ...rangeArgs
9750
10515
  };
9751
- if (output)
9752
- args["output"] = output;
10516
+ if (output2)
10517
+ args["output"] = output2;
9753
10518
  if (noColor2)
9754
10519
  args["noColor"] = true;
9755
10520
  applySelectedProviders(args, providers);
@@ -9796,14 +10561,30 @@ async function askFormatChoice() {
9796
10561
  { value: "json", label: "JSON", description: "Structured machine-readable output" },
9797
10562
  { value: "svg", label: "SVG", description: "Shareable vector export" },
9798
10563
  { value: "png", label: "PNG", description: "Raster export for social and docs" },
9799
- { value: "wrapped", label: "\uD83C\uDF89 Wrapped", description: "Your AI coding story card (PNG)" }
10564
+ {
10565
+ value: "wrapped",
10566
+ label: "\uD83C\uDF89 Wrapped",
10567
+ description: "Your AI coding story card (PNG)"
10568
+ }
9800
10569
  ]);
9801
10570
  }
9802
10571
  async function buildCustomCommand() {
9803
10572
  const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
9804
- { value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
9805
- { value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
9806
- { value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
10573
+ {
10574
+ value: "run",
10575
+ label: "Standard command",
10576
+ description: "Render terminal, JSON, SVG, or PNG output"
10577
+ },
10578
+ {
10579
+ value: "live-server",
10580
+ label: "Live server",
10581
+ description: "Launch the browser dashboard locally"
10582
+ },
10583
+ {
10584
+ value: "list-providers",
10585
+ label: "List providers",
10586
+ description: "Inspect registered provider backends"
10587
+ }
9807
10588
  ]);
9808
10589
  if (mode === "live-server") {
9809
10590
  return buildLivePreset();
@@ -9817,7 +10598,7 @@ async function buildCustomCommand() {
9817
10598
  const providers = await promptProviderSelection();
9818
10599
  const compare = await promptCompareSetting();
9819
10600
  const width = format === "terminal" ? await promptWidth() : null;
9820
- const output = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
10601
+ const output2 = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
9821
10602
  const noColor2 = await askYesNo("Disable ANSI colors", false);
9822
10603
  const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
9823
10604
  const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
@@ -9834,8 +10615,8 @@ async function buildCustomCommand() {
9834
10615
  args["compare"] = compare;
9835
10616
  if (width)
9836
10617
  args["width"] = width;
9837
- if (output)
9838
- args["output"] = output;
10618
+ if (output2)
10619
+ args["output"] = output2;
9839
10620
  if (noColor2)
9840
10621
  args["noColor"] = true;
9841
10622
  if (noInsights)
@@ -10250,11 +11031,11 @@ async function uploadToGist(content, filename, description) {
10250
11031
  if (!available) {
10251
11032
  throw new Error("GitHub CLI (gh) is not installed or not authenticated. " + "Install it from https://cli.github.com and run `gh auth login`.");
10252
11033
  }
10253
- const { join: join6 } = await import("path");
11034
+ const { join: join8 } = await import("path");
10254
11035
  const { tmpdir } = await import("os");
10255
- const { writeFileSync, unlinkSync } = await import("fs");
10256
- const tmpPath = join6(tmpdir(), `tokenleak-gist-${Date.now()}-${filename}`);
10257
- writeFileSync(tmpPath, content, "utf-8");
11036
+ const { writeFileSync: writeFileSync2, unlinkSync: unlinkSync2 } = await import("fs");
11037
+ const tmpPath = join8(tmpdir(), `tokenleak-gist-${Date.now()}-${filename}`);
11038
+ writeFileSync2(tmpPath, content, "utf-8");
10258
11039
  try {
10259
11040
  const proc = Bun.spawn(["gh", "gist", "create", tmpPath, "--desc", description, "--public"], {
10260
11041
  stdout: "pipe",
@@ -10272,7 +11053,7 @@ async function uploadToGist(content, filename, description) {
10272
11053
  return stdout;
10273
11054
  } finally {
10274
11055
  try {
10275
- unlinkSync(tmpPath);
11056
+ unlinkSync2(tmpPath);
10276
11057
  } catch {}
10277
11058
  }
10278
11059
  }
@@ -10323,9 +11104,9 @@ async function loadForRange(state, providers, timeRange) {
10323
11104
  if (inflight)
10324
11105
  return inflight;
10325
11106
  const range = resolveRange(state, timeRange);
10326
- const loadPromise = (state.compare ? loadCompareTokenleakData(providers, range, state.compare).then((result) => result.output) : loadTokenleakData(providers, range)).then((output) => {
10327
- state.dataCache.set(timeRange, output);
10328
- return output;
11107
+ const loadPromise = (state.compare ? loadCompareTokenleakData(providers, range, state.compare).then((result) => result.output) : loadTokenleakData(providers, range)).then((output2) => {
11108
+ state.dataCache.set(timeRange, output2);
11109
+ return output2;
10329
11110
  }).finally(() => {
10330
11111
  state.inflightLoads.delete(timeRange);
10331
11112
  });
@@ -10345,7 +11126,7 @@ function getViewportHeight(state, width, rows) {
10345
11126
  const footerLines = 1;
10346
11127
  return Math.max(4, rows - headerLines - footerLines - 1);
10347
11128
  }
10348
- function renderActiveView(output, tab, width, noColor2, noInsights) {
11129
+ function renderActiveView(output2, tab, width, noColor2, noInsights) {
10349
11130
  const options = {
10350
11131
  format: "terminal",
10351
11132
  theme: "dark",
@@ -10357,38 +11138,38 @@ function renderActiveView(output, tab, width, noColor2, noInsights) {
10357
11138
  };
10358
11139
  switch (tab) {
10359
11140
  case "overview":
10360
- return renderOverviewView(output, options);
11141
+ return renderOverviewView(output2, options);
10361
11142
  case "delta":
10362
- return renderCompareView(output, width, noColor2);
11143
+ return renderCompareView(output2, width, noColor2);
10363
11144
  case "provider":
10364
- return renderProviderView(output, width, noColor2);
11145
+ return renderProviderView(output2, width, noColor2);
10365
11146
  case "sess":
10366
- return renderSessionView(output, width, noColor2);
11147
+ return renderSessionView(output2, width, noColor2);
10367
11148
  case "tok":
10368
- return renderTokenView(output, width, noColor2);
11149
+ return renderTokenView(output2, width, noColor2);
10369
11150
  case "model":
10370
- return renderModelView(output, width, noColor2);
11151
+ return renderModelView(output2, width, noColor2);
10371
11152
  case "cwd":
10372
- return renderCwdView(output, width, noColor2);
11153
+ return renderCwdView(output2, width, noColor2);
10373
11154
  case "dow":
10374
- return renderDowView(output, width, noColor2);
11155
+ return renderDowView(output2, width, noColor2);
10375
11156
  case "tod":
10376
- return renderTodView(output, width, noColor2);
11157
+ return renderTodView(output2, width, noColor2);
10377
11158
  default:
10378
- return renderOverviewView(output, options);
11159
+ return renderOverviewView(output2, options);
10379
11160
  }
10380
11161
  }
10381
- function renderScreen(output, state) {
11162
+ function renderScreen(output2, state) {
10382
11163
  const width = getRenderWidth(state);
10383
11164
  const rows = process.stdout.rows ?? 40;
10384
11165
  const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
10385
11166
  const tabBarLines = tabBar.split(`
10386
11167
  `);
10387
- const rangeLabel = state.noColor ? ` ${output.dateRange.since} \u2192 ${output.dateRange.until}` : ` ${DIM2}${output.dateRange.since} \u2192 ${output.dateRange.until}${RESET3}`;
11168
+ const rangeLabel = state.noColor ? ` ${output2.dateRange.since} \u2192 ${output2.dateRange.until}` : ` ${DIM2}${output2.dateRange.since} \u2192 ${output2.dateRange.until}${RESET3}`;
10388
11169
  const headerLines = [...tabBarLines, rangeLabel, ""];
10389
11170
  const footerLines = [""];
10390
11171
  const viewportHeight = getViewportHeight(state, width, rows);
10391
- const viewContent = renderActiveView(output, state.metricTab, width, state.noColor, state.noInsights);
11172
+ const viewContent = renderActiveView(output2, state.metricTab, width, state.noColor, state.noInsights);
10392
11173
  const contentLines = viewContent.split(`
10393
11174
  `);
10394
11175
  const effectiveOffset = clampScrollOffset(state.scrollOffset, contentLines.length, viewportHeight);
@@ -10458,9 +11239,9 @@ async function startTabbedDashboard(providers, options) {
10458
11239
  const loadAndRender = async (timeRange) => {
10459
11240
  const loadId = ++activeLoadId;
10460
11241
  paint2(renderLoading2(state));
10461
- let output;
11242
+ let output2;
10462
11243
  try {
10463
- output = await loadForRange(state, providers, timeRange);
11244
+ output2 = await loadForRange(state, providers, timeRange);
10464
11245
  } catch (error) {
10465
11246
  if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
10466
11247
  return;
@@ -10470,7 +11251,7 @@ async function startTabbedDashboard(providers, options) {
10470
11251
  if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
10471
11252
  return;
10472
11253
  }
10473
- currentOutput = output;
11254
+ currentOutput = output2;
10474
11255
  paint2(renderScreen(currentOutput, state));
10475
11256
  };
10476
11257
  const rerender = () => {
@@ -10598,6 +11379,7 @@ var THEME_VALUES = ["dark", "light"];
10598
11379
  var PROVIDER_SHORTCUTS = {
10599
11380
  claude: "claude-code",
10600
11381
  codex: "codex",
11382
+ cursor: "cursor",
10601
11383
  pi: "pi",
10602
11384
  openCode: "open-code"
10603
11385
  };
@@ -10607,6 +11389,9 @@ var PROVIDER_ALIASES = {
10607
11389
  "claude-code": "claude-code",
10608
11390
  claudecode: "claude-code",
10609
11391
  codex: "codex",
11392
+ cursor: "cursor",
11393
+ "cursor-ide": "cursor",
11394
+ cursoride: "cursor",
10610
11395
  openai: "codex",
10611
11396
  pi: "pi",
10612
11397
  "pi-mono": "pi",
@@ -10617,6 +11402,7 @@ var PROVIDER_ALIASES = {
10617
11402
  var PROVIDER_ALIAS_GROUPS = {
10618
11403
  "claude-code": ["anthropic", "claude", "claudecode"],
10619
11404
  codex: ["openai"],
11405
+ cursor: ["cursor-ide", "cursoride"],
10620
11406
  pi: ["pi-mono"],
10621
11407
  "open-code": ["opencode", "open_code"]
10622
11408
  };
@@ -10638,6 +11424,8 @@ function getRequestedProviders(config) {
10638
11424
  requested.add(PROVIDER_SHORTCUTS.claude);
10639
11425
  if (config.codex)
10640
11426
  requested.add(PROVIDER_SHORTCUTS.codex);
11427
+ if (config.cursor)
11428
+ requested.add(PROVIDER_SHORTCUTS.cursor);
10641
11429
  if (config.pi)
10642
11430
  requested.add(PROVIDER_SHORTCUTS.pi);
10643
11431
  if (config.openCode)
@@ -10663,14 +11451,17 @@ function buildHelpText() {
10663
11451
  " tokenleak [flags]",
10664
11452
  " tokenleak explain <date> [flags]",
10665
11453
  " tokenleak focus [flags]",
11454
+ " tokenleak cursor <command>",
10666
11455
  "",
10667
11456
  "Subcommands:",
10668
11457
  " explain <date> Explain what drove usage on one day",
10669
11458
  " focus Rank sessions by deep-work score",
11459
+ " cursor Manage Cursor auth and cache sync",
10670
11460
  "",
10671
11461
  "Provider Shortcuts:",
10672
11462
  " --claude Only include Claude Code",
10673
11463
  " --codex Only include Codex",
11464
+ " --cursor Only include Cursor",
10674
11465
  " --pi Only include Pi",
10675
11466
  " --open-code Only include OpenCode",
10676
11467
  " --all-providers Ignore provider filters and use every available provider",
@@ -10739,6 +11530,7 @@ function buildFocusHelpText() {
10739
11530
  " -p, --provider <list> Provider filter list, comma-separated",
10740
11531
  " --claude Only include Claude Code",
10741
11532
  " --codex Only include Codex",
11533
+ " --cursor Only include Cursor",
10742
11534
  " --pi Only include Pi",
10743
11535
  " --open-code Only include OpenCode",
10744
11536
  " --all-providers Ignore provider filters and use every available provider",
@@ -10787,6 +11579,9 @@ function buildInteractiveSummary(cliArgs, ok, exitCode) {
10787
11579
  if (cliArgs["subcommand"] === "focus") {
10788
11580
  return "Focus report generated.";
10789
11581
  }
11582
+ if (cliArgs["subcommand"] === "cursor") {
11583
+ return "Cursor command completed.";
11584
+ }
10790
11585
  if (cliArgs["listProviders"]) {
10791
11586
  return "Provider registry loaded.";
10792
11587
  }
@@ -10885,6 +11680,7 @@ function normalizeCliArgv(argv) {
10885
11680
  function registerBuiltInProviders(registry) {
10886
11681
  registry.register(new ClaudeCodeProvider);
10887
11682
  registry.register(new CodexProvider);
11683
+ registry.register(new CursorProvider);
10888
11684
  registry.register(new PiProvider);
10889
11685
  registry.register(new OpenCodeProvider);
10890
11686
  }
@@ -10908,17 +11704,33 @@ function createRegistry() {
10908
11704
  return registry;
10909
11705
  }
10910
11706
  function validateProviderSelection(config) {
10911
- if (config.allProviders && (config.provider || config.claude || config.codex || config.pi || config.openCode)) {
11707
+ if (config.allProviders && (config.provider || config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
10912
11708
  throw new TokenleakError("--all-providers cannot be combined with provider filters");
10913
11709
  }
10914
11710
  }
10915
11711
  async function selectAvailableProviders(config) {
10916
11712
  validateProviderSelection(config);
11713
+ const requestedProviders = getRequestedProviders(config);
11714
+ const requestedCursor = requestedProviders.has(PROVIDER_SHORTCUTS.cursor);
11715
+ if (requestedCursor && !isCursorLoggedIn() && !hasCursorUsageCache()) {
11716
+ throw new TokenleakError("Cursor is selected but not authenticated. Run `tokenleak cursor login` first.");
11717
+ }
11718
+ const cursorSync = await shouldSyncCursorForRun(config);
11719
+ if (cursorSync.attempted && cursorSync.error) {
11720
+ if (hasCursorUsageCache()) {
11721
+ process.stderr.write(`Cursor sync failed, using cached data: ${cursorSync.error}
11722
+ `);
11723
+ } else if (requestedCursor) {
11724
+ throw new TokenleakError(cursorSync.error);
11725
+ } else {
11726
+ process.stderr.write(`Cursor sync skipped: ${cursorSync.error}
11727
+ `);
11728
+ }
11729
+ }
10917
11730
  const registry = createRegistry();
10918
11731
  let available = await registry.getAvailable();
10919
- const requestedProviders = getRequestedProviders(config);
10920
11732
  if (!config.allProviders && requestedProviders.size > 0) {
10921
- if (config.provider && (config.claude || config.codex || config.pi || config.openCode)) {
11733
+ if (config.provider && (config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
10922
11734
  process.stderr.write(`Combining provider filters: ${Array.from(requestedProviders).join(", ")}
10923
11735
  `);
10924
11736
  }
@@ -10926,6 +11738,20 @@ async function selectAvailableProviders(config) {
10926
11738
  }
10927
11739
  return available;
10928
11740
  }
11741
+ function resolveTabbedDashboardProviderConfig(opts) {
11742
+ return {
11743
+ provider: opts.providerNames && opts.providerNames.length > 0 ? opts.providerNames.join(",") : undefined,
11744
+ claude: false,
11745
+ codex: false,
11746
+ cursor: false,
11747
+ pi: false,
11748
+ openCode: false,
11749
+ allProviders: false
11750
+ };
11751
+ }
11752
+ async function resolveTabbedDashboardProviders(opts) {
11753
+ return selectAvailableProviders(resolveTabbedDashboardProviderConfig(opts));
11754
+ }
10929
11755
  async function loadProviderData(config) {
10930
11756
  const dateRange = computeDateRange({
10931
11757
  since: config.since,
@@ -10980,6 +11806,7 @@ function resolveConfig(cliArgs) {
10980
11806
  more: false,
10981
11807
  claude: false,
10982
11808
  codex: false,
11809
+ cursor: false,
10983
11810
  pi: false,
10984
11811
  openCode: false,
10985
11812
  allProviders: false,
@@ -11062,6 +11889,9 @@ function resolveConfig(cliArgs) {
11062
11889
  if (cliArgs["codex"] !== undefined) {
11063
11890
  result.codex = cliArgs["codex"];
11064
11891
  }
11892
+ if (cliArgs["cursor"] !== undefined) {
11893
+ result.cursor = cliArgs["cursor"];
11894
+ }
11065
11895
  if (cliArgs["pi"] !== undefined) {
11066
11896
  result.pi = cliArgs["pi"];
11067
11897
  }
@@ -11108,6 +11938,7 @@ function resolveFocusConfig(cliArgs) {
11108
11938
  provider: undefined,
11109
11939
  claude: false,
11110
11940
  codex: false,
11941
+ cursor: false,
11111
11942
  pi: false,
11112
11943
  openCode: false,
11113
11944
  allProviders: false,
@@ -11169,6 +12000,9 @@ function resolveFocusConfig(cliArgs) {
11169
12000
  if (cliArgs["codex"] !== undefined) {
11170
12001
  result.codex = cliArgs["codex"];
11171
12002
  }
12003
+ if (cliArgs["cursor"] !== undefined) {
12004
+ result.cursor = cliArgs["cursor"];
12005
+ }
11172
12006
  if (cliArgs["pi"] !== undefined) {
11173
12007
  result.pi = cliArgs["pi"];
11174
12008
  }
@@ -11215,9 +12049,10 @@ function formatFocusDuration(durationMs) {
11215
12049
  function formatFocusDensity(tokensPerHour) {
11216
12050
  return `${Math.round(tokensPerHour).toLocaleString("en-US")}/h`;
11217
12051
  }
11218
- var PROVIDER_COLORS2 = {
12052
+ var PROVIDER_COLORS3 = {
11219
12053
  "claude-code": 179,
11220
12054
  codex: 71,
12055
+ cursor: 78,
11221
12056
  pi: 73,
11222
12057
  "open-code": 68
11223
12058
  };
@@ -11243,7 +12078,7 @@ function colorDensity(tokensPerHour, text2, noColor2) {
11243
12078
  return dim(text2, noColor2);
11244
12079
  }
11245
12080
  function colorProvider(provider, text2, noColor2) {
11246
- const code = PROVIDER_COLORS2[provider] ?? 246;
12081
+ const code = PROVIDER_COLORS3[provider] ?? 246;
11247
12082
  return colorize256(text2, code, noColor2);
11248
12083
  }
11249
12084
  function colorStreak(streak, text2, noColor2) {
@@ -11382,7 +12217,7 @@ async function runFocus(cliArgs) {
11382
12217
  if (events.length === 0) {
11383
12218
  const emptyMsg = config.format === "json" ? JSON.stringify({ method: "No event data", entries: [] }, null, 2) : renderFocusReport({ method: "No event-level data found for focus analysis.", entries: [] }, config.width, config.noColor);
11384
12219
  if (config.output) {
11385
- writeFileSync(config.output, emptyMsg);
12220
+ writeFileSync2(config.output, emptyMsg);
11386
12221
  } else {
11387
12222
  process.stdout.write(`${emptyMsg}
11388
12223
  `);
@@ -11392,7 +12227,7 @@ async function runFocus(cliArgs) {
11392
12227
  const report = buildFocusReport(events);
11393
12228
  const rendered = config.format === "json" ? JSON.stringify(report, null, 2) : renderFocusReport(report, config.width, config.noColor);
11394
12229
  if (config.output) {
11395
- writeFileSync(config.output, rendered);
12230
+ writeFileSync2(config.output, rendered);
11396
12231
  } else {
11397
12232
  process.stdout.write(`${rendered}
11398
12233
  `);
@@ -11441,7 +12276,7 @@ async function run(cliArgs) {
11441
12276
  const rendered3 = await renderer2.render(compareResult.output, renderOptions2);
11442
12277
  if (config.output) {
11443
12278
  const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
11444
- writeFileSync(config.output, data);
12279
+ writeFileSync2(config.output, data);
11445
12280
  } else {
11446
12281
  const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
11447
12282
  process.stdout.write(text2 + `
@@ -11462,7 +12297,7 @@ async function run(cliArgs) {
11462
12297
  };
11463
12298
  const rendered3 = await renderer2.render(compareResult.output, renderOptions2);
11464
12299
  if (config.output) {
11465
- writeFileSync(config.output, rendered3);
12300
+ writeFileSync2(config.output, rendered3);
11466
12301
  } else {
11467
12302
  process.stdout.write(`${rendered3}
11468
12303
  `);
@@ -11475,7 +12310,7 @@ async function run(cliArgs) {
11475
12310
  }
11476
12311
  const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
11477
12312
  if (config.output) {
11478
- writeFileSync(config.output, rendered2);
12313
+ writeFileSync2(config.output, rendered2);
11479
12314
  } else {
11480
12315
  process.stdout.write(rendered2 + `
11481
12316
  `);
@@ -11502,7 +12337,7 @@ async function run(cliArgs) {
11502
12337
  process.stderr.write(`Computing extended analytics (hourOfDay, sessions, cache, projections)...
11503
12338
  `);
11504
12339
  }
11505
- const output = {
12340
+ const output2 = {
11506
12341
  schemaVersion: SCHEMA_VERSION,
11507
12342
  generated: new Date().toISOString(),
11508
12343
  dateRange,
@@ -11514,11 +12349,11 @@ async function run(cliArgs) {
11514
12349
  if (config.format !== "terminal" && config.format !== "json") {
11515
12350
  throw new TokenleakError(`--advisor only supports terminal and json formats, got "${config.format}".`);
11516
12351
  }
11517
- const advisorReport = analyzeEfficiency(output, MODEL_PRICING);
12352
+ const advisorReport = analyzeEfficiency(output2, MODEL_PRICING);
11518
12353
  if (config.format === "json") {
11519
12354
  const rendered3 = JSON.stringify(advisorReport, null, 2);
11520
12355
  if (config.output) {
11521
- writeFileSync(config.output, rendered3);
12356
+ writeFileSync2(config.output, rendered3);
11522
12357
  } else {
11523
12358
  process.stdout.write(rendered3 + `
11524
12359
  `);
@@ -11530,7 +12365,7 @@ async function run(cliArgs) {
11530
12365
  noColor: config.noColor
11531
12366
  });
11532
12367
  if (config.output) {
11533
- writeFileSync(config.output, rendered2);
12368
+ writeFileSync2(config.output, rendered2);
11534
12369
  } else {
11535
12370
  process.stdout.write(rendered2 + `
11536
12371
  `);
@@ -11539,8 +12374,8 @@ async function run(cliArgs) {
11539
12374
  }
11540
12375
  if (config.format === "wrapped") {
11541
12376
  const outputPath = config.output ?? "tokenleak-wrapped.png";
11542
- const wrappedBuffer = await renderWrappedPng(output, { theme: config.theme });
11543
- writeFileSync(outputPath, wrappedBuffer);
12377
+ const wrappedBuffer = await renderWrappedPng(output2, { theme: config.theme });
12378
+ writeFileSync2(outputPath, wrappedBuffer);
11544
12379
  process.stderr.write(`Wrapped PNG written to ${outputPath}
11545
12380
  `);
11546
12381
  if (config.clipboard) {
@@ -11587,7 +12422,7 @@ async function run(cliArgs) {
11587
12422
  output: config.output,
11588
12423
  more: config.more
11589
12424
  };
11590
- const { port } = await startLiveServer(output, renderOptions2);
12425
+ const { port } = await startLiveServer(output2, renderOptions2);
11591
12426
  await new Promise((resolve) => {
11592
12427
  process.on("SIGINT", () => {
11593
12428
  process.stderr.write(`
@@ -11617,7 +12452,7 @@ Shutting down server...
11617
12452
  }
11618
12453
  process.stderr.write(`Generating wrapped presentation...
11619
12454
  `);
11620
- const { stop } = await startWrappedLiveServer(output);
12455
+ const { stop } = await startWrappedLiveServer(output2);
11621
12456
  process.stderr.write(`Press Ctrl+C to stop the server.
11622
12457
  `);
11623
12458
  await new Promise((resolve) => {
@@ -11646,10 +12481,10 @@ Shutting down wrapped live server...
11646
12481
  output: config.output,
11647
12482
  more: config.more
11648
12483
  };
11649
- const rendered = await renderer.render(output, renderOptions);
12484
+ const rendered = await renderer.render(output2, renderOptions);
11650
12485
  if (config.output) {
11651
12486
  const data = typeof rendered === "string" ? rendered : Buffer.from(rendered);
11652
- writeFileSync(config.output, data);
12487
+ writeFileSync2(config.output, data);
11653
12488
  } else {
11654
12489
  const text2 = typeof rendered === "string" ? rendered : rendered.toString("utf-8");
11655
12490
  process.stdout.write(text2 + `
@@ -11751,6 +12586,10 @@ function parseExplainArgs(argv) {
11751
12586
  cliArgs["codex"] = true;
11752
12587
  index += 1;
11753
12588
  break;
12589
+ case "--cursor":
12590
+ cliArgs["cursor"] = true;
12591
+ index += 1;
12592
+ break;
11754
12593
  case "--pi":
11755
12594
  cliArgs["pi"] = true;
11756
12595
  index += 1;
@@ -11795,17 +12634,11 @@ function resolveExplainFormat(cliArgs) {
11795
12634
  async function runExplain(date, cliArgs) {
11796
12635
  const config = resolveConfig(cliArgs);
11797
12636
  const format = resolveExplainFormat(cliArgs);
11798
- if (config.allProviders && (config.provider || config.claude || config.codex || config.pi || config.openCode)) {
12637
+ if (config.allProviders && (config.provider || config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
11799
12638
  throw new TokenleakError("--all-providers cannot be combined with provider filters");
11800
12639
  }
11801
12640
  const explainRange = computeDateRange({ until: date, days: 30 });
11802
- const registry = new ProviderRegistry;
11803
- registerBuiltInProviders(registry);
11804
- let available = await registry.getAvailable();
11805
- const requestedProviders = getRequestedProviders(config);
11806
- if (!config.allProviders && requestedProviders.size > 0) {
11807
- available = available.filter((provider) => providerMatchesFilter(provider, requestedProviders));
11808
- }
12641
+ const available = await selectAvailableProviders(config);
11809
12642
  if (available.length === 0) {
11810
12643
  throw new TokenleakError("No provider data found");
11811
12644
  }
@@ -11813,7 +12646,7 @@ async function runExplain(date, cliArgs) {
11813
12646
  const report = buildExplainReport(explainOutput.providers, date);
11814
12647
  const rendered = format === "json" ? JSON.stringify(report, null, 2) : renderExplainTerminal(report, config.width);
11815
12648
  if (config.output) {
11816
- writeFileSync(config.output, rendered);
12649
+ writeFileSync2(config.output, rendered);
11817
12650
  } else {
11818
12651
  process.stdout.write(rendered + `
11819
12652
  `);
@@ -11895,6 +12728,11 @@ var main = defineCommand({
11895
12728
  description: "Shortcut for --provider codex",
11896
12729
  default: false
11897
12730
  },
12731
+ cursor: {
12732
+ type: "boolean",
12733
+ description: "Shortcut for --provider cursor",
12734
+ default: false
12735
+ },
11898
12736
  pi: {
11899
12737
  type: "boolean",
11900
12738
  description: "Shortcut for --provider pi",
@@ -11977,6 +12815,8 @@ var main = defineCommand({
11977
12815
  cliArgs["claude"] = true;
11978
12816
  if (args.codex)
11979
12817
  cliArgs["codex"] = true;
12818
+ if (args.cursor)
12819
+ cliArgs["cursor"] = true;
11980
12820
  if (args.pi)
11981
12821
  cliArgs["pi"] = true;
11982
12822
  if (args.openCode)
@@ -12060,6 +12900,11 @@ var focusMain = defineCommand({
12060
12900
  description: "Shortcut for --provider codex",
12061
12901
  default: false
12062
12902
  },
12903
+ cursor: {
12904
+ type: "boolean",
12905
+ description: "Shortcut for --provider cursor",
12906
+ default: false
12907
+ },
12063
12908
  pi: {
12064
12909
  type: "boolean",
12065
12910
  description: "Shortcut for --provider pi",
@@ -12104,6 +12949,8 @@ var focusMain = defineCommand({
12104
12949
  cliArgs["claude"] = true;
12105
12950
  if (args.codex)
12106
12951
  cliArgs["codex"] = true;
12952
+ if (args.cursor)
12953
+ cliArgs["cursor"] = true;
12107
12954
  if (args.pi)
12108
12955
  cliArgs["pi"] = true;
12109
12956
  if (args.openCode)
@@ -12153,6 +13000,22 @@ if (isDirectExecution) {
12153
13000
  await runMain(focusMain);
12154
13001
  process.exit(0);
12155
13002
  }
13003
+ if (argv[0] === "cursor") {
13004
+ try {
13005
+ if (argv[1] === "--help" || argv[1] === "-h" || argv.length === 1) {
13006
+ process.stdout.write(buildCursorHelpText());
13007
+ process.exit(0);
13008
+ }
13009
+ if (argv[1] === "--version" || argv[1] === "-v") {
13010
+ process.stdout.write(buildVersionText());
13011
+ process.exit(0);
13012
+ }
13013
+ await runCursorCommand(argv.slice(1));
13014
+ process.exit(0);
13015
+ } catch (error) {
13016
+ handleError(error);
13017
+ }
13018
+ }
12156
13019
  process.argv = [...process.argv.slice(0, 2), ...normalizedArgv];
12157
13020
  if (argv.includes("--help") || argv.includes("-h")) {
12158
13021
  process.stdout.write(buildHelpText());
@@ -12163,11 +13026,8 @@ if (isDirectExecution) {
12163
13026
  process.exit(0);
12164
13027
  }
12165
13028
  if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
12166
- const registry = createRegistry();
12167
- const available = await registry.getAvailable();
12168
13029
  const launchTabbed = async (opts) => {
12169
- const requested = new Set(opts.providerNames ?? []);
12170
- const scopedProviders = requested.size > 0 ? available.filter((provider) => providerMatchesFilter(provider, requested)) : available;
13030
+ const scopedProviders = await resolveTabbedDashboardProviders(opts);
12171
13031
  if (scopedProviders.length === 0) {
12172
13032
  throw new TokenleakError("No provider data found");
12173
13033
  }
@@ -12184,6 +13044,8 @@ if (isDirectExecution) {
12184
13044
  export {
12185
13045
  runFocus,
12186
13046
  run,
13047
+ resolveTabbedDashboardProviders,
13048
+ resolveTabbedDashboardProviderConfig,
12187
13049
  resolveFocusConfig,
12188
13050
  resolveConfig,
12189
13051
  renderFocusReport,