codexuse-cli 2.4.17 → 2.4.19

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 CHANGED
@@ -566,6 +566,10 @@ async function persistLicense(license) {
566
566
  }
567
567
  });
568
568
  }
569
+ async function getStoredAutoRollSettings() {
570
+ const state = await getAppState();
571
+ return normalizeAutoRollSettings(state.autoRoll);
572
+ }
569
573
  async function getCodexCliChannel() {
570
574
  const state = await getAppState();
571
575
  return normalizeCodexCliChannel(state.app.cliChannel);
@@ -1410,16 +1414,35 @@ var ProfileManager = class {
1410
1414
  if ("tokens" in data) {
1411
1415
  return data;
1412
1416
  }
1413
- return {
1417
+ const lastRefresh = typeof data.last_refresh === "string" ? data.last_refresh.trim() : "";
1418
+ let normalizedLastRefresh = lastRefresh.length > 0 ? lastRefresh : void 0;
1419
+ if (!normalizedLastRefresh) {
1420
+ const accessPayload = this.decodeJwtPayload(data.access_token);
1421
+ const tokenIssuedAt = typeof accessPayload?.iat === "number" ? this.toIsoStringFromSeconds(accessPayload.iat) : void 0;
1422
+ if (tokenIssuedAt) {
1423
+ normalizedLastRefresh = tokenIssuedAt;
1424
+ }
1425
+ }
1426
+ if (!normalizedLastRefresh) {
1427
+ const createdAt = typeof data.created_at === "string" ? data.created_at.trim() : "";
1428
+ if (createdAt) {
1429
+ normalizedLastRefresh = createdAt;
1430
+ }
1431
+ }
1432
+ const codexAuth = {
1414
1433
  OPENAI_API_KEY: null,
1415
1434
  tokens: {
1416
1435
  id_token: data.id_token,
1417
1436
  access_token: data.access_token,
1418
1437
  refresh_token: data.refresh_token,
1419
- account_id: data.account_id
1420
- },
1421
- last_refresh: (/* @__PURE__ */ new Date()).toISOString()
1438
+ account_id: data.account_id,
1439
+ ...normalizedLastRefresh ? { last_refresh: normalizedLastRefresh } : {}
1440
+ }
1422
1441
  };
1442
+ if (normalizedLastRefresh) {
1443
+ codexAuth.last_refresh = normalizedLastRefresh;
1444
+ }
1445
+ return codexAuth;
1423
1446
  }
1424
1447
  /**
1425
1448
  * Normalize Codex auth data to flat profile format
@@ -1435,6 +1458,12 @@ var ProfileManager = class {
1435
1458
  refresh_token: tokens.refresh_token,
1436
1459
  account_id: tokens.account_id
1437
1460
  };
1461
+ const tokenLastRefresh = typeof tokens.last_refresh === "string" ? tokens.last_refresh.trim() : "";
1462
+ const rootLastRefresh = typeof data.last_refresh === "string" ? data.last_refresh.trim() : "";
1463
+ const normalizedLastRefresh = tokenLastRefresh || rootLastRefresh;
1464
+ if (normalizedLastRefresh) {
1465
+ profile.last_refresh = normalizedLastRefresh;
1466
+ }
1438
1467
  if (data.email) {
1439
1468
  profile.email = data.email;
1440
1469
  }
@@ -1470,7 +1499,14 @@ var ProfileManager = class {
1470
1499
  return { profile: merged, metadata };
1471
1500
  }
1472
1501
  hasTokenChanges(existing, incoming) {
1473
- const tokenKeys = ["id_token", "access_token", "refresh_token", "account_id", "workspace_id"];
1502
+ const tokenKeys = [
1503
+ "id_token",
1504
+ "access_token",
1505
+ "refresh_token",
1506
+ "last_refresh",
1507
+ "account_id",
1508
+ "workspace_id"
1509
+ ];
1474
1510
  return tokenKeys.some((key) => {
1475
1511
  const nextValue = incoming[key];
1476
1512
  if (typeof nextValue !== "string" || nextValue.length === 0) {
@@ -1479,12 +1515,12 @@ var ProfileManager = class {
1479
1515
  return nextValue !== existing[key];
1480
1516
  });
1481
1517
  }
1482
- async flagRefreshTokenRedeemed(name, reason, options) {
1518
+ async flagAuthIssue(name, issue, reason, options) {
1483
1519
  await this.initialize();
1484
1520
  const profileName = this.normalizeProfileName(name);
1485
1521
  const record = await this.getProfileRowByName(profileName);
1486
1522
  if (!record) {
1487
- logWarn(`Cannot flag refresh token issue for missing profile '${profileName}'.`);
1523
+ logWarn(`Cannot flag auth issue '${issue}' for missing profile '${profileName}'.`);
1488
1524
  return;
1489
1525
  }
1490
1526
  const observedAt = options?.observedAt ? Date.parse(options.observedAt) : null;
@@ -1495,9 +1531,9 @@ var ProfileManager = class {
1495
1531
  const data = record.data;
1496
1532
  const trimmedReason = typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : void 0;
1497
1533
  const normalized = trimmedReason?.toLowerCase() ?? "";
1498
- const storedReason = normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET) ? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason ?? REFRESH_TOKEN_REDEEMED_REASON;
1534
+ const storedReason = issue === "refresh-redeemed" ? normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET) ? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason ?? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason;
1499
1535
  const alert = {
1500
- issue: "refresh-redeemed",
1536
+ issue,
1501
1537
  reason: storedReason,
1502
1538
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
1503
1539
  };
@@ -1509,6 +1545,9 @@ var ProfileManager = class {
1509
1545
  logError(`Failed to persist token alert for profile '${profileName}':`, error);
1510
1546
  }
1511
1547
  }
1548
+ async flagRefreshTokenRedeemed(name, reason, options) {
1549
+ await this.flagAuthIssue(name, "refresh-redeemed", reason, options);
1550
+ }
1512
1551
  async syncProfileTokensFromActiveAuth(profileName, baselineProfile, authPath) {
1513
1552
  const targetPath = authPath ?? this.activeAuth;
1514
1553
  try {
@@ -1791,7 +1830,12 @@ var ProfileManager = class {
1791
1830
  if (!record) {
1792
1831
  throw new Error(`Profile '${profileName}' not found!`);
1793
1832
  }
1794
- const profileData = record.data;
1833
+ let profileData = record.data;
1834
+ await this.syncProfileTokensFromActiveAuth(profileName, profileData);
1835
+ const updatedRecord = await this.getProfileRowByName(profileName);
1836
+ if (updatedRecord) {
1837
+ profileData = updatedRecord.data;
1838
+ }
1795
1839
  const profileHome = this.getProfileHomePath(profileName);
1796
1840
  await import_fs.promises.mkdir(profileHome, { recursive: true });
1797
1841
  if (profileData.auth_method && profileData.auth_method !== "codex-cli") {
@@ -4017,7 +4061,6 @@ async function runCodexLogin(mode) {
4017
4061
  // ../../lib/codex-rpc.ts
4018
4062
  var import_node_child_process3 = require("child_process");
4019
4063
  var import_node_readline = __toESM(require("readline"));
4020
- var import_node_events = require("events");
4021
4064
  var import_node_fs6 = require("fs");
4022
4065
  var import_node_os2 = __toESM(require("os"));
4023
4066
  var import_node_path8 = __toESM(require("path"));
@@ -4026,20 +4069,50 @@ async function sendPayload(child, payload) {
4026
4069
  child.stdin?.write(JSON.stringify(payload));
4027
4070
  child.stdin?.write("\n");
4028
4071
  }
4029
- async function readMessage(rl, timeoutMs) {
4030
- const timeout = new Promise((_, reject) => {
4072
+ function isRpcResponseForRequest(message, requestId) {
4073
+ if (message.id !== requestId) {
4074
+ return false;
4075
+ }
4076
+ return Object.prototype.hasOwnProperty.call(message, "result") || Object.prototype.hasOwnProperty.call(message, "error");
4077
+ }
4078
+ async function readRpcResponseById(rl, requestId, timeoutMs) {
4079
+ return new Promise((resolve, reject) => {
4031
4080
  const timer = setTimeout(() => {
4032
- clearTimeout(timer);
4081
+ cleanup();
4033
4082
  reject(new Error("codex RPC timed out"));
4034
- }, timeoutMs);
4083
+ }, Math.max(1, timeoutMs));
4084
+ const cleanup = () => {
4085
+ clearTimeout(timer);
4086
+ rl.off("line", onLine);
4087
+ rl.off("close", onClose);
4088
+ };
4089
+ const onClose = () => {
4090
+ cleanup();
4091
+ reject(new Error("codex RPC stream closed before response"));
4092
+ };
4093
+ const onLine = (line) => {
4094
+ let parsed;
4095
+ try {
4096
+ parsed = JSON.parse(line);
4097
+ } catch {
4098
+ cleanup();
4099
+ reject(new Error("codex RPC returned malformed JSON"));
4100
+ return;
4101
+ }
4102
+ if (!isRpcResponseForRequest(parsed, requestId)) {
4103
+ return;
4104
+ }
4105
+ cleanup();
4106
+ resolve(parsed);
4107
+ };
4108
+ rl.on("line", onLine);
4109
+ rl.on("close", onClose);
4035
4110
  });
4036
- const linePromise = (0, import_node_events.once)(rl, "line").then((args) => args[0]);
4037
- const line = await Promise.race([linePromise, timeout]);
4038
- try {
4039
- return JSON.parse(line);
4040
- } catch {
4041
- throw new Error("codex RPC returned malformed JSON");
4042
- }
4111
+ }
4112
+ function formatRpcError(method, error) {
4113
+ const codeText = typeof error.code === "number" ? ` (code ${error.code})` : "";
4114
+ const message = typeof error.message === "string" && error.message.trim().length > 0 ? error.message.trim() : "Unknown RPC error";
4115
+ return `${method} failed${codeText}: ${message}`;
4043
4116
  }
4044
4117
  function toWindow(rpc) {
4045
4118
  if (!rpc) return void 0;
@@ -4070,9 +4143,33 @@ function toWindow(rpc) {
4070
4143
  }
4071
4144
  return window;
4072
4145
  }
4146
+ function parseRateLimitSnapshotFromRpcMessage(message) {
4147
+ if (message.error) {
4148
+ throw new Error(formatRpcError("account/rateLimits/read", message.error));
4149
+ }
4150
+ const result = message.result;
4151
+ const limits = result?.rateLimits;
4152
+ const primary = toWindow(limits?.primary ?? null);
4153
+ const secondary = toWindow(limits?.secondary ?? null);
4154
+ if (!primary && !secondary) {
4155
+ return null;
4156
+ }
4157
+ const snapshot = {
4158
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4159
+ source: "codex-rpc"
4160
+ };
4161
+ if (primary) {
4162
+ snapshot.primary = primary;
4163
+ }
4164
+ if (secondary) {
4165
+ snapshot.secondary = secondary;
4166
+ }
4167
+ return snapshot;
4168
+ }
4073
4169
  async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4074
4170
  const binaryPath = options.codexPath ?? await requireCodexCli();
4075
4171
  const tempHome = await import_node_fs6.promises.mkdtemp(import_node_path8.default.join(import_node_os2.default.tmpdir(), "codex-rpc-"));
4172
+ const tempAuthPath = import_node_path8.default.join(tempHome, "auth.json");
4076
4173
  const sourceAuthPath = options.authPath ?? (envOverride?.CODEX_HOME ? import_node_path8.default.join(envOverride.CODEX_HOME, "auth.json") : import_node_path8.default.join(
4077
4174
  envOverride?.HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? import_node_os2.default.homedir(),
4078
4175
  ".codex",
@@ -4083,7 +4180,7 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4083
4180
  if (!authContent) {
4084
4181
  return null;
4085
4182
  }
4086
- await import_node_fs6.promises.writeFile(import_node_path8.default.join(tempHome, "auth.json"), authContent, "utf8");
4183
+ await import_node_fs6.promises.writeFile(tempAuthPath, authContent, "utf8");
4087
4184
  } catch {
4088
4185
  await import_node_fs6.promises.rm(tempHome, { recursive: true, force: true }).catch(() => {
4089
4186
  });
@@ -4111,31 +4208,24 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
4111
4208
  method: "initialize",
4112
4209
  params: { clientInfo: { name: "codexuse", version: "0.0.0" } }
4113
4210
  });
4114
- await readMessage(rl, RPC_TIMEOUT_MS);
4211
+ const initializeResponse = await readRpcResponseById(rl, 1, RPC_TIMEOUT_MS);
4212
+ if (initializeResponse.error) {
4213
+ throw new Error(formatRpcError("initialize", initializeResponse.error));
4214
+ }
4115
4215
  await sendPayload(child, { method: "initialized", params: {} });
4116
4216
  await sendPayload(child, { id: 2, method: "account/rateLimits/read", params: {} });
4117
- const message = await readMessage(rl, RPC_TIMEOUT_MS);
4118
- const result = message.result;
4119
- const limits = result?.rateLimits;
4120
- const primary = toWindow(limits?.primary ?? null);
4121
- const secondary = toWindow(limits?.secondary ?? null);
4122
- if (!primary && !secondary) {
4123
- return null;
4124
- }
4125
- const snapshot = {
4126
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4127
- source: "codex-rpc"
4128
- };
4129
- if (primary) {
4130
- snapshot.primary = primary;
4131
- }
4132
- if (secondary) {
4133
- snapshot.secondary = secondary;
4134
- }
4135
- return snapshot;
4217
+ const message = await readRpcResponseById(rl, 2, RPC_TIMEOUT_MS);
4218
+ return parseRateLimitSnapshotFromRpcMessage(message);
4136
4219
  } finally {
4137
4220
  child.kill();
4138
4221
  rl.close();
4222
+ try {
4223
+ const updatedAuth = await import_node_fs6.promises.readFile(tempAuthPath, "utf8");
4224
+ if (updatedAuth.trim().length > 0) {
4225
+ await import_node_fs6.promises.writeFile(sourceAuthPath, updatedAuth, "utf8");
4226
+ }
4227
+ } catch {
4228
+ }
4139
4229
  await import_node_fs6.promises.rm(tempHome, { recursive: true, force: true }).catch(() => {
4140
4230
  });
4141
4231
  }
@@ -4920,7 +5010,7 @@ async function importLegacyLocalStorageOnce(payload) {
4920
5010
  }
4921
5011
 
4922
5012
  // src/index.ts
4923
- var VERSION = true ? "2.4.17" : "0.0.0";
5013
+ var VERSION = true ? "2.4.19" : "0.0.0";
4924
5014
  var cliStorageReadyPromise = null;
4925
5015
  async function ensureCliStorageReady() {
4926
5016
  if (cliStorageReadyPromise) {
@@ -4935,7 +5025,7 @@ async function ensureCliStorageReady() {
4935
5025
  const state = await getAppState();
4936
5026
  if (state.migration.status !== "complete") {
4937
5027
  throw new Error(
4938
- `Storage migration is not complete (status: ${state.migration.status}). Open CodexUse desktop once and try again.`
5028
+ `Storage migration is not complete (status: ${state.migration.status}). CLI cannot continue until migration succeeds.`
4939
5029
  );
4940
5030
  }
4941
5031
  })().catch((error) => {
@@ -4953,6 +5043,7 @@ Usage:
4953
5043
  codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
4954
5044
  codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
4955
5045
  codexuse profile switch <name>
5046
+ codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
4956
5047
  codexuse profile delete <name>
4957
5048
  codexuse profile rename <old> <new>
4958
5049
 
@@ -4970,6 +5061,10 @@ Flags:
4970
5061
  --compact Names only
4971
5062
  --device-auth Use device auth for Codex login
4972
5063
  --login=MODE Login mode: browser | device
5064
+ --threshold=NN Auto-roll switch threshold percent (50-100)
5065
+ --watch Keep checking and auto-switch when threshold is reached
5066
+ --interval=SEC Watch interval in seconds (default: 30)
5067
+ --dry-run Print planned switch without changing active profile
4973
5068
  `);
4974
5069
  }
4975
5070
  function hasFlag(args, flag) {
@@ -4988,6 +5083,26 @@ function parseLoginMode(flags) {
4988
5083
  }
4989
5084
  return null;
4990
5085
  }
5086
+ var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
5087
+ var DEFAULT_AUTOROLL_THRESHOLD = 95;
5088
+ function parseNumericFlag(flags, name) {
5089
+ const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
5090
+ if (!explicit) {
5091
+ return null;
5092
+ }
5093
+ const raw = explicit.slice(`${name}=`.length);
5094
+ const value = Number.parseFloat(raw);
5095
+ return Number.isFinite(value) ? value : Number.NaN;
5096
+ }
5097
+ function parseIntegerFlag(flags, name) {
5098
+ const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
5099
+ if (!explicit) {
5100
+ return null;
5101
+ }
5102
+ const raw = explicit.slice(`${name}=`.length);
5103
+ const value = Number.parseInt(raw, 10);
5104
+ return Number.isFinite(value) ? value : Number.NaN;
5105
+ }
4991
5106
  function formatProfileLabel(name, displayName) {
4992
5107
  if (displayName && displayName.trim() && displayName !== name) {
4993
5108
  return `${displayName} (${name})`;
@@ -5099,14 +5214,99 @@ function formatWindowUsage(window) {
5099
5214
  reset: resetText ? `reset ${resetText}` : null
5100
5215
  };
5101
5216
  }
5217
+ var EMPTY_USAGE_SUMMARY = {
5218
+ windowKey: "primary",
5219
+ usagePercent: 0,
5220
+ hasUsage: false,
5221
+ resetsInSeconds: null,
5222
+ windowMinutes: null
5223
+ };
5224
+ function toFiniteNumberOrNull(value) {
5225
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
5226
+ }
5227
+ function summarizeRateLimitSnapshot(snapshot) {
5228
+ if (!snapshot) {
5229
+ return { ...EMPTY_USAGE_SUMMARY };
5230
+ }
5231
+ const entries = [
5232
+ { key: "primary", window: snapshot.primary },
5233
+ { key: "secondary", window: snapshot.secondary }
5234
+ ];
5235
+ let summary = { ...EMPTY_USAGE_SUMMARY };
5236
+ let initialized = false;
5237
+ for (const entry of entries) {
5238
+ if (!entry.window) {
5239
+ continue;
5240
+ }
5241
+ const usageValueRaw = toFiniteNumberOrNull(entry.window.usedPercent);
5242
+ const usageValue = usageValueRaw === null ? 0 : Math.max(0, Math.min(100, usageValueRaw));
5243
+ const hasUsage = usageValueRaw !== null;
5244
+ const resetsInSeconds = toFiniteNumberOrNull(entry.window.resetsInSeconds);
5245
+ const windowMinutes = toFiniteNumberOrNull(entry.window.windowMinutes);
5246
+ if (!initialized) {
5247
+ initialized = true;
5248
+ summary = {
5249
+ windowKey: entry.key,
5250
+ usagePercent: usageValue,
5251
+ hasUsage,
5252
+ resetsInSeconds,
5253
+ windowMinutes
5254
+ };
5255
+ continue;
5256
+ }
5257
+ if (hasUsage && summary.hasUsage && usageValue > summary.usagePercent) {
5258
+ summary = {
5259
+ windowKey: entry.key,
5260
+ usagePercent: usageValue,
5261
+ hasUsage,
5262
+ resetsInSeconds,
5263
+ windowMinutes
5264
+ };
5265
+ continue;
5266
+ }
5267
+ if (!hasUsage && !summary.hasUsage && summary.resetsInSeconds !== null) {
5268
+ if (resetsInSeconds !== null && resetsInSeconds < summary.resetsInSeconds) {
5269
+ summary = {
5270
+ windowKey: entry.key,
5271
+ usagePercent: usageValue,
5272
+ hasUsage,
5273
+ resetsInSeconds,
5274
+ windowMinutes
5275
+ };
5276
+ }
5277
+ continue;
5278
+ }
5279
+ if (!hasUsage && !summary.hasUsage && summary.resetsInSeconds === null && resetsInSeconds !== null) {
5280
+ summary = {
5281
+ windowKey: entry.key,
5282
+ usagePercent: usageValue,
5283
+ hasUsage,
5284
+ resetsInSeconds,
5285
+ windowMinutes
5286
+ };
5287
+ continue;
5288
+ }
5289
+ if (hasUsage && !summary.hasUsage) {
5290
+ summary = {
5291
+ windowKey: entry.key,
5292
+ usagePercent: usageValue,
5293
+ hasUsage,
5294
+ resetsInSeconds,
5295
+ windowMinutes
5296
+ };
5297
+ }
5298
+ }
5299
+ return summary;
5300
+ }
5102
5301
  function fallbackUsage(profile) {
5302
+ const usageSummary = summarizeRateLimitSnapshot(profile.rateLimit ?? null);
5103
5303
  if (!profile.isValid) {
5104
- return { summary: "invalid", rows: null };
5304
+ return { summary: "invalid", rows: null, usageSummary };
5105
5305
  }
5106
5306
  if (profile.tokenStatus?.requiresUserAction) {
5107
- return { summary: "auth required", rows: null };
5307
+ return { summary: "auth required", rows: null, usageSummary };
5108
5308
  }
5109
- return { summary: "n/a", rows: null };
5309
+ return { summary: "n/a", rows: null, usageSummary };
5110
5310
  }
5111
5311
  async function resolveProfileUsage(manager, profile, codexPath) {
5112
5312
  if (!profile.isValid || profile.tokenStatus?.requiresUserAction) {
@@ -5118,7 +5318,8 @@ async function resolveProfileUsage(manager, profile, codexPath) {
5118
5318
  (env) => fetchRateLimitsViaRpc(env, { codexPath })
5119
5319
  );
5120
5320
  if (!snapshot) {
5121
- return { summary: "n/a", rows: null };
5321
+ const usageSummary = summarizeRateLimitSnapshot(profile.rateLimit ?? null);
5322
+ return { summary: "n/a", rows: null, usageSummary };
5122
5323
  }
5123
5324
  const rows = [];
5124
5325
  if (snapshot.primary) {
@@ -5131,10 +5332,61 @@ async function resolveProfileUsage(manager, profile, codexPath) {
5131
5332
  }
5132
5333
  const maxPercent = maxUsedPercent(snapshot);
5133
5334
  const summary = typeof maxPercent === "number" ? `${Math.round(maxPercent)}%` : "n/a";
5134
- return { summary, rows: rows.length > 0 ? rows : null };
5335
+ return { summary, rows: rows.length > 0 ? rows : null, usageSummary: summarizeRateLimitSnapshot(snapshot) };
5135
5336
  } catch {
5136
- return { summary: "n/a", rows: null };
5337
+ return fallbackUsage(profile);
5338
+ }
5339
+ }
5340
+ function compareAutoRollCandidates(a, b) {
5341
+ if (a.usageSummary.usagePercent !== b.usageSummary.usagePercent) {
5342
+ return a.usageSummary.usagePercent - b.usageSummary.usagePercent;
5343
+ }
5344
+ const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5345
+ const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5346
+ if (aReset !== bReset) {
5347
+ return aReset - bReset;
5348
+ }
5349
+ return a.profile.name.localeCompare(b.profile.name);
5350
+ }
5351
+ function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchThreshold) {
5352
+ const candidates = profiles.filter((profile) => profile.name !== currentProfileName && profile.isValid && !profile.tokenStatus?.requiresUserAction).map((profile) => ({
5353
+ profile,
5354
+ usageSummary: usageMap.get(profile.name)?.usageSummary ?? summarizeRateLimitSnapshot(profile.rateLimit ?? null)
5355
+ }));
5356
+ if (candidates.length === 0) {
5357
+ return null;
5137
5358
  }
5359
+ const belowThreshold = candidates.filter((candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.usagePercent < switchThreshold).sort(compareAutoRollCandidates);
5360
+ let selected = belowThreshold[0] ?? null;
5361
+ if (!selected) {
5362
+ selected = candidates.filter((candidate) => candidate.usageSummary.hasUsage).sort(compareAutoRollCandidates)[0] ?? null;
5363
+ }
5364
+ if (!selected) {
5365
+ selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => {
5366
+ const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5367
+ const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5368
+ if (aReset !== bReset) {
5369
+ return aReset - bReset;
5370
+ }
5371
+ return a.profile.name.localeCompare(b.profile.name);
5372
+ })[0] ?? null;
5373
+ }
5374
+ if (!selected) {
5375
+ return null;
5376
+ }
5377
+ if (selected.usageSummary.hasUsage && currentSummary.hasUsage) {
5378
+ if (selected.usageSummary.usagePercent > currentSummary.usagePercent) {
5379
+ return null;
5380
+ }
5381
+ if (selected.usageSummary.usagePercent === currentSummary.usagePercent) {
5382
+ const selectedReset = selected.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5383
+ const currentReset = currentSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
5384
+ if (selectedReset >= currentReset) {
5385
+ return null;
5386
+ }
5387
+ }
5388
+ }
5389
+ return selected;
5138
5390
  }
5139
5391
  function formatPlan(profile) {
5140
5392
  const metadata = profile.metadata;
@@ -5210,6 +5462,118 @@ async function mapWithConcurrency(items, limit, fn) {
5210
5462
  await Promise.all(workers);
5211
5463
  return results;
5212
5464
  }
5465
+ function parseAutoRollThreshold(flags, fallbackThreshold) {
5466
+ const explicitThreshold = parseNumericFlag(flags, "--threshold");
5467
+ if (explicitThreshold === null) {
5468
+ return fallbackThreshold;
5469
+ }
5470
+ if (Number.isNaN(explicitThreshold)) {
5471
+ throw new Error("Invalid --threshold value. Use --threshold=50-100.");
5472
+ }
5473
+ if (explicitThreshold < 50 || explicitThreshold > 100) {
5474
+ throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
5475
+ }
5476
+ return Math.round(explicitThreshold);
5477
+ }
5478
+ function parseAutoRollIntervalSeconds(flags) {
5479
+ const explicitInterval = parseIntegerFlag(flags, "--interval");
5480
+ if (explicitInterval === null) {
5481
+ return DEFAULT_AUTOROLL_INTERVAL_SECONDS;
5482
+ }
5483
+ if (Number.isNaN(explicitInterval) || explicitInterval <= 0) {
5484
+ throw new Error("Invalid --interval value. Use --interval=<seconds> with a positive integer.");
5485
+ }
5486
+ return explicitInterval;
5487
+ }
5488
+ async function collectUsageMap(manager, profiles, codexPath) {
5489
+ const usageMap = /* @__PURE__ */ new Map();
5490
+ const limitEnv = Number.parseInt(process.env.CODEXUSE_CLI_USAGE_CONCURRENCY ?? "3", 10);
5491
+ const limit = Number.isFinite(limitEnv) && limitEnv > 0 ? limitEnv : 3;
5492
+ const results = await mapWithConcurrency(
5493
+ profiles,
5494
+ limit,
5495
+ (profile) => resolveProfileUsage(manager, profile, codexPath)
5496
+ );
5497
+ for (const [index, profile] of profiles.entries()) {
5498
+ usageMap.set(profile.name, results[index]);
5499
+ }
5500
+ return usageMap;
5501
+ }
5502
+ async function runAutoRollPass(manager, options) {
5503
+ const profiles = await manager.listProfiles();
5504
+ if (!profiles.length) {
5505
+ return { switched: false, message: "No profiles found.", source: null, target: null };
5506
+ }
5507
+ const current = await manager.getCurrentProfile();
5508
+ if (!current.name) {
5509
+ return { switched: false, message: "No active profile.", source: null, target: null };
5510
+ }
5511
+ const codexPath = resolveCodexBinary();
5512
+ if (!codexPath) {
5513
+ throw new Error("Codex CLI binary not found on PATH. Auto-roll requires codex for live rate limits.");
5514
+ }
5515
+ const usageMap = await collectUsageMap(manager, profiles, codexPath);
5516
+ const currentProfile = profiles.find((profile) => profile.name === current.name);
5517
+ if (!currentProfile) {
5518
+ return {
5519
+ switched: false,
5520
+ message: `Current profile '${current.name}' is missing from saved profiles.`,
5521
+ source: current.name,
5522
+ target: null
5523
+ };
5524
+ }
5525
+ const currentUsage = usageMap.get(current.name)?.usageSummary ?? summarizeRateLimitSnapshot(currentProfile.rateLimit ?? null);
5526
+ if (!currentUsage.hasUsage) {
5527
+ return {
5528
+ switched: false,
5529
+ message: `No live rate-limit usage for '${current.name}'. Skipping auto-roll.`,
5530
+ source: current.name,
5531
+ target: null
5532
+ };
5533
+ }
5534
+ if (currentUsage.usagePercent < options.threshold) {
5535
+ return {
5536
+ switched: false,
5537
+ message: `'${current.name}' at ${Math.round(currentUsage.usagePercent)}%, below threshold ${options.threshold}%.`,
5538
+ source: current.name,
5539
+ target: null
5540
+ };
5541
+ }
5542
+ const candidate = computeAutoRollCandidate(
5543
+ profiles,
5544
+ usageMap,
5545
+ current.name,
5546
+ currentUsage,
5547
+ options.threshold
5548
+ );
5549
+ if (!candidate) {
5550
+ return {
5551
+ switched: false,
5552
+ message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.usagePercent)}%.`,
5553
+ source: current.name,
5554
+ target: null
5555
+ };
5556
+ }
5557
+ const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.usagePercent)}%` : "n/a";
5558
+ if (options.dryRun) {
5559
+ return {
5560
+ switched: false,
5561
+ message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
5562
+ source: current.name,
5563
+ target: candidate.profile.name
5564
+ };
5565
+ }
5566
+ await manager.switchToProfile(candidate.profile.name);
5567
+ return {
5568
+ switched: true,
5569
+ message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
5570
+ source: current.name,
5571
+ target: candidate.profile.name
5572
+ };
5573
+ }
5574
+ function delay(ms) {
5575
+ return new Promise((resolve) => setTimeout(resolve, ms));
5576
+ }
5213
5577
  async function handleProfile(args) {
5214
5578
  const flags = args.filter((arg) => arg.startsWith("-"));
5215
5579
  const params = stripFlags(args);
@@ -5231,24 +5595,18 @@ async function handleProfile(args) {
5231
5595
  const withUsage = !hasFlag(flags, "--no-usage");
5232
5596
  let usageMap = null;
5233
5597
  if (withUsage) {
5234
- usageMap = /* @__PURE__ */ new Map();
5235
5598
  const codexPath = resolveCodexBinary();
5236
- const codexAvailable = Boolean(codexPath);
5237
- if (!codexAvailable || !codexPath) {
5599
+ if (!codexPath) {
5600
+ usageMap = /* @__PURE__ */ new Map();
5238
5601
  for (const profile of profiles) {
5239
- usageMap.set(profile.name, { summary: "codex cli missing", rows: null });
5602
+ usageMap.set(profile.name, {
5603
+ summary: "codex cli missing",
5604
+ rows: null,
5605
+ usageSummary: summarizeRateLimitSnapshot(profile.rateLimit ?? null)
5606
+ });
5240
5607
  }
5241
5608
  } else {
5242
- const limitEnv = Number.parseInt(process.env.CODEXUSE_CLI_USAGE_CONCURRENCY ?? "3", 10);
5243
- const limit = Number.isFinite(limitEnv) && limitEnv > 0 ? limitEnv : 3;
5244
- const results = await mapWithConcurrency(
5245
- profiles,
5246
- limit,
5247
- (profile) => resolveProfileUsage(manager, profile, codexPath)
5248
- );
5249
- for (const [index, profile] of profiles.entries()) {
5250
- usageMap.set(profile.name, results[index]);
5251
- }
5609
+ usageMap = await collectUsageMap(manager, profiles, codexPath);
5252
5610
  }
5253
5611
  }
5254
5612
  for (const [index, profile] of profiles.entries()) {
@@ -5312,6 +5670,38 @@ async function handleProfile(args) {
5312
5670
  console.log(`Switched to profile: ${name}`);
5313
5671
  return;
5314
5672
  }
5673
+ case "autoroll":
5674
+ case "auto-roll": {
5675
+ const watch = hasFlag(flags, "--watch");
5676
+ const dryRun = hasFlag(flags, "--dry-run");
5677
+ const settings = await getStoredAutoRollSettings().catch(() => null);
5678
+ const fallbackThreshold = typeof settings?.switchThreshold === "number" && Number.isFinite(settings.switchThreshold) ? Math.round(settings.switchThreshold) : DEFAULT_AUTOROLL_THRESHOLD;
5679
+ const threshold = parseAutoRollThreshold(flags, fallbackThreshold);
5680
+ const intervalSeconds = parseAutoRollIntervalSeconds(flags);
5681
+ if (watch) {
5682
+ console.log(`Auto-roll watch: threshold ${threshold}% | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}`);
5683
+ let stop = false;
5684
+ process.on("SIGINT", () => {
5685
+ stop = true;
5686
+ });
5687
+ process.on("SIGTERM", () => {
5688
+ stop = true;
5689
+ });
5690
+ while (!stop) {
5691
+ const result2 = await runAutoRollPass(manager, { threshold, dryRun });
5692
+ console.log(result2.message);
5693
+ if (stop) {
5694
+ break;
5695
+ }
5696
+ await delay(intervalSeconds * 1e3);
5697
+ }
5698
+ console.log("Auto-roll watch stopped.");
5699
+ return;
5700
+ }
5701
+ const result = await runAutoRollPass(manager, { threshold, dryRun });
5702
+ console.log(result.message);
5703
+ return;
5704
+ }
5315
5705
  case "delete": {
5316
5706
  const name = params[1];
5317
5707
  if (!name) {