vibestats 1.3.11 → 1.3.13

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 (2) hide show
  1. package/dist/index.js +1173 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -848,9 +848,9 @@ function buildActivityGraph(stats, metric, requestedDays = 365) {
848
848
  };
849
849
  }
850
850
  function buildActivityTitle(stats, metric) {
851
- const sourceLabel = stats.scopeLabel ? stats.scopeLabel : stats.source === "codex" ? "Codex" : stats.source === "combined" ? "AI Coding" : "Local AI";
851
+ const sourceLabel2 = stats.scopeLabel ? stats.scopeLabel : stats.source === "codex" ? "Codex" : stats.source === "combined" ? "AI Coding" : "Local AI";
852
852
  const metricLabel = metric === "tokens" ? "Activity" : metric === "sessions" ? "Session Activity" : "Message Activity";
853
- return `${sourceLabel} ${metricLabel}`;
853
+ return `${sourceLabel2} ${metricLabel}`;
854
854
  }
855
855
  function buildActivityArtifactPayload(stats, metric, requestedDays = 365) {
856
856
  return {
@@ -2199,9 +2199,9 @@ function renderBreakdownLines(items, options = {}) {
2199
2199
  return items.map((item) => {
2200
2200
  const filled = clamp(Math.round(item.percentage / 100 * width), 0, width);
2201
2201
  const empty = Math.max(0, width - filled);
2202
- const bar = `${filledColor}${"\u2588".repeat(filled)}${emptyColor}${"\u2591".repeat(empty)}${reset}`;
2202
+ const bar2 = `${filledColor}${"\u2588".repeat(filled)}${emptyColor}${"\u2591".repeat(empty)}${reset}`;
2203
2203
  const suffix = item.valueLabel ? ` ${item.valueLabel}` : "";
2204
- return `${item.label.padEnd(labelWidth)} ${bar} ${item.percentage}%${suffix}`;
2204
+ return `${item.label.padEnd(labelWidth)} ${bar2} ${item.percentage}%${suffix}`;
2205
2205
  });
2206
2206
  }
2207
2207
 
@@ -3204,6 +3204,1091 @@ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
3204
3204
  };
3205
3205
  }
3206
3206
 
3207
+ // src/cli-intent.ts
3208
+ var COMMANDS = /* @__PURE__ */ new Set(["usage", "limits", "limit", "pace"]);
3209
+ var SCOPES = /* @__PURE__ */ new Set(["claude", "codex", "all", "combined"]);
3210
+ var USAGE_COMMAND_FLAGS = /* @__PURE__ */ new Set([
3211
+ "activity",
3212
+ "compact",
3213
+ "daily",
3214
+ "days",
3215
+ "last",
3216
+ "metric",
3217
+ "model",
3218
+ "monthly",
3219
+ "project",
3220
+ "sessions",
3221
+ "share",
3222
+ "since",
3223
+ "total",
3224
+ "until",
3225
+ "wrapped"
3226
+ ]);
3227
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
3228
+ "activity",
3229
+ "claude",
3230
+ "claude-limits",
3231
+ "claude-system",
3232
+ "codex",
3233
+ "combined",
3234
+ "compact",
3235
+ "config",
3236
+ "daily",
3237
+ "init",
3238
+ "json",
3239
+ "kimi",
3240
+ "limits",
3241
+ "minimax",
3242
+ "model",
3243
+ "monthly",
3244
+ "no-short",
3245
+ "project",
3246
+ "quiet",
3247
+ "sessions",
3248
+ "share",
3249
+ "total",
3250
+ "wrapped"
3251
+ ]);
3252
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["days", "last", "metric", "since", "until", "url"]);
3253
+ var SHORT_BOOLEAN_FLAGS = {
3254
+ c: "compact",
3255
+ d: "daily",
3256
+ m: "monthly",
3257
+ p: "project",
3258
+ q: "quiet",
3259
+ s: "share",
3260
+ t: "total",
3261
+ w: "wrapped"
3262
+ };
3263
+ var SHORT_VALUE_FLAGS = {
3264
+ l: "last"
3265
+ };
3266
+ function parseRawFlags(argv) {
3267
+ const parsed = {};
3268
+ for (let index = 0; index < argv.length; index++) {
3269
+ const arg = argv[index];
3270
+ if (!arg?.startsWith("-")) continue;
3271
+ if (arg.startsWith("--")) {
3272
+ const raw = arg.slice(2);
3273
+ const [key, inlineValue] = raw.split("=", 2);
3274
+ if (!key) continue;
3275
+ if (/^last\d+$/.test(key)) {
3276
+ parsed[key] = true;
3277
+ } else if (BOOLEAN_FLAGS.has(key)) {
3278
+ parsed[key] = true;
3279
+ } else if (VALUE_FLAGS.has(key)) {
3280
+ if (inlineValue !== void 0) {
3281
+ parsed[key] = inlineValue;
3282
+ } else {
3283
+ const next = argv[index + 1];
3284
+ if (next && !next.startsWith("-")) {
3285
+ parsed[key] = next;
3286
+ index++;
3287
+ }
3288
+ }
3289
+ }
3290
+ continue;
3291
+ }
3292
+ const short = arg.slice(1);
3293
+ if (!short) continue;
3294
+ if (SHORT_VALUE_FLAGS[short]) {
3295
+ const next = argv[index + 1];
3296
+ if (next && !next.startsWith("-")) {
3297
+ parsed[SHORT_VALUE_FLAGS[short]] = next;
3298
+ index++;
3299
+ }
3300
+ continue;
3301
+ }
3302
+ for (const char of short) {
3303
+ const key = SHORT_BOOLEAN_FLAGS[char];
3304
+ if (key) parsed[key] = true;
3305
+ }
3306
+ }
3307
+ return parsed;
3308
+ }
3309
+ function collectPositionals(argv) {
3310
+ const positionals = [];
3311
+ for (let index = 0; index < argv.length; index++) {
3312
+ const arg = argv[index];
3313
+ if (!arg) continue;
3314
+ if (arg.startsWith("--")) {
3315
+ const [key] = arg.slice(2).split("=", 2);
3316
+ if (key && VALUE_FLAGS.has(key) && !arg.includes("=")) {
3317
+ index++;
3318
+ }
3319
+ continue;
3320
+ }
3321
+ if (arg.startsWith("-")) {
3322
+ const short = arg.slice(1);
3323
+ if (SHORT_VALUE_FLAGS[short]) {
3324
+ index++;
3325
+ }
3326
+ continue;
3327
+ }
3328
+ positionals.push(arg);
3329
+ }
3330
+ return positionals;
3331
+ }
3332
+ function resolveIntentError(positionals) {
3333
+ const first = positionals[0]?.toLowerCase();
3334
+ if (!first) return void 0;
3335
+ if (!COMMANDS.has(first) && !SCOPES.has(first)) {
3336
+ return { message: `Unknown command: ${positionals[0]}` };
3337
+ }
3338
+ for (let index = 0; index < positionals.length; index++) {
3339
+ const token = positionals[index];
3340
+ const lower = token.toLowerCase();
3341
+ if (index === 0 && COMMANDS.has(lower)) continue;
3342
+ if (SCOPES.has(lower)) continue;
3343
+ return { message: `Unknown argument for ${first}: ${token}` };
3344
+ }
3345
+ return void 0;
3346
+ }
3347
+ function hasUsageCommandFlag(flags) {
3348
+ return Object.keys(flags).some((key) => /^last\d+$/.test(key) || USAGE_COMMAND_FLAGS.has(key));
3349
+ }
3350
+ function resolveCliIntent(argv) {
3351
+ const positionals = collectPositionals(argv);
3352
+ const first = positionals[0]?.toLowerCase();
3353
+ const parsedFlags = parseRawFlags(argv);
3354
+ let command = first === "limits" || first === "limit" || first === "pace" ? "limits" : "usage";
3355
+ const error = resolveIntentError(positionals);
3356
+ const scopeToken = positionals.find((token, index) => {
3357
+ const lower = token.toLowerCase();
3358
+ if (index === 0 && COMMANDS.has(lower)) return false;
3359
+ return SCOPES.has(lower);
3360
+ })?.toLowerCase();
3361
+ const sourceScope = scopeToken === "combined" ? "all" : scopeToken === "all" ? "all" : scopeToken === "codex" ? "codex" : scopeToken === "claude" ? "claude" : void 0;
3362
+ const normalizedArgs = {
3363
+ ...parsedFlags
3364
+ };
3365
+ if (first === "usage" && sourceScope && !hasUsageCommandFlag(parsedFlags)) {
3366
+ command = "limits";
3367
+ }
3368
+ if (sourceScope === "codex") {
3369
+ normalizedArgs.codex = true;
3370
+ normalizedArgs.combined = false;
3371
+ } else if (sourceScope === "all") {
3372
+ normalizedArgs.codex = false;
3373
+ normalizedArgs.combined = true;
3374
+ } else if (sourceScope === "claude") {
3375
+ normalizedArgs.codex = false;
3376
+ normalizedArgs.combined = false;
3377
+ }
3378
+ if (command === "limits") {
3379
+ normalizedArgs.limits = true;
3380
+ }
3381
+ return { command, sourceScope, normalizedArgs, error };
3382
+ }
3383
+ function applyCliIntent(args, intent) {
3384
+ return {
3385
+ ...args,
3386
+ ...intent.normalizedArgs
3387
+ };
3388
+ }
3389
+
3390
+ // src/limits/claude.ts
3391
+ import { execFile as execFile2 } from "child_process";
3392
+ import { randomUUID } from "crypto";
3393
+ import { dirname } from "path";
3394
+ import { fileURLToPath } from "url";
3395
+
3396
+ // src/limits/pace.ts
3397
+ var PACE_EPSILON_PERCENT = 1;
3398
+ function roundPercent(value) {
3399
+ return Math.round(value * 100) / 100;
3400
+ }
3401
+ function clampPercent(value) {
3402
+ if (!Number.isFinite(value)) return 0;
3403
+ return Math.max(0, Math.min(100, value));
3404
+ }
3405
+ function computeLinearPace(window, now = /* @__PURE__ */ new Date()) {
3406
+ if (!window.resetsAt || !window.windowMinutes || window.windowMinutes <= 0) {
3407
+ return null;
3408
+ }
3409
+ const resetTime = new Date(window.resetsAt).getTime();
3410
+ const nowTime = now.getTime();
3411
+ if (!Number.isFinite(resetTime) || !Number.isFinite(nowTime)) {
3412
+ return null;
3413
+ }
3414
+ const durationMs = window.windowMinutes * 60 * 1e3;
3415
+ const remainingMs = Math.max(0, resetTime - nowTime);
3416
+ const elapsedMs = Math.max(0, Math.min(durationMs, durationMs - remainingMs));
3417
+ const expectedUsedPercent = durationMs === 0 ? 0 : elapsedMs / durationMs * 100;
3418
+ const actualUsedPercent = clampPercent(window.usedPercent);
3419
+ const deltaPercent = actualUsedPercent - expectedUsedPercent;
3420
+ let status = "on-pace";
3421
+ if (deltaPercent > PACE_EPSILON_PERCENT) status = "deficit";
3422
+ else if (deltaPercent < -PACE_EPSILON_PERCENT) status = "reserve";
3423
+ let runsOutAt;
3424
+ let lastsUntilReset = true;
3425
+ if (actualUsedPercent > 0 && elapsedMs > 0 && actualUsedPercent < 100) {
3426
+ const ratePerMs = actualUsedPercent / elapsedMs;
3427
+ const etaMs = (100 - actualUsedPercent) / ratePerMs;
3428
+ if (etaMs < remainingMs) {
3429
+ lastsUntilReset = false;
3430
+ runsOutAt = new Date(nowTime + etaMs).toISOString();
3431
+ }
3432
+ } else if (actualUsedPercent >= 100) {
3433
+ lastsUntilReset = false;
3434
+ runsOutAt = now.toISOString();
3435
+ }
3436
+ return {
3437
+ provider: window.provider,
3438
+ kind: window.kind,
3439
+ label: window.label,
3440
+ actualUsedPercent: roundPercent(actualUsedPercent),
3441
+ expectedUsedPercent: roundPercent(expectedUsedPercent),
3442
+ deltaPercent: roundPercent(deltaPercent),
3443
+ status,
3444
+ lastsUntilReset,
3445
+ runsOutAt,
3446
+ source: window.source
3447
+ };
3448
+ }
3449
+ function computeLinearPaces(windows, now = /* @__PURE__ */ new Date()) {
3450
+ return windows.filter((window) => window.kind === "weekly" || window.kind === "model-weekly").map((window) => computeLinearPace(window, now)).filter((pace) => pace !== null);
3451
+ }
3452
+
3453
+ // src/limits/claude.ts
3454
+ var TMUX_SESSION_PREFIX = "vibestats-claude-usage";
3455
+ var DEFAULT_TIMEOUT_MS = 2e4;
3456
+ var POLL_INTERVAL_MS = 400;
3457
+ var READY_FALLBACK_MS = 3e3;
3458
+ var USAGE_COMMAND_RETRY_MS = 2500;
3459
+ function expandTerminalCursorSpacing(text) {
3460
+ return text.replace(/\u001B\[(\d*)C/g, (_match, count) => " ".repeat(Number.parseInt(count || "1", 10))).replace(/\u001B\[(\d*)[AB]/g, "\n").replace(/\u001B\[(\d*)D/g, "");
3461
+ }
3462
+ function stripAnsi(text) {
3463
+ return expandTerminalCursorSpacing(text).replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
3464
+ }
3465
+ function normalizeTerminalText(text) {
3466
+ return stripAnsi(text).replaceAll("\xA0", " ").replace(/\r/g, "\n").replace(/[ \t]+/g, " ").replace(/\n+/g, "\n").trim();
3467
+ }
3468
+ var MONTHS = {
3469
+ jan: 1,
3470
+ january: 1,
3471
+ feb: 2,
3472
+ february: 2,
3473
+ mar: 3,
3474
+ march: 3,
3475
+ apr: 4,
3476
+ april: 4,
3477
+ may: 5,
3478
+ jun: 6,
3479
+ june: 6,
3480
+ jul: 7,
3481
+ july: 7,
3482
+ aug: 8,
3483
+ august: 8,
3484
+ sep: 9,
3485
+ sept: 9,
3486
+ september: 9,
3487
+ oct: 10,
3488
+ october: 10,
3489
+ nov: 11,
3490
+ november: 11,
3491
+ dec: 12,
3492
+ december: 12
3493
+ };
3494
+ function normalizeLabel(text) {
3495
+ return text.toLowerCase().replace(/\s+/g, "");
3496
+ }
3497
+ function clampPercent2(value) {
3498
+ if (!Number.isFinite(value)) return 0;
3499
+ return Math.max(0, Math.min(100, value));
3500
+ }
3501
+ function parseUsedPercent(line) {
3502
+ const match = /([0-9]{1,3}(?:\.[0-9]+)?)\s*%/i.exec(line);
3503
+ if (!match) return null;
3504
+ const percent = clampPercent2(Number(match[1]));
3505
+ const lower = line.toLowerCase();
3506
+ if (lower.includes("used") || lower.includes("spent") || lower.includes("consumed")) {
3507
+ return percent;
3508
+ }
3509
+ if (lower.includes("left") || lower.includes("remaining") || lower.includes("available")) {
3510
+ return Math.max(0, Math.round((100 - percent) * 100) / 100);
3511
+ }
3512
+ return null;
3513
+ }
3514
+ function parseResetDescription(line) {
3515
+ const match = /\bresets?\s+(.+)$/i.exec(line);
3516
+ return match?.[1]?.trim() || void 0;
3517
+ }
3518
+ function remainingPercentFromUsed(usedPercent) {
3519
+ return Math.max(0, Math.round((100 - usedPercent) * 100) / 100);
3520
+ }
3521
+ function parseTimeToken(value) {
3522
+ const match = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(value.trim());
3523
+ if (!match) return null;
3524
+ let hour = Number(match[1]);
3525
+ const minute = Number(match[2] || "0");
3526
+ const meridiem = match[3]?.toLowerCase();
3527
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
3528
+ if (minute < 0 || minute > 59) return null;
3529
+ if (meridiem === "pm" && hour < 12) hour += 12;
3530
+ if (meridiem === "am" && hour === 12) hour = 0;
3531
+ if (hour < 0 || hour > 23) return null;
3532
+ return { hour, minute };
3533
+ }
3534
+ function localParts(date) {
3535
+ return {
3536
+ year: date.getFullYear(),
3537
+ month: date.getMonth() + 1,
3538
+ day: date.getDate(),
3539
+ hour: date.getHours(),
3540
+ minute: date.getMinutes()
3541
+ };
3542
+ }
3543
+ function getZonedParts(date, timeZone) {
3544
+ try {
3545
+ const formatter = new Intl.DateTimeFormat("en-US", {
3546
+ timeZone,
3547
+ calendar: "gregory",
3548
+ hourCycle: "h23",
3549
+ year: "numeric",
3550
+ month: "2-digit",
3551
+ day: "2-digit",
3552
+ hour: "2-digit",
3553
+ minute: "2-digit"
3554
+ });
3555
+ const parts = Object.fromEntries(formatter.formatToParts(date).map((part) => [part.type, part.value]));
3556
+ const year = Number(parts.year);
3557
+ const month = Number(parts.month);
3558
+ const day = Number(parts.day);
3559
+ const hour = Number(parts.hour);
3560
+ const minute = Number(parts.minute);
3561
+ if ([year, month, day, hour, minute].some((value) => !Number.isFinite(value))) return null;
3562
+ return { year, month, day, hour, minute };
3563
+ } catch {
3564
+ return null;
3565
+ }
3566
+ }
3567
+ function dateInTimeZone(parts, timeZone) {
3568
+ if (!timeZone) {
3569
+ return new Date(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
3570
+ }
3571
+ const utcGuess = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
3572
+ const observed = getZonedParts(new Date(utcGuess), timeZone);
3573
+ if (!observed) return null;
3574
+ const observedAsUtc = Date.UTC(
3575
+ observed.year,
3576
+ observed.month - 1,
3577
+ observed.day,
3578
+ observed.hour,
3579
+ observed.minute,
3580
+ 0,
3581
+ 0
3582
+ );
3583
+ return new Date(utcGuess - (observedAsUtc - utcGuess));
3584
+ }
3585
+ function splitResetTimeZone(description) {
3586
+ const match = /\s*\(([^)]+)\)\s*$/.exec(description);
3587
+ if (!match?.[1]) return { body: description.trim() };
3588
+ return {
3589
+ body: description.slice(0, match.index).trim(),
3590
+ timeZone: match[1].trim()
3591
+ };
3592
+ }
3593
+ function addCalendarDays(parts, days) {
3594
+ const shifted = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + days, 12, 0, 0, 0));
3595
+ return {
3596
+ year: shifted.getUTCFullYear(),
3597
+ month: shifted.getUTCMonth() + 1,
3598
+ day: shifted.getUTCDate(),
3599
+ hour: parts.hour,
3600
+ minute: parts.minute
3601
+ };
3602
+ }
3603
+ function parseResetDate(description, now) {
3604
+ if (!description) return void 0;
3605
+ const { body, timeZone } = splitResetTimeZone(description);
3606
+ const nowParts = timeZone ? getZonedParts(now, timeZone) : localParts(now);
3607
+ if (!nowParts) return void 0;
3608
+ const explicitDate = /^([A-Za-z]{3,9})\s+(\d{1,2})\s+at\s+(.+)$/i.exec(body);
3609
+ if (explicitDate?.[1] && explicitDate[2] && explicitDate[3]) {
3610
+ const month = MONTHS[explicitDate[1].toLowerCase()];
3611
+ const day = Number(explicitDate[2]);
3612
+ const time2 = parseTimeToken(explicitDate[3]);
3613
+ if (!month || !Number.isFinite(day) || !time2) return void 0;
3614
+ let candidate2 = dateInTimeZone({ year: nowParts.year, month, day, ...time2 }, timeZone);
3615
+ if (candidate2 && candidate2.getTime() <= now.getTime()) {
3616
+ candidate2 = dateInTimeZone({ year: nowParts.year + 1, month, day, ...time2 }, timeZone);
3617
+ }
3618
+ return candidate2?.toISOString();
3619
+ }
3620
+ const dayMonthDate = /^(.+?)\s+on\s+(\d{1,2})\s+([A-Za-z]{3,9})$/i.exec(body);
3621
+ if (dayMonthDate?.[1] && dayMonthDate[2] && dayMonthDate[3]) {
3622
+ const time2 = parseTimeToken(dayMonthDate[1]);
3623
+ const day = Number(dayMonthDate[2]);
3624
+ const month = MONTHS[dayMonthDate[3].toLowerCase()];
3625
+ if (!month || !Number.isFinite(day) || !time2) return void 0;
3626
+ let candidate2 = dateInTimeZone({ year: nowParts.year, month, day, ...time2 }, timeZone);
3627
+ if (candidate2 && candidate2.getTime() <= now.getTime()) {
3628
+ candidate2 = dateInTimeZone({ year: nowParts.year + 1, month, day, ...time2 }, timeZone);
3629
+ }
3630
+ return candidate2?.toISOString();
3631
+ }
3632
+ const time = parseTimeToken(body);
3633
+ if (!time) return void 0;
3634
+ let candidateParts = { ...nowParts, ...time };
3635
+ let candidate = dateInTimeZone(candidateParts, timeZone);
3636
+ if (candidate && candidate.getTime() <= now.getTime()) {
3637
+ candidateParts = addCalendarDays(candidateParts, 1);
3638
+ candidate = dateInTimeZone(candidateParts, timeZone);
3639
+ }
3640
+ return candidate?.toISOString();
3641
+ }
3642
+ function makeWindow(kind, label, usedPercent, windowMinutes, resetDescription, now, model) {
3643
+ return {
3644
+ provider: "claude",
3645
+ kind,
3646
+ label,
3647
+ usedPercent,
3648
+ remainingPercent: remainingPercentFromUsed(usedPercent),
3649
+ resetDescription,
3650
+ resetsAt: parseResetDate(resetDescription, now),
3651
+ windowMinutes,
3652
+ source: "claude-usage-tmux",
3653
+ model
3654
+ };
3655
+ }
3656
+ function extractWindow(lines, labelNeedles, kind, label, windowMinutes, now, model, excludeNeedles = []) {
3657
+ const index = lines.findIndex((line) => {
3658
+ const normalized = normalizeLabel(line);
3659
+ return labelNeedles.some((needle) => normalized.includes(normalizeLabel(needle))) && !excludeNeedles.some((needle) => normalized.includes(normalizeLabel(needle)));
3660
+ });
3661
+ if (index === -1) return null;
3662
+ const candidates = lines.slice(index, index + 12);
3663
+ const usedPercent = candidates.map(parseUsedPercent).find((value) => value !== null);
3664
+ if (usedPercent === void 0) return null;
3665
+ const resetDescription = candidates.map(parseResetDescription).find((value) => Boolean(value));
3666
+ return makeWindow(kind, label, usedPercent, windowMinutes, resetDescription, now, model);
3667
+ }
3668
+ function parseCompactUsageRow(line) {
3669
+ const match = /^resets?\s+(.+?)\s+([0-9]{1,3}(?:\.[0-9]+)?)\s*%\s*(used|spent|consumed|remaining|left|available)\b/i.exec(line);
3670
+ if (!match?.[1] || !match[2] || !match[3]) return null;
3671
+ const percent = clampPercent2(Number(match[2]));
3672
+ const mode = match[3].toLowerCase();
3673
+ const usedPercent = mode === "used" || mode === "spent" || mode === "consumed" ? percent : remainingPercentFromUsed(percent);
3674
+ return {
3675
+ resetDescription: match[1].trim(),
3676
+ usedPercent
3677
+ };
3678
+ }
3679
+ function parseCompactUsageRows(lines, now) {
3680
+ const rows = lines.map(parseCompactUsageRow).filter((row) => row !== null);
3681
+ const specs = [
3682
+ { kind: "session", label: "Session (5h)", windowMinutes: 300 },
3683
+ { kind: "weekly", label: "Weekly (7 day)", windowMinutes: 10080 },
3684
+ { kind: "model-weekly", label: "Sonnet Weekly (7 day)", windowMinutes: 10080, model: "Sonnet" }
3685
+ ];
3686
+ return rows.slice(0, specs.length).map((row, index) => {
3687
+ const spec = specs[index];
3688
+ return makeWindow(
3689
+ spec.kind,
3690
+ spec.label,
3691
+ row.usedPercent,
3692
+ spec.windowMinutes,
3693
+ row.resetDescription,
3694
+ now,
3695
+ spec.model
3696
+ );
3697
+ });
3698
+ }
3699
+ function parseClaudeUsageText(text, now = /* @__PURE__ */ new Date()) {
3700
+ const lines = normalizeTerminalText(text).split("\n").map((line) => line.trim()).filter(Boolean);
3701
+ const compactWindows = parseCompactUsageRows(lines, now);
3702
+ if (compactWindows.length > 0) return compactWindows;
3703
+ const windows = [];
3704
+ const session = extractWindow(lines, ["Current session"], "session", "Session (5h)", 300, now);
3705
+ const weekly = extractWindow(
3706
+ lines,
3707
+ ["Current week (all models)", "Current week"],
3708
+ "weekly",
3709
+ "Weekly (7 day)",
3710
+ 10080,
3711
+ now,
3712
+ void 0,
3713
+ ["sonnet", "opus"]
3714
+ );
3715
+ const sonnet = extractWindow(
3716
+ lines,
3717
+ ["Current week (Sonnet only)", "Current week (Sonnet)"],
3718
+ "model-weekly",
3719
+ "Sonnet Weekly (7 day)",
3720
+ 10080,
3721
+ now,
3722
+ "Sonnet"
3723
+ );
3724
+ const opus = extractWindow(
3725
+ lines,
3726
+ ["Current week (Opus)"],
3727
+ "model-weekly",
3728
+ "Opus Weekly (7 day)",
3729
+ 10080,
3730
+ now,
3731
+ "Opus"
3732
+ );
3733
+ if (session) windows.push(session);
3734
+ if (weekly) windows.push(weekly);
3735
+ if (sonnet) windows.push(sonnet);
3736
+ if (opus) windows.push(opus);
3737
+ return windows;
3738
+ }
3739
+ function isClaudePromptReady(text) {
3740
+ const normalized = normalizeTerminalText(text);
3741
+ return /Claude Code v\d+\.\d+\.\d+/.test(normalized) && (normalized.includes("\u276F") || normalized.includes(">"));
3742
+ }
3743
+ function execFileCommand(command, args, options = {}) {
3744
+ return new Promise((resolve, reject) => {
3745
+ execFile2(
3746
+ command,
3747
+ [...args],
3748
+ {
3749
+ cwd: options.cwd,
3750
+ timeout: options.timeoutMs ?? 2e3,
3751
+ maxBuffer: 1024 * 1024
3752
+ },
3753
+ (error, stdout, stderr) => {
3754
+ if (error) {
3755
+ reject(error);
3756
+ return;
3757
+ }
3758
+ resolve({ stdout, stderr });
3759
+ }
3760
+ );
3761
+ });
3762
+ }
3763
+ function sleep(ms) {
3764
+ return new Promise((resolve) => setTimeout(resolve, ms));
3765
+ }
3766
+ function resolveDefaultClaudeUsageCwd() {
3767
+ const envCwd = process.env.VIBESTATS_CLAUDE_USAGE_CWD ?? process.env.VIBESTATS_USAGE_CWD;
3768
+ if (envCwd?.trim()) return envCwd;
3769
+ return dirname(fileURLToPath(import.meta.url));
3770
+ }
3771
+ async function captureUsageWithTmux(options) {
3772
+ const sessionName = `${TMUX_SESSION_PREFIX}-${randomUUID().replaceAll("-", "").slice(0, 16)}`;
3773
+ let started = false;
3774
+ const runTmux = (args, timeoutMs = 2e3) => options.runCommand("tmux", args, { cwd: options.cwd, timeoutMs });
3775
+ try {
3776
+ await runTmux([
3777
+ "new-session",
3778
+ "-d",
3779
+ "-s",
3780
+ sessionName,
3781
+ "-x",
3782
+ "120",
3783
+ "-y",
3784
+ "40",
3785
+ ...options.cwd ? ["-c", options.cwd] : [],
3786
+ options.command
3787
+ ], 5e3);
3788
+ started = true;
3789
+ const startedAt = Date.now();
3790
+ const deadline = startedAt + options.timeoutMs;
3791
+ let lastUsageCommandAt = null;
3792
+ let promptReady = false;
3793
+ while (Date.now() < deadline) {
3794
+ await options.sleepFn(POLL_INTERVAL_MS);
3795
+ const capture = await runTmux([
3796
+ "capture-pane",
3797
+ "-p",
3798
+ "-J",
3799
+ "-S",
3800
+ "-",
3801
+ "-E",
3802
+ "-",
3803
+ "-t",
3804
+ sessionName
3805
+ ]);
3806
+ const windows = parseClaudeUsageText(capture.stdout, options.now);
3807
+ if (windows.length > 0) {
3808
+ return windows;
3809
+ }
3810
+ const now = Date.now();
3811
+ promptReady = promptReady || isClaudePromptReady(capture.stdout);
3812
+ const waitedLongEnough = now - startedAt >= Math.min(READY_FALLBACK_MS, options.timeoutMs / 2);
3813
+ const shouldSendUsage = (promptReady || waitedLongEnough) && (lastUsageCommandAt === null || now - lastUsageCommandAt >= USAGE_COMMAND_RETRY_MS);
3814
+ if (shouldSendUsage) {
3815
+ await runTmux(["send-keys", "-t", sessionName, "/usage", "Enter"]);
3816
+ lastUsageCommandAt = now;
3817
+ }
3818
+ }
3819
+ return [];
3820
+ } finally {
3821
+ if (started) {
3822
+ await runTmux(["kill-session", "-t", sessionName]).catch(() => void 0);
3823
+ }
3824
+ }
3825
+ }
3826
+ async function fetchClaudeLimits(options = {}) {
3827
+ const command = options.command || "claude";
3828
+ const now = options.now ?? /* @__PURE__ */ new Date();
3829
+ let windows;
3830
+ try {
3831
+ windows = await captureUsageWithTmux({
3832
+ command,
3833
+ cwd: options.cwd ?? resolveDefaultClaudeUsageCwd(),
3834
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3835
+ now,
3836
+ runCommand: options.runCommand ?? execFileCommand,
3837
+ sleepFn: options.sleepFn ?? sleep
3838
+ });
3839
+ } catch (error) {
3840
+ return {
3841
+ provider: "claude",
3842
+ source: "claude-usage-tmux",
3843
+ windows: [],
3844
+ pace: [],
3845
+ error: error instanceof Error ? error.message : String(error)
3846
+ };
3847
+ }
3848
+ if (windows.length === 0) {
3849
+ return {
3850
+ provider: "claude",
3851
+ source: "claude-usage-tmux",
3852
+ windows: [],
3853
+ pace: [],
3854
+ error: "could not parse claude /usage output"
3855
+ };
3856
+ }
3857
+ return {
3858
+ provider: "claude",
3859
+ source: "claude-usage-tmux",
3860
+ windows,
3861
+ pace: computeLinearPaces(windows, now)
3862
+ };
3863
+ }
3864
+
3865
+ // src/limits/codex.ts
3866
+ import { spawn } from "child_process";
3867
+ function clampPercent3(value) {
3868
+ const numeric = typeof value === "number" ? value : Number(value);
3869
+ if (!Number.isFinite(numeric)) return 0;
3870
+ return Math.max(0, Math.min(100, numeric));
3871
+ }
3872
+ function isoFromUnixSeconds(value) {
3873
+ const seconds = typeof value === "number" ? value : Number(value);
3874
+ if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
3875
+ return new Date(seconds * 1e3).toISOString();
3876
+ }
3877
+ function sourceLabel(kind, model) {
3878
+ const suffix = kind === "session" || kind === "model-session" ? "Session (5h)" : "Weekly (7 day)";
3879
+ return model ? `${model} ${suffix}` : suffix;
3880
+ }
3881
+ function stripAnsi2(text) {
3882
+ return text.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
3883
+ }
3884
+ function parsePercentLeft(line) {
3885
+ const match = /([0-9]{1,3}(?:\.[0-9]+)?)\s*%\s+(?:left|remaining|available)/i.exec(line);
3886
+ if (!match) return null;
3887
+ return clampPercent3(match[1]);
3888
+ }
3889
+ function parseResetDescription2(line) {
3890
+ const match = /\bresets?\s+(.+)$/i.exec(line);
3891
+ return match?.[1]?.trim() || void 0;
3892
+ }
3893
+ function makeWindow2(bucket, rawWindow, kind, model) {
3894
+ if (!rawWindow) return null;
3895
+ const usedPercent = clampPercent3(rawWindow.usedPercent);
3896
+ const windowMinutes = typeof rawWindow.windowDurationMins === "number" ? rawWindow.windowDurationMins : void 0;
3897
+ return {
3898
+ provider: "codex",
3899
+ kind,
3900
+ label: sourceLabel(kind, model),
3901
+ usedPercent,
3902
+ remainingPercent: Math.max(0, Math.round((100 - usedPercent) * 100) / 100),
3903
+ resetsAt: isoFromUnixSeconds(rawWindow.resetsAt),
3904
+ windowMinutes,
3905
+ source: "codex-app-server",
3906
+ limitId: bucket.limitId,
3907
+ model
3908
+ };
3909
+ }
3910
+ function normalizeCodexRateLimitWindows(payload) {
3911
+ const windows = [];
3912
+ const primaryBucket = payload.rateLimits;
3913
+ const primaryLimitId = primaryBucket?.limitId;
3914
+ if (primaryBucket) {
3915
+ const primary = makeWindow2(primaryBucket, primaryBucket.primary, "session");
3916
+ const secondary = makeWindow2(primaryBucket, primaryBucket.secondary, "weekly");
3917
+ if (primary) windows.push(primary);
3918
+ if (secondary) windows.push(secondary);
3919
+ }
3920
+ for (const [limitId, bucket] of Object.entries(payload.rateLimitsByLimitId || {})) {
3921
+ if (limitId === primaryLimitId) continue;
3922
+ const model = typeof bucket.limitName === "string" && bucket.limitName.trim() ? bucket.limitName.trim() : void 0;
3923
+ const normalizedBucket = { ...bucket, limitId: bucket.limitId || limitId };
3924
+ const primary = makeWindow2(normalizedBucket, bucket.primary, "model-session", model);
3925
+ const secondary = makeWindow2(normalizedBucket, bucket.secondary, "model-weekly", model);
3926
+ if (primary) windows.push(primary);
3927
+ if (secondary) windows.push(secondary);
3928
+ }
3929
+ return windows;
3930
+ }
3931
+ function parseCodexStatusText(text) {
3932
+ const clean = stripAnsi2(text);
3933
+ const lines = clean.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3934
+ const windows = [];
3935
+ const specs = [
3936
+ { pattern: /\b5(?:h|-hour)\s+limit\b/i, kind: "session", windowMinutes: 300 },
3937
+ { pattern: /\bweekly\s+limit\b/i, kind: "weekly", windowMinutes: 10080 }
3938
+ ];
3939
+ for (const spec of specs) {
3940
+ const line = lines.find((candidate) => spec.pattern.test(candidate));
3941
+ if (!line) continue;
3942
+ const remainingPercent = parsePercentLeft(line);
3943
+ if (remainingPercent === null) continue;
3944
+ const usedPercent = Math.max(0, Math.round((100 - remainingPercent) * 100) / 100);
3945
+ windows.push({
3946
+ provider: "codex",
3947
+ kind: spec.kind,
3948
+ label: sourceLabel(spec.kind),
3949
+ usedPercent,
3950
+ remainingPercent,
3951
+ resetDescription: parseResetDescription2(line),
3952
+ windowMinutes: spec.windowMinutes,
3953
+ source: "codex-status-pty"
3954
+ });
3955
+ }
3956
+ return windows;
3957
+ }
3958
+ function unwrapRateLimitsResult(result) {
3959
+ const outer = result;
3960
+ const nested = outer.rateLimits;
3961
+ const payload = nested && typeof nested === "object" && ("rateLimits" in nested || "rateLimitsByLimitId" in nested) ? outer.rateLimits : result;
3962
+ return payload;
3963
+ }
3964
+ function unwrapAccountResult(result) {
3965
+ const payload = result;
3966
+ const account = payload.account;
3967
+ if (!account && typeof payload.requiresOpenaiAuth !== "boolean") return void 0;
3968
+ return {
3969
+ provider: "codex",
3970
+ accountType: account?.type,
3971
+ planType: account?.planType,
3972
+ requiresAuth: payload.requiresOpenaiAuth
3973
+ };
3974
+ }
3975
+ var JsonRpcClient = class {
3976
+ constructor(child) {
3977
+ this.child = child;
3978
+ child.stdout.setEncoding("utf8");
3979
+ child.stdout.on("data", (chunk) => this.handleOutput(chunk));
3980
+ child.stdin.on("error", (error) => this.rejectAll(error instanceof Error ? error : new Error(String(error))));
3981
+ child.on("error", (error) => this.rejectAll(error instanceof Error ? error : new Error(String(error))));
3982
+ child.on("exit", (code) => {
3983
+ if (this.pending.size > 0) {
3984
+ this.rejectAll(new Error(`codex app-server exited before response${code === null ? "" : ` (${code})`}`));
3985
+ }
3986
+ });
3987
+ }
3988
+ nextId = 1;
3989
+ buffer = "";
3990
+ pending = /* @__PURE__ */ new Map();
3991
+ request(method, params) {
3992
+ const id = this.nextId++;
3993
+ const message = {
3994
+ jsonrpc: "2.0",
3995
+ id,
3996
+ method,
3997
+ ...params === void 0 ? {} : { params }
3998
+ };
3999
+ const promise = new Promise((resolve, reject) => {
4000
+ this.pending.set(id, { resolve, reject });
4001
+ });
4002
+ try {
4003
+ this.child.stdin.write(`${JSON.stringify(message)}
4004
+ `);
4005
+ } catch (error) {
4006
+ this.pending.delete(id);
4007
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
4008
+ }
4009
+ return promise;
4010
+ }
4011
+ notify(method, params) {
4012
+ const message = {
4013
+ jsonrpc: "2.0",
4014
+ method,
4015
+ ...params === void 0 ? {} : { params }
4016
+ };
4017
+ try {
4018
+ this.child.stdin.write(`${JSON.stringify(message)}
4019
+ `);
4020
+ } catch {
4021
+ }
4022
+ }
4023
+ handleOutput(chunk) {
4024
+ this.buffer += chunk;
4025
+ let newlineIndex = this.buffer.indexOf("\n");
4026
+ while (newlineIndex !== -1) {
4027
+ const line = this.buffer.slice(0, newlineIndex).trim();
4028
+ this.buffer = this.buffer.slice(newlineIndex + 1);
4029
+ this.handleLine(line);
4030
+ newlineIndex = this.buffer.indexOf("\n");
4031
+ }
4032
+ }
4033
+ handleLine(line) {
4034
+ if (!line.startsWith("{")) return;
4035
+ let message;
4036
+ try {
4037
+ message = JSON.parse(line);
4038
+ } catch {
4039
+ return;
4040
+ }
4041
+ if (typeof message.id !== "number") return;
4042
+ const pending = this.pending.get(message.id);
4043
+ if (!pending) return;
4044
+ this.pending.delete(message.id);
4045
+ if (message.error) {
4046
+ const msg = typeof message.error === "string" ? message.error : message.error.message || "JSON-RPC error";
4047
+ pending.reject(new Error(msg));
4048
+ } else {
4049
+ pending.resolve(message.result);
4050
+ }
4051
+ }
4052
+ rejectAll(error) {
4053
+ for (const pending of this.pending.values()) {
4054
+ pending.reject(error);
4055
+ }
4056
+ this.pending.clear();
4057
+ }
4058
+ };
4059
+ function withTimeout(promise, timeoutMs) {
4060
+ let timer = null;
4061
+ const timeout = new Promise((_, reject) => {
4062
+ timer = setTimeout(() => reject(new Error("codex app-server timed out")), timeoutMs);
4063
+ timer.unref();
4064
+ });
4065
+ return Promise.race([promise, timeout]).finally(() => {
4066
+ if (timer) clearTimeout(timer);
4067
+ });
4068
+ }
4069
+ function scriptArgsForCodexStatus(command) {
4070
+ const quotedCommand = `'${command.replace(/'/g, "'\\''")}'`;
4071
+ const shellCommand = `printf '/status\\n' | ${quotedCommand} -s read-only -a untrusted`;
4072
+ if (process.platform === "darwin") {
4073
+ return ["-q", "/dev/null", "sh", "-c", shellCommand];
4074
+ }
4075
+ return ["-q", "-c", shellCommand, "/dev/null"];
4076
+ }
4077
+ async function fetchCodexStatusFallback(options = {}) {
4078
+ const command = options.command || "codex";
4079
+ const timeoutMs = options.timeoutMs ?? 8e3;
4080
+ const child = spawn("script", scriptArgsForCodexStatus(command), {
4081
+ stdio: ["ignore", "pipe", "pipe"],
4082
+ env: process.env
4083
+ });
4084
+ let output = "";
4085
+ child.stdout.setEncoding("utf8");
4086
+ child.stderr.setEncoding("utf8");
4087
+ child.stdout.on("data", (chunk) => {
4088
+ output += chunk;
4089
+ });
4090
+ child.stderr.on("data", (chunk) => {
4091
+ output += chunk;
4092
+ });
4093
+ await new Promise((resolve) => {
4094
+ const timer = setTimeout(() => {
4095
+ child.kill("SIGTERM");
4096
+ resolve();
4097
+ }, timeoutMs);
4098
+ timer.unref();
4099
+ child.on("error", () => {
4100
+ clearTimeout(timer);
4101
+ resolve();
4102
+ });
4103
+ child.on("exit", () => {
4104
+ clearTimeout(timer);
4105
+ resolve();
4106
+ });
4107
+ });
4108
+ const windows = parseCodexStatusText(output);
4109
+ if (windows.length === 0) {
4110
+ return {
4111
+ provider: "codex",
4112
+ source: "codex-status-pty",
4113
+ windows: [],
4114
+ pace: [],
4115
+ error: "could not parse codex /status output"
4116
+ };
4117
+ }
4118
+ return {
4119
+ provider: "codex",
4120
+ source: "codex-status-pty",
4121
+ windows,
4122
+ pace: computeLinearPaces(windows, options.now)
4123
+ };
4124
+ }
4125
+ async function fetchCodexLimits(options = {}) {
4126
+ const command = options.command || "codex";
4127
+ const timeoutMs = options.timeoutMs ?? 8e3;
4128
+ const child = spawn(command, ["-s", "read-only", "-a", "untrusted", "app-server"], {
4129
+ stdio: ["pipe", "pipe", "pipe"],
4130
+ env: process.env
4131
+ });
4132
+ let stderr = "";
4133
+ child.stderr.setEncoding("utf8");
4134
+ child.stderr.on("data", (chunk) => {
4135
+ stderr += chunk;
4136
+ });
4137
+ const client = new JsonRpcClient(child);
4138
+ try {
4139
+ await withTimeout(
4140
+ client.request("initialize", { clientInfo: { name: "vibestats", version: "1" } }),
4141
+ timeoutMs
4142
+ );
4143
+ client.notify("initialized");
4144
+ const [rateLimitsResult, accountResult] = await withTimeout(
4145
+ Promise.all([
4146
+ client.request("account/rateLimits/read"),
4147
+ client.request("account/read").catch(() => null)
4148
+ ]),
4149
+ timeoutMs
4150
+ );
4151
+ const payload = unwrapRateLimitsResult(rateLimitsResult);
4152
+ const windows = normalizeCodexRateLimitWindows(payload);
4153
+ const pace = computeLinearPaces(windows, options.now);
4154
+ return {
4155
+ provider: "codex",
4156
+ source: "codex-app-server",
4157
+ windows,
4158
+ pace,
4159
+ account: accountResult ? unwrapAccountResult(accountResult) : void 0
4160
+ };
4161
+ } catch (error) {
4162
+ child.kill("SIGTERM");
4163
+ const appServerError = error instanceof Error ? error.message : String(error || stderr);
4164
+ const fallback = await fetchCodexStatusFallback(options);
4165
+ return fallback.windows.length > 0 ? fallback : {
4166
+ provider: "codex",
4167
+ source: "codex-app-server",
4168
+ windows: [],
4169
+ pace: [],
4170
+ error: `${appServerError}; ${fallback.error || "codex /status fallback failed"}`
4171
+ };
4172
+ } finally {
4173
+ child.kill("SIGTERM");
4174
+ }
4175
+ }
4176
+
4177
+ // src/limits/index.ts
4178
+ function selectedProviders(options) {
4179
+ if (options.codexOnly) return ["codex"];
4180
+ if (options.combined) return ["claude", "codex"];
4181
+ return ["claude"];
4182
+ }
4183
+ async function loadUsageLimits(options = {}) {
4184
+ const providers = selectedProviders(options);
4185
+ const snapshots = await Promise.all(
4186
+ providers.map((provider) => {
4187
+ if (provider === "codex") {
4188
+ return fetchCodexLimits({ timeoutMs: options.timeoutMs });
4189
+ }
4190
+ return fetchClaudeLimits({ timeoutMs: options.timeoutMs });
4191
+ })
4192
+ );
4193
+ return {
4194
+ snapshots,
4195
+ windows: snapshots.flatMap((snapshot) => snapshot.windows),
4196
+ pace: snapshots.flatMap((snapshot) => snapshot.pace),
4197
+ accounts: snapshots.map((snapshot) => snapshot.account).filter((account) => Boolean(account)),
4198
+ errors: snapshots.filter((snapshot) => snapshot.error).map((snapshot) => ({
4199
+ provider: snapshot.provider,
4200
+ source: snapshot.source,
4201
+ message: snapshot.error || "unknown error"
4202
+ }))
4203
+ };
4204
+ }
4205
+
4206
+ // src/limits/table.ts
4207
+ var colors3 = {
4208
+ reset: "\x1B[0m",
4209
+ bold: "\x1B[1m",
4210
+ dim: "\x1B[2m",
4211
+ orange: "\x1B[38;5;208m",
4212
+ amber: "\x1B[38;5;214m",
4213
+ green: "\x1B[32m",
4214
+ blue: "\x1B[34m",
4215
+ red: "\x1B[31m",
4216
+ gray: "\x1B[90m"
4217
+ };
4218
+ var noColors3 = Object.fromEntries(Object.keys(colors3).map((k) => [k, ""]));
4219
+ function getColors3(showColors) {
4220
+ return showColors === false ? noColors3 : colors3;
4221
+ }
4222
+ function formatProvider(provider) {
4223
+ return provider === "codex" ? "Codex" : provider === "claude" ? "Claude" : provider;
4224
+ }
4225
+ function formatReset(window) {
4226
+ if (window.resetsAt) {
4227
+ const ms = new Date(window.resetsAt).getTime() - Date.now();
4228
+ if (Number.isFinite(ms) && ms > 0) return `resets in ${formatDuration(ms)}`;
4229
+ }
4230
+ if (window.resetDescription) return `resets ${window.resetDescription}`;
4231
+ return "reset unknown";
4232
+ }
4233
+ function formatDuration(ms) {
4234
+ const totalMinutes = Math.max(0, Math.round(ms / 6e4));
4235
+ const days = Math.floor(totalMinutes / 1440);
4236
+ const hours = Math.floor(totalMinutes % 1440 / 60);
4237
+ const minutes = totalMinutes % 60;
4238
+ if (days > 0) return `${days}d ${hours}h`;
4239
+ if (hours > 0) return `${hours}h ${minutes}m`;
4240
+ return `${minutes}m`;
4241
+ }
4242
+ function providerBarColor(provider, showColors) {
4243
+ const c = getColors3(showColors);
4244
+ return provider === "codex" ? c.blue : c.green;
4245
+ }
4246
+ function bar(window, showColors) {
4247
+ const c = getColors3(showColors);
4248
+ const width = 20;
4249
+ const filled = Math.round(Math.max(0, Math.min(100, window.usedPercent)) / 100 * width);
4250
+ return `${providerBarColor(window.provider, showColors)}${"\u2588".repeat(filled)}${c.gray}${"\u2591".repeat(width - filled)}${c.reset}`;
4251
+ }
4252
+ function findPace(window, paces) {
4253
+ return paces.find(
4254
+ (pace) => pace.provider === window.provider && pace.kind === window.kind && pace.label === window.label
4255
+ );
4256
+ }
4257
+ function paceLine(pace) {
4258
+ if (!pace) return null;
4259
+ const amount = Math.abs(pace.deltaPercent).toFixed(1);
4260
+ const status = pace.status === "reserve" ? `${amount}% reserve` : pace.status === "deficit" ? `${amount}% deficit` : "on pace";
4261
+ const eta = pace.lastsUntilReset ? "lasts until reset" : pace.runsOutAt ? `runs out in ${formatDuration(new Date(pace.runsOutAt).getTime() - Date.now())}` : "run-out unknown";
4262
+ return `${status} \xB7 ${eta} \xB7 expected ${pace.expectedUsedPercent.toFixed(1)}% used`;
4263
+ }
4264
+ function displayUsageLimits(limits, options = {}) {
4265
+ const c = getColors3(options.showColors);
4266
+ console.log();
4267
+ console.log(`${c.orange}${c.bold}Local CLI Limits${c.reset}`);
4268
+ console.log(`${c.gray}Sources: Codex app-server with /status fallback; Claude /usage tmux${c.reset}`);
4269
+ console.log(`${c.gray}${"\u2500".repeat(72)}${c.reset}`);
4270
+ if (limits.windows.length === 0) {
4271
+ console.log(`${c.gray}No live limit windows available.${c.reset}`);
4272
+ }
4273
+ for (const window of limits.windows) {
4274
+ const pace = findPace(window, limits.pace);
4275
+ const paceText = paceLine(pace);
4276
+ const riskColor = window.usedPercent >= 90 ? c.red : window.usedPercent >= 70 ? c.amber : c.green;
4277
+ console.log(
4278
+ `${c.bold}${formatProvider(window.provider)} ${window.label}${c.reset} ${bar(window, options.showColors)} ${riskColor}${window.usedPercent.toFixed(0)}% used${c.reset} ${c.gray}${formatReset(window)} \xB7 ${window.source}${c.reset}`
4279
+ );
4280
+ if (paceText) {
4281
+ console.log(` ${c.dim}${paceText}${c.reset}`);
4282
+ }
4283
+ }
4284
+ if (limits.errors.length > 0) {
4285
+ console.log();
4286
+ for (const error of limits.errors) {
4287
+ console.log(`${c.gray}${formatProvider(error.provider)} ${error.source}: ${error.message}${c.reset}`);
4288
+ }
4289
+ }
4290
+ }
4291
+
3207
4292
  // src/config.ts
3208
4293
  import { readFileSync, existsSync, writeFileSync } from "fs";
3209
4294
  import { homedir as homedir4 } from "os";
@@ -3298,6 +4383,20 @@ function getSelectedModelFamilies(args) {
3298
4383
  }
3299
4384
 
3300
4385
  // src/index.ts
4386
+ function printCommandHelp(error) {
4387
+ console.error(`Error: ${error}`);
4388
+ console.error("");
4389
+ console.error("Usage:");
4390
+ console.error(" vibestats limits all");
4391
+ console.error(" vibestats limits claude");
4392
+ console.error(" vibestats limits codex");
4393
+ console.error(" vibestats usage all");
4394
+ console.error(" vibestats usage --combined --daily");
4395
+ console.error(" vibestats usage codex --total");
4396
+ console.error(" vibestats wrapped");
4397
+ console.error("");
4398
+ console.error("Run `vibestats --help` for all flags.");
4399
+ }
3301
4400
  function createSpinner(label = "Loading vibestats...") {
3302
4401
  const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3303
4402
  const orange = "\x1B[38;5;208m";
@@ -3440,7 +4539,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
3440
4539
  var main = defineCommand({
3441
4540
  meta: {
3442
4541
  name: "vibestats",
3443
- version: "1.3.10",
4542
+ version: "1.3.13",
3444
4543
  description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
3445
4544
  },
3446
4545
  args: {
@@ -3523,6 +4622,11 @@ var main = defineCommand({
3523
4622
  description: "Use compact table format (hide cache columns)",
3524
4623
  default: false
3525
4624
  },
4625
+ limits: {
4626
+ type: "boolean",
4627
+ description: "Include live local CLI limit windows (Codex app-server or PTY, Claude /usage PTY)",
4628
+ default: false
4629
+ },
3526
4630
  metric: {
3527
4631
  type: "string",
3528
4632
  description: "Activity metric: tokens, sessions, or messages"
@@ -3583,30 +4687,40 @@ var main = defineCommand({
3583
4687
  }
3584
4688
  },
3585
4689
  async run({ args }) {
3586
- if (args.init) {
4690
+ const intent = resolveCliIntent(process.argv.slice(2));
4691
+ if (intent.error) {
4692
+ printCommandHelp(intent.error.message);
4693
+ process.exit(1);
4694
+ }
4695
+ const normalizedArgs = applyCliIntent(args, intent);
4696
+ if (normalizedArgs.init) {
3587
4697
  initConfig();
3588
4698
  return;
3589
4699
  }
3590
4700
  const config = loadConfig();
3591
- if (args.config) {
4701
+ if (normalizedArgs.config) {
3592
4702
  console.log(`Config file: ${CONFIG_PATH}`);
3593
4703
  console.log(JSON.stringify(config, null, 2));
3594
4704
  return;
3595
4705
  }
3596
- if (args["claude-system"]) {
3597
- await runClaudeSystem(args);
4706
+ if (normalizedArgs["claude-system"]) {
4707
+ await runClaudeSystem(normalizedArgs);
4708
+ return;
4709
+ }
4710
+ if (normalizedArgs["claude-limits"]) {
4711
+ await runClaudeLimits(normalizedArgs);
3598
4712
  return;
3599
4713
  }
3600
- if (args["claude-limits"]) {
3601
- await runClaudeLimits(args);
4714
+ if (intent.command === "limits") {
4715
+ await runLiveLimits(normalizedArgs, config);
3602
4716
  return;
3603
4717
  }
3604
- if (args.activity) {
3605
- await runActivity(args, config);
3606
- } else if (args.wrapped) {
3607
- await runWrapped(args, config);
4718
+ if (normalizedArgs.activity) {
4719
+ await runActivity(normalizedArgs, config);
4720
+ } else if (normalizedArgs.wrapped) {
4721
+ await runWrapped(normalizedArgs, config);
3608
4722
  } else {
3609
- await runUsage(args, config);
4723
+ await runUsage(normalizedArgs, config);
3610
4724
  }
3611
4725
  }
3612
4726
  });
@@ -3635,19 +4749,18 @@ async function runUsage(args, config) {
3635
4749
  else if (args.monthly) aggregation = "monthly";
3636
4750
  else if (args.model) aggregation = "model";
3637
4751
  else if (args.total) aggregation = "total";
3638
- const spinner = createSpinner("Loading vibestats...");
3639
- const stats = await spinner.whilePromise(
3640
- loadUsageStats({
3641
- aggregation,
3642
- since,
3643
- until,
3644
- codexOnly: args.codex,
3645
- combined: args.combined,
3646
- projectFilter: args.project ? process.cwd() : void 0,
3647
- families,
3648
- scopeLabel
3649
- })
3650
- );
4752
+ const shouldShowSpinner = !args.json && !args.quiet;
4753
+ const statsPromise = loadUsageStats({
4754
+ aggregation,
4755
+ since,
4756
+ until,
4757
+ codexOnly: args.codex,
4758
+ combined: args.combined,
4759
+ projectFilter: args.project ? process.cwd() : void 0,
4760
+ families,
4761
+ scopeLabel
4762
+ });
4763
+ const stats = shouldShowSpinner ? await createSpinner("Loading vibestats...").whilePromise(statsPromise) : await statsPromise;
3651
4764
  if (!stats) {
3652
4765
  if (requestedSince || requestedUntil || requestedLastDays) {
3653
4766
  const rangeStr = `${since || "(start)"} to ${until || "(end)"}`;
@@ -3674,6 +4787,18 @@ async function runUsage(args, config) {
3674
4787
  }
3675
4788
  process.exit(1);
3676
4789
  }
4790
+ let liveLimits = null;
4791
+ if (args.limits) {
4792
+ const limitsPromise = loadUsageLimits({
4793
+ codexOnly: args.codex,
4794
+ combined: args.combined
4795
+ });
4796
+ liveLimits = shouldShowSpinner ? await createSpinner("Loading local CLI limits...").whilePromise(limitsPromise) : await limitsPromise;
4797
+ stats.limits = liveLimits.windows;
4798
+ stats.pace = liveLimits.pace;
4799
+ stats.limitAccounts = liveLimits.accounts;
4800
+ stats.limitErrors = liveLimits.errors;
4801
+ }
3677
4802
  const baseUrl = args.url || config.baseUrl || "https://vibestats.wolfai.dev";
3678
4803
  let shareUrl = null;
3679
4804
  if (args.share) {
@@ -3708,6 +4833,11 @@ async function runUsage(args, config) {
3708
4833
  showColors: config.theme?.enabled !== false
3709
4834
  });
3710
4835
  }
4836
+ if (liveLimits && !args.json && !args.quiet) {
4837
+ displayUsageLimits(liveLimits, {
4838
+ showColors: config.theme?.enabled !== false
4839
+ });
4840
+ }
3711
4841
  if (args.share && shareUrl && !args.json) {
3712
4842
  const orange = "\x1B[38;5;208m";
3713
4843
  const reset = "\x1B[0m";
@@ -3715,6 +4845,20 @@ async function runUsage(args, config) {
3715
4845
  console.log(`\u{1F517} ${orange}${shareUrl}${reset}`);
3716
4846
  }
3717
4847
  }
4848
+ async function runLiveLimits(args, config) {
4849
+ const limitsPromise = loadUsageLimits({
4850
+ codexOnly: args.codex,
4851
+ combined: args.combined
4852
+ });
4853
+ const limits = args.json || args.quiet ? await limitsPromise : await createSpinner("Loading local CLI limits...").whilePromise(limitsPromise);
4854
+ if (args.json) {
4855
+ console.log(JSON.stringify(limits, null, 2));
4856
+ return;
4857
+ }
4858
+ displayUsageLimits(limits, {
4859
+ showColors: config.theme?.enabled !== false
4860
+ });
4861
+ }
3718
4862
  async function runWrapped(args, config) {
3719
4863
  const options = resolveOptions(args, config);
3720
4864
  const families = getSelectedModelFamilies(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",