oc-codex-multi-auth 6.19.0 → 6.21.0

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 (133) hide show
  1. package/README.md +15 -12
  2. package/config/README.md +8 -8
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +473 -145
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-display.d.ts.map +1 -1
  7. package/dist/lib/account-display.js +3 -1
  8. package/dist/lib/account-display.js.map +1 -1
  9. package/dist/lib/accounts/persistence.d.ts +25 -1
  10. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  11. package/dist/lib/accounts/persistence.js +105 -2
  12. package/dist/lib/accounts/persistence.js.map +1 -1
  13. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  14. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  15. package/dist/lib/accounts/rate-limits.js +25 -0
  16. package/dist/lib/accounts/rate-limits.js.map +1 -1
  17. package/dist/lib/accounts/rotation.d.ts +22 -23
  18. package/dist/lib/accounts/rotation.d.ts.map +1 -1
  19. package/dist/lib/accounts/rotation.js +62 -37
  20. package/dist/lib/accounts/rotation.js.map +1 -1
  21. package/dist/lib/accounts/stale-state.d.ts +32 -0
  22. package/dist/lib/accounts/stale-state.d.ts.map +1 -1
  23. package/dist/lib/accounts/stale-state.js +86 -1
  24. package/dist/lib/accounts/stale-state.js.map +1 -1
  25. package/dist/lib/accounts/state.d.ts +6 -0
  26. package/dist/lib/accounts/state.d.ts.map +1 -1
  27. package/dist/lib/accounts/state.js +15 -1
  28. package/dist/lib/accounts/state.js.map +1 -1
  29. package/dist/lib/accounts/warm-recovery.d.ts +8 -0
  30. package/dist/lib/accounts/warm-recovery.d.ts.map +1 -0
  31. package/dist/lib/accounts/warm-recovery.js +70 -0
  32. package/dist/lib/accounts/warm-recovery.js.map +1 -0
  33. package/dist/lib/accounts/warm-request.d.ts +2 -0
  34. package/dist/lib/accounts/warm-request.d.ts.map +1 -1
  35. package/dist/lib/accounts/warm-request.js +4 -2
  36. package/dist/lib/accounts/warm-request.js.map +1 -1
  37. package/dist/lib/accounts.d.ts +1 -1
  38. package/dist/lib/accounts.d.ts.map +1 -1
  39. package/dist/lib/accounts.js +2 -2
  40. package/dist/lib/accounts.js.map +1 -1
  41. package/dist/lib/auth/login-runner.d.ts +3 -0
  42. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  43. package/dist/lib/auth/login-runner.js +35 -0
  44. package/dist/lib/auth/login-runner.js.map +1 -1
  45. package/dist/lib/circuit-breaker.d.ts +5 -0
  46. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  47. package/dist/lib/circuit-breaker.js +23 -6
  48. package/dist/lib/circuit-breaker.js.map +1 -1
  49. package/dist/lib/codex-reset.d.ts +7 -1
  50. package/dist/lib/codex-reset.d.ts.map +1 -1
  51. package/dist/lib/codex-reset.js +9 -2
  52. package/dist/lib/codex-reset.js.map +1 -1
  53. package/dist/lib/codex-usage.d.ts +16 -6
  54. package/dist/lib/codex-usage.d.ts.map +1 -1
  55. package/dist/lib/codex-usage.js +68 -23
  56. package/dist/lib/codex-usage.js.map +1 -1
  57. package/dist/lib/config.d.ts +2 -1
  58. package/dist/lib/config.d.ts.map +1 -1
  59. package/dist/lib/config.js +38 -14
  60. package/dist/lib/config.js.map +1 -1
  61. package/dist/lib/parallel-probe.d.ts.map +1 -1
  62. package/dist/lib/parallel-probe.js +5 -2
  63. package/dist/lib/parallel-probe.js.map +1 -1
  64. package/dist/lib/quota-notifications.d.ts.map +1 -1
  65. package/dist/lib/quota-notifications.js +10 -1
  66. package/dist/lib/quota-notifications.js.map +1 -1
  67. package/dist/lib/request/fetch-helpers.d.ts +24 -0
  68. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  69. package/dist/lib/request/fetch-helpers.js +88 -38
  70. package/dist/lib/request/fetch-helpers.js.map +1 -1
  71. package/dist/lib/request/response-handler.d.ts.map +1 -1
  72. package/dist/lib/request/response-handler.js +85 -43
  73. package/dist/lib/request/response-handler.js.map +1 -1
  74. package/dist/lib/rotation.d.ts +1 -1
  75. package/dist/lib/schemas.d.ts +9 -0
  76. package/dist/lib/schemas.d.ts.map +1 -1
  77. package/dist/lib/schemas.js +3 -0
  78. package/dist/lib/schemas.js.map +1 -1
  79. package/dist/lib/storage/flagged.d.ts.map +1 -1
  80. package/dist/lib/storage/flagged.js +5 -0
  81. package/dist/lib/storage/flagged.js.map +1 -1
  82. package/dist/lib/storage/load-save.d.ts +1 -0
  83. package/dist/lib/storage/load-save.d.ts.map +1 -1
  84. package/dist/lib/storage/load-save.js +11 -0
  85. package/dist/lib/storage/load-save.js.map +1 -1
  86. package/dist/lib/storage/migrations.d.ts +21 -0
  87. package/dist/lib/storage/migrations.d.ts.map +1 -1
  88. package/dist/lib/storage/migrations.js.map +1 -1
  89. package/dist/lib/storage/normalize.d.ts.map +1 -1
  90. package/dist/lib/storage/normalize.js +93 -1
  91. package/dist/lib/storage/normalize.js.map +1 -1
  92. package/dist/lib/storage/state.d.ts +1 -0
  93. package/dist/lib/storage/state.d.ts.map +1 -1
  94. package/dist/lib/storage/state.js +12 -0
  95. package/dist/lib/storage/state.js.map +1 -1
  96. package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
  97. package/dist/lib/tools/codex-doctor.js +8 -66
  98. package/dist/lib/tools/codex-doctor.js.map +1 -1
  99. package/dist/lib/tools/codex-health.d.ts.map +1 -1
  100. package/dist/lib/tools/codex-health.js +7 -1
  101. package/dist/lib/tools/codex-health.js.map +1 -1
  102. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  103. package/dist/lib/tools/codex-limits.js +13 -1
  104. package/dist/lib/tools/codex-limits.js.map +1 -1
  105. package/dist/lib/tools/codex-list.d.ts.map +1 -1
  106. package/dist/lib/tools/codex-list.js +14 -1
  107. package/dist/lib/tools/codex-list.js.map +1 -1
  108. package/dist/lib/tools/codex-reset.d.ts.map +1 -1
  109. package/dist/lib/tools/codex-reset.js +40 -1
  110. package/dist/lib/tools/codex-reset.js.map +1 -1
  111. package/dist/lib/tools/codex-status.d.ts.map +1 -1
  112. package/dist/lib/tools/codex-status.js +6 -1
  113. package/dist/lib/tools/codex-status.js.map +1 -1
  114. package/dist/lib/tools/codex-warm.d.ts +2 -1
  115. package/dist/lib/tools/codex-warm.d.ts.map +1 -1
  116. package/dist/lib/tools/codex-warm.js +48 -5
  117. package/dist/lib/tools/codex-warm.js.map +1 -1
  118. package/dist/lib/tools/doctor-repair.d.ts +10 -0
  119. package/dist/lib/tools/doctor-repair.d.ts.map +1 -0
  120. package/dist/lib/tools/doctor-repair.js +86 -0
  121. package/dist/lib/tools/doctor-repair.js.map +1 -0
  122. package/dist/lib/tools/index.d.ts +4 -1
  123. package/dist/lib/tools/index.d.ts.map +1 -1
  124. package/dist/lib/tools/index.js.map +1 -1
  125. package/dist/lib/tui-quota-cache.d.ts.map +1 -1
  126. package/dist/lib/tui-quota-cache.js +3 -1
  127. package/dist/lib/tui-quota-cache.js.map +1 -1
  128. package/dist/tui.d.ts +4 -1
  129. package/dist/tui.d.ts.map +1 -1
  130. package/dist/tui.js +24 -14
  131. package/dist/tui.js.map +1 -1
  132. package/package.json +1 -1
  133. package/scripts/install-oc-codex-multi-auth-core.js +168 -11
@@ -257,8 +257,28 @@ async function readStandaloneStorage(path) {
257
257
  try {
258
258
  const raw = await readFile(path, "utf-8");
259
259
  const parsed = JSON.parse(raw);
260
+ // Shape validation, not just parse validation: a JSON array, scalar, or
261
+ // object without an `accounts` array is unreadable by the plugin runtime
262
+ // too (normalizeAccountStorage rejects it), so reporting it as a healthy
263
+ // empty pool (exit 0, "No accounts configured") hides the corruption
264
+ // from scripted callers that key on exit codes.
265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
266
+ return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
267
+ }
268
+ // Forward-compat mirror of the runtime guard: a newer schema version
269
+ // must not be shown as readable accounts by this build.
270
+ const version = parsed.version;
271
+ if (typeof version === "number" && Number.isFinite(version) && version > 3) {
272
+ return {
273
+ storage: null,
274
+ error: `Unsupported account storage schema version ${version}; this build supports up to version 3.`,
275
+ };
276
+ }
277
+ if (!Array.isArray(parsed.accounts)) {
278
+ return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
279
+ }
260
280
  return {
261
- storage: parsed && typeof parsed === "object" ? normalizeStandaloneStorage(parsed) : null,
281
+ storage: normalizeStandaloneStorage(parsed),
262
282
  error: null,
263
283
  };
264
284
  } catch (error) {
@@ -417,6 +437,7 @@ function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
417
437
  tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
418
438
  note: account?.accountNote,
419
439
  rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
440
+ quotaExhaustedUntil: account?.quotaExhaustedUntil,
420
441
  };
421
442
  });
422
443
  }
@@ -440,6 +461,8 @@ function printStandaloneResult(command, payload, json) {
440
461
  }
441
462
  }
442
463
  if (payload.error) console.log(`Error: ${payload.error}`);
464
+ for (const fix of payload.appliedFixes ?? []) console.log(`Fixed: ${fix}`);
465
+ for (const error of payload.fixErrors ?? []) console.log(`Repair failed: ${error}`);
443
466
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
444
467
  }
445
468
 
@@ -462,13 +485,14 @@ async function loadDistModules(relativePaths, label) {
462
485
  }
463
486
 
464
487
  async function loadWarmRuntime(env) {
465
- const [storageMod, usageMod, warmReqMod, warmMod, shutdownMod] = await loadDistModules(
488
+ const [storageMod, usageMod, warmReqMod, warmMod, shutdownMod, recoveryMod] = await loadDistModules(
466
489
  [
467
490
  "storage.js",
468
491
  "codex-usage.js",
469
492
  "accounts/warm-request.js",
470
493
  "accounts/warm.js",
471
494
  "shutdown.js",
495
+ "accounts/warm-recovery.js",
472
496
  ],
473
497
  "warm",
474
498
  );
@@ -477,7 +501,7 @@ async function loadWarmRuntime(env) {
477
501
  // Refreshing a token here persists credentials, which registers the
478
502
  // shutdown handler via the storage lock.
479
503
  shutdownMod.setShutdownOwnsProcess(true);
480
- return { storageMod, usageMod, warmReqMod, warmMod, shutdownMod };
504
+ return { storageMod, usageMod, warmReqMod, warmMod, shutdownMod, recoveryMod };
481
505
  }
482
506
 
483
507
  async function loadLimitsRuntime(env) {
@@ -497,26 +521,47 @@ export async function runWarmCommand(parsed, options = {}) {
497
521
 
498
522
  let runtime;
499
523
  try {
500
- runtime = await loadWarmRuntime(env);
524
+ runtime = await (options.loadWarmRuntime ?? loadWarmRuntime)(env);
501
525
  } catch (error) {
502
526
  const payload = { command: "warm", storagePath, error: formatErrorForLog(error) };
503
527
  printWarmResult(payload, parsed.json);
504
528
  return { exitCode: 1, action: "warm", storagePath };
505
529
  }
506
530
 
507
- const { storageMod, usageMod, warmReqMod, warmMod } = runtime;
531
+ const { storageMod, usageMod, warmReqMod, warmMod, recoveryMod } = runtime;
508
532
  // Point dist storage at the resolved accounts file so a refreshed token is
509
533
  // persisted to the SAME file the rest of the toolchain reads.
510
534
  storageMod.setStoragePathDirect(storagePath);
511
535
 
512
- const storage = await storageMod.loadAccounts();
536
+ let storage = null;
537
+ try {
538
+ storage = await storageMod.loadAccounts();
539
+ } catch (error) {
540
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
541
+ // upgrade hint; surface them rather than crashing the CLI.
542
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
543
+ const payload = { command: "warm", storagePath, error: `${formatErrorForLog(error)}${hint}` };
544
+ printWarmResult(payload, parsed.json);
545
+ return { exitCode: 1, action: "warm", storagePath };
546
+ }
513
547
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
514
548
  if (accounts.length === 0) {
549
+ // `loadAccounts` swallows parse/IO errors and returns null. Probe the
550
+ // file so a corrupt storage fails like `status`/`doctor` do (exit 1)
551
+ // instead of reporting a healthy empty pool. ENOENT stays a silent
552
+ // empty pool: a missing file legitimately means no accounts yet.
553
+ const probe = await readStandaloneStorage(storagePath);
554
+ if (probe.error) {
555
+ const payload = { command: "warm", storagePath, error: probe.error };
556
+ printWarmResult(payload, parsed.json);
557
+ return { exitCode: 1, action: "warm", storagePath };
558
+ }
515
559
  const payload = {
516
560
  command: "warm",
517
561
  storagePath,
518
562
  totalAccounts: 0,
519
563
  warmed: 0,
564
+ blocksCleared: 0,
520
565
  failed: 0,
521
566
  skipped: 0,
522
567
  results: [],
@@ -530,7 +575,9 @@ export async function runWarmCommand(parsed, options = {}) {
530
575
  // Same adapter as lib/tools/codex-warm.ts createWarmOne: refresh → resolve
531
576
  // account id → open the usage window; map an exhausted (quota-429) account
532
577
  // to a failure so it is not reported as warmed.
578
+ const succeeded = [];
533
579
  const warmOne = async (account) => {
580
+ const snapshot = { ...account, rateLimitResetTimes: { ...account.rateLimitResetTimes } };
534
581
  const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
535
582
  const accountId = usageMod.resolveCodexUsageAccountId({ account, accessToken });
536
583
  if (!accountId) {
@@ -544,12 +591,29 @@ export async function runWarmCommand(parsed, options = {}) {
544
591
  if (result.status === "exhausted") {
545
592
  return { status: "failed", detail: result.detail ?? "quota/usage limit reached" };
546
593
  }
594
+ if (!result.rateLimited && result.model) succeeded.push({
595
+ account: { ...snapshot, refreshToken: account.refreshToken }, model: result.model, accessToken,
596
+ });
547
597
  return { status: "warmed" };
548
598
  };
549
599
 
550
600
  const summary = await warmMod.warmAccounts(accounts, warmOne);
601
+ let blocksCleared = 0;
602
+ let blockClearError;
603
+ for (const observation of succeeded) {
604
+ let changed = false;
605
+ try {
606
+ const completed = await recoveryMod.recoverWarmedAccount(observation, () => { changed = true; });
607
+ changed = completed || changed;
608
+ } catch {
609
+ blockClearError = "Failed to clear local blocks; warm results are unchanged.";
610
+ }
611
+ if (changed) blocksCleared++;
612
+ }
551
613
  const payload = {
552
614
  command: "warm",
615
+ blocksCleared,
616
+ blockClearError,
553
617
  storagePath,
554
618
  totalAccounts: summary.total,
555
619
  warmed: summary.warmedCount,
@@ -585,6 +649,8 @@ function printWarmResult(payload, json) {
585
649
  console.log(`- ${label}: ${r.status}${detail}`);
586
650
  }
587
651
  console.log(`Summary: ${payload.warmed} warmed, ${payload.failed} failed, ${payload.skipped} skipped`);
652
+ console.log(`Blocks cleared: ${payload.blocksCleared ?? 0}`);
653
+ if (payload.blockClearError) console.log(payload.blockClearError);
588
654
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
589
655
  }
590
656
 
@@ -603,7 +669,7 @@ export async function runLimitsCommand(parsed, options = {}) {
603
669
 
604
670
  let runtime;
605
671
  try {
606
- runtime = await loadLimitsRuntime(env);
672
+ runtime = await (options.loadLimitsRuntime ?? loadLimitsRuntime)(env);
607
673
  } catch (error) {
608
674
  const payload = { command: "limits", storagePath, error: formatErrorForLog(error) };
609
675
  printLimitsResult(payload, parsed.json);
@@ -615,9 +681,27 @@ export async function runLimitsCommand(parsed, options = {}) {
615
681
  // persisted to the SAME file the rest of the toolchain reads.
616
682
  storageMod.setStoragePathDirect(storagePath);
617
683
 
618
- const storage = await storageMod.loadAccounts();
684
+ let storage = null;
685
+ try {
686
+ storage = await storageMod.loadAccounts();
687
+ } catch (error) {
688
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
689
+ // upgrade hint; surface them rather than crashing the CLI.
690
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
691
+ const payload = { command: "limits", storagePath, error: `${formatErrorForLog(error)}${hint}` };
692
+ printLimitsResult(payload, parsed.json);
693
+ return { exitCode: 1, action: "limits", storagePath };
694
+ }
619
695
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
620
696
  if (accounts.length === 0) {
697
+ // Same probe contract as `warm`: a corrupt file exits 1 like
698
+ // `status`/`doctor`; a missing file stays a silent empty pool.
699
+ const probe = await readStandaloneStorage(storagePath);
700
+ if (probe.error) {
701
+ const payload = { command: "limits", storagePath, error: probe.error };
702
+ printLimitsResult(payload, parsed.json);
703
+ return { exitCode: 1, action: "limits", storagePath };
704
+ }
621
705
  const payload = {
622
706
  command: "limits",
623
707
  storagePath,
@@ -658,6 +742,7 @@ export async function runLimitsCommand(parsed, options = {}) {
658
742
  label: account.accountLabel ?? `Account ${index + 1}`,
659
743
  email: maskValue(account.email, parsed.includeSensitive),
660
744
  rateLimitResetTimes: account.rateLimitResetTimes ?? {},
745
+ quotaExhaustedUntil: account.quotaExhaustedUntil,
661
746
  };
662
747
  try {
663
748
  const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
@@ -685,6 +770,13 @@ export async function runLimitsCommand(parsed, options = {}) {
685
770
  );
686
771
  }
687
772
  }
773
+ if (usageMod.isUsageQuotaRecovered([usage.primary, usage.secondary])) {
774
+ try {
775
+ await usageMod.persistUsageQuotaRecovery(account);
776
+ } catch {
777
+ loggerMod.logWarn("Failed to persist recovered usage quota");
778
+ }
779
+ }
688
780
  entry.planType = usage.planType;
689
781
  entry.credits = usage.credits;
690
782
  // Raw counts stay in `resetCredits` and the rendered line lives in
@@ -771,7 +863,68 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
771
863
  }
772
864
  const { env = process.env } = options;
773
865
  const storagePath = getStandaloneStoragePath(parsed, env);
774
- const { storage, error } = await readStandaloneStorage(storagePath);
866
+ const repairRequested = command === "doctor" && parsed.fix;
867
+ let storage = null;
868
+ let error = null;
869
+ if (parsed.configPath || !repairRequested) {
870
+ ({ storage, error } = await readStandaloneStorage(storagePath));
871
+ }
872
+ const appliedFixes = [];
873
+ const fixErrors = [];
874
+ if (repairRequested && !error) {
875
+ const previousKeychain = process.env.CODEX_KEYCHAIN;
876
+ try {
877
+ const loadDoctorRuntime = options.loadDoctorRuntime ?? (() => loadDistModules(
878
+ ["storage.js", "tools/doctor-repair.js", "shutdown.js"], "doctor",
879
+ ));
880
+ const [storageMod, repairMod, shutdownMod] = await loadDoctorRuntime();
881
+ // A CLI file selection must not read or replace the global keychain pool.
882
+ if (parsed.configPath) process.env.CODEX_KEYCHAIN = "0";
883
+ storageMod.setStoragePathDirect(storagePath);
884
+ shutdownMod.setShutdownOwnsProcess(true);
885
+ try {
886
+ storage = await storageMod.loadAccounts();
887
+ } catch (loadError) {
888
+ // Typed storage errors (UNSUPPORTED_SCHEMA_VERSION, unknown V2)
889
+ // carry exact in-tree copy plus an upgrade/recovery hint, and the
890
+ // load never got far enough to attempt a repair. Surface them on
891
+ // the error channel (exit 1) instead of the generic catch below,
892
+ // which is reserved for unknown throws so upstream failure text
893
+ // never reaches output unredacted.
894
+ if (loadError && typeof loadError.code === "string") {
895
+ const hint = typeof loadError.hint === "string" ? ` ${loadError.hint}` : "";
896
+ error = `${formatErrorForLog(loadError)}${hint}`;
897
+ } else {
898
+ throw loadError;
899
+ }
900
+ }
901
+ if (!storage && !error) {
902
+ // `loadAccounts` swallows JSON parse/IO errors and returns null. In
903
+ // default-path mode the pre-read above was skipped (keychain routing
904
+ // may own the pool), so probe the JSON file here: a corrupt file must
905
+ // surface as a parse error (exit 1) instead of "No accounts
906
+ // configured" (exit 0). ENOENT stays silent - a missing file with an
907
+ // empty keychain legitimately means no accounts yet. Skipped when a
908
+ // typed load error already set `error`; the probe would only
909
+ // overwrite the precise schema message with its own paraphrase.
910
+ const probe = await readStandaloneStorage(storagePath);
911
+ if (probe.error) error = probe.error;
912
+ }
913
+ if (!error) {
914
+ const repair = await repairMod.repairDoctorAccounts(storage?.accounts ?? []);
915
+ appliedFixes.push(...repair.appliedFixes);
916
+ fixErrors.push(...repair.fixErrors);
917
+ storage = (await storageMod.loadAccounts()) ?? storage;
918
+ }
919
+ } catch {
920
+ fixErrors.push("Doctor repair could not complete. Check the selected storage file and installed runtime.");
921
+ } finally {
922
+ if (parsed.configPath) {
923
+ if (previousKeychain === undefined) delete process.env.CODEX_KEYCHAIN;
924
+ else process.env.CODEX_KEYCHAIN = previousKeychain;
925
+ }
926
+ }
927
+ }
775
928
  const accounts = summarizeStandaloneAccounts(storage, parsed.includeSensitive, parsed.tag);
776
929
  const totalAccounts = Array.isArray(storage?.accounts) ? storage.accounts.length : 0;
777
930
  const payload = {
@@ -790,7 +943,11 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
790
943
  } else if (command === "doctor") {
791
944
  payload.message = error ? "Storage could not be parsed." : totalAccounts > 0 ? "Local diagnostics completed." : "No accounts configured.";
792
945
  payload.deep = parsed.deep;
793
- payload.fixApplied = parsed.fix ? false : undefined;
946
+ payload.fixApplied = parsed.fix ? appliedFixes.length > 0 : undefined;
947
+ if (parsed.fix) {
948
+ payload.appliedFixes = appliedFixes;
949
+ payload.fixErrors = fixErrors;
950
+ }
794
951
  payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
795
952
  } else if (command === "health") {
796
953
  payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
@@ -799,7 +956,7 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
799
956
  payload.message = totalAccounts > 0 ? "Account storage loaded." : "No accounts configured.";
800
957
  }
801
958
  printStandaloneResult(command, payload, parsed.json);
802
- return { exitCode: error ? 1 : 0, action: command, storagePath };
959
+ return { exitCode: error || fixErrors.length > 0 ? 1 : 0, action: command, storagePath };
803
960
  }
804
961
 
805
962
  // Top-level keys inside `provider.openai` that the installer owns absolutely.