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.
- package/dist/index.js +1091 -28
- 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
|
|
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 `${
|
|
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
|
|
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)} ${
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
3597
|
-
await runClaudeSystem(
|
|
4625
|
+
if (normalizedArgs["claude-system"]) {
|
|
4626
|
+
await runClaudeSystem(normalizedArgs);
|
|
3598
4627
|
return;
|
|
3599
4628
|
}
|
|
3600
|
-
if (
|
|
3601
|
-
await runClaudeLimits(
|
|
4629
|
+
if (normalizedArgs["claude-limits"]) {
|
|
4630
|
+
await runClaudeLimits(normalizedArgs);
|
|
3602
4631
|
return;
|
|
3603
4632
|
}
|
|
3604
|
-
if (
|
|
3605
|
-
await
|
|
3606
|
-
|
|
3607
|
-
|
|
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(
|
|
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
|
|
3639
|
-
const
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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);
|