codex-multi-auth 0.1.8 → 1.1.10

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.
@@ -6,7 +6,7 @@ import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationI
6
6
  import { startLocalOAuthServer } from "./auth/server.js";
7
7
  import { copyTextToClipboard, openBrowserUrl } from "./auth/browser.js";
8
8
  import { promptAddAnotherAccount, promptLoginMode } from "./cli.js";
9
- import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, } from "./accounts.js";
9
+ import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./accounts.js";
10
10
  import { ACCOUNT_LIMITS } from "./constants.js";
11
11
  import { loadDashboardDisplaySettings, DEFAULT_DASHBOARD_DISPLAY_SETTINGS, } from "./dashboard-settings.js";
12
12
  import { evaluateForecastAccounts, isHardRefreshFailure, recommendForecastAccount, summarizeForecast, } from "./forecast.js";
@@ -14,7 +14,7 @@ import { MODEL_FAMILIES } from "./prompts/codex.js";
14
14
  import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./quota-probe.js";
15
15
  import { queuedRefresh } from "./refresh-queue.js";
16
16
  import { loadQuotaCache, saveQuotaCache, } from "./quota-cache.js";
17
- import { clearAccounts, getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, } from "./storage.js";
17
+ import { clearAccounts, findMatchingAccountIndex, getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, withAccountAndFlaggedStorageTransaction, withAccountStorageTransaction, } from "./storage.js";
18
18
  import { getCodexCliAuthPath, getCodexCliConfigPath, loadCodexCliState, } from "./codex-cli/state.js";
19
19
  import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
20
20
  import { ANSI } from "./ui/ansi.js";
@@ -219,6 +219,7 @@ function printUsage() {
219
219
  " codex auth list",
220
220
  " codex auth status",
221
221
  " codex auth switch <index>",
222
+ " codex auth best [--live] [--json] [--model <model>]",
222
223
  " codex auth check",
223
224
  " codex auth features",
224
225
  " codex auth verify-flagged [--dry-run] [--json] [--no-restore]",
@@ -273,6 +274,7 @@ const IMPLEMENTED_FEATURES = [
273
274
  { id: 38, name: "Dashboard display customization" },
274
275
  { id: 39, name: "Unified color/theme runtime (v2 UI)" },
275
276
  { id: 40, name: "OAuth browser-first flow with manual callback fallback" },
277
+ { id: 41, name: "Auto-switch to best account command" },
276
278
  ];
277
279
  function runFeaturesReport() {
278
280
  console.log(`Implemented features (${IMPLEMENTED_FEATURES.length})`);
@@ -322,6 +324,26 @@ function normalizeQuotaEmail(email) {
322
324
  const normalized = sanitizeEmail(email);
323
325
  return normalized && normalized.length > 0 ? normalized : null;
324
326
  }
327
+ function normalizeQuotaAccountId(accountId) {
328
+ if (typeof accountId !== "string")
329
+ return null;
330
+ const trimmed = accountId.trim();
331
+ return trimmed.length > 0 ? trimmed : null;
332
+ }
333
+ function hasUniqueQuotaAccountId(accounts, account) {
334
+ const accountId = normalizeQuotaAccountId(account.accountId);
335
+ if (!accountId)
336
+ return false;
337
+ let matchCount = 0;
338
+ for (const candidate of accounts) {
339
+ if (normalizeQuotaAccountId(candidate.accountId) !== accountId)
340
+ continue;
341
+ matchCount += 1;
342
+ if (matchCount > 1)
343
+ return false;
344
+ }
345
+ return matchCount === 1;
346
+ }
325
347
  function quotaCacheEntryToSnapshot(entry) {
326
348
  return {
327
349
  status: entry.status,
@@ -389,17 +411,20 @@ function formatAccountQuotaSummary(entry) {
389
411
  }
390
412
  return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
391
413
  }
392
- function getQuotaCacheEntryForAccount(cache, account) {
393
- if (account.accountId && cache.byAccountId[account.accountId]) {
394
- return cache.byAccountId[account.accountId] ?? null;
395
- }
414
+ function getQuotaCacheEntryForAccount(cache, account, accounts) {
396
415
  const email = normalizeQuotaEmail(account.email);
397
416
  if (email && cache.byEmail[email]) {
398
417
  return cache.byEmail[email] ?? null;
399
418
  }
419
+ const accountId = normalizeQuotaAccountId(account.accountId);
420
+ if (accountId &&
421
+ hasUniqueQuotaAccountId(accounts, account) &&
422
+ cache.byAccountId[accountId]) {
423
+ return cache.byAccountId[accountId] ?? null;
424
+ }
400
425
  return null;
401
426
  }
402
- function updateQuotaCacheForAccount(cache, account, snapshot) {
427
+ function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
403
428
  const nextEntry = {
404
429
  updatedAt: Date.now(),
405
430
  status: snapshot.status,
@@ -417,25 +442,27 @@ function updateQuotaCacheForAccount(cache, account, snapshot) {
417
442
  },
418
443
  };
419
444
  let changed = false;
420
- if (account.accountId) {
421
- cache.byAccountId[account.accountId] = nextEntry;
422
- changed = true;
423
- }
424
445
  const email = normalizeQuotaEmail(account.email);
425
446
  if (email) {
426
447
  cache.byEmail[email] = nextEntry;
427
448
  changed = true;
449
+ return changed;
450
+ }
451
+ const accountId = normalizeQuotaAccountId(account.accountId);
452
+ if (accountId && hasUniqueQuotaAccountId(accounts, account)) {
453
+ cache.byAccountId[accountId] = nextEntry;
454
+ changed = true;
428
455
  }
429
456
  return changed;
430
457
  }
431
458
  const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
432
459
  const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
433
- function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now) {
460
+ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
434
461
  if (account.enabled === false)
435
462
  return null;
436
463
  if (!hasUsableAccessToken(account, now))
437
464
  return null;
438
- const existing = getQuotaCacheEntryForAccount(cache, account);
465
+ const existing = getQuotaCacheEntryForAccount(cache, account, accounts);
439
466
  if (existing &&
440
467
  typeof existing.updatedAt === "number" &&
441
468
  Number.isFinite(existing.updatedAt) &&
@@ -453,7 +480,7 @@ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now) {
453
480
  function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
454
481
  const targets = [];
455
482
  for (const account of storage.accounts) {
456
- const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now);
483
+ const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts);
457
484
  if (!probeInput)
458
485
  continue;
459
486
  targets.push({
@@ -467,7 +494,7 @@ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now
467
494
  function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
468
495
  let count = 0;
469
496
  for (const account of storage.accounts) {
470
- if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now)) {
497
+ if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts)) {
471
498
  count += 1;
472
499
  }
473
500
  }
@@ -492,7 +519,9 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
492
519
  accessToken: target.accessToken,
493
520
  model: MENU_QUOTA_REFRESH_MODEL,
494
521
  });
495
- changed = updateQuotaCacheForAccount(cache, target.account, snapshot) || changed;
522
+ changed =
523
+ updateQuotaCacheForAccount(cache, target.account, snapshot, storage.accounts) ||
524
+ changed;
496
525
  }
497
526
  catch {
498
527
  // Keep existing cached values if probing fails.
@@ -615,7 +644,9 @@ function toExistingAccountInfo(storage, quotaCache, displaySettings) {
615
644
  const activeIndex = resolveActiveIndex(storage, "codex");
616
645
  const layoutMode = resolveMenuLayoutMode(displaySettings);
617
646
  const baseAccounts = storage.accounts.map((account, index) => {
618
- const entry = quotaCache ? getQuotaCacheEntryForAccount(quotaCache, account) : null;
647
+ const entry = quotaCache
648
+ ? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts)
649
+ : null;
619
650
  return {
620
651
  index,
621
652
  sourceIndex: index,
@@ -690,6 +721,35 @@ function resolveAccountSelection(tokens) {
690
721
  accountLabel: best.label,
691
722
  };
692
723
  }
724
+ function resolveStoredAccountIdentity(storedAccountId, storedAccountIdSource, tokenAccountId) {
725
+ const accountId = resolveRequestAccountId(storedAccountId, storedAccountIdSource, tokenAccountId);
726
+ if (!accountId) {
727
+ return {};
728
+ }
729
+ if (!shouldUpdateAccountIdFromToken(storedAccountIdSource, storedAccountId)) {
730
+ return {
731
+ accountId,
732
+ accountIdSource: storedAccountIdSource,
733
+ };
734
+ }
735
+ return {
736
+ accountId,
737
+ accountIdSource: accountId === tokenAccountId ? "token" : storedAccountIdSource,
738
+ };
739
+ }
740
+ function applyTokenAccountIdentity(account, tokenAccountId) {
741
+ const nextIdentity = resolveStoredAccountIdentity(account.accountId, account.accountIdSource, tokenAccountId);
742
+ if (!nextIdentity.accountId) {
743
+ return false;
744
+ }
745
+ if (nextIdentity.accountId === account.accountId
746
+ && nextIdentity.accountIdSource === account.accountIdSource) {
747
+ return false;
748
+ }
749
+ account.accountId = nextIdentity.accountId;
750
+ account.accountIdSource = nextIdentity.accountIdSource;
751
+ return true;
752
+ }
693
753
  async function promptManualCallback(state) {
694
754
  if (!input.isTTY || !output.isTTY) {
695
755
  return null;
@@ -1058,118 +1118,83 @@ async function runOAuthFlow(forceNewLogin) {
1058
1118
  async function persistAccountPool(results, replaceAll) {
1059
1119
  if (results.length === 0)
1060
1120
  return;
1061
- const loadedStorage = replaceAll
1062
- ? null
1063
- : await loadAccounts();
1064
- const now = Date.now();
1065
- const accounts = loadedStorage?.accounts ? [...loadedStorage.accounts] : [];
1066
- const indexByRefreshToken = new Map();
1067
- const indexByAccountId = new Map();
1068
- const indexByEmail = new Map();
1069
- let selectedAccountIndex = null;
1070
- for (let i = 0; i < accounts.length; i += 1) {
1071
- const account = accounts[i];
1072
- if (!account)
1073
- continue;
1074
- if (account.refreshToken)
1075
- indexByRefreshToken.set(account.refreshToken, i);
1076
- if (account.accountId)
1077
- indexByAccountId.set(account.accountId, i);
1078
- if (account.email)
1079
- indexByEmail.set(account.email, i);
1080
- }
1081
- for (const result of results) {
1082
- const tokenAccountId = extractAccountId(result.access);
1083
- const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1084
- const accountIdSource = accountId
1085
- ? (result.accountIdSource ?? (result.accountIdOverride ? "manual" : "token"))
1086
- : undefined;
1087
- const accountLabel = result.accountLabel;
1088
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1089
- const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
1090
- ? indexByEmail.get(accountEmail)
1091
- : undefined;
1092
- const existingById = accountId && indexByAccountId.has(accountId)
1093
- ? indexByAccountId.get(accountId)
1094
- : undefined;
1095
- const existingByToken = indexByRefreshToken.get(result.refresh);
1096
- const existingIndex = existingById ?? existingByEmail ?? existingByToken;
1097
- if (existingIndex === undefined) {
1098
- const newIndex = accounts.length;
1099
- accounts.push({
1121
+ await withAccountStorageTransaction(async (loadedStorage, persist) => {
1122
+ const stored = replaceAll ? null : loadedStorage;
1123
+ const now = Date.now();
1124
+ const accounts = stored?.accounts ? [...stored.accounts] : [];
1125
+ let selectedAccountIndex = null;
1126
+ for (const result of results) {
1127
+ const tokenAccountId = extractAccountId(result.access);
1128
+ const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1129
+ const accountIdSource = accountId
1130
+ ? (result.accountIdSource ?? (result.accountIdOverride ? "manual" : "token"))
1131
+ : undefined;
1132
+ const accountLabel = result.accountLabel;
1133
+ const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1134
+ const existingIndex = findMatchingAccountIndex(accounts, {
1100
1135
  accountId,
1101
- accountIdSource,
1102
- accountLabel,
1103
1136
  email: accountEmail,
1104
1137
  refreshToken: result.refresh,
1138
+ }, {
1139
+ allowUniqueAccountIdFallbackWithoutEmail: true,
1140
+ });
1141
+ if (existingIndex === undefined) {
1142
+ const newIndex = accounts.length;
1143
+ accounts.push({
1144
+ accountId,
1145
+ accountIdSource,
1146
+ accountLabel,
1147
+ email: accountEmail,
1148
+ refreshToken: result.refresh,
1149
+ accessToken: result.access,
1150
+ expiresAt: result.expires,
1151
+ enabled: true,
1152
+ addedAt: now,
1153
+ lastUsed: now,
1154
+ });
1155
+ selectedAccountIndex = newIndex;
1156
+ continue;
1157
+ }
1158
+ const existing = accounts[existingIndex];
1159
+ if (!existing)
1160
+ continue;
1161
+ const nextEmail = accountEmail ?? existing.email;
1162
+ const nextAccountId = accountId ?? existing.accountId;
1163
+ const nextAccountIdSource = accountId
1164
+ ? (accountIdSource ?? existing.accountIdSource)
1165
+ : existing.accountIdSource;
1166
+ accounts[existingIndex] = {
1167
+ ...existing,
1168
+ accountId: nextAccountId,
1169
+ accountIdSource: nextAccountIdSource,
1170
+ accountLabel: accountLabel ?? existing.accountLabel,
1171
+ email: nextEmail,
1172
+ refreshToken: result.refresh,
1105
1173
  accessToken: result.access,
1106
1174
  expiresAt: result.expires,
1107
1175
  enabled: true,
1108
- addedAt: now,
1109
1176
  lastUsed: now,
1110
- });
1111
- indexByRefreshToken.set(result.refresh, newIndex);
1112
- if (accountId)
1113
- indexByAccountId.set(accountId, newIndex);
1114
- if (accountEmail)
1115
- indexByEmail.set(accountEmail, newIndex);
1116
- selectedAccountIndex = newIndex;
1117
- continue;
1118
- }
1119
- const existing = accounts[existingIndex];
1120
- if (!existing)
1121
- continue;
1122
- const oldToken = existing.refreshToken;
1123
- const oldEmail = existing.email;
1124
- const nextEmail = accountEmail ?? existing.email;
1125
- const nextAccountId = accountId ?? existing.accountId;
1126
- const nextAccountIdSource = accountId
1127
- ? (accountIdSource ?? existing.accountIdSource)
1128
- : existing.accountIdSource;
1129
- accounts[existingIndex] = {
1130
- ...existing,
1131
- accountId: nextAccountId,
1132
- accountIdSource: nextAccountIdSource,
1133
- accountLabel: accountLabel ?? existing.accountLabel,
1134
- email: nextEmail,
1135
- refreshToken: result.refresh,
1136
- accessToken: result.access,
1137
- expiresAt: result.expires,
1138
- enabled: true,
1139
- lastUsed: now,
1140
- };
1141
- if (oldToken !== result.refresh) {
1142
- indexByRefreshToken.delete(oldToken);
1143
- indexByRefreshToken.set(result.refresh, existingIndex);
1144
- }
1145
- if (nextAccountId) {
1146
- indexByAccountId.set(nextAccountId, existingIndex);
1147
- }
1148
- if (oldEmail && oldEmail !== nextEmail) {
1149
- indexByEmail.delete(oldEmail);
1150
- }
1151
- if (nextEmail) {
1152
- indexByEmail.set(nextEmail, existingIndex);
1153
- }
1154
- selectedAccountIndex = existingIndex;
1155
- }
1156
- const fallbackActiveIndex = accounts.length === 0
1157
- ? 0
1158
- : Math.max(0, Math.min(loadedStorage?.activeIndex ?? 0, accounts.length - 1));
1159
- const nextActiveIndex = accounts.length === 0
1160
- ? 0
1161
- : selectedAccountIndex === null
1162
- ? fallbackActiveIndex
1163
- : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1164
- const activeIndexByFamily = {};
1165
- for (const family of MODEL_FAMILIES) {
1166
- activeIndexByFamily[family] = nextActiveIndex;
1167
- }
1168
- await saveAccounts({
1169
- version: 3,
1170
- accounts,
1171
- activeIndex: nextActiveIndex,
1172
- activeIndexByFamily,
1177
+ };
1178
+ selectedAccountIndex = existingIndex;
1179
+ }
1180
+ const fallbackActiveIndex = accounts.length === 0
1181
+ ? 0
1182
+ : Math.max(0, Math.min(stored?.activeIndex ?? 0, accounts.length - 1));
1183
+ const nextActiveIndex = accounts.length === 0
1184
+ ? 0
1185
+ : selectedAccountIndex === null
1186
+ ? fallbackActiveIndex
1187
+ : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1188
+ const activeIndexByFamily = {};
1189
+ for (const family of MODEL_FAMILIES) {
1190
+ activeIndexByFamily[family] = nextActiveIndex;
1191
+ }
1192
+ await persist({
1193
+ version: 3,
1194
+ accounts,
1195
+ activeIndex: nextActiveIndex,
1196
+ activeIndexByFamily,
1197
+ });
1173
1198
  });
1174
1199
  }
1175
1200
  async function syncSelectionToCodex(tokens) {
@@ -1279,7 +1304,7 @@ async function runHealthCheck(options = {}) {
1279
1304
  });
1280
1305
  if (quotaCache) {
1281
1306
  quotaCacheChanged =
1282
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
1307
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1283
1308
  }
1284
1309
  healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
1285
1310
  }
@@ -1319,9 +1344,7 @@ async function runHealthCheck(options = {}) {
1319
1344
  account.email = nextEmail;
1320
1345
  changed = true;
1321
1346
  }
1322
- if (tokenAccountId && tokenAccountId !== account.accountId) {
1323
- account.accountId = tokenAccountId;
1324
- account.accountIdSource = "token";
1347
+ if (applyTokenAccountIdentity(account, tokenAccountId)) {
1325
1348
  changed = true;
1326
1349
  }
1327
1350
  if (account.enabled === false) {
@@ -1349,7 +1372,7 @@ async function runHealthCheck(options = {}) {
1349
1372
  });
1350
1373
  if (quotaCache) {
1351
1374
  quotaCacheChanged =
1352
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
1375
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1353
1376
  }
1354
1377
  healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
1355
1378
  }
@@ -1419,6 +1442,21 @@ function printForecastUsage() {
1419
1442
  " --model, -m Probe model for live mode (default: gpt-5-codex)",
1420
1443
  ].join("\n"));
1421
1444
  }
1445
+ function printBestUsage() {
1446
+ console.log([
1447
+ "Usage:",
1448
+ " codex auth best [--live] [--json] [--model <model>]",
1449
+ "",
1450
+ "Options:",
1451
+ " --live, -l Probe live quota headers via Codex backend before switching",
1452
+ " --json, -j Print machine-readable JSON output",
1453
+ " --model, -m Probe model for live mode (default: gpt-5-codex)",
1454
+ "",
1455
+ "Behavior:",
1456
+ " - Chooses the healthiest account using forecast scoring",
1457
+ " - Switches to the recommended account when it is not already active",
1458
+ ].join("\n"));
1459
+ }
1422
1460
  function printFixUsage() {
1423
1461
  console.log([
1424
1462
  "Usage:",
@@ -1492,6 +1530,48 @@ function parseForecastArgs(args) {
1492
1530
  }
1493
1531
  return { ok: true, options };
1494
1532
  }
1533
+ function parseBestArgs(args) {
1534
+ const options = {
1535
+ live: false,
1536
+ json: false,
1537
+ model: "gpt-5-codex",
1538
+ modelProvided: false,
1539
+ };
1540
+ for (let i = 0; i < args.length; i += 1) {
1541
+ const arg = args[i];
1542
+ if (!arg)
1543
+ continue;
1544
+ if (arg === "--live" || arg === "-l") {
1545
+ options.live = true;
1546
+ continue;
1547
+ }
1548
+ if (arg === "--json" || arg === "-j") {
1549
+ options.json = true;
1550
+ continue;
1551
+ }
1552
+ if (arg === "--model" || arg === "-m") {
1553
+ const value = args[i + 1];
1554
+ if (!value) {
1555
+ return { ok: false, message: "Missing value for --model" };
1556
+ }
1557
+ options.model = value;
1558
+ options.modelProvided = true;
1559
+ i += 1;
1560
+ continue;
1561
+ }
1562
+ if (arg.startsWith("--model=")) {
1563
+ const value = arg.slice("--model=".length).trim();
1564
+ if (!value) {
1565
+ return { ok: false, message: "Missing value for --model" };
1566
+ }
1567
+ options.model = value;
1568
+ options.modelProvided = true;
1569
+ continue;
1570
+ }
1571
+ return { ok: false, message: `Unknown option: ${arg}` };
1572
+ }
1573
+ return { ok: true, options };
1574
+ }
1495
1575
  function parseFixArgs(args) {
1496
1576
  const options = {
1497
1577
  dryRun: false,
@@ -1751,7 +1831,7 @@ async function runForecast(args) {
1751
1831
  const account = storage.accounts[i];
1752
1832
  if (account) {
1753
1833
  quotaCacheChanged =
1754
- updateQuotaCacheForAccount(quotaCache, account, liveQuota) || quotaCacheChanged;
1834
+ updateQuotaCacheForAccount(quotaCache, account, liveQuota, storage.accounts) || quotaCacheChanged;
1755
1835
  }
1756
1836
  }
1757
1837
  }
@@ -2007,28 +2087,29 @@ function findExistingAccountIndexForFlagged(storage, flagged, nextRefreshToken,
2007
2087
  const flaggedEmail = sanitizeEmail(flagged.email);
2008
2088
  const candidateAccountId = nextAccountId ?? flagged.accountId;
2009
2089
  const candidateEmail = sanitizeEmail(nextEmail) ?? flaggedEmail;
2010
- for (let i = 0; i < storage.accounts.length; i += 1) {
2011
- const account = storage.accounts[i];
2012
- if (!account)
2013
- continue;
2014
- if (account.refreshToken === flagged.refreshToken || account.refreshToken === nextRefreshToken) {
2015
- return i;
2016
- }
2017
- if (candidateAccountId &&
2018
- typeof account.accountId === "string" &&
2019
- account.accountId === candidateAccountId) {
2020
- return i;
2021
- }
2022
- const existingEmail = sanitizeEmail(account.email);
2023
- if (candidateEmail && existingEmail && existingEmail === candidateEmail) {
2024
- return i;
2025
- }
2026
- }
2027
- return -1;
2090
+ const nextMatchIndex = findMatchingAccountIndex(storage.accounts, {
2091
+ accountId: candidateAccountId,
2092
+ email: candidateEmail,
2093
+ refreshToken: nextRefreshToken,
2094
+ }, {
2095
+ allowUniqueAccountIdFallbackWithoutEmail: true,
2096
+ });
2097
+ if (nextMatchIndex !== undefined) {
2098
+ return nextMatchIndex;
2099
+ }
2100
+ const flaggedMatchIndex = findMatchingAccountIndex(storage.accounts, {
2101
+ accountId: candidateAccountId,
2102
+ email: candidateEmail,
2103
+ refreshToken: flagged.refreshToken,
2104
+ }, {
2105
+ allowUniqueAccountIdFallbackWithoutEmail: true,
2106
+ });
2107
+ return flaggedMatchIndex ?? -1;
2028
2108
  }
2029
2109
  function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2030
2110
  const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken)) ?? flagged.email;
2031
- const nextAccountId = extractAccountId(refreshResult.access) ?? flagged.accountId;
2111
+ const tokenAccountId = extractAccountId(refreshResult.access);
2112
+ const { accountId: nextAccountId, accountIdSource: nextAccountIdSource } = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2032
2113
  const existingIndex = findExistingAccountIndexForFlagged(storage, flagged, refreshResult.refresh, nextAccountId, nextEmail);
2033
2114
  if (existingIndex >= 0) {
2034
2115
  const existing = storage.accounts[existingIndex];
@@ -2052,9 +2133,11 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2052
2133
  existing.email = nextEmail;
2053
2134
  changed = true;
2054
2135
  }
2055
- if (nextAccountId && nextAccountId !== existing.accountId) {
2136
+ if (nextAccountId !== undefined &&
2137
+ ((nextAccountId !== existing.accountId)
2138
+ || (nextAccountIdSource !== existing.accountIdSource))) {
2056
2139
  existing.accountId = nextAccountId;
2057
- existing.accountIdSource = "token";
2140
+ existing.accountIdSource = nextAccountIdSource;
2058
2141
  changed = true;
2059
2142
  }
2060
2143
  if (existing.enabled === false) {
@@ -2084,7 +2167,7 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2084
2167
  accessToken: refreshResult.access,
2085
2168
  expiresAt: refreshResult.expires,
2086
2169
  accountId: nextAccountId,
2087
- accountIdSource: nextAccountId ? "token" : flagged.accountIdSource,
2170
+ accountIdSource: nextAccountIdSource,
2088
2171
  accountLabel: flagged.accountLabel,
2089
2172
  email: nextEmail,
2090
2173
  addedAt: flagged.addedAt ?? now,
@@ -2129,113 +2212,142 @@ async function runVerifyFlagged(args) {
2129
2212
  console.log("No flagged accounts to check.");
2130
2213
  return 0;
2131
2214
  }
2132
- let storage = await loadAccounts();
2133
- if (!storage) {
2134
- storage = createEmptyAccountStorage();
2135
- }
2136
2215
  let storageChanged = false;
2137
2216
  let flaggedChanged = false;
2138
2217
  const reports = [];
2139
2218
  const nextFlaggedAccounts = [];
2140
2219
  const now = Date.now();
2220
+ const refreshChecks = [];
2141
2221
  for (let i = 0; i < flaggedStorage.accounts.length; i += 1) {
2142
2222
  const flagged = flaggedStorage.accounts[i];
2143
2223
  if (!flagged)
2144
2224
  continue;
2145
2225
  const label = formatAccountLabel(flagged, i);
2146
- const result = await queuedRefresh(flagged.refreshToken);
2147
- if (result.type === "success") {
2148
- if (!options.restore) {
2149
- const nextFlagged = {
2226
+ refreshChecks.push({
2227
+ index: i,
2228
+ flagged,
2229
+ label,
2230
+ result: await queuedRefresh(flagged.refreshToken),
2231
+ });
2232
+ }
2233
+ const applyRefreshChecks = (storage) => {
2234
+ for (const check of refreshChecks) {
2235
+ const { index: i, flagged, label, result } = check;
2236
+ if (result.type === "success") {
2237
+ if (!options.restore) {
2238
+ const tokenAccountId = extractAccountId(result.access);
2239
+ const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2240
+ const nextFlagged = {
2241
+ ...flagged,
2242
+ refreshToken: result.refresh,
2243
+ accessToken: result.access,
2244
+ expiresAt: result.expires,
2245
+ accountId: nextIdentity.accountId,
2246
+ accountIdSource: nextIdentity.accountIdSource,
2247
+ email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2248
+ lastUsed: now,
2249
+ lastError: undefined,
2250
+ };
2251
+ nextFlaggedAccounts.push(nextFlagged);
2252
+ if (JSON.stringify(nextFlagged) !== JSON.stringify(flagged)) {
2253
+ flaggedChanged = true;
2254
+ }
2255
+ reports.push({
2256
+ index: i,
2257
+ label,
2258
+ outcome: "healthy-flagged",
2259
+ message: "session is healthy (left in flagged list due to --no-restore)",
2260
+ });
2261
+ continue;
2262
+ }
2263
+ const upsertResult = upsertRecoveredFlaggedAccount(storage, flagged, result, now);
2264
+ if (upsertResult.restored) {
2265
+ storageChanged = storageChanged || upsertResult.changed;
2266
+ flaggedChanged = true;
2267
+ reports.push({
2268
+ index: i,
2269
+ label,
2270
+ outcome: "restored",
2271
+ message: upsertResult.message,
2272
+ });
2273
+ continue;
2274
+ }
2275
+ const tokenAccountId = extractAccountId(result.access);
2276
+ const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2277
+ const updatedFlagged = {
2150
2278
  ...flagged,
2151
2279
  refreshToken: result.refresh,
2152
2280
  accessToken: result.access,
2153
2281
  expiresAt: result.expires,
2154
- accountId: extractAccountId(result.access) ?? flagged.accountId,
2155
- accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2282
+ accountId: nextIdentity.accountId,
2283
+ accountIdSource: nextIdentity.accountIdSource,
2156
2284
  email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2157
2285
  lastUsed: now,
2158
- lastError: undefined,
2286
+ lastError: upsertResult.message,
2159
2287
  };
2160
- nextFlaggedAccounts.push(nextFlagged);
2161
- if (JSON.stringify(nextFlagged) !== JSON.stringify(flagged)) {
2288
+ nextFlaggedAccounts.push(updatedFlagged);
2289
+ if (JSON.stringify(updatedFlagged) !== JSON.stringify(flagged)) {
2162
2290
  flaggedChanged = true;
2163
2291
  }
2164
2292
  reports.push({
2165
2293
  index: i,
2166
2294
  label,
2167
- outcome: "healthy-flagged",
2168
- message: "session is healthy (left in flagged list due to --no-restore)",
2169
- });
2170
- continue;
2171
- }
2172
- const upsertResult = upsertRecoveredFlaggedAccount(storage, flagged, result, now);
2173
- if (upsertResult.restored) {
2174
- storageChanged = storageChanged || upsertResult.changed;
2175
- flaggedChanged = true;
2176
- reports.push({
2177
- index: i,
2178
- label,
2179
- outcome: "restored",
2295
+ outcome: "restore-skipped",
2180
2296
  message: upsertResult.message,
2181
2297
  });
2182
2298
  continue;
2183
2299
  }
2184
- const updatedFlagged = {
2300
+ const detail = normalizeFailureDetail(result.message, result.reason);
2301
+ const failedFlagged = {
2185
2302
  ...flagged,
2186
- refreshToken: result.refresh,
2187
- accessToken: result.access,
2188
- expiresAt: result.expires,
2189
- accountId: extractAccountId(result.access) ?? flagged.accountId,
2190
- accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2191
- email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2192
- lastUsed: now,
2193
- lastError: upsertResult.message,
2303
+ lastError: detail,
2194
2304
  };
2195
- nextFlaggedAccounts.push(updatedFlagged);
2196
- if (JSON.stringify(updatedFlagged) !== JSON.stringify(flagged)) {
2305
+ nextFlaggedAccounts.push(failedFlagged);
2306
+ if ((flagged.lastError ?? "") !== detail) {
2197
2307
  flaggedChanged = true;
2198
2308
  }
2199
2309
  reports.push({
2200
2310
  index: i,
2201
2311
  label,
2202
- outcome: "restore-skipped",
2203
- message: upsertResult.message,
2312
+ outcome: "still-flagged",
2313
+ message: detail,
2204
2314
  });
2205
- continue;
2206
2315
  }
2207
- const detail = normalizeFailureDetail(result.message, result.reason);
2208
- const failedFlagged = {
2209
- ...flagged,
2210
- lastError: detail,
2211
- };
2212
- nextFlaggedAccounts.push(failedFlagged);
2213
- if ((flagged.lastError ?? "") !== detail) {
2214
- flaggedChanged = true;
2316
+ };
2317
+ if (options.restore) {
2318
+ if (options.dryRun) {
2319
+ applyRefreshChecks((await loadAccounts()) ?? createEmptyAccountStorage());
2215
2320
  }
2216
- reports.push({
2217
- index: i,
2218
- label,
2219
- outcome: "still-flagged",
2220
- message: detail,
2221
- });
2321
+ else {
2322
+ await withAccountAndFlaggedStorageTransaction(async (loadedStorage, persist) => {
2323
+ const nextStorage = loadedStorage
2324
+ ? structuredClone(loadedStorage)
2325
+ : createEmptyAccountStorage();
2326
+ applyRefreshChecks(nextStorage);
2327
+ if (!storageChanged) {
2328
+ return;
2329
+ }
2330
+ normalizeDoctorIndexes(nextStorage);
2331
+ await persist(nextStorage, {
2332
+ version: 1,
2333
+ accounts: nextFlaggedAccounts,
2334
+ });
2335
+ });
2336
+ }
2337
+ }
2338
+ else {
2339
+ applyRefreshChecks(createEmptyAccountStorage());
2222
2340
  }
2223
2341
  const remainingFlagged = nextFlaggedAccounts.length;
2224
2342
  const restored = reports.filter((report) => report.outcome === "restored").length;
2225
2343
  const healthyFlagged = reports.filter((report) => report.outcome === "healthy-flagged").length;
2226
2344
  const stillFlagged = reports.filter((report) => report.outcome === "still-flagged").length;
2227
2345
  const changed = storageChanged || flaggedChanged;
2228
- if (!options.dryRun) {
2229
- if (storageChanged) {
2230
- normalizeDoctorIndexes(storage);
2231
- await saveAccounts(storage);
2232
- }
2233
- if (flaggedChanged) {
2234
- await saveFlaggedAccounts({
2235
- version: 1,
2236
- accounts: nextFlaggedAccounts,
2237
- });
2238
- }
2346
+ if (!options.dryRun && flaggedChanged && (!options.restore || !storageChanged)) {
2347
+ await saveFlaggedAccounts({
2348
+ version: 1,
2349
+ accounts: nextFlaggedAccounts,
2350
+ });
2239
2351
  }
2240
2352
  if (options.json) {
2241
2353
  console.log(JSON.stringify({
@@ -2340,7 +2452,7 @@ async function runFix(args) {
2340
2452
  });
2341
2453
  if (quotaCache) {
2342
2454
  quotaCacheChanged =
2343
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
2455
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2344
2456
  }
2345
2457
  reports.push({
2346
2458
  index: i,
@@ -2413,7 +2525,7 @@ async function runFix(args) {
2413
2525
  });
2414
2526
  if (quotaCache) {
2415
2527
  quotaCacheChanged =
2416
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
2528
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2417
2529
  }
2418
2530
  reports.push({
2419
2531
  index: i,
@@ -2615,6 +2727,12 @@ function normalizeDoctorIndexes(storage) {
2615
2727
  }
2616
2728
  return changed;
2617
2729
  }
2730
+ function getDoctorRefreshTokenKey(refreshToken) {
2731
+ if (typeof refreshToken !== "string")
2732
+ return undefined;
2733
+ const trimmed = refreshToken.trim();
2734
+ return trimmed || undefined;
2735
+ }
2618
2736
  function applyDoctorFixes(storage) {
2619
2737
  let changed = false;
2620
2738
  const actions = [];
@@ -2630,7 +2748,9 @@ function applyDoctorFixes(storage) {
2630
2748
  const account = storage.accounts[i];
2631
2749
  if (!account)
2632
2750
  continue;
2633
- const refreshToken = account.refreshToken.trim();
2751
+ const refreshToken = getDoctorRefreshTokenKey(account.refreshToken);
2752
+ if (!refreshToken)
2753
+ continue;
2634
2754
  const existingTokenIndex = seenRefreshTokens.get(refreshToken);
2635
2755
  if (typeof existingTokenIndex === "number") {
2636
2756
  if (account.enabled !== false) {
@@ -2879,7 +2999,9 @@ async function runDoctor(args) {
2879
2999
  const seenRefreshTokens = new Set();
2880
3000
  let duplicateTokenCount = 0;
2881
3001
  for (const account of storage.accounts) {
2882
- const token = account.refreshToken.trim();
3002
+ const token = getDoctorRefreshTokenKey(account.refreshToken);
3003
+ if (!token)
3004
+ continue;
2883
3005
  if (seenRefreshTokens.has(token)) {
2884
3006
  duplicateTokenCount += 1;
2885
3007
  }
@@ -2998,10 +3120,7 @@ async function runDoctor(args) {
2998
3120
  activeAccount.expiresAt = refreshResult.expires;
2999
3121
  if (refreshedEmail)
3000
3122
  activeAccount.email = refreshedEmail;
3001
- if (refreshedAccountId) {
3002
- activeAccount.accountId = refreshedAccountId;
3003
- activeAccount.accountIdSource = "token";
3004
- }
3123
+ applyTokenAccountIdentity(activeAccount, refreshedAccountId);
3005
3124
  syncAccessToken = refreshResult.access;
3006
3125
  syncRefreshToken = refreshResult.refresh;
3007
3126
  syncExpiresAt = refreshResult.expires;
@@ -3320,6 +3439,23 @@ async function runSwitch(args) {
3320
3439
  console.error(`Account ${parsed} not found.`);
3321
3440
  return 1;
3322
3441
  }
3442
+ const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
3443
+ storage,
3444
+ targetIndex,
3445
+ parsed,
3446
+ switchReason: "rotation",
3447
+ });
3448
+ if (!synced) {
3449
+ console.warn(`Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`);
3450
+ }
3451
+ console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3452
+ return 0;
3453
+ }
3454
+ async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, }) {
3455
+ const account = storage.accounts[targetIndex];
3456
+ if (!account) {
3457
+ throw new Error(`Account ${parsed} not found.`);
3458
+ }
3323
3459
  storage.activeIndex = targetIndex;
3324
3460
  storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
3325
3461
  for (const family of MODEL_FAMILIES) {
@@ -3333,7 +3469,7 @@ async function runSwitch(args) {
3333
3469
  let syncAccessToken = account.accessToken;
3334
3470
  let syncRefreshToken = account.refreshToken;
3335
3471
  let syncExpiresAt = account.expiresAt;
3336
- let syncIdToken;
3472
+ let syncIdToken = initialSyncIdToken;
3337
3473
  if (!hasUsableAccessToken(account, switchNow)) {
3338
3474
  const refreshResult = await queuedRefresh(account.refreshToken);
3339
3475
  if (refreshResult.type === "success") {
@@ -3351,10 +3487,7 @@ async function runSwitch(args) {
3351
3487
  if (nextEmail && nextEmail !== account.email) {
3352
3488
  account.email = nextEmail;
3353
3489
  }
3354
- if (tokenAccountId && tokenAccountId !== account.accountId) {
3355
- account.accountId = tokenAccountId;
3356
- account.accountIdSource = "token";
3357
- }
3490
+ applyTokenAccountIdentity(account, tokenAccountId);
3358
3491
  syncAccessToken = refreshResult.access;
3359
3492
  syncRefreshToken = refreshResult.refresh;
3360
3493
  syncExpiresAt = refreshResult.expires;
@@ -3365,7 +3498,7 @@ async function runSwitch(args) {
3365
3498
  }
3366
3499
  }
3367
3500
  account.lastUsed = switchNow;
3368
- account.lastSwitchReason = "rotation";
3501
+ account.lastSwitchReason = switchReason;
3369
3502
  await saveAccounts(storage);
3370
3503
  const synced = await setCodexCliActiveSelection({
3371
3504
  accountId: account.accountId,
@@ -3375,10 +3508,224 @@ async function runSwitch(args) {
3375
3508
  expiresAt: syncExpiresAt,
3376
3509
  ...(syncIdToken ? { idToken: syncIdToken } : {}),
3377
3510
  });
3378
- if (!synced) {
3379
- console.warn(`Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`);
3511
+ return { synced, wasDisabled };
3512
+ }
3513
+ async function runBest(args) {
3514
+ if (args.includes("--help") || args.includes("-h")) {
3515
+ printBestUsage();
3516
+ return 0;
3517
+ }
3518
+ const parsedArgs = parseBestArgs(args);
3519
+ if (!parsedArgs.ok) {
3520
+ console.error(parsedArgs.message);
3521
+ printBestUsage();
3522
+ return 1;
3523
+ }
3524
+ const options = parsedArgs.options;
3525
+ if (options.modelProvided && !options.live) {
3526
+ console.error("--model requires --live for codex auth best");
3527
+ printBestUsage();
3528
+ return 1;
3529
+ }
3530
+ setStoragePath(null);
3531
+ const storage = await loadAccounts();
3532
+ if (!storage || storage.accounts.length === 0) {
3533
+ if (options.json) {
3534
+ console.log(JSON.stringify({ error: "No accounts configured." }, null, 2));
3535
+ }
3536
+ else {
3537
+ console.log("No accounts configured.");
3538
+ }
3539
+ return 1;
3540
+ }
3541
+ const now = Date.now();
3542
+ const refreshFailures = new Map();
3543
+ const liveQuotaByIndex = new Map();
3544
+ const probeIdTokenByIndex = new Map();
3545
+ const probeRefreshedIndices = new Set();
3546
+ const probeErrors = [];
3547
+ let changed = false;
3548
+ const printProbeNotes = () => {
3549
+ if (probeErrors.length === 0)
3550
+ return;
3551
+ console.log(`Live check notes (${probeErrors.length}):`);
3552
+ for (const error of probeErrors) {
3553
+ console.log(` - ${error}`);
3554
+ }
3555
+ };
3556
+ const persistProbeChangesIfNeeded = async () => {
3557
+ if (!changed)
3558
+ return;
3559
+ await saveAccounts(storage);
3560
+ changed = false;
3561
+ };
3562
+ for (let i = 0; i < storage.accounts.length; i += 1) {
3563
+ const account = storage.accounts[i];
3564
+ if (!account || !options.live)
3565
+ continue;
3566
+ if (account.enabled === false)
3567
+ continue;
3568
+ let probeAccessToken = account.accessToken;
3569
+ let probeAccountId = account.accountId ?? extractAccountId(account.accessToken);
3570
+ if (!hasUsableAccessToken(account, now)) {
3571
+ const refreshResult = await queuedRefresh(account.refreshToken);
3572
+ if (refreshResult.type !== "success") {
3573
+ refreshFailures.set(i, {
3574
+ ...refreshResult,
3575
+ message: normalizeFailureDetail(refreshResult.message, refreshResult.reason),
3576
+ });
3577
+ continue;
3578
+ }
3579
+ const refreshedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
3580
+ const refreshedAccountId = extractAccountId(refreshResult.access);
3581
+ if (account.refreshToken !== refreshResult.refresh) {
3582
+ account.refreshToken = refreshResult.refresh;
3583
+ changed = true;
3584
+ }
3585
+ if (account.accessToken !== refreshResult.access) {
3586
+ account.accessToken = refreshResult.access;
3587
+ changed = true;
3588
+ }
3589
+ if (account.expiresAt !== refreshResult.expires) {
3590
+ account.expiresAt = refreshResult.expires;
3591
+ changed = true;
3592
+ }
3593
+ if (refreshedEmail && refreshedEmail !== account.email) {
3594
+ account.email = refreshedEmail;
3595
+ changed = true;
3596
+ }
3597
+ if (refreshedAccountId && refreshedAccountId !== account.accountId) {
3598
+ account.accountId = refreshedAccountId;
3599
+ account.accountIdSource = "token";
3600
+ changed = true;
3601
+ }
3602
+ if (refreshResult.idToken) {
3603
+ probeIdTokenByIndex.set(i, refreshResult.idToken);
3604
+ }
3605
+ probeRefreshedIndices.add(i);
3606
+ probeAccessToken = account.accessToken;
3607
+ probeAccountId = account.accountId ?? refreshedAccountId;
3608
+ }
3609
+ if (!probeAccessToken || !probeAccountId) {
3610
+ probeErrors.push(`${formatAccountLabel(account, i)}: missing accountId for live probe`);
3611
+ continue;
3612
+ }
3613
+ try {
3614
+ const liveQuota = await fetchCodexQuotaSnapshot({
3615
+ accountId: probeAccountId,
3616
+ accessToken: probeAccessToken,
3617
+ model: options.model,
3618
+ });
3619
+ liveQuotaByIndex.set(i, liveQuota);
3620
+ }
3621
+ catch (error) {
3622
+ const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
3623
+ probeErrors.push(`${formatAccountLabel(account, i)}: ${message}`);
3624
+ }
3625
+ }
3626
+ const forecastInputs = storage.accounts.map((account, index) => ({
3627
+ index,
3628
+ account,
3629
+ isCurrent: index === resolveActiveIndex(storage, "codex"),
3630
+ now,
3631
+ refreshFailure: refreshFailures.get(index),
3632
+ liveQuota: liveQuotaByIndex.get(index),
3633
+ }));
3634
+ const forecastResults = evaluateForecastAccounts(forecastInputs);
3635
+ const recommendation = recommendForecastAccount(forecastResults);
3636
+ if (recommendation.recommendedIndex === null) {
3637
+ await persistProbeChangesIfNeeded();
3638
+ if (options.json) {
3639
+ console.log(JSON.stringify({
3640
+ error: recommendation.reason,
3641
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3642
+ }, null, 2));
3643
+ }
3644
+ else {
3645
+ console.log(`No best account available: ${recommendation.reason}`);
3646
+ printProbeNotes();
3647
+ }
3648
+ return 1;
3649
+ }
3650
+ const bestIndex = recommendation.recommendedIndex;
3651
+ const bestAccount = storage.accounts[bestIndex];
3652
+ if (!bestAccount) {
3653
+ await persistProbeChangesIfNeeded();
3654
+ if (options.json) {
3655
+ console.log(JSON.stringify({ error: "Best account not found." }, null, 2));
3656
+ }
3657
+ else {
3658
+ console.log("Best account not found.");
3659
+ }
3660
+ return 1;
3661
+ }
3662
+ // Check if already on best account
3663
+ const currentIndex = resolveActiveIndex(storage, "codex");
3664
+ if (currentIndex === bestIndex) {
3665
+ const shouldSyncCurrentBest = probeRefreshedIndices.has(bestIndex) || probeIdTokenByIndex.has(bestIndex);
3666
+ let alreadyBestSynced;
3667
+ if (changed) {
3668
+ bestAccount.lastUsed = now;
3669
+ await persistProbeChangesIfNeeded();
3670
+ }
3671
+ if (shouldSyncCurrentBest) {
3672
+ alreadyBestSynced = await setCodexCliActiveSelection({
3673
+ accountId: bestAccount.accountId,
3674
+ email: bestAccount.email,
3675
+ accessToken: bestAccount.accessToken,
3676
+ refreshToken: bestAccount.refreshToken,
3677
+ expiresAt: bestAccount.expiresAt,
3678
+ ...(probeIdTokenByIndex.has(bestIndex)
3679
+ ? { idToken: probeIdTokenByIndex.get(bestIndex) }
3680
+ : {}),
3681
+ });
3682
+ if (!alreadyBestSynced && !options.json) {
3683
+ console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
3684
+ }
3685
+ }
3686
+ if (options.json) {
3687
+ console.log(JSON.stringify({
3688
+ message: `Already on best account: ${formatAccountLabel(bestAccount, bestIndex)}`,
3689
+ accountIndex: bestIndex + 1,
3690
+ reason: recommendation.reason,
3691
+ ...(alreadyBestSynced !== undefined ? { synced: alreadyBestSynced } : {}),
3692
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3693
+ }, null, 2));
3694
+ }
3695
+ else {
3696
+ console.log(`Already on best account ${bestIndex + 1}: ${formatAccountLabel(bestAccount, bestIndex)}`);
3697
+ console.log(`Reason: ${recommendation.reason}`);
3698
+ printProbeNotes();
3699
+ }
3700
+ return 0;
3701
+ }
3702
+ const targetIndex = bestIndex;
3703
+ const parsed = targetIndex + 1;
3704
+ const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
3705
+ storage,
3706
+ targetIndex,
3707
+ parsed,
3708
+ switchReason: "best",
3709
+ initialSyncIdToken: probeIdTokenByIndex.get(bestIndex),
3710
+ });
3711
+ if (options.json) {
3712
+ console.log(JSON.stringify({
3713
+ message: `Switched to best account: ${formatAccountLabel(bestAccount, targetIndex)}`,
3714
+ accountIndex: parsed,
3715
+ reason: recommendation.reason,
3716
+ synced,
3717
+ wasDisabled,
3718
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3719
+ }, null, 2));
3720
+ }
3721
+ else {
3722
+ console.log(`Switched to best account ${parsed}: ${formatAccountLabel(bestAccount, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3723
+ console.log(`Reason: ${recommendation.reason}`);
3724
+ printProbeNotes();
3725
+ if (!synced) {
3726
+ console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
3727
+ }
3380
3728
  }
3381
- console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3382
3729
  return 0;
3383
3730
  }
3384
3731
  export async function autoSyncActiveAccountToCodex() {
@@ -3422,9 +3769,7 @@ export async function autoSyncActiveAccountToCodex() {
3422
3769
  account.email = nextEmail;
3423
3770
  changed = true;
3424
3771
  }
3425
- if (tokenAccountId && tokenAccountId !== account.accountId) {
3426
- account.accountId = tokenAccountId;
3427
- account.accountIdSource = "token";
3772
+ if (applyTokenAccountIdentity(account, tokenAccountId)) {
3428
3773
  changed = true;
3429
3774
  }
3430
3775
  syncAccessToken = refreshResult.access;
@@ -3490,6 +3835,9 @@ export async function runCodexMultiAuthCli(rawArgs) {
3490
3835
  if (command === "forecast") {
3491
3836
  return runForecast(rest);
3492
3837
  }
3838
+ if (command === "best") {
3839
+ return runBest(rest);
3840
+ }
3493
3841
  if (command === "report") {
3494
3842
  return runReport(rest);
3495
3843
  }