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.
- package/README.md +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -63
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +29 -10
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +583 -235
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/entitlement-cache.d.ts +8 -5
- package/dist/lib/entitlement-cache.d.ts.map +1 -1
- package/dist/lib/entitlement-cache.js +24 -9
- package/dist/lib/entitlement-cache.js.map +1 -1
- package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -1
- package/dist/lib/oc-chatgpt-import-adapter.js +49 -33
- package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -1
- package/dist/lib/schemas.d.ts +7 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +1 -1
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +2 -2
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +25 -13
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +307 -144
- package/dist/lib/storage.js.map +1 -1
- package/package.json +132 -132
- package/scripts/codex-routing.js +1 -0
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
1062
|
-
? null
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
:
|
|
1129
|
-
|
|
1130
|
-
|
|
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 (
|
|
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
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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
|
|
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
|
|
2136
|
+
if (nextAccountId !== undefined &&
|
|
2137
|
+
((nextAccountId !== existing.accountId)
|
|
2138
|
+
|| (nextAccountIdSource !== existing.accountIdSource))) {
|
|
2056
2139
|
existing.accountId = nextAccountId;
|
|
2057
|
-
existing.accountIdSource =
|
|
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:
|
|
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
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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:
|
|
2155
|
-
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:
|
|
2286
|
+
lastError: upsertResult.message,
|
|
2159
2287
|
};
|
|
2160
|
-
nextFlaggedAccounts.push(
|
|
2161
|
-
if (JSON.stringify(
|
|
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: "
|
|
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
|
|
2300
|
+
const detail = normalizeFailureDetail(result.message, result.reason);
|
|
2301
|
+
const failedFlagged = {
|
|
2185
2302
|
...flagged,
|
|
2186
|
-
|
|
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(
|
|
2196
|
-
if (
|
|
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: "
|
|
2203
|
-
message:
|
|
2312
|
+
outcome: "still-flagged",
|
|
2313
|
+
message: detail,
|
|
2204
2314
|
});
|
|
2205
|
-
continue;
|
|
2206
2315
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3379
|
-
|
|
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 (
|
|
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
|
}
|