codex-multi-auth 0.1.9 → 1.1.11

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";
@@ -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})`);
@@ -342,6 +344,46 @@ function hasUniqueQuotaAccountId(accounts, account) {
342
344
  }
343
345
  return matchCount === 1;
344
346
  }
347
+ function buildQuotaEmailFallbackState(accounts) {
348
+ const stateByEmail = new Map();
349
+ for (const account of accounts) {
350
+ const email = normalizeQuotaEmail(account.email);
351
+ if (!email)
352
+ continue;
353
+ const existing = stateByEmail.get(email);
354
+ if (existing) {
355
+ existing.matchingCount += 1;
356
+ const accountId = normalizeQuotaAccountId(account.accountId);
357
+ if (accountId) {
358
+ existing.distinctAccountIds.add(accountId);
359
+ }
360
+ continue;
361
+ }
362
+ const distinctAccountIds = new Set();
363
+ const accountId = normalizeQuotaAccountId(account.accountId);
364
+ if (accountId) {
365
+ distinctAccountIds.add(accountId);
366
+ }
367
+ stateByEmail.set(email, {
368
+ matchingCount: 1,
369
+ distinctAccountIds,
370
+ });
371
+ }
372
+ return stateByEmail;
373
+ }
374
+ function hasSafeQuotaEmailFallback(emailFallbackState, account) {
375
+ const email = normalizeQuotaEmail(account.email);
376
+ if (!email)
377
+ return false;
378
+ const state = emailFallbackState.get(email);
379
+ if (!state)
380
+ return false;
381
+ // size > 1 only matters when multiple accounts share the same email but
382
+ // disagree on accountId; a single matching account already implies size <= 1.
383
+ if (state.distinctAccountIds.size > 1)
384
+ return false;
385
+ return state.matchingCount === 1;
386
+ }
345
387
  function quotaCacheEntryToSnapshot(entry) {
346
388
  return {
347
389
  status: entry.status,
@@ -409,20 +451,22 @@ function formatAccountQuotaSummary(entry) {
409
451
  }
410
452
  return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
411
453
  }
412
- function getQuotaCacheEntryForAccount(cache, account, accounts) {
413
- const email = normalizeQuotaEmail(account.email);
414
- if (email && cache.byEmail[email]) {
415
- return cache.byEmail[email] ?? null;
416
- }
454
+ function getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
417
455
  const accountId = normalizeQuotaAccountId(account.accountId);
418
456
  if (accountId &&
419
457
  hasUniqueQuotaAccountId(accounts, account) &&
420
458
  cache.byAccountId[accountId]) {
421
459
  return cache.byAccountId[accountId] ?? null;
422
460
  }
461
+ const email = normalizeQuotaEmail(account.email);
462
+ if (email &&
463
+ hasSafeQuotaEmailFallback(emailFallbackState, account) &&
464
+ cache.byEmail[email]) {
465
+ return cache.byEmail[email] ?? null;
466
+ }
423
467
  return null;
424
468
  }
425
- function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
469
+ function updateQuotaCacheForAccount(cache, account, snapshot, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
426
470
  const nextEntry = {
427
471
  updatedAt: Date.now(),
428
472
  status: snapshot.status,
@@ -440,33 +484,67 @@ function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
440
484
  },
441
485
  };
442
486
  let changed = false;
487
+ const accountId = normalizeQuotaAccountId(account.accountId);
488
+ const hasUniqueAccountId = accountId !== null && hasUniqueQuotaAccountId(accounts, account);
489
+ if (hasUniqueAccountId) {
490
+ cache.byAccountId[accountId] = nextEntry;
491
+ changed = true;
492
+ }
443
493
  const email = normalizeQuotaEmail(account.email);
444
- if (email) {
494
+ if (email &&
495
+ hasSafeQuotaEmailFallback(emailFallbackState, account) &&
496
+ !hasUniqueAccountId) {
445
497
  cache.byEmail[email] = nextEntry;
446
498
  changed = true;
447
- return changed;
448
499
  }
449
- const accountId = normalizeQuotaAccountId(account.accountId);
450
- if (accountId && hasUniqueQuotaAccountId(accounts, account)) {
451
- cache.byAccountId[accountId] = nextEntry;
500
+ else if (email && cache.byEmail[email]) {
501
+ delete cache.byEmail[email];
452
502
  changed = true;
453
503
  }
454
504
  return changed;
455
505
  }
506
+ function cloneQuotaCacheData(cache) {
507
+ // Shallow spreading is safe because quota cache entries are always replaced,
508
+ // never mutated in-place.
509
+ return {
510
+ byAccountId: { ...cache.byAccountId },
511
+ byEmail: { ...cache.byEmail },
512
+ };
513
+ }
514
+ function pruneUnsafeQuotaEmailCacheEntry(cache, email, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
515
+ const normalizedEmail = normalizeQuotaEmail(email);
516
+ if (!normalizedEmail || !cache.byEmail[normalizedEmail]) {
517
+ return false;
518
+ }
519
+ const hasSafeFallbackAccount = accounts.some((account) => normalizeQuotaEmail(account.email) === normalizedEmail &&
520
+ hasSafeQuotaEmailFallback(emailFallbackState, account));
521
+ if (hasSafeFallbackAccount) {
522
+ return false;
523
+ }
524
+ delete cache.byEmail[normalizedEmail];
525
+ return true;
526
+ }
456
527
  const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
457
528
  const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
458
- function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
529
+ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
459
530
  if (account.enabled === false)
460
531
  return null;
461
532
  if (!hasUsableAccessToken(account, now))
462
533
  return null;
463
- const existing = getQuotaCacheEntryForAccount(cache, account, accounts);
534
+ const existing = getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState);
464
535
  if (existing &&
465
536
  typeof existing.updatedAt === "number" &&
466
537
  Number.isFinite(existing.updatedAt) &&
467
538
  now - existing.updatedAt < maxAgeMs) {
468
539
  return null;
469
540
  }
541
+ // Menu auto-refresh is cache-backed, so only probe when the result can be
542
+ // written behind a safe lookup key for later reuse.
543
+ const canStore = (normalizeQuotaAccountId(account.accountId) !== null &&
544
+ hasUniqueQuotaAccountId(accounts, account)) ||
545
+ hasSafeQuotaEmailFallback(emailFallbackState, account);
546
+ if (!canStore)
547
+ return null;
470
548
  const accessToken = account.accessToken;
471
549
  const accountId = accessToken
472
550
  ? (account.accountId ?? extractAccountId(accessToken))
@@ -475,10 +553,10 @@ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
475
553
  return null;
476
554
  return { accountId, accessToken };
477
555
  }
478
- function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
556
+ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now(), emailFallbackState = buildQuotaEmailFallbackState(storage.accounts)) {
479
557
  const targets = [];
480
558
  for (const account of storage.accounts) {
481
- const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts);
559
+ const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState);
482
560
  if (!probeInput)
483
561
  continue;
484
562
  targets.push({
@@ -490,9 +568,10 @@ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now
490
568
  return targets;
491
569
  }
492
570
  function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
571
+ const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
493
572
  let count = 0;
494
573
  for (const account of storage.accounts) {
495
- if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts)) {
574
+ if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState)) {
496
575
  count += 1;
497
576
  }
498
577
  }
@@ -502,8 +581,10 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
502
581
  if (storage.accounts.length === 0) {
503
582
  return cache;
504
583
  }
584
+ const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
585
+ const nextCache = cloneQuotaCacheData(cache);
505
586
  const now = Date.now();
506
- const targets = collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now);
587
+ const targets = collectMenuQuotaRefreshTargets(storage, nextCache, maxAgeMs, now, emailFallbackState);
507
588
  const total = targets.length;
508
589
  let processed = 0;
509
590
  onProgress?.(processed, total);
@@ -518,17 +599,16 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
518
599
  model: MENU_QUOTA_REFRESH_MODEL,
519
600
  });
520
601
  changed =
521
- updateQuotaCacheForAccount(cache, target.account, snapshot, storage.accounts) ||
522
- changed;
602
+ updateQuotaCacheForAccount(nextCache, target.account, snapshot, storage.accounts, emailFallbackState) || changed;
523
603
  }
524
604
  catch {
525
605
  // Keep existing cached values if probing fails.
526
606
  }
527
607
  }
528
608
  if (changed) {
529
- await saveQuotaCache(cache);
609
+ await saveQuotaCache(nextCache);
530
610
  }
531
- return cache;
611
+ return nextCache;
532
612
  }
533
613
  const ACCESS_TOKEN_FRESH_WINDOW_MS = 5 * 60 * 1000;
534
614
  function hasUsableAccessToken(account, now) {
@@ -641,9 +721,10 @@ function toExistingAccountInfo(storage, quotaCache, displaySettings) {
641
721
  const now = Date.now();
642
722
  const activeIndex = resolveActiveIndex(storage, "codex");
643
723
  const layoutMode = resolveMenuLayoutMode(displaySettings);
724
+ const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
644
725
  const baseAccounts = storage.accounts.map((account, index) => {
645
726
  const entry = quotaCache
646
- ? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts)
727
+ ? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts, emailFallbackState)
647
728
  : null;
648
729
  return {
649
730
  index,
@@ -719,6 +800,35 @@ function resolveAccountSelection(tokens) {
719
800
  accountLabel: best.label,
720
801
  };
721
802
  }
803
+ function resolveStoredAccountIdentity(storedAccountId, storedAccountIdSource, tokenAccountId) {
804
+ const accountId = resolveRequestAccountId(storedAccountId, storedAccountIdSource, tokenAccountId);
805
+ if (!accountId) {
806
+ return {};
807
+ }
808
+ if (!shouldUpdateAccountIdFromToken(storedAccountIdSource, storedAccountId)) {
809
+ return {
810
+ accountId,
811
+ accountIdSource: storedAccountIdSource,
812
+ };
813
+ }
814
+ return {
815
+ accountId,
816
+ accountIdSource: accountId === tokenAccountId ? "token" : storedAccountIdSource,
817
+ };
818
+ }
819
+ function applyTokenAccountIdentity(account, tokenAccountId) {
820
+ const nextIdentity = resolveStoredAccountIdentity(account.accountId, account.accountIdSource, tokenAccountId);
821
+ if (!nextIdentity.accountId) {
822
+ return false;
823
+ }
824
+ if (nextIdentity.accountId === account.accountId
825
+ && nextIdentity.accountIdSource === account.accountIdSource) {
826
+ return false;
827
+ }
828
+ account.accountId = nextIdentity.accountId;
829
+ account.accountIdSource = nextIdentity.accountIdSource;
830
+ return true;
831
+ }
722
832
  async function promptManualCallback(state) {
723
833
  if (!input.isTTY || !output.isTTY) {
724
834
  return null;
@@ -1222,6 +1332,7 @@ async function runHealthCheck(options = {}) {
1222
1332
  const probeModel = options.model?.trim() || "gpt-5-codex";
1223
1333
  const display = options.display ?? DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
1224
1334
  const quotaCache = liveProbe ? await loadQuotaCache() : null;
1335
+ const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
1225
1336
  let quotaCacheChanged = false;
1226
1337
  setStoragePath(null);
1227
1338
  const storage = await loadAccounts();
@@ -1229,6 +1340,9 @@ async function runHealthCheck(options = {}) {
1229
1340
  console.log("No accounts configured.");
1230
1341
  return;
1231
1342
  }
1343
+ let quotaEmailFallbackState = liveProbe && quotaCache
1344
+ ? buildQuotaEmailFallbackState(storage.accounts)
1345
+ : null;
1232
1346
  let changed = false;
1233
1347
  let ok = 0;
1234
1348
  let failed = 0;
@@ -1271,9 +1385,9 @@ async function runHealthCheck(options = {}) {
1271
1385
  accessToken: currentAccessToken,
1272
1386
  model: probeModel,
1273
1387
  });
1274
- if (quotaCache) {
1388
+ if (workingQuotaCache) {
1275
1389
  quotaCacheChanged =
1276
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1390
+ updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
1277
1391
  }
1278
1392
  healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
1279
1393
  }
@@ -1297,6 +1411,8 @@ async function runHealthCheck(options = {}) {
1297
1411
  if (result.type === "success") {
1298
1412
  const tokenAccountId = extractAccountId(result.access);
1299
1413
  const nextEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1414
+ const previousEmail = account.email;
1415
+ let accountIdentityChanged = false;
1300
1416
  if (account.refreshToken !== result.refresh) {
1301
1417
  account.refreshToken = result.refresh;
1302
1418
  changed = true;
@@ -1312,16 +1428,21 @@ async function runHealthCheck(options = {}) {
1312
1428
  if (nextEmail && nextEmail !== account.email) {
1313
1429
  account.email = nextEmail;
1314
1430
  changed = true;
1431
+ accountIdentityChanged = true;
1315
1432
  }
1316
- if (tokenAccountId && tokenAccountId !== account.accountId) {
1317
- account.accountId = tokenAccountId;
1318
- account.accountIdSource = "token";
1433
+ if (applyTokenAccountIdentity(account, tokenAccountId)) {
1319
1434
  changed = true;
1435
+ accountIdentityChanged = true;
1320
1436
  }
1321
1437
  if (account.enabled === false) {
1322
1438
  account.enabled = true;
1323
1439
  changed = true;
1324
1440
  }
1441
+ if (accountIdentityChanged && liveProbe && workingQuotaCache) {
1442
+ quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
1443
+ quotaCacheChanged =
1444
+ pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
1445
+ }
1325
1446
  account.lastUsed = Date.now();
1326
1447
  if (i === activeIndex) {
1327
1448
  activeAccountRefreshed = true;
@@ -1341,9 +1462,9 @@ async function runHealthCheck(options = {}) {
1341
1462
  accessToken: result.access,
1342
1463
  model: probeModel,
1343
1464
  });
1344
- if (quotaCache) {
1465
+ if (workingQuotaCache) {
1345
1466
  quotaCacheChanged =
1346
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1467
+ updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
1347
1468
  }
1348
1469
  healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
1349
1470
  }
@@ -1377,8 +1498,8 @@ async function runHealthCheck(options = {}) {
1377
1498
  if (!display.showPerAccountRows) {
1378
1499
  console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
1379
1500
  }
1380
- if (quotaCache && quotaCacheChanged) {
1381
- await saveQuotaCache(quotaCache);
1501
+ if (workingQuotaCache && quotaCacheChanged) {
1502
+ await saveQuotaCache(workingQuotaCache);
1382
1503
  }
1383
1504
  if (changed) {
1384
1505
  await saveAccounts(storage);
@@ -1413,6 +1534,21 @@ function printForecastUsage() {
1413
1534
  " --model, -m Probe model for live mode (default: gpt-5-codex)",
1414
1535
  ].join("\n"));
1415
1536
  }
1537
+ function printBestUsage() {
1538
+ console.log([
1539
+ "Usage:",
1540
+ " codex auth best [--live] [--json] [--model <model>]",
1541
+ "",
1542
+ "Options:",
1543
+ " --live, -l Probe live quota headers via Codex backend before switching",
1544
+ " --json, -j Print machine-readable JSON output",
1545
+ " --model, -m Probe model for live mode (default: gpt-5-codex)",
1546
+ "",
1547
+ "Behavior:",
1548
+ " - Chooses the healthiest account using forecast scoring",
1549
+ " - Switches to the recommended account when it is not already active",
1550
+ ].join("\n"));
1551
+ }
1416
1552
  function printFixUsage() {
1417
1553
  console.log([
1418
1554
  "Usage:",
@@ -1486,6 +1622,48 @@ function parseForecastArgs(args) {
1486
1622
  }
1487
1623
  return { ok: true, options };
1488
1624
  }
1625
+ function parseBestArgs(args) {
1626
+ const options = {
1627
+ live: false,
1628
+ json: false,
1629
+ model: "gpt-5-codex",
1630
+ modelProvided: false,
1631
+ };
1632
+ for (let i = 0; i < args.length; i += 1) {
1633
+ const arg = args[i];
1634
+ if (!arg)
1635
+ continue;
1636
+ if (arg === "--live" || arg === "-l") {
1637
+ options.live = true;
1638
+ continue;
1639
+ }
1640
+ if (arg === "--json" || arg === "-j") {
1641
+ options.json = true;
1642
+ continue;
1643
+ }
1644
+ if (arg === "--model" || arg === "-m") {
1645
+ const value = args[i + 1];
1646
+ if (!value) {
1647
+ return { ok: false, message: "Missing value for --model" };
1648
+ }
1649
+ options.model = value;
1650
+ options.modelProvided = true;
1651
+ i += 1;
1652
+ continue;
1653
+ }
1654
+ if (arg.startsWith("--model=")) {
1655
+ const value = arg.slice("--model=".length).trim();
1656
+ if (!value) {
1657
+ return { ok: false, message: "Missing value for --model" };
1658
+ }
1659
+ options.model = value;
1660
+ options.modelProvided = true;
1661
+ continue;
1662
+ }
1663
+ return { ok: false, message: `Unknown option: ${arg}` };
1664
+ }
1665
+ return { ok: true, options };
1666
+ }
1489
1667
  function parseFixArgs(args) {
1490
1668
  const options = {
1491
1669
  dryRun: false,
@@ -1698,6 +1876,7 @@ async function runForecast(args) {
1698
1876
  const options = parsedArgs.options;
1699
1877
  const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
1700
1878
  const quotaCache = options.live ? await loadQuotaCache() : null;
1879
+ const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
1701
1880
  let quotaCacheChanged = false;
1702
1881
  setStoragePath(null);
1703
1882
  const storage = await loadAccounts();
@@ -1705,6 +1884,9 @@ async function runForecast(args) {
1705
1884
  console.log("No accounts configured.");
1706
1885
  return 0;
1707
1886
  }
1887
+ const quotaEmailFallbackState = options.live && quotaCache
1888
+ ? buildQuotaEmailFallbackState(storage.accounts)
1889
+ : null;
1708
1890
  const now = Date.now();
1709
1891
  const activeIndex = resolveActiveIndex(storage, "codex");
1710
1892
  const refreshFailures = new Map();
@@ -1741,11 +1923,11 @@ async function runForecast(args) {
1741
1923
  model: options.model,
1742
1924
  });
1743
1925
  liveQuotaByIndex.set(i, liveQuota);
1744
- if (quotaCache) {
1926
+ if (workingQuotaCache) {
1745
1927
  const account = storage.accounts[i];
1746
1928
  if (account) {
1747
1929
  quotaCacheChanged =
1748
- updateQuotaCacheForAccount(quotaCache, account, liveQuota, storage.accounts) || quotaCacheChanged;
1930
+ updateQuotaCacheForAccount(workingQuotaCache, account, liveQuota, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
1749
1931
  }
1750
1932
  }
1751
1933
  }
@@ -1766,8 +1948,8 @@ async function runForecast(args) {
1766
1948
  const summary = summarizeForecast(forecastResults);
1767
1949
  const recommendation = recommendForecastAccount(forecastResults);
1768
1950
  if (options.json) {
1769
- if (quotaCache && quotaCacheChanged) {
1770
- await saveQuotaCache(quotaCache);
1951
+ if (workingQuotaCache && quotaCacheChanged) {
1952
+ await saveQuotaCache(workingQuotaCache);
1771
1953
  }
1772
1954
  console.log(JSON.stringify({
1773
1955
  command: "forecast",
@@ -1837,8 +2019,8 @@ async function runForecast(args) {
1837
2019
  console.log(` ${stylePromptText("-", "warning")} ${stylePromptText(error, "muted")}`);
1838
2020
  }
1839
2021
  }
1840
- if (quotaCache && quotaCacheChanged) {
1841
- await saveQuotaCache(quotaCache);
2022
+ if (workingQuotaCache && quotaCacheChanged) {
2023
+ await saveQuotaCache(workingQuotaCache);
1842
2024
  }
1843
2025
  return 0;
1844
2026
  }
@@ -2022,7 +2204,8 @@ function findExistingAccountIndexForFlagged(storage, flagged, nextRefreshToken,
2022
2204
  }
2023
2205
  function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2024
2206
  const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken)) ?? flagged.email;
2025
- const nextAccountId = extractAccountId(refreshResult.access) ?? flagged.accountId;
2207
+ const tokenAccountId = extractAccountId(refreshResult.access);
2208
+ const { accountId: nextAccountId, accountIdSource: nextAccountIdSource } = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2026
2209
  const existingIndex = findExistingAccountIndexForFlagged(storage, flagged, refreshResult.refresh, nextAccountId, nextEmail);
2027
2210
  if (existingIndex >= 0) {
2028
2211
  const existing = storage.accounts[existingIndex];
@@ -2046,9 +2229,11 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2046
2229
  existing.email = nextEmail;
2047
2230
  changed = true;
2048
2231
  }
2049
- if (nextAccountId && nextAccountId !== existing.accountId) {
2232
+ if (nextAccountId !== undefined &&
2233
+ ((nextAccountId !== existing.accountId)
2234
+ || (nextAccountIdSource !== existing.accountIdSource))) {
2050
2235
  existing.accountId = nextAccountId;
2051
- existing.accountIdSource = "token";
2236
+ existing.accountIdSource = nextAccountIdSource;
2052
2237
  changed = true;
2053
2238
  }
2054
2239
  if (existing.enabled === false) {
@@ -2078,7 +2263,7 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2078
2263
  accessToken: refreshResult.access,
2079
2264
  expiresAt: refreshResult.expires,
2080
2265
  accountId: nextAccountId,
2081
- accountIdSource: nextAccountId ? "token" : flagged.accountIdSource,
2266
+ accountIdSource: nextAccountIdSource,
2082
2267
  accountLabel: flagged.accountLabel,
2083
2268
  email: nextEmail,
2084
2269
  addedAt: flagged.addedAt ?? now,
@@ -2146,13 +2331,15 @@ async function runVerifyFlagged(args) {
2146
2331
  const { index: i, flagged, label, result } = check;
2147
2332
  if (result.type === "success") {
2148
2333
  if (!options.restore) {
2334
+ const tokenAccountId = extractAccountId(result.access);
2335
+ const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2149
2336
  const nextFlagged = {
2150
2337
  ...flagged,
2151
2338
  refreshToken: result.refresh,
2152
2339
  accessToken: result.access,
2153
2340
  expiresAt: result.expires,
2154
- accountId: extractAccountId(result.access) ?? flagged.accountId,
2155
- accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2341
+ accountId: nextIdentity.accountId,
2342
+ accountIdSource: nextIdentity.accountIdSource,
2156
2343
  email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2157
2344
  lastUsed: now,
2158
2345
  lastError: undefined,
@@ -2181,13 +2368,15 @@ async function runVerifyFlagged(args) {
2181
2368
  });
2182
2369
  continue;
2183
2370
  }
2371
+ const tokenAccountId = extractAccountId(result.access);
2372
+ const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
2184
2373
  const updatedFlagged = {
2185
2374
  ...flagged,
2186
2375
  refreshToken: result.refresh,
2187
2376
  accessToken: result.access,
2188
2377
  expiresAt: result.expires,
2189
- accountId: extractAccountId(result.access) ?? flagged.accountId,
2190
- accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2378
+ accountId: nextIdentity.accountId,
2379
+ accountIdSource: nextIdentity.accountIdSource,
2191
2380
  email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2192
2381
  lastUsed: now,
2193
2382
  lastError: upsertResult.message,
@@ -2317,6 +2506,7 @@ async function runFix(args) {
2317
2506
  const options = parsedArgs.options;
2318
2507
  const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
2319
2508
  const quotaCache = options.live ? await loadQuotaCache() : null;
2509
+ const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
2320
2510
  let quotaCacheChanged = false;
2321
2511
  setStoragePath(null);
2322
2512
  const storage = await loadAccounts();
@@ -2324,6 +2514,9 @@ async function runFix(args) {
2324
2514
  console.log("No accounts configured.");
2325
2515
  return 0;
2326
2516
  }
2517
+ let quotaEmailFallbackState = options.live && quotaCache
2518
+ ? buildQuotaEmailFallbackState(storage.accounts)
2519
+ : null;
2327
2520
  const now = Date.now();
2328
2521
  const activeIndex = resolveActiveIndex(storage, "codex");
2329
2522
  let changed = false;
@@ -2357,9 +2550,9 @@ async function runFix(args) {
2357
2550
  accessToken: currentAccessToken,
2358
2551
  model: options.model,
2359
2552
  });
2360
- if (quotaCache) {
2553
+ if (workingQuotaCache) {
2361
2554
  quotaCacheChanged =
2362
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2555
+ updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
2363
2556
  }
2364
2557
  reports.push({
2365
2558
  index: i,
@@ -2397,7 +2590,9 @@ async function runFix(args) {
2397
2590
  if (refreshResult.type === "success") {
2398
2591
  const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
2399
2592
  const nextAccountId = extractAccountId(refreshResult.access);
2593
+ const previousEmail = account.email;
2400
2594
  let accountChanged = false;
2595
+ let accountIdentityChanged = false;
2401
2596
  if (account.refreshToken !== refreshResult.refresh) {
2402
2597
  account.refreshToken = refreshResult.refresh;
2403
2598
  accountChanged = true;
@@ -2413,14 +2608,19 @@ async function runFix(args) {
2413
2608
  if (nextEmail && nextEmail !== account.email) {
2414
2609
  account.email = nextEmail;
2415
2610
  accountChanged = true;
2611
+ accountIdentityChanged = true;
2416
2612
  }
2417
- if (!account.accountId && nextAccountId) {
2418
- account.accountId = nextAccountId;
2419
- account.accountIdSource = "token";
2613
+ if (applyTokenAccountIdentity(account, nextAccountId)) {
2420
2614
  accountChanged = true;
2615
+ accountIdentityChanged = true;
2421
2616
  }
2422
2617
  if (accountChanged)
2423
2618
  changed = true;
2619
+ if (accountIdentityChanged && options.live && workingQuotaCache) {
2620
+ quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
2621
+ quotaCacheChanged =
2622
+ pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
2623
+ }
2424
2624
  if (options.live) {
2425
2625
  const probeAccountId = account.accountId ?? nextAccountId;
2426
2626
  if (probeAccountId) {
@@ -2430,9 +2630,9 @@ async function runFix(args) {
2430
2630
  accessToken: refreshResult.access,
2431
2631
  model: options.model,
2432
2632
  });
2433
- if (quotaCache) {
2633
+ if (workingQuotaCache) {
2434
2634
  quotaCacheChanged =
2435
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2635
+ updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
2436
2636
  }
2437
2637
  reports.push({
2438
2638
  index: i,
@@ -2521,8 +2721,8 @@ async function runFix(args) {
2521
2721
  await saveAccounts(storage);
2522
2722
  }
2523
2723
  if (options.json) {
2524
- if (quotaCache && quotaCacheChanged) {
2525
- await saveQuotaCache(quotaCache);
2724
+ if (workingQuotaCache && quotaCacheChanged) {
2725
+ await saveQuotaCache(workingQuotaCache);
2526
2726
  }
2527
2727
  console.log(JSON.stringify({
2528
2728
  command: "fix",
@@ -2588,8 +2788,8 @@ async function runFix(args) {
2588
2788
  console.log(`${stylePromptText("Note:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
2589
2789
  }
2590
2790
  }
2591
- if (quotaCache && quotaCacheChanged) {
2592
- await saveQuotaCache(quotaCache);
2791
+ if (workingQuotaCache && quotaCacheChanged) {
2792
+ await saveQuotaCache(workingQuotaCache);
2593
2793
  }
2594
2794
  if (changed && options.dryRun) {
2595
2795
  console.log(`\n${stylePromptText("Preview only: no changes were saved.", "warning")}`);
@@ -3027,10 +3227,7 @@ async function runDoctor(args) {
3027
3227
  activeAccount.expiresAt = refreshResult.expires;
3028
3228
  if (refreshedEmail)
3029
3229
  activeAccount.email = refreshedEmail;
3030
- if (refreshedAccountId) {
3031
- activeAccount.accountId = refreshedAccountId;
3032
- activeAccount.accountIdSource = "token";
3033
- }
3230
+ applyTokenAccountIdentity(activeAccount, refreshedAccountId);
3034
3231
  syncAccessToken = refreshResult.access;
3035
3232
  syncRefreshToken = refreshResult.refresh;
3036
3233
  syncExpiresAt = refreshResult.expires;
@@ -3349,6 +3546,23 @@ async function runSwitch(args) {
3349
3546
  console.error(`Account ${parsed} not found.`);
3350
3547
  return 1;
3351
3548
  }
3549
+ const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
3550
+ storage,
3551
+ targetIndex,
3552
+ parsed,
3553
+ switchReason: "rotation",
3554
+ });
3555
+ if (!synced) {
3556
+ console.warn(`Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`);
3557
+ }
3558
+ console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3559
+ return 0;
3560
+ }
3561
+ async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, }) {
3562
+ const account = storage.accounts[targetIndex];
3563
+ if (!account) {
3564
+ throw new Error(`Account ${parsed} not found.`);
3565
+ }
3352
3566
  storage.activeIndex = targetIndex;
3353
3567
  storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
3354
3568
  for (const family of MODEL_FAMILIES) {
@@ -3362,7 +3576,7 @@ async function runSwitch(args) {
3362
3576
  let syncAccessToken = account.accessToken;
3363
3577
  let syncRefreshToken = account.refreshToken;
3364
3578
  let syncExpiresAt = account.expiresAt;
3365
- let syncIdToken;
3579
+ let syncIdToken = initialSyncIdToken;
3366
3580
  if (!hasUsableAccessToken(account, switchNow)) {
3367
3581
  const refreshResult = await queuedRefresh(account.refreshToken);
3368
3582
  if (refreshResult.type === "success") {
@@ -3380,10 +3594,7 @@ async function runSwitch(args) {
3380
3594
  if (nextEmail && nextEmail !== account.email) {
3381
3595
  account.email = nextEmail;
3382
3596
  }
3383
- if (tokenAccountId && tokenAccountId !== account.accountId) {
3384
- account.accountId = tokenAccountId;
3385
- account.accountIdSource = "token";
3386
- }
3597
+ applyTokenAccountIdentity(account, tokenAccountId);
3387
3598
  syncAccessToken = refreshResult.access;
3388
3599
  syncRefreshToken = refreshResult.refresh;
3389
3600
  syncExpiresAt = refreshResult.expires;
@@ -3394,7 +3605,7 @@ async function runSwitch(args) {
3394
3605
  }
3395
3606
  }
3396
3607
  account.lastUsed = switchNow;
3397
- account.lastSwitchReason = "rotation";
3608
+ account.lastSwitchReason = switchReason;
3398
3609
  await saveAccounts(storage);
3399
3610
  const synced = await setCodexCliActiveSelection({
3400
3611
  accountId: account.accountId,
@@ -3404,10 +3615,224 @@ async function runSwitch(args) {
3404
3615
  expiresAt: syncExpiresAt,
3405
3616
  ...(syncIdToken ? { idToken: syncIdToken } : {}),
3406
3617
  });
3407
- if (!synced) {
3408
- console.warn(`Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`);
3618
+ return { synced, wasDisabled };
3619
+ }
3620
+ async function runBest(args) {
3621
+ if (args.includes("--help") || args.includes("-h")) {
3622
+ printBestUsage();
3623
+ return 0;
3624
+ }
3625
+ const parsedArgs = parseBestArgs(args);
3626
+ if (!parsedArgs.ok) {
3627
+ console.error(parsedArgs.message);
3628
+ printBestUsage();
3629
+ return 1;
3630
+ }
3631
+ const options = parsedArgs.options;
3632
+ if (options.modelProvided && !options.live) {
3633
+ console.error("--model requires --live for codex auth best");
3634
+ printBestUsage();
3635
+ return 1;
3636
+ }
3637
+ setStoragePath(null);
3638
+ const storage = await loadAccounts();
3639
+ if (!storage || storage.accounts.length === 0) {
3640
+ if (options.json) {
3641
+ console.log(JSON.stringify({ error: "No accounts configured." }, null, 2));
3642
+ }
3643
+ else {
3644
+ console.log("No accounts configured.");
3645
+ }
3646
+ return 1;
3647
+ }
3648
+ const now = Date.now();
3649
+ const refreshFailures = new Map();
3650
+ const liveQuotaByIndex = new Map();
3651
+ const probeIdTokenByIndex = new Map();
3652
+ const probeRefreshedIndices = new Set();
3653
+ const probeErrors = [];
3654
+ let changed = false;
3655
+ const printProbeNotes = () => {
3656
+ if (probeErrors.length === 0)
3657
+ return;
3658
+ console.log(`Live check notes (${probeErrors.length}):`);
3659
+ for (const error of probeErrors) {
3660
+ console.log(` - ${error}`);
3661
+ }
3662
+ };
3663
+ const persistProbeChangesIfNeeded = async () => {
3664
+ if (!changed)
3665
+ return;
3666
+ await saveAccounts(storage);
3667
+ changed = false;
3668
+ };
3669
+ for (let i = 0; i < storage.accounts.length; i += 1) {
3670
+ const account = storage.accounts[i];
3671
+ if (!account || !options.live)
3672
+ continue;
3673
+ if (account.enabled === false)
3674
+ continue;
3675
+ let probeAccessToken = account.accessToken;
3676
+ let probeAccountId = account.accountId ?? extractAccountId(account.accessToken);
3677
+ if (!hasUsableAccessToken(account, now)) {
3678
+ const refreshResult = await queuedRefresh(account.refreshToken);
3679
+ if (refreshResult.type !== "success") {
3680
+ refreshFailures.set(i, {
3681
+ ...refreshResult,
3682
+ message: normalizeFailureDetail(refreshResult.message, refreshResult.reason),
3683
+ });
3684
+ continue;
3685
+ }
3686
+ const refreshedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
3687
+ const refreshedAccountId = extractAccountId(refreshResult.access);
3688
+ if (account.refreshToken !== refreshResult.refresh) {
3689
+ account.refreshToken = refreshResult.refresh;
3690
+ changed = true;
3691
+ }
3692
+ if (account.accessToken !== refreshResult.access) {
3693
+ account.accessToken = refreshResult.access;
3694
+ changed = true;
3695
+ }
3696
+ if (account.expiresAt !== refreshResult.expires) {
3697
+ account.expiresAt = refreshResult.expires;
3698
+ changed = true;
3699
+ }
3700
+ if (refreshedEmail && refreshedEmail !== account.email) {
3701
+ account.email = refreshedEmail;
3702
+ changed = true;
3703
+ }
3704
+ if (refreshedAccountId && refreshedAccountId !== account.accountId) {
3705
+ account.accountId = refreshedAccountId;
3706
+ account.accountIdSource = "token";
3707
+ changed = true;
3708
+ }
3709
+ if (refreshResult.idToken) {
3710
+ probeIdTokenByIndex.set(i, refreshResult.idToken);
3711
+ }
3712
+ probeRefreshedIndices.add(i);
3713
+ probeAccessToken = account.accessToken;
3714
+ probeAccountId = account.accountId ?? refreshedAccountId;
3715
+ }
3716
+ if (!probeAccessToken || !probeAccountId) {
3717
+ probeErrors.push(`${formatAccountLabel(account, i)}: missing accountId for live probe`);
3718
+ continue;
3719
+ }
3720
+ try {
3721
+ const liveQuota = await fetchCodexQuotaSnapshot({
3722
+ accountId: probeAccountId,
3723
+ accessToken: probeAccessToken,
3724
+ model: options.model,
3725
+ });
3726
+ liveQuotaByIndex.set(i, liveQuota);
3727
+ }
3728
+ catch (error) {
3729
+ const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
3730
+ probeErrors.push(`${formatAccountLabel(account, i)}: ${message}`);
3731
+ }
3732
+ }
3733
+ const forecastInputs = storage.accounts.map((account, index) => ({
3734
+ index,
3735
+ account,
3736
+ isCurrent: index === resolveActiveIndex(storage, "codex"),
3737
+ now,
3738
+ refreshFailure: refreshFailures.get(index),
3739
+ liveQuota: liveQuotaByIndex.get(index),
3740
+ }));
3741
+ const forecastResults = evaluateForecastAccounts(forecastInputs);
3742
+ const recommendation = recommendForecastAccount(forecastResults);
3743
+ if (recommendation.recommendedIndex === null) {
3744
+ await persistProbeChangesIfNeeded();
3745
+ if (options.json) {
3746
+ console.log(JSON.stringify({
3747
+ error: recommendation.reason,
3748
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3749
+ }, null, 2));
3750
+ }
3751
+ else {
3752
+ console.log(`No best account available: ${recommendation.reason}`);
3753
+ printProbeNotes();
3754
+ }
3755
+ return 1;
3756
+ }
3757
+ const bestIndex = recommendation.recommendedIndex;
3758
+ const bestAccount = storage.accounts[bestIndex];
3759
+ if (!bestAccount) {
3760
+ await persistProbeChangesIfNeeded();
3761
+ if (options.json) {
3762
+ console.log(JSON.stringify({ error: "Best account not found." }, null, 2));
3763
+ }
3764
+ else {
3765
+ console.log("Best account not found.");
3766
+ }
3767
+ return 1;
3768
+ }
3769
+ // Check if already on best account
3770
+ const currentIndex = resolveActiveIndex(storage, "codex");
3771
+ if (currentIndex === bestIndex) {
3772
+ const shouldSyncCurrentBest = probeRefreshedIndices.has(bestIndex) || probeIdTokenByIndex.has(bestIndex);
3773
+ let alreadyBestSynced;
3774
+ if (changed) {
3775
+ bestAccount.lastUsed = now;
3776
+ await persistProbeChangesIfNeeded();
3777
+ }
3778
+ if (shouldSyncCurrentBest) {
3779
+ alreadyBestSynced = await setCodexCliActiveSelection({
3780
+ accountId: bestAccount.accountId,
3781
+ email: bestAccount.email,
3782
+ accessToken: bestAccount.accessToken,
3783
+ refreshToken: bestAccount.refreshToken,
3784
+ expiresAt: bestAccount.expiresAt,
3785
+ ...(probeIdTokenByIndex.has(bestIndex)
3786
+ ? { idToken: probeIdTokenByIndex.get(bestIndex) }
3787
+ : {}),
3788
+ });
3789
+ if (!alreadyBestSynced && !options.json) {
3790
+ console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
3791
+ }
3792
+ }
3793
+ if (options.json) {
3794
+ console.log(JSON.stringify({
3795
+ message: `Already on best account: ${formatAccountLabel(bestAccount, bestIndex)}`,
3796
+ accountIndex: bestIndex + 1,
3797
+ reason: recommendation.reason,
3798
+ ...(alreadyBestSynced !== undefined ? { synced: alreadyBestSynced } : {}),
3799
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3800
+ }, null, 2));
3801
+ }
3802
+ else {
3803
+ console.log(`Already on best account ${bestIndex + 1}: ${formatAccountLabel(bestAccount, bestIndex)}`);
3804
+ console.log(`Reason: ${recommendation.reason}`);
3805
+ printProbeNotes();
3806
+ }
3807
+ return 0;
3808
+ }
3809
+ const targetIndex = bestIndex;
3810
+ const parsed = targetIndex + 1;
3811
+ const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
3812
+ storage,
3813
+ targetIndex,
3814
+ parsed,
3815
+ switchReason: "best",
3816
+ initialSyncIdToken: probeIdTokenByIndex.get(bestIndex),
3817
+ });
3818
+ if (options.json) {
3819
+ console.log(JSON.stringify({
3820
+ message: `Switched to best account: ${formatAccountLabel(bestAccount, targetIndex)}`,
3821
+ accountIndex: parsed,
3822
+ reason: recommendation.reason,
3823
+ synced,
3824
+ wasDisabled,
3825
+ ...(probeErrors.length > 0 ? { probeErrors } : {}),
3826
+ }, null, 2));
3827
+ }
3828
+ else {
3829
+ console.log(`Switched to best account ${parsed}: ${formatAccountLabel(bestAccount, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3830
+ console.log(`Reason: ${recommendation.reason}`);
3831
+ printProbeNotes();
3832
+ if (!synced) {
3833
+ console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
3834
+ }
3409
3835
  }
3410
- console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3411
3836
  return 0;
3412
3837
  }
3413
3838
  export async function autoSyncActiveAccountToCodex() {
@@ -3451,9 +3876,7 @@ export async function autoSyncActiveAccountToCodex() {
3451
3876
  account.email = nextEmail;
3452
3877
  changed = true;
3453
3878
  }
3454
- if (tokenAccountId && tokenAccountId !== account.accountId) {
3455
- account.accountId = tokenAccountId;
3456
- account.accountIdSource = "token";
3879
+ if (applyTokenAccountIdentity(account, tokenAccountId)) {
3457
3880
  changed = true;
3458
3881
  }
3459
3882
  syncAccessToken = refreshResult.access;
@@ -3519,6 +3942,9 @@ export async function runCodexMultiAuthCli(rawArgs) {
3519
3942
  if (command === "forecast") {
3520
3943
  return runForecast(rest);
3521
3944
  }
3945
+ if (command === "best") {
3946
+ return runBest(rest);
3947
+ }
3522
3948
  if (command === "report") {
3523
3949
  return runReport(rest);
3524
3950
  }