opencode-openai-codex-multi-auth 4.5.6 → 4.5.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.
Files changed (39) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +281 -41
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/accounts.d.ts +8 -0
  5. package/dist/lib/accounts.d.ts.map +1 -1
  6. package/dist/lib/accounts.js +179 -20
  7. package/dist/lib/accounts.js.map +1 -1
  8. package/dist/lib/auth/auth.d.ts +5 -0
  9. package/dist/lib/auth/auth.d.ts.map +1 -1
  10. package/dist/lib/auth/auth.js +16 -0
  11. package/dist/lib/auth/auth.js.map +1 -1
  12. package/dist/lib/cli.d.ts +4 -0
  13. package/dist/lib/cli.d.ts.map +1 -1
  14. package/dist/lib/cli.js +20 -0
  15. package/dist/lib/cli.js.map +1 -1
  16. package/dist/lib/codex-status.d.ts +34 -0
  17. package/dist/lib/codex-status.d.ts.map +1 -0
  18. package/dist/lib/codex-status.js +125 -0
  19. package/dist/lib/codex-status.js.map +1 -0
  20. package/dist/lib/config.d.ts +1 -0
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/config.js +4 -0
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/formatting.d.ts +9 -0
  25. package/dist/lib/formatting.d.ts.map +1 -0
  26. package/dist/lib/formatting.js +71 -0
  27. package/dist/lib/formatting.js.map +1 -0
  28. package/dist/lib/oauth-success.html +1 -1
  29. package/dist/lib/storage-scope.d.ts +5 -0
  30. package/dist/lib/storage-scope.d.ts.map +1 -0
  31. package/dist/lib/storage-scope.js +11 -0
  32. package/dist/lib/storage-scope.js.map +1 -0
  33. package/dist/lib/storage.d.ts +39 -1
  34. package/dist/lib/storage.d.ts.map +1 -1
  35. package/dist/lib/storage.js +495 -116
  36. package/dist/lib/storage.js.map +1 -1
  37. package/dist/lib/types.d.ts +7 -0
  38. package/dist/lib/types.d.ts.map +1 -1
  39. package/package.json +8 -4
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAQ,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AA4G1E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAu5B9B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAQ,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AA+H1E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,MA+sC9B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * @repository https://github.com/numman-ali/opencode-openai-codex-auth
23
23
  */
24
24
  import { tool } from "@opencode-ai/plugin";
25
- import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
25
+ import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInputForFlow, REDIRECT_URI, } from "./lib/auth/auth.js";
26
26
  import { openBrowserUrl } from "./lib/auth/browser.js";
27
27
  import { startLocalOAuthServer } from "./lib/auth/server.js";
28
28
  import { getAccountSelectionStrategy, getCodexMode, getDefaultRetryAfterMs, getMaxBackoffMs, getMaxCacheFirstWaitSeconds, getPidOffsetEnabled, getQuietMode, getRateLimitDedupWindowMs, getRateLimitStateResetMs, getRateLimitToastDebounceMs, getRequestJitterMaxMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getSchedulingMode, getSwitchOnFirstRateLimit, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
@@ -30,14 +30,17 @@ import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, HTTP_STATUS, LOG_STAGES, PL
30
30
  import { logRequest, logDebug } from "./lib/logger.js";
31
31
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, rewriteUrlForCodex, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
32
32
  import { AccountManager, extractAccountEmail, extractAccountId, extractAccountPlan, formatAccountLabel, formatWaitTime, isOAuthAuth, needsIdentityHydration, sanitizeEmail, } from "./lib/accounts.js";
33
- import { promptAddAnotherAccount, promptLoginMode, promptManageAccounts, promptOAuthCallbackValue, } from "./lib/cli.js";
33
+ import { promptAddAnotherAccount, promptLoginMode, promptManageAccounts, promptOAuthCallbackValue, promptRepairAccounts, } from "./lib/cli.js";
34
34
  import { withTerminalModeRestored } from "./lib/terminal.js";
35
- import { getStoragePath, loadAccounts, saveAccounts, toggleAccountEnabled } from "./lib/storage.js";
35
+ import { configureStorageForCurrentCwd, configureStorageForPluginConfig, } from "./lib/storage-scope.js";
36
+ import { getStoragePath, getStorageScope, autoQuarantineCorruptAccountsFile, inspectAccountsFile, loadAccounts, quarantineAccounts, quarantineCorruptFile, replaceAccountsFile, saveAccounts, toggleAccountEnabled, writeQuarantineFile, } from "./lib/storage.js";
36
37
  import { findAccountMatchIndex } from "./lib/account-matching.js";
37
38
  import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
38
39
  import { getHealthTracker, getTokenTracker } from "./lib/rotation.js";
39
40
  import { RateLimitTracker, decideRateLimitAction, parseRateLimitReason } from "./lib/rate-limit.js";
41
+ import { codexStatus } from "./lib/codex-status.js";
40
42
  import { ProactiveRefreshQueue, createRefreshScheduler, } from "./lib/refresh-queue.js";
43
+ import { formatToastMessage } from "./lib/formatting.js";
41
44
  const RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS = 5_000;
42
45
  const AUTH_FAILURE_COOLDOWN_MS = 60_000;
43
46
  const MAX_ACCOUNTS = 10;
@@ -83,22 +86,26 @@ function parseRetryAfterMs(headers) {
83
86
  export const OpenAIAuthPlugin = async ({ client }) => {
84
87
  let cachedAccountManager = null;
85
88
  let proactiveRefreshScheduler = null;
89
+ configureStorageForPluginConfig(loadPluginConfig(), process.cwd());
86
90
  const showToast = async (message, variant = "info", quietMode = false) => {
87
91
  if (quietMode)
88
92
  return;
89
93
  try {
90
- await client.tui.showToast({ body: { message, variant } });
94
+ await client.tui.showToast({ body: { message: formatToastMessage(message), variant } });
91
95
  }
92
96
  catch {
93
97
  // ignore (non-TUI contexts)
94
98
  }
95
99
  };
96
- const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
100
+ const buildManualOAuthFlow = (pkce, expectedState, url, onSuccess) => ({
97
101
  url,
98
102
  method: "code",
99
103
  instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
100
104
  callback: async (input) => {
101
- const parsed = parseAuthorizationInput(input);
105
+ const parsed = parseAuthorizationInputForFlow(input, expectedState);
106
+ if (parsed.stateStatus === "mismatch") {
107
+ return { type: "failed" };
108
+ }
102
109
  if (!parsed.code) {
103
110
  return { type: "failed" };
104
111
  }
@@ -181,9 +188,16 @@ export const OpenAIAuthPlugin = async ({ client }) => {
181
188
  if (!isOAuthAuth(auth)) {
182
189
  return {};
183
190
  }
191
+ const pluginConfig = loadPluginConfig();
192
+ configureStorageForPluginConfig(pluginConfig, process.cwd());
193
+ const quietMode = getQuietMode(pluginConfig);
184
194
  const accountManager = await AccountManager.loadFromDisk(auth);
185
195
  cachedAccountManager = accountManager;
186
196
  if (accountManager.getAccountCount() === 0) {
197
+ const quarantinePath = await autoQuarantineCorruptAccountsFile();
198
+ if (quarantinePath) {
199
+ await showToast("Accounts file was corrupted and has been quarantined. Run `opencode auth login`.", "warning", quietMode);
200
+ }
187
201
  logDebug(`[${PLUGIN_NAME}] No OAuth accounts available (run opencode auth login)`);
188
202
  return {};
189
203
  }
@@ -193,11 +207,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
193
207
  global: providerConfig?.options || {},
194
208
  models: providerConfig?.models || {},
195
209
  };
196
- const pluginConfig = loadPluginConfig();
197
210
  const codexMode = getCodexMode(pluginConfig);
198
211
  const accountSelectionStrategy = getAccountSelectionStrategy(pluginConfig);
199
212
  const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
200
- const quietMode = getQuietMode(pluginConfig);
201
213
  const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
202
214
  const proactiveRefreshEnabled = (() => {
203
215
  const rawConfig = pluginConfig;
@@ -236,6 +248,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
236
248
  getTasks: () => {
237
249
  const tasks = [];
238
250
  for (const account of accountManager.getAccountsSnapshot()) {
251
+ if (account.enabled === false)
252
+ continue;
239
253
  if (!Number.isFinite(account.expires))
240
254
  continue;
241
255
  tasks.push({
@@ -243,7 +257,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
243
257
  expires: account.expires ?? 0,
244
258
  refresh: async () => {
245
259
  const live = accountManager.getAccountByIndex(account.index);
246
- if (!live)
260
+ if (!live || live.enabled === false)
247
261
  return { type: "failed" };
248
262
  const refreshed = await accountManager.refreshAccountWithFallback(live);
249
263
  if (refreshed.type !== "success")
@@ -327,8 +341,36 @@ export const OpenAIAuthPlugin = async ({ client }) => {
327
341
  abortSignal?.addEventListener("abort", onAbort, { once: true });
328
342
  });
329
343
  let allRateLimitedRetries = 0;
344
+ let autoRepairAttempted = false;
330
345
  while (true) {
331
346
  const accountCount = accountManager.getAccountCount();
347
+ if (!autoRepairAttempted && accountCount === 0) {
348
+ const legacyAccounts = accountManager.getLegacyAccounts();
349
+ if (legacyAccounts.length > 0) {
350
+ autoRepairAttempted = true;
351
+ const repair = await accountManager.repairLegacyAccounts();
352
+ const snapshot = accountManager.getStorageSnapshot();
353
+ let quarantinePath = null;
354
+ if (repair.quarantined.length > 0) {
355
+ const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
356
+ const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
357
+ const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-auto-repair-failed");
358
+ quarantinePath = quarantineResult.quarantinePath;
359
+ accountManager.removeAccountsByRefreshToken(quarantinedTokens);
360
+ }
361
+ else {
362
+ await replaceAccountsFile(snapshot);
363
+ }
364
+ if (repair.quarantined.length > 0 && quarantinePath) {
365
+ logDebug(`[${PLUGIN_NAME}] Auto-repair quarantined: ${quarantinePath}`);
366
+ await showToast(`Auto-repair failed for ${repair.quarantined.length} account(s).`, "warning", quietMode);
367
+ }
368
+ else if (repair.repaired.length > 0) {
369
+ await showToast(`Auto-repaired ${repair.repaired.length} account(s).`, "success", quietMode);
370
+ }
371
+ continue;
372
+ }
373
+ }
332
374
  const attempted = new Set();
333
375
  while (attempted.size < Math.max(1, accountCount)) {
334
376
  const account = accountManager.getCurrentOrNextForFamily(modelFamily, model, accountSelectionStrategy, usePidOffset);
@@ -407,6 +449,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
407
449
  let res;
408
450
  try {
409
451
  res = await fetch(url, { ...requestInit, headers });
452
+ // Update Codex rate limit snapshot from response headers
453
+ const codexHeaders = {};
454
+ res.headers.forEach((val, key) => {
455
+ if (key.toLowerCase().startsWith("x-codex-")) {
456
+ codexHeaders[key] = val;
457
+ }
458
+ });
459
+ codexStatus.updateFromHeaders(account, codexHeaders);
410
460
  }
411
461
  catch (err) {
412
462
  if (tokenConsumed) {
@@ -497,12 +547,35 @@ export const OpenAIAuthPlugin = async ({ client }) => {
497
547
  await sleep(waitMs);
498
548
  continue;
499
549
  }
500
- const storePath = getStoragePath();
501
- const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
502
- const message = accountManager.getAccountCount() === 0
503
- ? "No OpenAI accounts configured. Run `opencode auth login`."
504
- : `All ${accountManager.getAccountCount()} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`. (Storage: ${storePath})`;
505
- return new Response(JSON.stringify({ error: { message } }), {
550
+ // Build detailed inline error message
551
+ const now = Date.now();
552
+ const accounts = accountManager.getAccountsSnapshot();
553
+ const statusLines = [
554
+ `All ${accountManager.getAccountCount()} account(s) are currently unavailable.`,
555
+ `Next reset in approximately ${formatWaitTime(waitMs)}.`,
556
+ "",
557
+ "Account Status:",
558
+ ];
559
+ accounts.forEach((acc, idx) => {
560
+ if (acc.enabled === false)
561
+ return;
562
+ const label = formatAccountLabel(acc, idx);
563
+ const rateLimited = acc.rateLimitResetTimes &&
564
+ Object.values(acc.rateLimitResetTimes).some((t) => typeof t === "number" && t > now);
565
+ const coolingDown = typeof acc.coolingDownUntil === "number" && acc.coolingDownUntil > now;
566
+ let status = "ok";
567
+ if (rateLimited)
568
+ status = "rate-limited";
569
+ else if (coolingDown)
570
+ status = "cooldown";
571
+ statusLines.push(`- ${label} [${status}]`);
572
+ const codexLines = codexStatus.renderStatus(acc);
573
+ if (codexLines.length > 0 && !codexLines[0]?.includes("No Codex status")) {
574
+ statusLines.push(...codexLines.map((l) => " " + l.trim()));
575
+ }
576
+ });
577
+ statusLines.push("", "Run `opencode auth login` to add more accounts.");
578
+ return new Response(JSON.stringify({ error: { message: statusLines.join("\n") } }), {
506
579
  status: 429,
507
580
  headers: { "content-type": "application/json; charset=utf-8" },
508
581
  });
@@ -528,8 +601,75 @@ export const OpenAIAuthPlugin = async ({ client }) => {
528
601
  */
529
602
  authorize: async (inputs) => {
530
603
  const pluginConfig = loadPluginConfig();
604
+ configureStorageForPluginConfig(pluginConfig, process.cwd());
531
605
  const quietMode = getQuietMode(pluginConfig);
532
606
  const isCliFlow = Boolean(inputs);
607
+ const notifyRepairResult = async (message) => {
608
+ if (isCliFlow) {
609
+ console.log(`\n${message}\n`);
610
+ return;
611
+ }
612
+ await showToast(message, "info", quietMode);
613
+ };
614
+ const maybeRepairAccounts = async () => {
615
+ const inspection = await inspectAccountsFile();
616
+ if (inspection.status === "missing" || inspection.status === "ok") {
617
+ return null;
618
+ }
619
+ const corruptCount = inspection.status === "corrupt-file"
620
+ ? 1
621
+ : inspection.corruptEntries.length;
622
+ const legacyCount = inspection.status === "needs-repair" ? inspection.legacyEntries.length : 0;
623
+ const shouldRepair = await promptRepairAccounts({
624
+ legacyCount,
625
+ corruptCount,
626
+ });
627
+ if (!shouldRepair)
628
+ return null;
629
+ const quarantinePaths = [];
630
+ if (inspection.status === "corrupt-file") {
631
+ const quarantinePath = await quarantineCorruptFile();
632
+ if (quarantinePath) {
633
+ quarantinePaths.push(quarantinePath);
634
+ logDebug(`[${PLUGIN_NAME}] Accounts file quarantined: ${quarantinePath}`);
635
+ await notifyRepairResult("Accounts file was corrupted and quarantined.");
636
+ }
637
+ return await loadAccounts();
638
+ }
639
+ if (inspection.corruptEntries.length > 0) {
640
+ const quarantinePath = await writeQuarantineFile(inspection.corruptEntries, "corrupt-entry");
641
+ quarantinePaths.push(quarantinePath);
642
+ }
643
+ const storage = await loadAccounts();
644
+ if (!storage) {
645
+ await notifyRepairResult("Repair skipped: no valid accounts found.");
646
+ return null;
647
+ }
648
+ const manager = new AccountManager(undefined, storage);
649
+ const repair = await manager.repairLegacyAccounts();
650
+ const snapshot = manager.getStorageSnapshot();
651
+ let updatedStorage = snapshot;
652
+ if (repair.quarantined.length > 0) {
653
+ const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
654
+ const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
655
+ const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-repair-failed");
656
+ updatedStorage = quarantineResult.storage;
657
+ quarantinePaths.push(quarantineResult.quarantinePath);
658
+ }
659
+ else {
660
+ await replaceAccountsFile(snapshot);
661
+ }
662
+ const summaryParts = [
663
+ `Repaired ${repair.repaired.length}`,
664
+ `quarantined ${repair.quarantined.length}`,
665
+ ];
666
+ if (quarantinePaths.length > 0) {
667
+ logDebug(`[${PLUGIN_NAME}] Repair quarantine paths: ${quarantinePaths.join(", ")}`);
668
+ }
669
+ await notifyRepairResult(`Account repair complete. ${summaryParts.join(", ")}.`);
670
+ return updatedStorage;
671
+ };
672
+ const repairedStorage = await maybeRepairAccounts();
533
673
  // CLI flow (`opencode auth login`) passes inputs; TUI does not.
534
674
  if (isCliFlow) {
535
675
  debugAuth("[OAuthAuthorize] Starting OAuth flow in CLI mode");
@@ -541,8 +681,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
541
681
  const { pkce, state, url } = await createAuthorizationFlow();
542
682
  console.log("\nOAuth URL:\n" + url + "\n");
543
683
  if (noBrowser) {
544
- const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
545
- const parsed = parseAuthorizationInput(callbackInput);
684
+ const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
685
+ const parsed = parseAuthorizationInputForFlow(callbackInput, state);
686
+ if (parsed.stateStatus === "mismatch") {
687
+ console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
688
+ return { type: "failed" };
689
+ }
690
+ if (parsed.stateStatus === "missing") {
691
+ console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
692
+ }
546
693
  if (!parsed.code)
547
694
  return { type: "failed" };
548
695
  return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
@@ -557,8 +704,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
557
704
  openBrowserUrl(url);
558
705
  if (!serverInfo || !serverInfo.ready) {
559
706
  serverInfo?.close();
560
- const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
561
- const parsed = parseAuthorizationInput(callbackInput);
707
+ const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
708
+ const parsed = parseAuthorizationInputForFlow(callbackInput, state);
709
+ if (parsed.stateStatus === "mismatch") {
710
+ console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
711
+ return { type: "failed" };
712
+ }
713
+ if (parsed.stateStatus === "missing") {
714
+ console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
715
+ }
562
716
  if (!parsed.code)
563
717
  return { type: "failed" };
564
718
  return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
@@ -571,7 +725,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
571
725
  };
572
726
  const authenticated = [];
573
727
  let startFresh = true;
574
- let existingStorage = await loadAccounts();
728
+ let existingStorage = repairedStorage ?? (await loadAccounts());
575
729
  if (existingStorage && existingStorage.accounts.length > 0) {
576
730
  const needsHydration = needsIdentityHydration(existingStorage.accounts);
577
731
  if (needsHydration) {
@@ -609,7 +763,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
609
763
  break;
610
764
  const toggled = toggleAccountEnabled(updatedStorage, toggleIndex);
611
765
  if (toggled) {
612
- await saveAccounts(toggled);
766
+ await saveAccounts(toggled, { preserveRefreshTokens: true });
613
767
  updatedStorage = toggled;
614
768
  }
615
769
  }
@@ -631,7 +785,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
631
785
  accounts: [],
632
786
  activeIndex: 0,
633
787
  activeIndexByFamily: {},
634
- });
788
+ }, { replace: true });
635
789
  }
636
790
  while (authenticated.length < MAX_ACCOUNTS) {
637
791
  console.log(`\n=== OpenAI OAuth (Account ${authenticated.length + 1}) ===`);
@@ -684,7 +838,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
684
838
  process.env.SSH_TTY ||
685
839
  process.env.OPENCODE_HEADLESS);
686
840
  const useManualFlow = isHeadless || process.env.OPENCODE_NO_BROWSER === "1";
687
- const existingStorage = await loadAccounts();
841
+ const existingStorage = repairedStorage ?? (await loadAccounts());
688
842
  const existingCount = existingStorage?.accounts.length ?? 0;
689
843
  const { pkce, state, url } = await createAuthorizationFlow();
690
844
  let serverInfo = null;
@@ -730,10 +884,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
730
884
  serverInfo?.close();
731
885
  return {
732
886
  url,
733
- instructions: "Visit the URL above, complete OAuth, then paste either the full redirect URL or the authorization code.",
887
+ instructions: "Visit the URL above, complete OAuth, then paste the full redirect URL (recommended) or the authorization code.",
734
888
  method: "code",
735
889
  callback: async (input) => {
736
- const parsed = parseAuthorizationInput(input);
890
+ const parsed = parseAuthorizationInputForFlow(input, state);
891
+ if (parsed.stateStatus === "mismatch") {
892
+ await showToast("OAuth state mismatch. Paste the redirect URL from this login session.", "error", quietMode);
893
+ return { type: "failed" };
894
+ }
737
895
  if (!parsed.code)
738
896
  return { type: "failed" };
739
897
  const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
@@ -759,8 +917,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
759
917
  label: AUTH_LABELS.OAUTH_MANUAL,
760
918
  type: "oauth",
761
919
  authorize: async () => {
762
- const { pkce, url } = await createAuthorizationFlow();
763
- return buildManualOAuthFlow(pkce, url, async (tokens) => {
920
+ const { pkce, state, url } = await createAuthorizationFlow();
921
+ return buildManualOAuthFlow(pkce, state, url, async (tokens) => {
764
922
  await persistAccount(tokens);
765
923
  });
766
924
  },
@@ -776,27 +934,39 @@ export const OpenAIAuthPlugin = async ({ client }) => {
776
934
  description: "List all configured OpenAI OAuth accounts.",
777
935
  args: {},
778
936
  async execute() {
937
+ configureStorageForCurrentCwd();
779
938
  const storage = await loadAccounts();
780
- const storePath = getStoragePath();
939
+ const { scope, storagePath } = getStorageScope();
940
+ const scopeLabel = scope === "project" ? "project" : "global";
781
941
  if (!storage || storage.accounts.length === 0) {
782
942
  return [
783
- "No OpenAI accounts configured.",
784
- "",
785
- "Add accounts:",
786
- " opencode auth login",
787
- "",
788
- `Storage: ${storePath}`,
943
+ `OpenAI Codex Status`,
944
+ ``,
945
+ ` Scope: ${scopeLabel}`,
946
+ ` Accounts: 0`,
947
+ ``,
948
+ `Add accounts:`,
949
+ ` opencode auth login`,
950
+ ``,
951
+ `Storage: ${storagePath}`,
789
952
  ].join("\n");
790
953
  }
791
954
  const activeIndex = typeof storage.activeIndex === "number" && Number.isFinite(storage.activeIndex)
792
955
  ? storage.activeIndex
793
956
  : 0;
794
957
  const now = Date.now();
958
+ const enabledCount = storage.accounts.filter((a) => a.enabled !== false).length;
959
+ const rateLimitedCount = storage.accounts.filter((a) => a.rateLimitResetTimes &&
960
+ Object.values(a.rateLimitResetTimes).some((t) => typeof t === "number" && t > now)).length;
795
961
  const lines = [
796
- `OpenAI Accounts (${storage.accounts.length}):`,
797
- "",
798
- " # Label Status",
799
- "----------------------------------------------- ---------------------",
962
+ `OpenAI Codex Status`,
963
+ ``,
964
+ ` Scope: ${scopeLabel}`,
965
+ ` Accounts: ${enabledCount}/${storage.accounts.length} enabled`,
966
+ ...(rateLimitedCount > 0 ? [` Rate-limited: ${rateLimitedCount}`] : []),
967
+ ``,
968
+ ` # Label Status`,
969
+ `--- ----------------------------------------- ---------------------`,
800
970
  ];
801
971
  storage.accounts.forEach((account, index) => {
802
972
  const label = formatAccountLabel(account, index);
@@ -813,9 +983,47 @@ export const OpenAIAuthPlugin = async ({ client }) => {
813
983
  account.coolingDownUntil > now) {
814
984
  statuses.push("cooldown");
815
985
  }
816
- lines.push(`${String(index + 1).padEnd(3)} ${label.padEnd(40)} ${statuses.length > 0 ? statuses.join(", ") : "ok"}`);
986
+ lines.push(`${String(index + 1).padEnd(3)} ${label.padEnd(41)} ${statuses.length > 0 ? statuses.join(", ") : "ok"}`);
987
+ // Add Codex status details
988
+ const codexLines = codexStatus.renderStatus(account);
989
+ lines.push(...codexLines);
990
+ lines.push(""); // Spacer between accounts
991
+ });
992
+ lines.push(`Storage: ${storagePath}`);
993
+ return lines.join("\n");
994
+ },
995
+ }),
996
+ "status-codex": tool({
997
+ description: "Show a compact inline status of all OpenAI Codex accounts.",
998
+ args: {},
999
+ async execute() {
1000
+ configureStorageForCurrentCwd();
1001
+ const storage = await loadAccounts();
1002
+ if (!storage || storage.accounts.length === 0) {
1003
+ return "No OpenAI accounts configured. Run `opencode auth login`.";
1004
+ }
1005
+ const activeIndex = storage.activeIndex ?? 0;
1006
+ const now = Date.now();
1007
+ const lines = ["OpenAI Codex Accounts Status:"];
1008
+ storage.accounts.forEach((account, index) => {
1009
+ const label = formatAccountLabel(account, index);
1010
+ const rateLimited = account.rateLimitResetTimes &&
1011
+ Object.values(account.rateLimitResetTimes).some((t) => typeof t === "number" && t > now);
1012
+ const coolingDown = typeof account.coolingDownUntil === "number" && account.coolingDownUntil > now;
1013
+ const disabled = account.enabled === false;
1014
+ let status = "ok";
1015
+ if (disabled)
1016
+ status = "disabled";
1017
+ else if (rateLimited)
1018
+ status = "rate-limited";
1019
+ else if (coolingDown)
1020
+ status = "cooldown";
1021
+ lines.push(`${index === activeIndex ? "*" : "-"} ${label} [${status}]`);
1022
+ const codexLines = codexStatus.renderStatus(account);
1023
+ if (codexLines.length > 0 && !codexLines[0]?.includes("No Codex status")) {
1024
+ lines.push(...codexLines);
1025
+ }
817
1026
  });
818
- lines.push("", `Storage: ${storePath}`);
819
1027
  return lines.join("\n");
820
1028
  },
821
1029
  }),
@@ -825,6 +1033,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
825
1033
  index: tool.schema.number().describe("Account number (1-based)"),
826
1034
  },
827
1035
  async execute({ index }) {
1036
+ configureStorageForCurrentCwd();
828
1037
  const storage = await loadAccounts();
829
1038
  if (!storage || storage.accounts.length === 0) {
830
1039
  return "No OpenAI accounts configured. Run: opencode auth login";
@@ -843,7 +1052,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
843
1052
  account.lastUsed = Date.now();
844
1053
  account.lastSwitchReason = "rotation";
845
1054
  }
846
- await saveAccounts(storage);
1055
+ await saveAccounts(storage, { preserveRefreshTokens: true });
847
1056
  if (cachedAccountManager) {
848
1057
  cachedAccountManager.setActiveIndex(targetIndex);
849
1058
  await cachedAccountManager.saveToDisk();
@@ -851,6 +1060,37 @@ export const OpenAIAuthPlugin = async ({ client }) => {
851
1060
  return `Switched to ${formatAccountLabel(account, targetIndex)}`;
852
1061
  },
853
1062
  }),
1063
+ "openai-accounts-toggle": tool({
1064
+ description: "Enable or disable an OpenAI account by index (1-based).",
1065
+ args: {
1066
+ index: tool.schema.number().describe("Account number (1-based)"),
1067
+ },
1068
+ async execute({ index }) {
1069
+ configureStorageForCurrentCwd();
1070
+ const storage = await loadAccounts();
1071
+ if (!storage || storage.accounts.length === 0) {
1072
+ return "No OpenAI accounts configured. Run: opencode auth login";
1073
+ }
1074
+ const targetIndex = Math.floor((index ?? 0) - 1);
1075
+ if (targetIndex < 0 || targetIndex >= storage.accounts.length) {
1076
+ return `Invalid account number: ${index}\nValid range: 1-${storage.accounts.length}`;
1077
+ }
1078
+ const updated = toggleAccountEnabled(storage, targetIndex);
1079
+ if (!updated) {
1080
+ return `Failed to toggle account number: ${index}`;
1081
+ }
1082
+ await saveAccounts(updated, { preserveRefreshTokens: true });
1083
+ const account = updated.accounts[targetIndex];
1084
+ if (cachedAccountManager) {
1085
+ const live = cachedAccountManager.getAccountByIndex(targetIndex);
1086
+ if (live)
1087
+ live.enabled = account?.enabled !== false;
1088
+ }
1089
+ const enabled = account?.enabled !== false;
1090
+ const verb = enabled ? "Enabled" : "Disabled";
1091
+ return `${verb} ${formatAccountLabel(account, targetIndex)} (${targetIndex + 1}/${updated.accounts.length})`;
1092
+ },
1093
+ }),
854
1094
  },
855
1095
  };
856
1096
  };