opencode-enhancer 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +85 -5
  3. package/dist/cli.js.map +1 -1
  4. package/dist/providers/auth.d.ts +1 -1
  5. package/dist/providers/auth.d.ts.map +1 -1
  6. package/dist/providers/auth.js +142 -48
  7. package/dist/providers/auth.js.map +1 -1
  8. package/dist/providers/chutes.d.ts +1 -1
  9. package/dist/providers/chutes.d.ts.map +1 -1
  10. package/dist/providers/chutes.js +19 -16
  11. package/dist/providers/chutes.js.map +1 -1
  12. package/dist/providers/codex.d.ts +1 -1
  13. package/dist/providers/codex.d.ts.map +1 -1
  14. package/dist/providers/codex.js +60 -35
  15. package/dist/providers/codex.js.map +1 -1
  16. package/dist/providers/copilot.d.ts +1 -1
  17. package/dist/providers/copilot.d.ts.map +1 -1
  18. package/dist/providers/copilot.js +96 -60
  19. package/dist/providers/copilot.js.map +1 -1
  20. package/dist/providers/gemini.d.ts.map +1 -1
  21. package/dist/providers/gemini.js +23 -0
  22. package/dist/providers/gemini.js.map +1 -1
  23. package/dist/providers/opencode.d.ts.map +1 -1
  24. package/dist/providers/opencode.js +23 -10
  25. package/dist/providers/opencode.js.map +1 -1
  26. package/dist/providers/types.d.ts +10 -4
  27. package/dist/providers/types.d.ts.map +1 -1
  28. package/dist/providers/types.js.map +1 -1
  29. package/dist/providers/zai.d.ts +1 -1
  30. package/dist/providers/zai.d.ts.map +1 -1
  31. package/dist/providers/zai.js +41 -14
  32. package/dist/providers/zai.js.map +1 -1
  33. package/dist/rotation.d.ts.map +1 -1
  34. package/dist/rotation.js +13 -3
  35. package/dist/rotation.js.map +1 -1
  36. package/dist/usage-command.d.ts.map +1 -1
  37. package/dist/usage-command.js +589 -85
  38. package/dist/usage-command.js.map +1 -1
  39. package/package.json +1 -1
@@ -1,26 +1,28 @@
1
1
  // CLI usage command: formatted table output for provider usage
2
- import { fetchAllUsage, allProviders } from './providers/index.js';
3
- import { readUsageCache, writeUsageCache } from './usage-cache.js';
2
+ import { fetchAllUsage, allProviders } from "./providers/index.js";
3
+ import { decodeJwtPayload } from "./jwt.js";
4
+ import { loadStore } from "./store.js";
5
+ import { readUsageCache, writeUsageCache } from "./usage-cache.js";
4
6
  // ── ANSI helpers ──────────────────────────────────────────────
5
- const isColorSupported = process.env.FORCE_COLOR !== '0' &&
7
+ const isColorSupported = process.env.FORCE_COLOR !== "0" &&
6
8
  process.env.NO_COLOR === undefined &&
7
9
  (process.stdout.isTTY || process.env.FORCE_COLOR !== undefined);
8
10
  const c = {
9
- reset: isColorSupported ? '\x1b[0m' : '',
10
- bold: isColorSupported ? '\x1b[1m' : '',
11
- dim: isColorSupported ? '\x1b[2m' : '',
12
- red: isColorSupported ? '\x1b[31m' : '',
13
- green: isColorSupported ? '\x1b[32m' : '',
14
- yellow: isColorSupported ? '\x1b[33m' : '',
15
- blue: isColorSupported ? '\x1b[34m' : '',
16
- cyan: isColorSupported ? '\x1b[36m' : '',
17
- gray: isColorSupported ? '\x1b[90m' : '',
18
- white: isColorSupported ? '\x1b[37m' : '',
11
+ reset: isColorSupported ? "\x1b[0m" : "",
12
+ bold: isColorSupported ? "\x1b[1m" : "",
13
+ dim: isColorSupported ? "\x1b[2m" : "",
14
+ red: isColorSupported ? "\x1b[31m" : "",
15
+ green: isColorSupported ? "\x1b[32m" : "",
16
+ yellow: isColorSupported ? "\x1b[33m" : "",
17
+ blue: isColorSupported ? "\x1b[34m" : "",
18
+ cyan: isColorSupported ? "\x1b[36m" : "",
19
+ gray: isColorSupported ? "\x1b[90m" : "",
20
+ white: isColorSupported ? "\x1b[37m" : "",
19
21
  };
20
22
  // ── Formatting helpers ────────────────────────────────────────
21
23
  function formatDuration(ms) {
22
24
  if (ms <= 0)
23
- return 'now';
25
+ return "now";
24
26
  const totalSeconds = Math.floor(ms / 1000);
25
27
  const days = Math.floor(totalSeconds / 86400);
26
28
  const hours = Math.floor((totalSeconds % 86400) / 3600);
@@ -50,90 +52,528 @@ function buildBar(pct, width = 8) {
50
52
  const filled = Math.round((pct / 100) * width);
51
53
  const empty = width - filled;
52
54
  const color = utilizationColor(pct);
53
- return `${color}${''.repeat(filled)}${c.dim}${''.repeat(empty)}${c.reset}`;
55
+ return `${color}${"".repeat(filled)}${c.dim}${"".repeat(empty)}${c.reset}`;
56
+ }
57
+ function stripAnsi(str) {
58
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
59
+ }
60
+ function visibleLength(str) {
61
+ return stripAnsi(str).length;
62
+ }
63
+ function clamp(value, min, max) {
64
+ return Math.min(Math.max(value, min), max);
65
+ }
66
+ function truncateText(str, maxLen) {
67
+ if (str.length <= maxLen)
68
+ return str;
69
+ if (maxLen <= 1)
70
+ return "…";
71
+ return `${str.slice(0, maxLen - 1)}…`;
54
72
  }
55
73
  function padRight(str, len) {
56
- // Strip ANSI codes for length calculation
57
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
58
- const pad = Math.max(0, len - stripped.length);
59
- return str + ' '.repeat(pad);
74
+ const pad = Math.max(0, len - visibleLength(str));
75
+ return str + " ".repeat(pad);
60
76
  }
61
77
  function padLeft(str, len) {
62
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
63
- const pad = Math.max(0, len - stripped.length);
64
- return ' '.repeat(pad) + str;
78
+ const pad = Math.max(0, len - visibleLength(str));
79
+ return " ".repeat(pad) + str;
80
+ }
81
+ const TABLE_INDENT = " ";
82
+ const COLUMN_GAP = " ";
83
+ const MINI_BAR_WIDTH = 4;
84
+ function normalizeWindowLabel(label) {
85
+ return label.trim().toLowerCase();
86
+ }
87
+ function abbreviateWindowLabel(label) {
88
+ const normalized = normalizeWindowLabel(label);
89
+ if (normalized === "weekly")
90
+ return "wk";
91
+ if (normalized === "monthly")
92
+ return "mo";
93
+ return truncateText(label, 14);
94
+ }
95
+ function formatCount(value) {
96
+ if (!Number.isFinite(value))
97
+ return "0";
98
+ if (Math.abs(value) >= 1000) {
99
+ return new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(value);
100
+ }
101
+ if (Number.isInteger(value))
102
+ return String(value);
103
+ return new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(value);
104
+ }
105
+ function getUsageWindows(usage) {
106
+ if (usage.type !== "quotaBased")
107
+ return [];
108
+ return usage.windows.filter((window) => window.label !== "balance" && !window.label.startsWith("$"));
109
+ }
110
+ function formatMiniChart(utilization) {
111
+ return buildBar(utilization, MINI_BAR_WIDTH);
112
+ }
113
+ function getUsageTotals(usage) {
114
+ if (usage.type === "payAsYouGo") {
115
+ return {
116
+ usedLabel: `$${usage.used.toFixed(2)}`,
117
+ totalLabel: `$${usage.total.toFixed(2)}`,
118
+ remainingLabel: `$${usage.remaining.toFixed(2)}`,
119
+ };
120
+ }
121
+ if (typeof usage.entitlement === "number" &&
122
+ typeof usage.remaining === "number" &&
123
+ usage.entitlement > 0) {
124
+ const remaining = Math.max(0, usage.remaining);
125
+ const used = usage.remaining < 0
126
+ ? usage.entitlement + Math.abs(usage.remaining)
127
+ : usage.entitlement - remaining;
128
+ return {
129
+ usedLabel: formatCount(used),
130
+ totalLabel: formatCount(usage.entitlement),
131
+ remainingLabel: formatCount(remaining),
132
+ };
133
+ }
134
+ const usedPct = Math.round(usage.utilization);
135
+ const remainingPct = Math.max(0, 100 - usedPct);
136
+ return {
137
+ usedLabel: `${usedPct}%`,
138
+ totalLabel: "100%",
139
+ remainingLabel: `${remainingPct}%`,
140
+ };
141
+ }
142
+ function hasAbsoluteQuotaTotals(usage) {
143
+ return (usage.type === "quotaBased" &&
144
+ typeof usage.entitlement === "number" &&
145
+ typeof usage.remaining === "number" &&
146
+ usage.entitlement > 0);
147
+ }
148
+ function formatUsageSummary(usage) {
149
+ const totals = getUsageTotals(usage);
150
+ const pctColor = utilizationColor(usage.utilization);
151
+ const parts = [
152
+ `${formatMiniChart(usage.utilization)} ${pctColor}${Math.round(usage.utilization)}%${c.reset}`,
153
+ `${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`,
154
+ `${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`,
155
+ `${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`,
156
+ ];
157
+ return parts.join(` ${c.dim}·${c.reset} `);
158
+ }
159
+ function formatQuotaWindowDetails(window, usage, index) {
160
+ const used = Math.round(window.utilization);
161
+ const left = Math.max(0, 100 - used);
162
+ const totals = index === 0 && hasAbsoluteQuotaTotals(usage) ? getUsageTotals(usage) : undefined;
163
+ const parts = [
164
+ `${c.bold}${window.label}${c.reset}`,
165
+ `${formatMiniChart(window.utilization)} ${utilizationColor(window.utilization)}${used}%${c.reset} ${c.dim}used${c.reset}`,
166
+ ];
167
+ if (totals) {
168
+ parts.push(`${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`, `${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`, `${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`);
169
+ }
170
+ else {
171
+ parts.push(`${utilizationColor(window.utilization)}${left}%${c.reset} ${c.dim}left${c.reset}`);
172
+ }
173
+ parts.push(`${c.dim}resets ${formatResetTime(window.resetsAt)}${c.reset}`);
174
+ return parts.join(` ${c.dim}·${c.reset} `);
175
+ }
176
+ function formatUsageDetailLines(usage) {
177
+ if (usage.type === "payAsYouGo") {
178
+ return [
179
+ `${c.dim}credits${c.reset} ${c.red}$${usage.used.toFixed(2)}${c.reset} ${c.dim}used${c.reset} ${c.dim}·${c.reset} ${c.cyan}$${usage.total.toFixed(2)}${c.reset} ${c.dim}total${c.reset} ${c.dim}·${c.reset} ${c.green}$${usage.remaining.toFixed(2)}${c.reset} ${c.dim}left${c.reset}`,
180
+ ];
181
+ }
182
+ return getUsageWindows(usage).map((window, index) => formatQuotaWindowDetails(window, usage, index));
183
+ }
184
+ function getStatusLabel(status) {
185
+ if (status === "auth_expired")
186
+ return "auth expired";
187
+ if (status === "not_configured")
188
+ return "not configured";
189
+ return status;
190
+ }
191
+ function formatStatusCell(status, width) {
192
+ const label = getStatusLabel(status);
193
+ const color = status === "ok"
194
+ ? c.green
195
+ : status === "not_configured"
196
+ ? c.dim
197
+ : status === "auth_expired"
198
+ ? c.yellow
199
+ : c.red;
200
+ return padRight(`${color}${label}${c.reset}`, width);
201
+ }
202
+ function formatMetricCell(value, width, color) {
203
+ return padLeft(`${color}${value}${c.reset}`, width);
204
+ }
205
+ function getPrimaryResetLabel(usage) {
206
+ if (!usage || usage.type !== "quotaBased")
207
+ return "—";
208
+ const resets = usage.windows
209
+ .filter((window) => window.resetsAt && window.resetsAt > Date.now())
210
+ .sort((a, b) => (a.resetsAt || 0) - (b.resetsAt || 0));
211
+ return resets.length > 0 ? stripAnsi(formatResetTime(resets[0].resetsAt)) : "—";
212
+ }
213
+ function getWindowTotals(window) {
214
+ if (typeof window.entitlement !== "number" ||
215
+ typeof window.remaining !== "number" ||
216
+ window.entitlement <= 0) {
217
+ return undefined;
218
+ }
219
+ const remaining = Math.max(0, window.remaining);
220
+ const used = window.remaining < 0
221
+ ? window.entitlement + Math.abs(window.remaining)
222
+ : window.entitlement - remaining;
223
+ return {
224
+ usedLabel: formatCount(used),
225
+ totalLabel: formatCount(window.entitlement),
226
+ remainingLabel: formatCount(remaining),
227
+ };
228
+ }
229
+ function formatWindowDetail(window) {
230
+ const pctUsed = Math.round(window.utilization);
231
+ const pctLeft = Math.max(0, 100 - pctUsed);
232
+ const totals = getWindowTotals(window);
233
+ const parts = totals
234
+ ? [
235
+ `${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`,
236
+ `${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`,
237
+ `${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`,
238
+ `${utilizationColor(window.utilization)}${pctUsed}%${c.reset}`,
239
+ ]
240
+ : [
241
+ `${utilizationColor(window.utilization)}${pctUsed}%${c.reset} ${c.dim}used${c.reset}`,
242
+ `${c.green}${pctLeft}%${c.reset} ${c.dim}left${c.reset}`,
243
+ ];
244
+ parts.push(`${c.dim}resets ${stripAnsi(formatResetTime(window.resetsAt))}${c.reset}`);
245
+ return `${c.bold}${window.label}${c.reset}: ${parts.join(` ${c.dim}·${c.reset} `)}`;
246
+ }
247
+ function getResultDetailLines(result, verbose) {
248
+ if (result.status === "error" || result.status === "auth_expired") {
249
+ return result.error ? [result.error] : [];
250
+ }
251
+ if (result.status !== "ok" || !result.usage)
252
+ return [];
253
+ if (result.usage.type === "payAsYouGo") {
254
+ return verbose ? formatUsageDetailLines(result.usage) : [];
255
+ }
256
+ const windows = getUsageWindows(result.usage);
257
+ if (windows.length === 0)
258
+ return [];
259
+ if (!verbose && windows.length === 1)
260
+ return [];
261
+ return windows.map((window) => formatWindowDetail(window));
262
+ }
263
+ function buildSummaryRow(result, context, verbose, labels) {
264
+ const rawLabel = labels?.rawLabel;
265
+ const provider = labels?.displayLabel || getDisplayLabel(result, rawLabel, context);
266
+ const plan = getPlanLabel(result, result.usage, rawLabel, context, result.plan) || "—";
267
+ if (result.status !== "ok" || !result.usage) {
268
+ return {
269
+ provider,
270
+ status: result.status,
271
+ plan,
272
+ used: "—",
273
+ total: "—",
274
+ left: "—",
275
+ reset: "—",
276
+ details: getResultDetailLines(result, verbose),
277
+ };
278
+ }
279
+ const totals = getUsageTotals(result.usage);
280
+ return {
281
+ provider,
282
+ status: result.status,
283
+ plan,
284
+ used: totals.usedLabel,
285
+ total: totals.totalLabel,
286
+ left: totals.remainingLabel,
287
+ reset: getPrimaryResetLabel(result.usage),
288
+ usage: result.usage,
289
+ details: getResultDetailLines(result, verbose),
290
+ };
291
+ }
292
+ function getSummaryTableLayout(rows) {
293
+ return {
294
+ providerWidth: Math.max(18, ...rows.map((row) => visibleLength(row.provider)), visibleLength("Provider / Account")),
295
+ statusWidth: Math.max(14, ...rows.map((row) => visibleLength(getStatusLabel(row.status))), visibleLength("Status")),
296
+ planWidth: Math.max(10, ...rows.map((row) => visibleLength(row.plan)), visibleLength("Plan")),
297
+ usedWidth: Math.max(8, ...rows.map((row) => visibleLength(row.used)), visibleLength("Used")),
298
+ totalWidth: Math.max(8, ...rows.map((row) => visibleLength(row.total)), visibleLength("Total")),
299
+ leftWidth: Math.max(8, ...rows.map((row) => visibleLength(row.left)), visibleLength("Left")),
300
+ resetWidth: Math.max(8, ...rows.map((row) => visibleLength(row.reset)), visibleLength("Reset")),
301
+ };
302
+ }
303
+ function formatSummaryRow(row, layout) {
304
+ const providerCell = padRight(row.provider, layout.providerWidth);
305
+ const planCell = padRight(row.plan === "—" ? `${c.dim}—${c.reset}` : `${c.cyan}${row.plan}${c.reset}`, layout.planWidth);
306
+ const usedCell = row.status === "ok"
307
+ ? formatMetricCell(row.used, layout.usedWidth, c.red)
308
+ : padLeft(`${c.dim}${row.used}${c.reset}`, layout.usedWidth);
309
+ const totalCell = row.status === "ok"
310
+ ? formatMetricCell(row.total, layout.totalWidth, c.cyan)
311
+ : padLeft(`${c.dim}${row.total}${c.reset}`, layout.totalWidth);
312
+ const leftCell = row.status === "ok"
313
+ ? formatMetricCell(row.left, layout.leftWidth, c.green)
314
+ : padLeft(`${c.dim}${row.left}${c.reset}`, layout.leftWidth);
315
+ const resetCell = padLeft(row.reset === "—" ? `${c.dim}—${c.reset}` : row.reset, layout.resetWidth);
316
+ return `${TABLE_INDENT}${providerCell}${COLUMN_GAP}${formatStatusCell(row.status, layout.statusWidth)}${COLUMN_GAP}${planCell}${COLUMN_GAP}${usedCell}${COLUMN_GAP}${totalCell}${COLUMN_GAP}${leftCell}${COLUMN_GAP}${resetCell}`;
317
+ }
318
+ function formatDetailLine(detail) {
319
+ return `${TABLE_INDENT} ${c.dim}↳${c.reset} ${detail}`;
320
+ }
321
+ function formatQuotaWindow(window, usage) {
322
+ const used = Math.round(window.utilization);
323
+ const left = Math.max(0, 100 - used);
324
+ const color = utilizationColor(used);
325
+ const label = `${c.dim}${abbreviateWindowLabel(window.label)}${c.reset}`;
326
+ const chart = formatMiniChart(window.utilization);
327
+ if (usage.type === "quotaBased" &&
328
+ typeof usage.remaining === "number" &&
329
+ typeof usage.entitlement === "number" &&
330
+ usage.entitlement > 0 &&
331
+ getUsageWindows(usage)[0] === window) {
332
+ return `${label} ${chart} ${c.green}${formatCount(usage.remaining)}${c.reset}${c.dim}/${c.reset}${c.cyan}${formatCount(usage.entitlement)}${c.reset}`;
333
+ }
334
+ return `${label} ${chart} ${color}${used}${c.reset}${c.dim}/${c.reset}${color}${left}${c.reset}`;
335
+ }
336
+ function formatUsageDetails(usage) {
337
+ return formatUsageSummary(usage);
338
+ }
339
+ function formatPlanValue(rawPlan) {
340
+ if (!rawPlan)
341
+ return undefined;
342
+ const trimmed = rawPlan.trim();
343
+ if (!trimmed)
344
+ return undefined;
345
+ const normalized = trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "_");
346
+ const knownPlans = {
347
+ plus: "Plus",
348
+ pro: "Pro",
349
+ team: "Team",
350
+ business: "Business",
351
+ enterprise: "Enterprise",
352
+ free: "Free",
353
+ individual_pro: "Individual Pro",
354
+ individual_free: "Individual Free",
355
+ chatgpt_plus: "Plus",
356
+ chatgptplus: "Plus",
357
+ chatgpt_pro: "Pro",
358
+ chatgptpro: "Pro",
359
+ chatgpt_team: "Team",
360
+ chatgptteam: "Team",
361
+ };
362
+ const known = knownPlans[normalized];
363
+ if (known)
364
+ return known;
365
+ return trimmed
366
+ .split(/[_\s-]+/)
367
+ .filter(Boolean)
368
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
369
+ .join(" ");
370
+ }
371
+ function extractPlanFromUsage(usage) {
372
+ if (!usage || usage.type !== "quotaBased")
373
+ return undefined;
374
+ for (const window of usage.windows) {
375
+ const match = window.label.match(/\(([^()]+)\)/);
376
+ if (match?.[1])
377
+ return formatPlanValue(match[1]);
378
+ }
379
+ return undefined;
380
+ }
381
+ function extractPlanFromProviderName(result) {
382
+ if (result.providerId !== "copilot")
383
+ return undefined;
384
+ const match = result.providerName.match(/\(([^()]+)\)\s*$/);
385
+ return match?.[1] ? formatPlanValue(match[1]) : undefined;
386
+ }
387
+ function getProviderDisplayName(result) {
388
+ if (result.providerId === "copilot") {
389
+ return result.providerName.replace(/\s*\([^()]+\)\s*$/, "");
390
+ }
391
+ return result.providerName;
392
+ }
393
+ function getPlanTypeFromClaims(claims) {
394
+ if (!claims)
395
+ return undefined;
396
+ const auth = claims["https://api.openai.com/auth"];
397
+ return typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type : undefined;
398
+ }
399
+ function getCodexPlanFromAccount(account) {
400
+ const explicitPlan = formatPlanValue(account.planType);
401
+ if (explicitPlan)
402
+ return explicitPlan;
403
+ const accessTokenPlan = formatPlanValue(getPlanTypeFromClaims(decodeJwtPayload(account.accessToken)));
404
+ if (accessTokenPlan)
405
+ return accessTokenPlan;
406
+ const idTokenPlan = formatPlanValue(getPlanTypeFromClaims(decodeJwtPayload(account.idToken || "")));
407
+ if (idTokenPlan)
408
+ return idTokenPlan;
409
+ return undefined;
410
+ }
411
+ function getRenderContext() {
412
+ const store = loadStore();
413
+ const codexPlansByAlias = new Map();
414
+ for (const account of Object.values(store.accounts)) {
415
+ const plan = getCodexPlanFromAccount(account);
416
+ if (plan) {
417
+ codexPlansByAlias.set(account.alias, plan);
418
+ }
419
+ }
420
+ return {
421
+ activeAlias: store.activeAlias,
422
+ codexPlansByAlias,
423
+ };
424
+ }
425
+ function hasAccountRows(result) {
426
+ return Array.isArray(result.accounts) && result.accounts.length > 0;
427
+ }
428
+ function getDisplayLabel(result, rawLabel, context) {
429
+ const label = rawLabel || getProviderDisplayName(result);
430
+ if (result.providerId === "codex" && rawLabel && rawLabel === context.activeAlias) {
431
+ return `${rawLabel} ${c.cyan}(active)${c.reset}`;
432
+ }
433
+ return label;
434
+ }
435
+ function normalizePlanLookupLabel(label) {
436
+ if (!label)
437
+ return undefined;
438
+ const plain = stripAnsi(label)
439
+ .replace(/\s+\(active\)\s*$/, "")
440
+ .trim();
441
+ return plain || undefined;
442
+ }
443
+ function getPlanLabel(result, usage, rawLabel, context, explicitPlan) {
444
+ const directPlan = formatPlanValue(explicitPlan || result.plan);
445
+ if (directPlan)
446
+ return directPlan;
447
+ if (result.providerId === "codex") {
448
+ const lookupLabel = normalizePlanLookupLabel(rawLabel);
449
+ if (lookupLabel) {
450
+ return context.codexPlansByAlias.get(lookupLabel);
451
+ }
452
+ }
453
+ return extractPlanFromUsage(usage) || extractPlanFromProviderName(result);
454
+ }
455
+ function formatPlanCell(plan, width) {
456
+ if (!plan) {
457
+ return padRight(`${c.dim}—${c.reset}`, width);
458
+ }
459
+ return padRight(`${c.cyan}${plan}${c.reset}`, width);
460
+ }
461
+ function getTableLayout(results, context) {
462
+ const labels = results.flatMap((result) => [
463
+ getProviderDisplayName(result),
464
+ ...(result.accounts?.map((acc) => ` ${getDisplayLabel(result, acc.label, context)}`) ?? []),
465
+ ]);
466
+ const providerWidth = clamp(Math.max(18, ...labels.map((label) => visibleLength(label))), 18, Number.MAX_SAFE_INTEGER);
467
+ const planLengths = results.flatMap((result) => {
468
+ const plans = [];
469
+ if (!hasAccountRows(result)) {
470
+ plans.push(getPlanLabel(result, result.usage, undefined, context, result.plan));
471
+ }
472
+ for (const account of result.accounts ?? []) {
473
+ plans.push(getPlanLabel(result, account.usage, account.label, context, account.plan));
474
+ }
475
+ return plans.map((plan) => visibleLength(plan ?? "—"));
476
+ });
477
+ const planWidth = clamp(Math.max(10, ...planLengths), 10, Number.MAX_SAFE_INTEGER);
478
+ return {
479
+ providerWidth,
480
+ usageWidth: clamp(Math.max(18, ...results.flatMap((result) => {
481
+ const values = [];
482
+ if (result.usage)
483
+ values.push(formatUsageDetails(result.usage));
484
+ for (const account of result.accounts ?? []) {
485
+ values.push(formatUsageDetails(account.usage));
486
+ }
487
+ return values.map((value) => visibleLength(value));
488
+ })), 18, Number.MAX_SAFE_INTEGER),
489
+ planWidth,
490
+ };
65
491
  }
66
492
  // ── Format a single result row ────────────────────────────────
67
- function formatResultRow(result, label) {
68
- const name = padRight(label || result.providerName, 18);
69
- if (result.status === 'not_configured') {
70
- return ` ${c.dim}${name}${padRight('not configured', 12)}${c.reset}`;
493
+ function formatResultRow(result, layout, context, labels) {
494
+ const rawLabel = labels?.rawLabel;
495
+ const displayLabel = labels?.displayLabel || getDisplayLabel(result, rawLabel, context);
496
+ const name = padRight(displayLabel, layout.providerWidth);
497
+ const tablePrefix = `${TABLE_INDENT}${name}${COLUMN_GAP}`;
498
+ if (result.status === "not_configured") {
499
+ return `${TABLE_INDENT}${c.dim}${name}${COLUMN_GAP}${padRight("not configured", layout.usageWidth + layout.planWidth + COLUMN_GAP.length + 6)}${c.reset}`;
71
500
  }
72
- if (result.status === 'auth_expired') {
73
- return ` ${c.dim}${padRight(label || result.providerName, 18)}${c.reset}${c.red}${padRight('auth expired', 12)}${c.reset} ${c.dim}${result.error || ''}${c.reset}`;
501
+ if (result.status === "auth_expired") {
502
+ return `${tablePrefix}${c.red}${padRight("auth expired", layout.usageWidth)}${c.reset}${COLUMN_GAP}${formatPlanCell(undefined, layout.planWidth)}${COLUMN_GAP}${c.dim}${result.error || ""}${c.reset}`;
74
503
  }
75
- if (result.status === 'error') {
76
- return ` ${padRight(label || result.providerName, 18)}${c.red}${padRight('error', 12)}${c.reset} ${c.dim}${(result.error || '').slice(0, 50)}${c.reset}`;
504
+ if (result.status === "error") {
505
+ return `${tablePrefix}${c.red}${padRight("error", layout.usageWidth)}${c.reset}${COLUMN_GAP}${formatPlanCell(undefined, layout.planWidth)}${COLUMN_GAP}${c.dim}${result.error || ""}${c.reset}`;
77
506
  }
78
507
  if (!result.usage) {
79
- return ` ${padRight(label || result.providerName, 18)}${c.dim}no data${c.reset}`;
508
+ return `${tablePrefix}${c.dim}no data${c.reset}`;
80
509
  }
81
510
  const usage = result.usage;
82
- if (usage.type === 'payAsYouGo') {
83
- const bar = buildBar(usage.utilization);
84
- const costStr = `$${usage.used.toFixed(2)} / $${usage.total.toFixed(2)}`;
85
- return ` ${padRight(label || result.providerName, 18)}${bar} ${padRight(costStr, 22)}${c.dim}—${c.reset}`;
86
- }
87
- // quotaBased
88
- const bar = buildBar(usage.utilization);
89
- const pctColor = utilizationColor(usage.utilization);
90
- // Build usage string from windows
91
- let usageStr = '';
92
- if (usage.windows.length > 0) {
93
- const parts = usage.windows
94
- .filter(w => w.label !== 'balance' && !w.label.startsWith('$'))
95
- .slice(0, 3) // Show up to 3 windows
96
- .map((w) => {
97
- const wColor = utilizationColor(w.utilization);
98
- return `${wColor}${Math.round(w.utilization)}%${c.reset} ${c.dim}(${w.label})${c.reset}`;
99
- });
100
- usageStr = parts.join(' ');
101
- }
102
- else {
103
- usageStr = `${pctColor}${Math.round(usage.utilization)}%${c.reset}`;
511
+ const usageCell = padRight(formatUsageDetails(usage), layout.usageWidth);
512
+ const planCell = formatPlanCell(getPlanLabel(result, usage, rawLabel, context, result.plan), layout.planWidth);
513
+ if (usage.type === "payAsYouGo") {
514
+ return `${tablePrefix}${usageCell}${COLUMN_GAP}${planCell}${COLUMN_GAP}${c.dim}—${c.reset}`;
104
515
  }
105
516
  // Reset time: pick earliest reset
106
517
  const resets = usage.windows
107
518
  .filter((w) => w.resetsAt && w.resetsAt > Date.now())
108
519
  .sort((a, b) => (a.resetsAt || 0) - (b.resetsAt || 0));
109
520
  const resetStr = resets.length > 0 ? formatResetTime(resets[0].resetsAt) : `${c.dim}—${c.reset}`;
110
- return ` ${padRight(label || result.providerName, 18)}${bar} ${padRight(usageStr, 32)}${resetStr}`;
521
+ return `${tablePrefix}${usageCell}${COLUMN_GAP}${planCell}${COLUMN_GAP}${resetStr}`;
522
+ }
523
+ function formatProviderHeaderRow(result, layout) {
524
+ const name = padRight(getProviderDisplayName(result), layout.providerWidth);
525
+ return `${TABLE_INDENT}${c.bold}${name}${c.reset}${COLUMN_GAP}${" ".repeat(layout.usageWidth)}${COLUMN_GAP}${" ".repeat(layout.planWidth)}`;
526
+ }
527
+ function formatDetailRow(layout, detail) {
528
+ const emptyProvider = " ".repeat(layout.providerWidth);
529
+ const emptyPlan = " ".repeat(layout.planWidth);
530
+ return `${TABLE_INDENT}${emptyProvider}${COLUMN_GAP}${c.dim}↳${c.reset} ${detail}${COLUMN_GAP}${emptyPlan}`;
531
+ }
532
+ function formatResultDetailRows(result, layout) {
533
+ if (result.status !== "ok" || !result.usage)
534
+ return [];
535
+ return formatUsageDetailLines(result.usage).map((detail) => formatDetailRow(layout, detail));
111
536
  }
112
537
  // ── Format account sub-rows ───────────────────────────────────
113
- function formatAccountRows(result) {
114
- if (!result.accounts || result.accounts.length <= 1)
538
+ function formatAccountRows(result, layout, context) {
539
+ if (!result.accounts || result.accounts.length === 0)
115
540
  return [];
116
541
  return result.accounts.map((acc) => {
117
- const subLabel = ` ${acc.label}`;
542
+ const subLabel = `${TABLE_INDENT}${getDisplayLabel(result, acc.label, context)}`;
118
543
  const subResult = {
119
544
  providerId: result.providerId,
120
- providerName: subLabel,
545
+ providerName: acc.label,
121
546
  billingType: result.billingType,
547
+ plan: acc.plan,
122
548
  status: acc.status,
123
549
  usage: acc.usage,
124
550
  error: acc.error,
125
551
  fetchedAt: result.fetchedAt,
126
552
  };
127
- return formatResultRow(subResult, subLabel);
553
+ return formatResultRow(subResult, layout, context, {
554
+ rawLabel: acc.label,
555
+ displayLabel: subLabel,
556
+ });
128
557
  });
129
558
  }
559
+ function isFullUsageCache(results) {
560
+ const expectedProviderIds = new Set(allProviders.map((provider) => provider.id));
561
+ if (results.length !== expectedProviderIds.size)
562
+ return false;
563
+ for (const result of results) {
564
+ if (!expectedProviderIds.has(result.providerId)) {
565
+ return false;
566
+ }
567
+ }
568
+ return true;
569
+ }
130
570
  export async function runUsageCommand(opts) {
131
571
  const providerIds = opts.provider ? [opts.provider] : undefined;
132
572
  // Validate provider name
133
573
  if (opts.provider) {
134
574
  const valid = allProviders.map((p) => p.id);
135
575
  if (!valid.includes(opts.provider)) {
136
- console.error(`Unknown provider: ${opts.provider}\nAvailable: ${valid.join(', ')}`);
576
+ console.error(`Unknown provider: ${opts.provider}\nAvailable: ${valid.join(", ")}`);
137
577
  process.exit(1);
138
578
  }
139
579
  }
@@ -141,11 +581,18 @@ export async function runUsageCommand(opts) {
141
581
  if (!opts.noCache && !opts.json) {
142
582
  const cached = readUsageCache();
143
583
  if (cached) {
144
- const filtered = providerIds
145
- ? cached.results.filter((r) => providerIds.includes(r.providerId))
146
- : cached.results;
147
- renderTable(filtered, opts.verbose);
148
- return;
584
+ if (!providerIds && !isFullUsageCache(cached.results)) {
585
+ // Ignore stale partial caches left by older provider-scoped fetches.
586
+ }
587
+ else {
588
+ const filtered = providerIds
589
+ ? cached.results.filter((r) => providerIds.includes(r.providerId))
590
+ : cached.results;
591
+ if (providerIds ? filtered.length > 0 : true) {
592
+ renderTable(filtered, opts.verbose);
593
+ return;
594
+ }
595
+ }
149
596
  }
150
597
  }
151
598
  // Show spinner
@@ -154,10 +601,12 @@ export async function runUsageCommand(opts) {
154
601
  }
155
602
  const results = await fetchAllUsage({ providerIds });
156
603
  // Cache results
157
- writeUsageCache(results);
604
+ if (!providerIds) {
605
+ writeUsageCache(results);
606
+ }
158
607
  // Clear spinner line
159
608
  if (!opts.json) {
160
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
609
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
161
610
  }
162
611
  // JSON output
163
612
  if (opts.json) {
@@ -167,32 +616,87 @@ export async function runUsageCommand(opts) {
167
616
  renderTable(results, opts.verbose);
168
617
  }
169
618
  function renderTable(results, verbose = false) {
170
- // Table output
171
- const configured = results.filter((r) => r.status !== 'not_configured');
172
- const notConfigured = results.filter((r) => r.status === 'not_configured');
619
+ const context = getRenderContext();
620
+ const configured = results.filter((r) => r.status !== "not_configured");
621
+ const notConfigured = results.filter((r) => r.status === "not_configured");
622
+ const summaryRows = [];
623
+ for (const result of results) {
624
+ if (hasAccountRows(result)) {
625
+ for (const account of result.accounts ?? []) {
626
+ const subResult = {
627
+ providerId: result.providerId,
628
+ providerName: account.label,
629
+ billingType: result.billingType,
630
+ plan: account.plan,
631
+ status: account.status,
632
+ usage: account.usage,
633
+ error: account.error,
634
+ fetchedAt: result.fetchedAt,
635
+ };
636
+ summaryRows.push(buildSummaryRow(subResult, context, verbose, {
637
+ rawLabel: account.label,
638
+ displayLabel: ` ${getDisplayLabel(result, account.label, context)}`,
639
+ }));
640
+ }
641
+ continue;
642
+ }
643
+ summaryRows.push(buildSummaryRow(result, context, verbose));
644
+ }
645
+ const layout = getSummaryTableLayout(summaryRows);
173
646
  console.log();
174
- console.log(` ${c.bold}${padRight('Provider', 18)}${padRight('Usage', 10)} ${padRight('Details', 32)}Resets${c.reset}`);
175
- console.log(` ${c.dim}${''.repeat(78)}${c.reset}`);
176
- for (const result of configured) {
177
- console.log(formatResultRow(result));
178
- // Show sub-accounts for multi-account providers
179
- if (verbose || (result.accounts && result.accounts.length > 1)) {
180
- const subRows = formatAccountRows(result);
181
- for (const row of subRows) {
182
- console.log(row);
647
+ const header = `${TABLE_INDENT}${c.bold}${padRight("Provider / Account", layout.providerWidth)}${COLUMN_GAP}${padRight("Status", layout.statusWidth)}${COLUMN_GAP}${padRight("Plan", layout.planWidth)}${COLUMN_GAP}${padLeft("Used", layout.usedWidth)}${COLUMN_GAP}${padLeft("Total", layout.totalWidth)}${COLUMN_GAP}${padLeft("Left", layout.leftWidth)}${COLUMN_GAP}${padLeft("Reset", layout.resetWidth)}${c.reset}`;
648
+ const divider = `${TABLE_INDENT}${c.dim}${"".repeat(visibleLength(header) - visibleLength(TABLE_INDENT))}${c.reset}`;
649
+ console.log(header);
650
+ console.log(divider);
651
+ for (const [resultIndex, result] of configured.entries()) {
652
+ if (hasAccountRows(result)) {
653
+ console.log(`${TABLE_INDENT}${c.bold}${getProviderDisplayName(result)}${c.reset}`);
654
+ for (const account of result.accounts ?? []) {
655
+ const subResult = {
656
+ providerId: result.providerId,
657
+ providerName: account.label,
658
+ billingType: result.billingType,
659
+ plan: account.plan,
660
+ status: account.status,
661
+ usage: account.usage,
662
+ error: account.error,
663
+ fetchedAt: result.fetchedAt,
664
+ };
665
+ const row = buildSummaryRow(subResult, context, verbose, {
666
+ rawLabel: account.label,
667
+ displayLabel: ` ${getDisplayLabel(result, account.label, context)}`,
668
+ });
669
+ console.log(formatSummaryRow(row, layout));
670
+ for (const detail of row.details) {
671
+ console.log(formatDetailLine(detail));
672
+ }
673
+ }
674
+ if (resultIndex < configured.length - 1) {
675
+ console.log();
183
676
  }
677
+ continue;
678
+ }
679
+ const row = buildSummaryRow(result, context, verbose);
680
+ console.log(formatSummaryRow(row, layout));
681
+ for (const detail of row.details) {
682
+ console.log(formatDetailLine(detail));
683
+ }
684
+ if (resultIndex < configured.length - 1) {
685
+ console.log();
184
686
  }
185
687
  }
186
688
  if (notConfigured.length > 0) {
187
- console.log(` ${c.dim}${'─'.repeat(78)}${c.reset}`);
689
+ if (configured.length > 0) {
690
+ console.log(divider);
691
+ }
188
692
  for (const result of notConfigured) {
189
- console.log(formatResultRow(result));
693
+ const row = buildSummaryRow(result, context, verbose);
694
+ console.log(formatSummaryRow(row, layout));
190
695
  }
191
696
  }
192
- console.log(` ${c.dim}${'─'.repeat(78)}${c.reset}`);
193
- // Summary
194
- const okCount = configured.filter((r) => r.status === 'ok').length;
195
- const errCount = configured.filter((r) => r.status === 'error' || r.status === 'auth_expired').length;
697
+ console.log(divider);
698
+ const okCount = configured.filter((r) => r.status === "ok").length;
699
+ const errCount = configured.filter((r) => r.status === "error" || r.status === "auth_expired").length;
196
700
  const notConfCount = notConfigured.length;
197
701
  const parts = [];
198
702
  if (okCount > 0)