vibestats 1.3.11 → 1.3.12

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