@99percentpeople/pi-codex-api 0.1.4 → 0.2.1

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.ts CHANGED
@@ -11,7 +11,8 @@ var DEFAULT_CODEX_API_CONFIG = {
11
11
  searchMode: "auto",
12
12
  searchContextSize: "medium",
13
13
  imageQuality: "auto",
14
- usageStatus: true
14
+ usageStatus: true,
15
+ usagePollInterval: 5
15
16
  };
16
17
  function oneOf(value, values, fallback) {
17
18
  return typeof value === "string" && values.includes(value) ? value : fallback;
@@ -26,7 +27,8 @@ function normalizeCodexApiConfig(value) {
26
27
  searchMode: oneOf(input.searchMode, ["auto", "cached", "indexed", "live"], DEFAULT_CODEX_API_CONFIG.searchMode),
27
28
  searchContextSize: oneOf(input.searchContextSize, ["low", "medium", "high"], DEFAULT_CODEX_API_CONFIG.searchContextSize),
28
29
  imageQuality: oneOf(input.imageQuality, ["auto", "low", "medium", "high"], DEFAULT_CODEX_API_CONFIG.imageQuality),
29
- usageStatus: typeof input.usageStatus === "boolean" ? input.usageStatus : DEFAULT_CODEX_API_CONFIG.usageStatus
30
+ usageStatus: typeof input.usageStatus === "boolean" ? input.usageStatus : DEFAULT_CODEX_API_CONFIG.usageStatus,
31
+ usagePollInterval: typeof input.usagePollInterval === "number" && Number.isFinite(input.usagePollInterval) && input.usagePollInterval > 0 ? Math.min(60, Math.round(input.usagePollInterval)) : DEFAULT_CODEX_API_CONFIG.usagePollInterval
30
32
  };
31
33
  }
32
34
  function getCodexApiConfigPath() {
@@ -381,7 +383,7 @@ function imagePhaseLabel(phase) {
381
383
  return "Saving generated PNG…";
382
384
  return "Image completed";
383
385
  }
384
- function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG, refreshUsageInBackground) {
386
+ function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG) {
385
387
  pi.registerTool({
386
388
  name: "codex_image",
387
389
  label: "Codex Image",
@@ -466,7 +468,6 @@ function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG,
466
468
  update("saving");
467
469
  await mkdir(dirname(savedPath), { recursive: true });
468
470
  await writeFile(savedPath, Buffer.from(data, "base64"));
469
- refreshUsageInBackground?.(ctx);
470
471
  return {
471
472
  content: [
472
473
  { type: "text", text: `${operation === "edit" ? "Edited" : "Generated"} image saved to ${savedPath}` },
@@ -1113,6 +1114,15 @@ var IMAGE_QUALITY_LABELS = {
1113
1114
  medium: "Medium",
1114
1115
  high: "High"
1115
1116
  };
1117
+ function usagePollLabel(minutes) {
1118
+ if (minutes <= 0)
1119
+ return "Off";
1120
+ return `${minutes}m`;
1121
+ }
1122
+ function usagePollMinutes(label) {
1123
+ const match = /^(\d+)m$/.exec(label);
1124
+ return match ? Number(match[1]) : 0;
1125
+ }
1116
1126
  function keyForLabel(labels, value) {
1117
1127
  return Object.entries(labels).find(([, label]) => label === value)?.[0];
1118
1128
  }
@@ -1164,6 +1174,13 @@ function registerCodexApiSettings(pi, controller) {
1164
1174
  description: "Show remaining Codex subscription usage in the status area",
1165
1175
  currentValue: config.usageStatus ? "Show" : "Hide",
1166
1176
  values: ["Show", "Hide"]
1177
+ },
1178
+ {
1179
+ id: "usagePollInterval",
1180
+ label: "Usage poll",
1181
+ description: "Periodically refresh the usage status while a session is active (Off disables polling)",
1182
+ currentValue: usagePollLabel(config.usagePollInterval),
1183
+ values: ["Off", "1m", "5m", "15m"]
1167
1184
  }
1168
1185
  ];
1169
1186
  },
@@ -1190,6 +1207,8 @@ function registerCodexApiSettings(pi, controller) {
1190
1207
  }, ctx);
1191
1208
  } else if (id === "usageStatus") {
1192
1209
  controller.updateConfig({ ...config, usageStatus: value === "Show" }, ctx);
1210
+ } else if (id === "usagePollInterval") {
1211
+ controller.updateConfig({ ...config, usagePollInterval: usagePollMinutes(value) }, ctx);
1193
1212
  }
1194
1213
  }
1195
1214
  });
@@ -1202,6 +1221,11 @@ import {
1202
1221
  getAgentDir
1203
1222
  } from "@earendil-works/pi-coding-agent";
1204
1223
  var USAGE_PATH = "../wham/usage";
1224
+ var REDEEM_CREDITS_PATH = "../wham/rate-limit-reset-credits";
1225
+ var REDEEM_PATH = "../wham/rate-limit-reset-credits/consume";
1226
+ var REDEEM_CONFIRM_WINDOW_MS = 1e4;
1227
+ var REDEEM_DIALOG_TIMEOUT_MS = 30000;
1228
+ var REDEEM_RETRY_WINDOW_MS = 5 * 60000;
1205
1229
  var USAGE_REFRESH_INTERVAL_MS = 60000;
1206
1230
  var AUTH_WATCH_DEBOUNCE_MS = 100;
1207
1231
  var STATUS_KEY = "codex-api-usage";
@@ -1215,6 +1239,18 @@ function payloadNumber(value) {
1215
1239
  const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
1216
1240
  return Number.isFinite(number) ? number : undefined;
1217
1241
  }
1242
+ function payloadDate(value) {
1243
+ if (typeof value === "number")
1244
+ return Number.isFinite(value) ? value : undefined;
1245
+ if (typeof value === "string") {
1246
+ const ms = Date.parse(value);
1247
+ return Number.isFinite(ms) ? ms : undefined;
1248
+ }
1249
+ return;
1250
+ }
1251
+ function payloadString(value) {
1252
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1253
+ }
1218
1254
  function payloadBool(value) {
1219
1255
  if (typeof value === "boolean")
1220
1256
  return value;
@@ -1253,14 +1289,65 @@ function payloadCredits(value) {
1253
1289
  balance: typeof balance === "string" && balance ? balance : undefined
1254
1290
  };
1255
1291
  }
1256
- function payloadSnapshot(limitId, limitName, rateLimitValue, creditsValue) {
1292
+ function payloadSnapshot(limitId, limitName, rateLimitValue, creditsValue, limitReached) {
1257
1293
  const rateLimit = object(rateLimitValue);
1258
1294
  return {
1259
1295
  limitId,
1260
1296
  limitName,
1261
1297
  primary: payloadWindow(rateLimit && property(rateLimit, "primary_window", "primaryWindow")),
1262
1298
  secondary: payloadWindow(rateLimit && property(rateLimit, "secondary_window", "secondaryWindow")),
1263
- credits: payloadCredits(creditsValue)
1299
+ credits: payloadCredits(creditsValue),
1300
+ limitReached
1301
+ };
1302
+ }
1303
+ function parseCodexAccountInfo(value) {
1304
+ const input = object(value);
1305
+ if (!input)
1306
+ return;
1307
+ const planType = payloadString(property(input, "plan_type", "planType"));
1308
+ const email = payloadString(property(input, "email", "email"));
1309
+ if (planType === undefined && email === undefined)
1310
+ return;
1311
+ return { planType, email };
1312
+ }
1313
+ function maskCodexEmail(email) {
1314
+ const [local, domain] = email.split("@");
1315
+ if (!domain)
1316
+ return "***";
1317
+ const head = local.length > 3 ? local.slice(0, 3) : local.slice(0, 1);
1318
+ return `${head}***@${domain}`;
1319
+ }
1320
+ function parseCodexRedeemCredits(value) {
1321
+ const input = object(value);
1322
+ if (!input)
1323
+ return;
1324
+ const availableCount = payloadNumber(property(input, "available_count", "availableCount"));
1325
+ if (availableCount === undefined)
1326
+ return;
1327
+ const credits = [];
1328
+ const list = property(input, "credits", "credits");
1329
+ if (Array.isArray(list)) {
1330
+ for (const item of list) {
1331
+ const credit = object(item);
1332
+ if (!credit)
1333
+ continue;
1334
+ const id = payloadString(property(credit, "id", "id"));
1335
+ if (!id)
1336
+ continue;
1337
+ credits.push({
1338
+ id,
1339
+ title: payloadString(property(credit, "title", "title")),
1340
+ description: payloadString(property(credit, "description", "description")),
1341
+ status: payloadString(property(credit, "status", "status")),
1342
+ grantedAt: payloadDate(property(credit, "granted_at", "grantedAt")),
1343
+ expiresAt: payloadDate(property(credit, "expires_at", "expiresAt"))
1344
+ });
1345
+ }
1346
+ }
1347
+ return {
1348
+ availableCount,
1349
+ totalEarnedCount: payloadNumber(property(input, "total_earned_count", "totalEarnedCount")),
1350
+ credits
1264
1351
  };
1265
1352
  }
1266
1353
  function parseCodexUsagePayload(value) {
@@ -1268,7 +1355,9 @@ function parseCodexUsagePayload(value) {
1268
1355
  if (!input)
1269
1356
  return [];
1270
1357
  const rateLimit = property(input, "rate_limit", "rateLimit");
1271
- const snapshots = rateLimit !== undefined || input.credits !== undefined ? [payloadSnapshot("codex", undefined, rateLimit, input.credits)] : [];
1358
+ const rateLimitObject = object(rateLimit);
1359
+ const limitReached = payloadBool(rateLimitObject && property(rateLimitObject, "limit_reached", "limitReached"));
1360
+ const snapshots = rateLimit !== undefined || input.credits !== undefined ? [payloadSnapshot("codex", undefined, rateLimit, input.credits, limitReached)] : [];
1272
1361
  const additional = property(input, "additional_rate_limits", "additionalRateLimits");
1273
1362
  if (Array.isArray(additional)) {
1274
1363
  for (const value2 of additional) {
@@ -1330,14 +1419,16 @@ function parseCodexRateLimits(input) {
1330
1419
  unlimited,
1331
1420
  balance: headers["x-codex-credits-balance"]
1332
1421
  } : undefined;
1333
- if (!primary && !secondary && !credits)
1422
+ const limitReached = prefix === "x-codex" ? headers["x-codex-rate-limit-reached-type"] !== undefined : undefined;
1423
+ if (!primary && !secondary && !credits && !limitReached)
1334
1424
  return [];
1335
1425
  return [{
1336
1426
  limitId: prefix.slice(2).replace(/-/g, "_"),
1337
1427
  limitName: headers[`${prefix}-limit-name`],
1338
1428
  primary,
1339
1429
  secondary,
1340
- credits
1430
+ credits,
1431
+ limitReached
1341
1432
  }];
1342
1433
  });
1343
1434
  }
@@ -1350,13 +1441,17 @@ function resetText(epochSeconds, now = Date.now()) {
1350
1441
  const remainingMs = epochSeconds * 1000 - now;
1351
1442
  if (remainingMs <= 0)
1352
1443
  return;
1353
- const minutes = Math.ceil(remainingMs / 60000);
1354
- if (minutes < 60)
1355
- return `${minutes}m`;
1356
- const hours = Math.ceil(minutes / 60);
1357
- if (hours < 48)
1358
- return `${hours}h`;
1359
- return `${Math.ceil(hours / 24)}d`;
1444
+ const totalMinutes = Math.ceil(remainingMs / 60000);
1445
+ if (totalMinutes < 60)
1446
+ return `${totalMinutes}m`;
1447
+ if (totalMinutes < 24 * 60) {
1448
+ const hours2 = Math.floor(totalMinutes / 60);
1449
+ const minutes = totalMinutes % 60;
1450
+ return minutes > 0 ? `${hours2}h ${minutes}m` : `${hours2}h`;
1451
+ }
1452
+ const days = Math.floor(totalMinutes / (24 * 60));
1453
+ const hours = Math.floor(totalMinutes % (24 * 60) / 60);
1454
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
1360
1455
  }
1361
1456
  var KNOWN_WINDOWS = [
1362
1457
  { minutes: 5 * 60, label: "5h" },
@@ -1393,10 +1488,11 @@ function usageBar(remaining) {
1393
1488
  const filled = Math.round(remaining / 100 * USAGE_BAR_WIDTH);
1394
1489
  return `[${"█".repeat(filled)}${"░".repeat(USAGE_BAR_WIDTH - filled)}]`;
1395
1490
  }
1396
- function windowText(item, labelWidth, now) {
1491
+ function windowText(item, labelWidth, now, limitReached) {
1397
1492
  const reset = resetText(item.window.resetsAt, now);
1398
1493
  const remaining = remainingPercent(item.window);
1399
- return `${item.label.padEnd(labelWidth)} ${usageBar(remaining)} ${percent(remaining)}% left${reset ? ` resets in ${reset}` : ""}`;
1494
+ const state = limitReached ? "limit reached" : `${percent(remaining)}% left`;
1495
+ return `${item.label.padEnd(labelWidth)} ${usageBar(limitReached ? 0 : remaining)} ${state}${reset ? ` resets in ${reset}` : ""}`;
1400
1496
  }
1401
1497
  function creditsText(credits) {
1402
1498
  if (credits.unlimited)
@@ -1406,11 +1502,41 @@ function creditsText(credits) {
1406
1502
  }
1407
1503
  return "no additional credits";
1408
1504
  }
1409
- function formatCodexUsage(snapshots, now = Date.now()) {
1505
+ function planLabel(planType) {
1506
+ return planType.charAt(0).toUpperCase() + planType.slice(1);
1507
+ }
1508
+ function formatDateTime(epochMs) {
1509
+ const date = new Date(epochMs);
1510
+ const pad = (value) => String(value).padStart(2, "0");
1511
+ const offsetMinutes = -new Date(epochMs).getTimezoneOffset();
1512
+ const sign = offsetMinutes >= 0 ? "+" : "-";
1513
+ const abs = Math.abs(offsetMinutes);
1514
+ const offset = abs % 60 === 0 ? `UTC${sign}${abs / 60}` : `UTC${sign}${Math.floor(abs / 60)}:${pad(abs % 60)}`;
1515
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())} ${offset}`;
1516
+ }
1517
+ function formatCodexRedeemCredits(redeemCredits, now = Date.now()) {
1518
+ if (!redeemCredits || redeemCredits.availableCount <= 0)
1519
+ return [];
1520
+ const lines = [`rate limit redeem${redeemCredits.availableCount === 1 ? "" : ` ×${redeemCredits.availableCount}`}`];
1521
+ const available = redeemCredits.credits.filter((credit) => credit.status === undefined || credit.status === "available").sort((left, right) => (left.expiresAt ?? Number.POSITIVE_INFINITY) - (right.expiresAt ?? Number.POSITIVE_INFINITY));
1522
+ for (const credit of available) {
1523
+ const details = ["available"];
1524
+ if (credit.expiresAt !== undefined) {
1525
+ details.push(credit.expiresAt > now ? `expires ${formatDateTime(credit.expiresAt)}` : "expired");
1526
+ }
1527
+ lines.push(` ${credit.title ?? "reset credit"} (${details.join(", ")})`);
1528
+ }
1529
+ return lines.length > 1 ? lines : [];
1530
+ }
1531
+ function formatCodexUsage(snapshots, now = Date.now(), extras = {}) {
1410
1532
  if (snapshots.length === 0) {
1411
1533
  return "No Codex usage data is available. Run /codex-usage with an active Codex subscription model to refresh it.";
1412
1534
  }
1413
1535
  const lines = ["Codex usage"];
1536
+ if (extras.account) {
1537
+ const plan = extras.account.planType ? planLabel(extras.account.planType) : "unknown plan";
1538
+ lines.push("", `account · ${plan}${extras.account.email ? ` (${maskCodexEmail(extras.account.email)})` : ""}`);
1539
+ }
1414
1540
  for (const snapshot of snapshots) {
1415
1541
  const name = snapshot.limitName ?? snapshot.limitId;
1416
1542
  const windows = activeWindows(snapshot, now);
@@ -1419,10 +1545,13 @@ function formatCodexUsage(snapshots, now = Date.now()) {
1419
1545
  if (windows.length === 0)
1420
1546
  lines.push(" no active usage windows");
1421
1547
  else
1422
- lines.push(...windows.map((window) => ` ${windowText(window, labelWidth, now)}`));
1548
+ lines.push(...windows.map((window) => ` ${windowText(window, labelWidth, now, snapshot.limitReached === true)}`));
1423
1549
  if (snapshot.credits)
1424
1550
  lines.push(` ${creditsText(snapshot.credits)}`);
1425
1551
  }
1552
+ const redeemLines = formatCodexRedeemCredits(extras.redeemCredits, now);
1553
+ if (redeemLines.length > 0)
1554
+ lines.push("", ...redeemLines);
1426
1555
  return lines.join(`
1427
1556
  `);
1428
1557
  }
@@ -1441,15 +1570,20 @@ function formatCodexStatus(snapshots, fastMode, now = Date.now()) {
1441
1570
  return;
1442
1571
  const remaining = remainingPercent(shortest.window);
1443
1572
  const reset = resetText(shortest.window.resetsAt, now);
1444
- return `Codex ${shortest.label} ${percent(remaining)}%${reset ? ` ${reset}` : ""}${fastMode ? " Fast" : ""}`;
1573
+ const usage = snapshot.limitReached === true ? "limit reached" : `${percent(remaining)}%`;
1574
+ return `Codex ${shortest.label} ${usage}${reset ? ` ${reset}` : ""}${fastMode ? " Fast" : ""}`;
1445
1575
  }
1446
1576
  function applyFastModePayload(payload, enabled) {
1447
1577
  if (!enabled || !payload || typeof payload !== "object" || Array.isArray(payload))
1448
1578
  return payload;
1449
1579
  return { ...payload, service_tier: "priority" };
1450
1580
  }
1581
+ function usageRefreshNeeded(prev, next) {
1582
+ return next.usageStatus && (prev.usageStatus !== next.usageStatus || prev.allowOtherProviders !== next.allowOtherProviders);
1583
+ }
1451
1584
  function registerCodexUsageAndFast(pi, controller, options = {}) {
1452
1585
  const usageByAccount = new Map;
1586
+ const pendingRedeemByAccount = new Map;
1453
1587
  let activeAccountId;
1454
1588
  let credentialRevision = 0;
1455
1589
  let latestContext;
@@ -1457,6 +1591,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1457
1591
  let accountObserverActive = false;
1458
1592
  let authWatcher;
1459
1593
  let authWatchDebounce;
1594
+ let pollDelay;
1460
1595
  const usageEnabled = (ctx) => {
1461
1596
  const config = controller.getConfig();
1462
1597
  return config.usageStatus && (ctx.model?.provider === "openai-codex" || config.allowOtherProviders);
@@ -1481,6 +1616,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1481
1616
  credentialRevision += 1;
1482
1617
  activeAccountId = undefined;
1483
1618
  usageByAccount.clear();
1619
+ pendingRedeemByAccount.clear();
1484
1620
  if (action === "set")
1485
1621
  showSyncingStatus(ctx);
1486
1622
  else
@@ -1492,6 +1628,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1492
1628
  credentialRevision += 1;
1493
1629
  activeAccountId = accountId;
1494
1630
  usageByAccount.clear();
1631
+ pendingRedeemByAccount.clear();
1495
1632
  showSyncingStatus(ctx);
1496
1633
  return true;
1497
1634
  };
@@ -1542,6 +1679,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1542
1679
  if (parsed.length === 0)
1543
1680
  throw new Error("Codex usage API returned no usage data");
1544
1681
  state.snapshots = parsed;
1682
+ state.account = parseCodexAccountInfo(payload);
1545
1683
  state.lastFetchAt = Date.now();
1546
1684
  })();
1547
1685
  let nextFetch;
@@ -1562,6 +1700,36 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1562
1700
  latestContext = ctx;
1563
1701
  refreshUsage(ctx, force).catch(() => refreshStatus(ctx));
1564
1702
  };
1703
+ const stopPolling = () => {
1704
+ if (pollDelay) {
1705
+ clearTimeout(pollDelay);
1706
+ pollDelay = undefined;
1707
+ }
1708
+ };
1709
+ const scheduleNextPoll = () => {
1710
+ if (pollDelay)
1711
+ return;
1712
+ const intervalMinutes = Math.round(controller.getConfig().usagePollInterval);
1713
+ if (intervalMinutes <= 0)
1714
+ return;
1715
+ pollDelay = setTimeout(() => {
1716
+ pollDelay = undefined;
1717
+ const active = latestContext;
1718
+ const state = currentState();
1719
+ if (active && usageEnabled(active)) {
1720
+ const intervalMs = intervalMinutes * 60000;
1721
+ if (!state || state.snapshots.length === 0 || Date.now() - state.lastFetchAt >= intervalMs) {
1722
+ refreshUsage(active).catch(() => refreshStatus(active));
1723
+ }
1724
+ }
1725
+ scheduleNextPoll();
1726
+ }, intervalMinutes * 60000);
1727
+ pollDelay.unref?.();
1728
+ };
1729
+ const startPolling = (ctx) => {
1730
+ latestContext = ctx;
1731
+ scheduleNextPoll();
1732
+ };
1565
1733
  const codexOAuthAvailable = (ctx, config) => {
1566
1734
  const model = ctx.model?.provider === "openai-codex" ? ctx.model : config.allowOtherProviders ? ctx.modelRegistry.getAll().find((candidate) => candidate.provider === "openai-codex" && ctx.modelRegistry.isUsingOAuth(candidate)) : undefined;
1567
1735
  return !!model && ctx.modelRegistry.isUsingOAuth(model);
@@ -1646,20 +1814,120 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1646
1814
  }
1647
1815
  };
1648
1816
  pi.registerCommand("codex-usage", {
1649
- description: "Refresh and show Codex subscription usage limits and credits",
1817
+ description: "Refresh and show Codex subscription usage, plan, and rate limit redeems",
1650
1818
  handler: async (_args, ctx) => {
1651
1819
  try {
1652
1820
  await refreshUsage(ctx, true);
1653
1821
  } catch (error) {
1654
- const message = error instanceof Error ? error.message : String(error);
1822
+ const message2 = error instanceof Error ? error.message : String(error);
1655
1823
  const snapshots = currentState()?.snapshots ?? [];
1656
1824
  if (snapshots.length === 0) {
1657
- ctx.ui.notify(`Failed to refresh Codex usage: ${message}`, "error");
1825
+ ctx.ui.notify(`Failed to refresh Codex usage: ${message2}`, "error");
1826
+ return;
1827
+ }
1828
+ ctx.ui.notify(`Failed to refresh Codex usage; showing the latest snapshot: ${message2}`, "warning");
1829
+ }
1830
+ try {
1831
+ const resolved = await resolveActiveClient(ctx, controller.getConfig());
1832
+ const state2 = accountState(resolved.accountId);
1833
+ const payload = await resolved.client.get(REDEEM_CREDITS_PATH);
1834
+ state2.redeemCredits = parseCodexRedeemCredits(payload);
1835
+ } catch {}
1836
+ const state = currentState();
1837
+ const message = formatCodexUsage(state?.snapshots ?? [], Date.now(), {
1838
+ account: state?.account,
1839
+ redeemCredits: state?.redeemCredits
1840
+ });
1841
+ ctx.ui.notify(ctx.ui.theme ? ctx.ui.theme.fg("muted", message) : message, "info");
1842
+ }
1843
+ });
1844
+ pi.registerCommand("codex-redeem", {
1845
+ description: "Preview and redeem an earned Codex rate limit reset credit (confirmation required)",
1846
+ handler: async (_args, ctx) => {
1847
+ const config = controller.getConfig();
1848
+ if (ctx.model?.provider !== "openai-codex" && !config.allowOtherProviders) {
1849
+ ctx.ui.notify("An active openai-codex model is required to redeem a rate limit reset. " + "Enable Other providers in /99settings to use the logged-in Codex subscription from another model.", "error");
1850
+ return;
1851
+ }
1852
+ let resolved;
1853
+ try {
1854
+ resolved = await resolveActiveClient(ctx, config);
1855
+ } catch (error) {
1856
+ const message = error instanceof Error ? error.message : String(error);
1857
+ ctx.ui.notify(`Failed to resolve the Codex subscription: ${message}`, "error");
1858
+ return;
1859
+ }
1860
+ const state = accountState(resolved.accountId);
1861
+ try {
1862
+ const payload = await resolved.client.get(REDEEM_CREDITS_PATH);
1863
+ const redeemCredits = parseCodexRedeemCredits(payload);
1864
+ state.redeemCredits = redeemCredits;
1865
+ const now = Date.now();
1866
+ if (!redeemCredits || redeemCredits.availableCount <= 0) {
1867
+ pendingRedeemByAccount.delete(resolved.accountId);
1868
+ ctx.ui.notify("No Codex rate limit reset credits are available to redeem.", "info");
1658
1869
  return;
1659
1870
  }
1660
- ctx.ui.notify(`Failed to refresh Codex usage; showing the latest snapshot: ${message}`, "warning");
1871
+ const availableCredits = [...redeemCredits.credits].filter((item) => item.status === undefined || item.status === "available").sort((left, right) => (left.expiresAt ?? Number.POSITIVE_INFINITY) - (right.expiresAt ?? Number.POSITIVE_INFINITY));
1872
+ const credit = availableCredits[0] ?? redeemCredits.credits[0];
1873
+ const expiryOf = (item) => item?.expiresAt !== undefined && item.expiresAt > now ? ` (expires ${formatDateTime(item.expiresAt)})` : "";
1874
+ let selected = credit;
1875
+ if (ctx.hasUI) {
1876
+ if (availableCredits.length > 1) {
1877
+ const options2 = availableCredits.map((item) => `${item.title ?? "reset credit"}${expiryOf(item)}`);
1878
+ const choice2 = await ctx.ui.select("Select a reset credit to redeem", options2, {
1879
+ timeout: REDEEM_DIALOG_TIMEOUT_MS
1880
+ });
1881
+ if (choice2 === undefined) {
1882
+ pendingRedeemByAccount.delete(resolved.accountId);
1883
+ ctx.ui.notify("Redeem cancelled — no reset credit was consumed.", "info");
1884
+ return;
1885
+ }
1886
+ const index = options2.indexOf(choice2);
1887
+ selected = availableCredits[index] ?? credit;
1888
+ }
1889
+ const confirmOptions = ["No", "Yes"];
1890
+ const choice = await ctx.ui.select(`Redeem ${selected?.title ?? "Full reset"}${expiryOf(selected)}?`, confirmOptions, { timeout: REDEEM_DIALOG_TIMEOUT_MS });
1891
+ if (choice !== confirmOptions[1]) {
1892
+ pendingRedeemByAccount.delete(resolved.accountId);
1893
+ ctx.ui.notify("Redeem cancelled — no reset credit was consumed.", "info");
1894
+ return;
1895
+ }
1896
+ }
1897
+ const targetId = selected?.id;
1898
+ const existing = pendingRedeemByAccount.get(resolved.accountId);
1899
+ const pending = existing && existing.expiresAt > now && existing.creditId === targetId ? existing : {
1900
+ redeemRequestId: crypto.randomUUID(),
1901
+ creditId: targetId,
1902
+ expiresAt: now + REDEEM_CONFIRM_WINDOW_MS
1903
+ };
1904
+ pendingRedeemByAccount.set(resolved.accountId, pending);
1905
+ if (!ctx.hasUI && (!existing || existing.expiresAt <= now || existing.creditId !== targetId)) {
1906
+ ctx.ui.notify(`${redeemCredits.availableCount} rate limit reset redeem available: ${credit?.title ?? "Full reset"}${expiryOf(credit)}. ` + `Run /codex-redeem again within ${REDEEM_CONFIRM_WINDOW_MS / 1000}s to confirm.`, "warning");
1907
+ return;
1908
+ }
1909
+ try {
1910
+ await resolved.client.post(REDEEM_PATH, {
1911
+ redeem_request_id: pending.redeemRequestId,
1912
+ credit_id: pending.creditId
1913
+ });
1914
+ pendingRedeemByAccount.delete(resolved.accountId);
1915
+ try {
1916
+ await refreshUsage(ctx, true);
1917
+ } catch {}
1918
+ const status = formatCodexStatus(currentState()?.snapshots ?? [], config.fastMode);
1919
+ ctx.ui.notify(`✓ Rate limit reset redeemed — usage reset.${status ? `
1920
+ ${status}` : ""}`, "info");
1921
+ } catch (error) {
1922
+ pending.expiresAt = Date.now() + REDEEM_RETRY_WINDOW_MS;
1923
+ pendingRedeemByAccount.set(resolved.accountId, pending);
1924
+ const message = error instanceof Error ? error.message : String(error);
1925
+ ctx.ui.notify(`Failed to redeem a rate limit reset: ${message}. ` + "Run /codex-redeem again to retry with the same request ID.", "error");
1926
+ }
1927
+ } catch (error) {
1928
+ const message = error instanceof Error ? error.message : String(error);
1929
+ ctx.ui.notify(`Failed to redeem a rate limit reset: ${message}`, "error");
1661
1930
  }
1662
- ctx.ui.notify(formatCodexUsage(currentState()?.snapshots ?? []), "info");
1663
1931
  }
1664
1932
  });
1665
1933
  pi.on("before_provider_request", (event, ctx) => {
@@ -1682,15 +1950,19 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
1682
1950
  startAccountObserver(ctx);
1683
1951
  checkCurrentAccount(ctx).catch(() => {});
1684
1952
  refreshInBackground(ctx, true);
1953
+ startPolling(ctx);
1685
1954
  });
1686
1955
  pi.on("session_start", (_event, ctx) => {
1687
1956
  startAccountObserver(ctx);
1688
1957
  refreshInBackground(ctx, true);
1958
+ startPolling(ctx);
1689
1959
  });
1690
1960
  pi.on("session_shutdown", (_event, ctx) => {
1961
+ stopPolling();
1691
1962
  credentialRevision += 1;
1692
1963
  activeAccountId = undefined;
1693
1964
  usageByAccount.clear();
1965
+ pendingRedeemByAccount.clear();
1694
1966
  latestContext = undefined;
1695
1967
  accountObserverActive = false;
1696
1968
  if (authWatchDebounce)
@@ -1715,6 +1987,7 @@ function codex_api_default(pi) {
1715
1987
  const controller = {
1716
1988
  getConfig: () => config,
1717
1989
  updateConfig: (next, ctx) => {
1990
+ const prev = config;
1718
1991
  config = next;
1719
1992
  try {
1720
1993
  saveCodexApiConfig(config);
@@ -1722,17 +1995,21 @@ function codex_api_default(pi) {
1722
1995
  ctx.ui.notify(`Failed to save Codex API settings: ${error instanceof Error ? error.message : String(error)}`, "error");
1723
1996
  }
1724
1997
  usageHandle?.refreshStatus(ctx);
1998
+ if (usageRefreshNeeded(prev, next)) {
1999
+ usageHandle?.refreshUsage(ctx, true).catch(() => {});
2000
+ }
1725
2001
  }
1726
2002
  };
1727
2003
  usageHandle = registerCodexUsageAndFast(pi, controller);
1728
2004
  const refreshUsageInBackground = (ctx) => {
1729
2005
  usageHandle?.refreshUsage(ctx).catch(() => {});
1730
2006
  };
1731
- registerCodexImageTool(pi, () => config, refreshUsageInBackground);
2007
+ registerCodexImageTool(pi, () => config);
1732
2008
  registerCodexSearchTool(pi, () => config, refreshUsageInBackground);
1733
2009
  registerCodexApiSettings(pi, controller);
1734
2010
  }
1735
2011
  export {
2012
+ usageRefreshNeeded,
1736
2013
  saveCodexApiConfig,
1737
2014
  resolveSearchMode,
1738
2015
  resolveCodexApiRoot,
@@ -1741,14 +2018,18 @@ export {
1741
2018
  registerCodexImageTool,
1742
2019
  registerCodexApiSettings,
1743
2020
  parseCodexUsagePayload,
2021
+ parseCodexRedeemCredits,
1744
2022
  parseCodexRateLimits,
2023
+ parseCodexAccountInfo,
1745
2024
  normalizeCodexImageSize,
1746
2025
  normalizeCodexApiConfig,
2026
+ maskCodexEmail,
1747
2027
  loadCodexApiConfig,
1748
2028
  getCodexApiConfigPath,
1749
2029
  formatCodexUsage,
1750
2030
  formatCodexStatus,
1751
2031
  formatCodexSearchDisplay,
2032
+ formatCodexRedeemCredits,
1752
2033
  extractCodexAccountId,
1753
2034
  codex_api_default as default,
1754
2035
  createCodexSearchDisplay,
@@ -1765,5 +2046,5 @@ export {
1765
2046
  CODEX_API_SETTINGS_NAMESPACE
1766
2047
  };
1767
2048
 
1768
- //# debugId=84C84A4D70A68DF064756E2164756E21
2049
+ //# debugId=F65EB4B48F9B1F0B64756E2164756E21
1769
2050
  //# sourceMappingURL=index.ts.map