oc-codex-multi-auth 6.18.0 → 6.20.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 (86) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +180 -79
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/accounts/persistence.d.ts +22 -0
  5. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  6. package/dist/lib/accounts/persistence.js +79 -0
  7. package/dist/lib/accounts/persistence.js.map +1 -1
  8. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  9. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  10. package/dist/lib/accounts/rate-limits.js +25 -0
  11. package/dist/lib/accounts/rate-limits.js.map +1 -1
  12. package/dist/lib/accounts/rotation.d.ts +22 -23
  13. package/dist/lib/accounts/rotation.d.ts.map +1 -1
  14. package/dist/lib/accounts/rotation.js +62 -37
  15. package/dist/lib/accounts/rotation.js.map +1 -1
  16. package/dist/lib/accounts/stale-state.d.ts +31 -0
  17. package/dist/lib/accounts/stale-state.d.ts.map +1 -1
  18. package/dist/lib/accounts/stale-state.js +56 -1
  19. package/dist/lib/accounts/stale-state.js.map +1 -1
  20. package/dist/lib/accounts/state.d.ts +6 -0
  21. package/dist/lib/accounts/state.d.ts.map +1 -1
  22. package/dist/lib/accounts/state.js +15 -1
  23. package/dist/lib/accounts/state.js.map +1 -1
  24. package/dist/lib/auth/login-runner.d.ts +3 -0
  25. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  26. package/dist/lib/auth/login-runner.js +35 -0
  27. package/dist/lib/auth/login-runner.js.map +1 -1
  28. package/dist/lib/auth/token-utils.d.ts +21 -0
  29. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  30. package/dist/lib/auth/token-utils.js +30 -0
  31. package/dist/lib/auth/token-utils.js.map +1 -1
  32. package/dist/lib/codex-reset.d.ts +13 -0
  33. package/dist/lib/codex-reset.d.ts.map +1 -1
  34. package/dist/lib/codex-reset.js +20 -4
  35. package/dist/lib/codex-reset.js.map +1 -1
  36. package/dist/lib/codex-usage.d.ts +55 -5
  37. package/dist/lib/codex-usage.d.ts.map +1 -1
  38. package/dist/lib/codex-usage.js +88 -21
  39. package/dist/lib/codex-usage.js.map +1 -1
  40. package/dist/lib/parallel-probe.d.ts.map +1 -1
  41. package/dist/lib/parallel-probe.js +4 -2
  42. package/dist/lib/parallel-probe.js.map +1 -1
  43. package/dist/lib/request/fetch-helpers.d.ts +24 -0
  44. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  45. package/dist/lib/request/fetch-helpers.js +66 -20
  46. package/dist/lib/request/fetch-helpers.js.map +1 -1
  47. package/dist/lib/schemas.d.ts +9 -0
  48. package/dist/lib/schemas.d.ts.map +1 -1
  49. package/dist/lib/schemas.js +3 -0
  50. package/dist/lib/schemas.js.map +1 -1
  51. package/dist/lib/storage/flagged.d.ts.map +1 -1
  52. package/dist/lib/storage/flagged.js +5 -0
  53. package/dist/lib/storage/flagged.js.map +1 -1
  54. package/dist/lib/storage/migrations.d.ts +21 -0
  55. package/dist/lib/storage/migrations.d.ts.map +1 -1
  56. package/dist/lib/storage/migrations.js.map +1 -1
  57. package/dist/lib/storage/normalize.d.ts.map +1 -1
  58. package/dist/lib/storage/normalize.js +106 -5
  59. package/dist/lib/storage/normalize.js.map +1 -1
  60. package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
  61. package/dist/lib/tools/codex-doctor.js +8 -66
  62. package/dist/lib/tools/codex-doctor.js.map +1 -1
  63. package/dist/lib/tools/codex-health.d.ts.map +1 -1
  64. package/dist/lib/tools/codex-health.js +7 -1
  65. package/dist/lib/tools/codex-health.js.map +1 -1
  66. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  67. package/dist/lib/tools/codex-limits.js +8 -1
  68. package/dist/lib/tools/codex-limits.js.map +1 -1
  69. package/dist/lib/tools/codex-list.d.ts.map +1 -1
  70. package/dist/lib/tools/codex-list.js +14 -1
  71. package/dist/lib/tools/codex-list.js.map +1 -1
  72. package/dist/lib/tools/codex-status.d.ts.map +1 -1
  73. package/dist/lib/tools/codex-status.js +6 -1
  74. package/dist/lib/tools/codex-status.js.map +1 -1
  75. package/dist/lib/tools/doctor-repair.d.ts +10 -0
  76. package/dist/lib/tools/doctor-repair.d.ts.map +1 -0
  77. package/dist/lib/tools/doctor-repair.js +86 -0
  78. package/dist/lib/tools/doctor-repair.js.map +1 -0
  79. package/dist/lib/tools/index.d.ts +3 -0
  80. package/dist/lib/tools/index.d.ts.map +1 -1
  81. package/dist/lib/tools/index.js.map +1 -1
  82. package/dist/lib/tui-status.d.ts.map +1 -1
  83. package/dist/lib/tui-status.js +176 -46
  84. package/dist/lib/tui-status.js.map +1 -1
  85. package/package.json +1 -1
  86. package/scripts/install-oc-codex-multi-auth-core.js +226 -25
@@ -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) {
@@ -307,6 +327,29 @@ function mergeStandaloneAccounts(target, source) {
307
327
  return mergeStandaloneAccounts(source, target);
308
328
  }
309
329
 
330
+ // Mirror of `isStaleGeneratedAccountLabel` / `dropStaleGeneratedLabel` in
331
+ // lib/auth/token-utils.ts and lib/storage/normalize.ts. The standalone CLI
332
+ // reads the pool through this normalizer and never through the compiled
333
+ // `normalizeAccountStorage`, so without the mirror `status`, `list`, `health`,
334
+ // `doctor` and `dashboard` keep printing the org-derived label the plugin
335
+ // itself now drops - next to the account id, which is the identity the label
336
+ // was misnaming. The marker must hold this account's own id suffix so a name
337
+ // set with `codex-label` survives.
338
+ const GENERATED_LABEL_PATTERN = /\s\[id:[^\]]*\]$/;
339
+
340
+ function dropStaleStandaloneLabel(account) {
341
+ const label = typeof account?.accountLabel === "string" ? account.accountLabel.trim() : "";
342
+ const accountId = typeof account?.accountId === "string" ? account.accountId.trim() : "";
343
+ if (!label || !accountId) return account;
344
+ const marker = label.match(GENERATED_LABEL_PATTERN)?.[0];
345
+ if (!marker) return account;
346
+ const suffix = accountId.length > 6 ? accountId.slice(-6) : accountId;
347
+ if (marker !== ` [id:${suffix}]`) return account;
348
+ const next = { ...account };
349
+ delete next.accountLabel;
350
+ return next;
351
+ }
352
+
310
353
  function normalizeStandaloneStorage(storage) {
311
354
  if (!Array.isArray(storage.accounts)) return storage;
312
355
  const accounts = [...storage.accounts];
@@ -324,7 +367,9 @@ function normalizeStandaloneStorage(storage) {
324
367
  if (sourceIndex === i) break;
325
368
  }
326
369
  }
327
- const normalizedAccounts = accounts.filter((_, index) => !removed.has(index));
370
+ const normalizedAccounts = accounts
371
+ .filter((_, index) => !removed.has(index))
372
+ .map(dropStaleStandaloneLabel);
328
373
  return {
329
374
  ...storage,
330
375
  accounts: normalizedAccounts,
@@ -332,9 +377,37 @@ function normalizeStandaloneStorage(storage) {
332
377
  };
333
378
  }
334
379
 
380
+ const MASKED_VALUE = "*****";
381
+ // The head/tail mask keeps eight characters, so it conceals nothing worth
382
+ // concealing below thirteen: `me@x.io` would print in full and `me@x.io12`
383
+ // all but its middle character. `doctor` output is what users paste into
384
+ // issues, so anything shorter is replaced outright instead. Input is trimmed
385
+ // first, or `" me@x.io "` clears the cutoff on padding alone and drops back
386
+ // into the partial mask.
387
+ const MASK_MIN_LENGTH = 13;
388
+
335
389
  function maskValue(value, includeSensitive) {
336
- if (includeSensitive || typeof value !== "string" || value.length <= 8) return value;
337
- return `${value.slice(0, 4)}...${value.slice(-4)}`;
390
+ if (includeSensitive || typeof value !== "string") return value;
391
+ const trimmed = value.trim();
392
+ if (!trimmed) return trimmed;
393
+ if (trimmed.length < MASK_MIN_LENGTH) return MASKED_VALUE;
394
+ return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
395
+ }
396
+
397
+ // Six characters when the id is shown in full, matching what the
398
+ // in-conversation surfaces print as `id:`. Four when it is head/tail masked,
399
+ // which is the tail `maskValue` already discloses as `accountId` in the same
400
+ // payload, so the printed identity never reveals more of an id than the field
401
+ // beside it. Nothing at all when the id was too short for that mask: the
402
+ // `accountId` next to it is then `*****`, and four raw characters of a short
403
+ // id can be the whole id.
404
+ function accountIdSuffix(accountId, includeSensitive) {
405
+ if (!accountId) return undefined;
406
+ if (includeSensitive) {
407
+ return accountId.length > 6 ? accountId.slice(-6) : accountId;
408
+ }
409
+ if (accountId.length < MASK_MIN_LENGTH) return undefined;
410
+ return accountId.slice(-4);
338
411
  }
339
412
 
340
413
  function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
@@ -345,21 +418,28 @@ function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
345
418
  .filter(({ account }) => !normalizedTag ||
346
419
  (Array.isArray(account?.accountTags) &&
347
420
  account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)))
348
- .map(({ account, index }) => ({
349
- index,
350
- label: account?.accountLabel ?? `Account ${index + 1}`,
351
- email: maskValue(account?.email, includeSensitive),
352
- accountId: maskValue(account?.accountId, includeSensitive),
353
- accountIdSource: account?.accountIdSource,
354
- enabled: account?.enabled !== false,
355
- hasRefreshToken: typeof account?.refreshToken === "string" && account.refreshToken.length > 0,
356
- hasAccessToken: typeof account?.accessToken === "string" && account.accessToken.length > 0,
357
- expiresAt: account?.expiresAt,
358
- expired: typeof account?.expiresAt === "number" ? account.expiresAt <= Date.now() : undefined,
359
- tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
360
- note: account?.accountNote,
361
- rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
362
- }));
421
+ .map(({ account, index }) => {
422
+ const trimmedId =
423
+ typeof account?.accountId === "string" ? account.accountId.trim() : "";
424
+ const accountId = trimmedId || undefined;
425
+ return {
426
+ index,
427
+ label: account?.accountLabel ?? `Account ${index + 1}`,
428
+ email: maskValue(account?.email, includeSensitive),
429
+ accountId: maskValue(accountId, includeSensitive),
430
+ idSuffix: accountIdSuffix(accountId, includeSensitive),
431
+ accountIdSource: account?.accountIdSource,
432
+ enabled: account?.enabled !== false,
433
+ hasRefreshToken: typeof account?.refreshToken === "string" && account.refreshToken.length > 0,
434
+ hasAccessToken: typeof account?.accessToken === "string" && account.accessToken.length > 0,
435
+ expiresAt: account?.expiresAt,
436
+ expired: typeof account?.expiresAt === "number" ? account.expiresAt <= Date.now() : undefined,
437
+ tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
438
+ note: account?.accountNote,
439
+ rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
440
+ quotaExhaustedUntil: account?.quotaExhaustedUntil,
441
+ };
442
+ });
363
443
  }
364
444
 
365
445
  function printStandaloneResult(command, payload, json) {
@@ -373,10 +453,16 @@ function printStandaloneResult(command, payload, json) {
373
453
  console.log(`Accounts: ${payload.totalAccounts}`);
374
454
  if (Array.isArray(payload.accounts)) {
375
455
  for (const account of payload.accounts) {
376
- console.log(`- [${account.index}] ${account.label} enabled=${account.enabled} refresh=${account.hasRefreshToken} access=${account.hasAccessToken}`);
456
+ const identity = [account.email, account.idSuffix ? `id:${account.idSuffix}` : undefined]
457
+ .filter(Boolean)
458
+ .join(", ");
459
+ const name = identity ? `${account.label} (${identity})` : account.label;
460
+ console.log(`- [${account.index}] ${name} enabled=${account.enabled} refresh=${account.hasRefreshToken} access=${account.hasAccessToken}`);
377
461
  }
378
462
  }
379
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}`);
380
466
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
381
467
  }
382
468
 
@@ -446,9 +532,29 @@ export async function runWarmCommand(parsed, options = {}) {
446
532
  // persisted to the SAME file the rest of the toolchain reads.
447
533
  storageMod.setStoragePathDirect(storagePath);
448
534
 
449
- const storage = await storageMod.loadAccounts();
535
+ let storage = null;
536
+ try {
537
+ storage = await storageMod.loadAccounts();
538
+ } catch (error) {
539
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
540
+ // upgrade hint; surface them rather than crashing the CLI.
541
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
542
+ const payload = { command: "warm", storagePath, error: `${formatErrorForLog(error)}${hint}` };
543
+ printWarmResult(payload, parsed.json);
544
+ return { exitCode: 1, action: "warm", storagePath };
545
+ }
450
546
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
451
547
  if (accounts.length === 0) {
548
+ // `loadAccounts` swallows parse/IO errors and returns null. Probe the
549
+ // file so a corrupt storage fails like `status`/`doctor` do (exit 1)
550
+ // instead of reporting a healthy empty pool. ENOENT stays a silent
551
+ // empty pool: a missing file legitimately means no accounts yet.
552
+ const probe = await readStandaloneStorage(storagePath);
553
+ if (probe.error) {
554
+ const payload = { command: "warm", storagePath, error: probe.error };
555
+ printWarmResult(payload, parsed.json);
556
+ return { exitCode: 1, action: "warm", storagePath };
557
+ }
452
558
  const payload = {
453
559
  command: "warm",
454
560
  storagePath,
@@ -552,9 +658,27 @@ export async function runLimitsCommand(parsed, options = {}) {
552
658
  // persisted to the SAME file the rest of the toolchain reads.
553
659
  storageMod.setStoragePathDirect(storagePath);
554
660
 
555
- const storage = await storageMod.loadAccounts();
661
+ let storage = null;
662
+ try {
663
+ storage = await storageMod.loadAccounts();
664
+ } catch (error) {
665
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
666
+ // upgrade hint; surface them rather than crashing the CLI.
667
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
668
+ const payload = { command: "limits", storagePath, error: `${formatErrorForLog(error)}${hint}` };
669
+ printLimitsResult(payload, parsed.json);
670
+ return { exitCode: 1, action: "limits", storagePath };
671
+ }
556
672
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
557
673
  if (accounts.length === 0) {
674
+ // Same probe contract as `warm`: a corrupt file exits 1 like
675
+ // `status`/`doctor`; a missing file stays a silent empty pool.
676
+ const probe = await readStandaloneStorage(storagePath);
677
+ if (probe.error) {
678
+ const payload = { command: "limits", storagePath, error: probe.error };
679
+ printLimitsResult(payload, parsed.json);
680
+ return { exitCode: 1, action: "limits", storagePath };
681
+ }
558
682
  const payload = {
559
683
  command: "limits",
560
684
  storagePath,
@@ -595,6 +719,7 @@ export async function runLimitsCommand(parsed, options = {}) {
595
719
  label: account.accountLabel ?? `Account ${index + 1}`,
596
720
  email: maskValue(account.email, parsed.includeSensitive),
597
721
  rateLimitResetTimes: account.rateLimitResetTimes ?? {},
722
+ quotaExhaustedUntil: account.quotaExhaustedUntil,
598
723
  };
599
724
  try {
600
725
  const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
@@ -624,6 +749,14 @@ export async function runLimitsCommand(parsed, options = {}) {
624
749
  }
625
750
  entry.planType = usage.planType;
626
751
  entry.credits = usage.credits;
752
+ // Raw counts stay in `resetCredits` and the rendered line lives in
753
+ // its own field: embedding the English summary inside the counts
754
+ // object would make `--json` consumers parse presentation text to
755
+ // reach a number that is already beside it.
756
+ entry.resetCredits = usage.resetCredits;
757
+ entry.resetCreditsSummary = usage.resetCredits
758
+ ? usageMod.formatResetCredits(usage.resetCredits)
759
+ : null;
627
760
  entry.limits = usage.limits;
628
761
  } catch (error) {
629
762
  // `ensureCodexUsageAccessToken` can surface a raw OAuth refresh
@@ -675,6 +808,9 @@ function printLimitsResult(payload, json) {
675
808
  }
676
809
  if (account.planType) console.log(` Plan: ${account.planType}`);
677
810
  if (account.credits) console.log(` Credits: ${account.credits}`);
811
+ if (account.resetCredits && account.resetCredits.available > 0) {
812
+ console.log(` Resets: ${account.resetCreditsSummary}`);
813
+ }
678
814
  }
679
815
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
680
816
  }
@@ -697,7 +833,68 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
697
833
  }
698
834
  const { env = process.env } = options;
699
835
  const storagePath = getStandaloneStoragePath(parsed, env);
700
- const { storage, error } = await readStandaloneStorage(storagePath);
836
+ const repairRequested = command === "doctor" && parsed.fix;
837
+ let storage = null;
838
+ let error = null;
839
+ if (parsed.configPath || !repairRequested) {
840
+ ({ storage, error } = await readStandaloneStorage(storagePath));
841
+ }
842
+ const appliedFixes = [];
843
+ const fixErrors = [];
844
+ if (repairRequested && !error) {
845
+ const previousKeychain = process.env.CODEX_KEYCHAIN;
846
+ try {
847
+ const loadDoctorRuntime = options.loadDoctorRuntime ?? (() => loadDistModules(
848
+ ["storage.js", "tools/doctor-repair.js", "shutdown.js"], "doctor",
849
+ ));
850
+ const [storageMod, repairMod, shutdownMod] = await loadDoctorRuntime();
851
+ // A CLI file selection must not read or replace the global keychain pool.
852
+ if (parsed.configPath) process.env.CODEX_KEYCHAIN = "0";
853
+ storageMod.setStoragePathDirect(storagePath);
854
+ shutdownMod.setShutdownOwnsProcess(true);
855
+ try {
856
+ storage = await storageMod.loadAccounts();
857
+ } catch (loadError) {
858
+ // Typed storage errors (UNSUPPORTED_SCHEMA_VERSION, unknown V2)
859
+ // carry exact in-tree copy plus an upgrade/recovery hint, and the
860
+ // load never got far enough to attempt a repair. Surface them on
861
+ // the error channel (exit 1) instead of the generic catch below,
862
+ // which is reserved for unknown throws so upstream failure text
863
+ // never reaches output unredacted.
864
+ if (loadError && typeof loadError.code === "string") {
865
+ const hint = typeof loadError.hint === "string" ? ` ${loadError.hint}` : "";
866
+ error = `${formatErrorForLog(loadError)}${hint}`;
867
+ } else {
868
+ throw loadError;
869
+ }
870
+ }
871
+ if (!storage && !error) {
872
+ // `loadAccounts` swallows JSON parse/IO errors and returns null. In
873
+ // default-path mode the pre-read above was skipped (keychain routing
874
+ // may own the pool), so probe the JSON file here: a corrupt file must
875
+ // surface as a parse error (exit 1) instead of "No accounts
876
+ // configured" (exit 0). ENOENT stays silent - a missing file with an
877
+ // empty keychain legitimately means no accounts yet. Skipped when a
878
+ // typed load error already set `error`; the probe would only
879
+ // overwrite the precise schema message with its own paraphrase.
880
+ const probe = await readStandaloneStorage(storagePath);
881
+ if (probe.error) error = probe.error;
882
+ }
883
+ if (!error) {
884
+ const repair = await repairMod.repairDoctorAccounts(storage?.accounts ?? []);
885
+ appliedFixes.push(...repair.appliedFixes);
886
+ fixErrors.push(...repair.fixErrors);
887
+ storage = (await storageMod.loadAccounts()) ?? storage;
888
+ }
889
+ } catch {
890
+ fixErrors.push("Doctor repair could not complete. Check the selected storage file and installed runtime.");
891
+ } finally {
892
+ if (parsed.configPath) {
893
+ if (previousKeychain === undefined) delete process.env.CODEX_KEYCHAIN;
894
+ else process.env.CODEX_KEYCHAIN = previousKeychain;
895
+ }
896
+ }
897
+ }
701
898
  const accounts = summarizeStandaloneAccounts(storage, parsed.includeSensitive, parsed.tag);
702
899
  const totalAccounts = Array.isArray(storage?.accounts) ? storage.accounts.length : 0;
703
900
  const payload = {
@@ -716,7 +913,11 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
716
913
  } else if (command === "doctor") {
717
914
  payload.message = error ? "Storage could not be parsed." : totalAccounts > 0 ? "Local diagnostics completed." : "No accounts configured.";
718
915
  payload.deep = parsed.deep;
719
- payload.fixApplied = parsed.fix ? false : undefined;
916
+ payload.fixApplied = parsed.fix ? appliedFixes.length > 0 : undefined;
917
+ if (parsed.fix) {
918
+ payload.appliedFixes = appliedFixes;
919
+ payload.fixErrors = fixErrors;
920
+ }
720
921
  payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
721
922
  } else if (command === "health") {
722
923
  payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
@@ -725,7 +926,7 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
725
926
  payload.message = totalAccounts > 0 ? "Account storage loaded." : "No accounts configured.";
726
927
  }
727
928
  printStandaloneResult(command, payload, parsed.json);
728
- return { exitCode: error ? 1 : 0, action: command, storagePath };
929
+ return { exitCode: error || fixErrors.length > 0 ? 1 : 0, action: command, storagePath };
729
930
  }
730
931
 
731
932
  // Top-level keys inside `provider.openai` that the installer owns absolutely.