oc-chatgpt-multi-auth 4.11.2 → 4.12.1

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 (47) hide show
  1. package/README.md +56 -20
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +114 -42
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/audit.d.ts +45 -0
  6. package/dist/lib/audit.d.ts.map +1 -0
  7. package/dist/lib/audit.js +131 -0
  8. package/dist/lib/audit.js.map +1 -0
  9. package/dist/lib/auth/auth.d.ts.map +1 -1
  10. package/dist/lib/auth/auth.js +12 -11
  11. package/dist/lib/auth/auth.js.map +1 -1
  12. package/dist/lib/auth-rate-limit.d.ts +20 -0
  13. package/dist/lib/auth-rate-limit.d.ts.map +1 -0
  14. package/dist/lib/auth-rate-limit.js +91 -0
  15. package/dist/lib/auth-rate-limit.js.map +1 -0
  16. package/dist/lib/circuit-breaker.d.ts +34 -0
  17. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  18. package/dist/lib/circuit-breaker.js +117 -0
  19. package/dist/lib/circuit-breaker.js.map +1 -0
  20. package/dist/lib/config.d.ts.map +1 -1
  21. package/dist/lib/config.js +5 -1
  22. package/dist/lib/config.js.map +1 -1
  23. package/dist/lib/logger.d.ts +5 -1
  24. package/dist/lib/logger.d.ts.map +1 -1
  25. package/dist/lib/logger.js +41 -4
  26. package/dist/lib/logger.js.map +1 -1
  27. package/dist/lib/proactive-refresh.d.ts +66 -0
  28. package/dist/lib/proactive-refresh.d.ts.map +1 -0
  29. package/dist/lib/proactive-refresh.js +143 -0
  30. package/dist/lib/proactive-refresh.js.map +1 -0
  31. package/dist/lib/request/response-handler.d.ts.map +1 -1
  32. package/dist/lib/request/response-handler.js +4 -0
  33. package/dist/lib/request/response-handler.js.map +1 -1
  34. package/dist/lib/rotation.d.ts.map +1 -1
  35. package/dist/lib/rotation.js +13 -0
  36. package/dist/lib/rotation.js.map +1 -1
  37. package/dist/lib/schemas.d.ts +363 -0
  38. package/dist/lib/schemas.d.ts.map +1 -0
  39. package/dist/lib/schemas.js +229 -0
  40. package/dist/lib/schemas.js.map +1 -0
  41. package/dist/lib/storage.d.ts +34 -1
  42. package/dist/lib/storage.d.ts.map +1 -1
  43. package/dist/lib/storage.js +182 -5
  44. package/dist/lib/storage.js.map +1 -1
  45. package/dist/lib/types.d.ts +1 -73
  46. package/dist/lib/types.d.ts.map +1 -1
  47. package/package.json +6 -3
package/README.md CHANGED
@@ -254,12 +254,14 @@ opencode auth login # Run again to add more accounts
254
254
 
255
255
  The plugin provides built-in tools for managing your OpenAI accounts. These are available directly in OpenCode — just ask the agent or type the tool name.
256
256
 
257
- ### openai-accounts
257
+ > **Note:** Tools were renamed from `openai-accounts-*` to `codex-*` in v4.12.0 for brevity.
258
+
259
+ ### codex-list
258
260
 
259
261
  List all configured accounts with their status.
260
262
 
261
263
  ```
262
- openai-accounts
264
+ codex-list
263
265
  ```
264
266
 
265
267
  **Output:**
@@ -270,17 +272,17 @@ OpenAI Accounts (3 total):
270
272
  [2] work@company.com
271
273
  [3] backup@email.com
272
274
 
273
- Use openai-accounts-switch to change active account.
275
+ Use codex-switch to change active account.
274
276
  ```
275
277
 
276
278
  ---
277
279
 
278
- ### openai-accounts-switch
280
+ ### codex-switch
279
281
 
280
282
  Switch to a different account by index (1-based).
281
283
 
282
284
  ```
283
- openai-accounts-switch index=2
285
+ codex-switch index=2
284
286
  ```
285
287
 
286
288
  **Output:**
@@ -290,12 +292,12 @@ Switched to account [2] work@company.com
290
292
 
291
293
  ---
292
294
 
293
- ### openai-accounts-status
295
+ ### codex-status
294
296
 
295
297
  Show detailed status including rate limits and health scores.
296
298
 
297
299
  ```
298
- openai-accounts-status
300
+ codex-status
299
301
  ```
300
302
 
301
303
  **Output:**
@@ -317,12 +319,12 @@ OpenAI Account Status:
317
319
 
318
320
  ---
319
321
 
320
- ### openai-accounts-health
322
+ ### codex-health
321
323
 
322
324
  Check if all account tokens are still valid (read-only check).
323
325
 
324
326
  ```
325
- openai-accounts-health
327
+ codex-health
326
328
  ```
327
329
 
328
330
  **Output:**
@@ -338,12 +340,12 @@ Summary: 2 healthy, 1 unhealthy
338
340
 
339
341
  ---
340
342
 
341
- ### openai-accounts-refresh
343
+ ### codex-refresh
342
344
 
343
345
  Refresh all OAuth tokens and save them to disk. Use this after long idle periods.
344
346
 
345
347
  ```
346
- openai-accounts-refresh
348
+ codex-refresh
347
349
  ```
348
350
 
349
351
  **Output:**
@@ -357,16 +359,16 @@ Refreshing 3 account(s):
357
359
  Summary: 2 refreshed, 1 failed
358
360
  ```
359
361
 
360
- **Difference from health check:** `openai-accounts-health` only validates tokens. `openai-accounts-refresh` actually refreshes them and saves new tokens to disk.
362
+ **Difference from health check:** `codex-health` only validates tokens. `codex-refresh` actually refreshes them and saves new tokens to disk.
361
363
 
362
364
  ---
363
365
 
364
- ### openai-accounts-remove
366
+ ### codex-remove
365
367
 
366
368
  Remove an account by index. Useful for cleaning up expired accounts.
367
369
 
368
370
  ```
369
- openai-accounts-remove index=3
371
+ codex-remove index=3
370
372
  ```
371
373
 
372
374
  **Output:**
@@ -378,16 +380,50 @@ Remaining accounts: 2
378
380
 
379
381
  ---
380
382
 
383
+ ### codex-export
384
+
385
+ Export all accounts to a portable JSON file. Useful for backup or migration.
386
+
387
+ ```
388
+ codex-export path="~/backup/accounts.json"
389
+ ```
390
+
391
+ **Output:**
392
+ ```
393
+ Exported 3 account(s) to ~/backup/accounts.json
394
+ ```
395
+
396
+ ---
397
+
398
+ ### codex-import
399
+
400
+ Import accounts from a JSON file (exported via `codex-export`). Merges with existing accounts.
401
+
402
+ ```
403
+ codex-import path="~/backup/accounts.json"
404
+ ```
405
+
406
+ **Output:**
407
+ ```
408
+ Imported 2 new account(s) (1 duplicate skipped)
409
+
410
+ Total accounts: 4
411
+ ```
412
+
413
+ ---
414
+
381
415
  ### Quick Reference
382
416
 
383
417
  | Tool | What It Does | Example |
384
418
  |------|--------------|---------|
385
- | `openai-accounts` | List all accounts | "list my accounts" |
386
- | `openai-accounts-switch` | Switch active account | "switch to account 2" |
387
- | `openai-accounts-status` | Show rate limits & health | "show account status" |
388
- | `openai-accounts-health` | Validate tokens (read-only) | "check account health" |
389
- | `openai-accounts-refresh` | Refresh & save tokens | "refresh my tokens" |
390
- | `openai-accounts-remove` | Remove an account | "remove account 3" |
419
+ | `codex-list` | List all accounts | "list my accounts" |
420
+ | `codex-switch` | Switch active account | "switch to account 2" |
421
+ | `codex-status` | Show rate limits & health | "show account status" |
422
+ | `codex-health` | Validate tokens (read-only) | "check account health" |
423
+ | `codex-refresh` | Refresh & save tokens | "refresh my tokens" |
424
+ | `codex-remove` | Remove an account | "remove account 3" |
425
+ | `codex-export` | Export accounts to file | "export my accounts" |
426
+ | `codex-import` | Import accounts from file | "import accounts from backup" |
391
427
 
392
428
  ---
393
429
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA0E/D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAs6C/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA0E/D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAi/C/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -30,11 +30,11 @@ import { startLocalOAuthServer } from "./lib/auth/server.js";
30
30
  import { promptAccountSelection, promptLoginMode } from "./lib/cli.js";
31
31
  import { getCodexMode, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, loadPluginConfig, } from "./lib/config.js";
32
32
  import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
33
- import { initLogger, logRequest, logDebug, logInfo, logWarn } from "./lib/logger.js";
33
+ import { initLogger, logRequest, logDebug, logInfo, logWarn, logError } from "./lib/logger.js";
34
34
  import { checkAndNotify } from "./lib/auto-update-checker.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
36
  import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, setStoragePath } from "./lib/storage.js";
37
+ import { getStoragePath, loadAccounts, saveAccounts, setStoragePath, exportAccounts, importAccounts, StorageError, formatStorageErrorHint } from "./lib/storage.js";
38
38
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
39
39
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
40
40
  import { addJitter } from "./lib/rotation.js";
@@ -573,7 +573,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
573
573
  continue;
574
574
  }
575
575
  const hadAccountId = !!account.accountId;
576
- const accountId = account.accountId ?? extractAccountId(accountAuth.access);
576
+ // Prefer fresh token-derived ID over stored ID (fixes Business plan workspace issues)
577
+ const accountId = extractAccountId(accountAuth.access) ?? account.accountId;
577
578
  if (!accountId) {
578
579
  accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
579
580
  accountManager.saveToDiskDebounced();
@@ -596,14 +597,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
596
597
  promptCacheKey,
597
598
  });
598
599
  while (true) {
599
- const response = await fetch(url, {
600
- ...requestInit,
601
- headers,
602
- });
600
+ let response;
601
+ const fetchStart = performance.now();
602
+ try {
603
+ response = await fetch(url, {
604
+ ...requestInit,
605
+ headers,
606
+ });
607
+ }
608
+ catch (networkError) {
609
+ const errorMsg = networkError instanceof Error ? networkError.message : String(networkError);
610
+ logWarn(`Network error for account ${account.index + 1}: ${errorMsg}`);
611
+ accountManager.recordFailure(account, modelFamily, model);
612
+ break;
613
+ }
614
+ const fetchLatencyMs = Math.round(performance.now() - fetchStart);
603
615
  logRequest(LOG_STAGES.RESPONSE, {
604
616
  status: response.status,
605
617
  ok: response.ok,
606
618
  statusText: response.statusText,
619
+ latencyMs: fetchLatencyMs,
607
620
  headers: Object.fromEntries(response.headers.entries()),
608
621
  });
609
622
  if (!response.ok) {
@@ -663,7 +676,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
663
676
  }
664
677
  const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
665
678
  const message = count === 0
666
- ? "No OpenAI accounts configured. Run `opencode auth login`."
679
+ ? "No Codex accounts configured. Run `opencode auth login`."
667
680
  : `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`;
668
681
  return new Response(JSON.stringify({ error: { message } }), {
669
682
  status: 429,
@@ -783,9 +796,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
783
796
  }
784
797
  catch (err) {
785
798
  const storagePath = getStoragePath();
786
- logWarn(`[${PLUGIN_NAME}] Failed to persist account to disk: ${err?.message ?? String(err)}`);
787
- logWarn(`Storage path: ${storagePath}`);
788
- await showToast(`Account authenticated but failed to save to disk. Storage path: ${storagePath}`, "warning", { title: "Account Persistence Failed", duration: 10000 });
799
+ const errorCode = err?.code || "UNKNOWN";
800
+ const hint = err instanceof StorageError ? err.hint : formatStorageErrorHint(err, storagePath);
801
+ logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
802
+ await showToast(hint, "error", { title: "Account Persistence Failed", duration: 10000 });
789
803
  }
790
804
  });
791
805
  }
@@ -832,9 +846,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
832
846
  }
833
847
  catch (err) {
834
848
  const storagePath = getStoragePath();
835
- logWarn(`[${PLUGIN_NAME}] Failed to persist account to disk: ${err?.message ?? String(err)}`);
836
- logWarn(`Storage path: ${storagePath}`);
837
- await showToast(`Account authenticated but failed to save to disk. Storage path: ${storagePath}`, "warning", { title: "Account Persistence Failed", duration: 10000 });
849
+ const errorCode = err?.code || "UNKNOWN";
850
+ const hint = err instanceof StorageError ? err.hint : formatStorageErrorHint(err, storagePath);
851
+ logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
852
+ await showToast(hint, "error", { title: "Account Persistence Failed", duration: 10000 });
838
853
  }
839
854
  if (accounts.length >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
840
855
  break;
@@ -880,9 +895,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
880
895
  }
881
896
  catch (err) {
882
897
  const storagePath = getStoragePath();
883
- logWarn(`[${PLUGIN_NAME}] Failed to persist account to disk: ${err?.message ?? String(err)}`);
884
- logWarn(`Storage path: ${storagePath}`);
885
- await showToast(`Account authenticated but failed to save to disk. Storage path: ${storagePath}`, "warning", { title: "Account Persistence Failed", duration: 10000 });
898
+ const errorCode = err?.code || "UNKNOWN";
899
+ const hint = err instanceof StorageError ? err.hint : formatStorageErrorHint(err, storagePath);
900
+ logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
901
+ await showToast(hint, "error", { title: "Account Persistence Failed", duration: 10000 });
886
902
  }
887
903
  });
888
904
  },
@@ -890,15 +906,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
890
906
  ],
891
907
  },
892
908
  tool: {
893
- "openai-accounts": tool({
894
- description: "List all OpenAI OAuth accounts and the current active index.",
909
+ "codex-list": tool({
910
+ description: "List all Codex OAuth accounts and the current active index.",
895
911
  args: {},
896
912
  async execute() {
897
913
  const storage = await loadAccounts();
898
914
  const storePath = getStoragePath();
899
915
  if (!storage || storage.accounts.length === 0) {
900
916
  return [
901
- "No OpenAI accounts configured.",
917
+ "No Codex accounts configured.",
902
918
  "",
903
919
  "Add accounts:",
904
920
  " opencode auth login",
@@ -909,7 +925,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
909
925
  const now = Date.now();
910
926
  const activeIndex = resolveActiveIndex(storage, "codex");
911
927
  const lines = [
912
- `OpenAI Accounts (${storage.accounts.length}):`,
928
+ `Codex Accounts (${storage.accounts.length}):`,
913
929
  "",
914
930
  " # Label Status",
915
931
  "----------------------------------------------- ---------------------",
@@ -936,20 +952,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
936
952
  lines.push("");
937
953
  lines.push("Commands:");
938
954
  lines.push(" - Add account: opencode auth login");
939
- lines.push(" - Switch account: openai-accounts-switch");
940
- lines.push(" - Status details: openai-accounts-status");
955
+ lines.push(" - Switch account: codex-switch");
956
+ lines.push(" - Status details: codex-status");
941
957
  return lines.join("\n");
942
958
  },
943
959
  }),
944
- "openai-accounts-switch": tool({
945
- description: "Switch active OpenAI account by index (1-based).",
960
+ "codex-switch": tool({
961
+ description: "Switch active Codex account by index (1-based).",
946
962
  args: {
947
963
  index: tool.schema.number().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
948
964
  },
949
965
  async execute({ index }) {
950
966
  const storage = await loadAccounts();
951
967
  if (!storage || storage.accounts.length === 0) {
952
- return "No OpenAI accounts configured. Run: opencode auth login";
968
+ return "No Codex accounts configured. Run: opencode auth login";
953
969
  }
954
970
  const targetIndex = Math.floor((index ?? 0) - 1);
955
971
  if (!Number.isFinite(targetIndex) ||
@@ -968,7 +984,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
968
984
  for (const family of MODEL_FAMILIES) {
969
985
  storage.activeIndexByFamily[family] = targetIndex;
970
986
  }
971
- await saveAccounts(storage);
987
+ try {
988
+ await saveAccounts(storage);
989
+ }
990
+ catch (saveError) {
991
+ logWarn("Failed to save account switch", { error: String(saveError) });
992
+ return `Switched to ${formatAccountLabel(account, targetIndex)} but failed to persist. Changes may be lost on restart.`;
993
+ }
972
994
  if (cachedAccountManager) {
973
995
  cachedAccountManager.setActiveIndex(targetIndex);
974
996
  await cachedAccountManager.saveToDisk();
@@ -977,13 +999,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
977
999
  return `Switched to account: ${label}`;
978
1000
  },
979
1001
  }),
980
- "openai-accounts-status": tool({
981
- description: "Show detailed status of OpenAI accounts and rate limits.",
1002
+ "codex-status": tool({
1003
+ description: "Show detailed status of Codex accounts and rate limits.",
982
1004
  args: {},
983
1005
  async execute() {
984
1006
  const storage = await loadAccounts();
985
1007
  if (!storage || storage.accounts.length === 0) {
986
- return "No OpenAI accounts configured. Run: opencode auth login";
1008
+ return "No Codex accounts configured. Run: opencode auth login";
987
1009
  }
988
1010
  const now = Date.now();
989
1011
  const activeIndex = resolveActiveIndex(storage, "codex");
@@ -1025,13 +1047,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1025
1047
  return lines.join("\n");
1026
1048
  },
1027
1049
  }),
1028
- "openai-accounts-health": tool({
1029
- description: "Check health of all OpenAI accounts by validating refresh tokens.",
1050
+ "codex-health": tool({
1051
+ description: "Check health of all Codex accounts by validating refresh tokens.",
1030
1052
  args: {},
1031
1053
  async execute() {
1032
1054
  const storage = await loadAccounts();
1033
1055
  if (!storage || storage.accounts.length === 0) {
1034
- return "No OpenAI accounts configured. Run: opencode auth login";
1056
+ return "No Codex accounts configured. Run: opencode auth login";
1035
1057
  }
1036
1058
  const results = [
1037
1059
  `Health Check (${storage.accounts.length} accounts):`,
@@ -1057,7 +1079,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1057
1079
  }
1058
1080
  catch (error) {
1059
1081
  const errorMsg = error instanceof Error ? error.message : String(error);
1060
- results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 50)}`);
1082
+ results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 120)}`);
1061
1083
  unhealthyCount++;
1062
1084
  }
1063
1085
  }
@@ -1066,21 +1088,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1066
1088
  return results.join("\n");
1067
1089
  },
1068
1090
  }),
1069
- "openai-accounts-remove": tool({
1070
- description: "Remove an OpenAI account by index (1-based). Use openai-accounts to list accounts first.",
1091
+ "codex-remove": tool({
1092
+ description: "Remove a Codex account by index (1-based). Use codex-list to list accounts first.",
1071
1093
  args: {
1072
1094
  index: tool.schema.number().describe("Account number to remove (1-based, e.g., 1 for first account)"),
1073
1095
  },
1074
1096
  async execute({ index }) {
1075
1097
  const storage = await loadAccounts();
1076
1098
  if (!storage || storage.accounts.length === 0) {
1077
- return "No OpenAI accounts configured. Nothing to remove.";
1099
+ return "No Codex accounts configured. Nothing to remove.";
1078
1100
  }
1079
1101
  const targetIndex = Math.floor((index ?? 0) - 1);
1080
1102
  if (!Number.isFinite(targetIndex) ||
1081
1103
  targetIndex < 0 ||
1082
1104
  targetIndex >= storage.accounts.length) {
1083
- return `Invalid account number: ${index}\n\nValid range: 1-${storage.accounts.length}\n\nUse openai-accounts to list all accounts.`;
1105
+ return `Invalid account number: ${index}\n\nValid range: 1-${storage.accounts.length}\n\nUse codex-list to list all accounts.`;
1084
1106
  }
1085
1107
  const account = storage.accounts[targetIndex];
1086
1108
  if (!account) {
@@ -1113,7 +1135,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1113
1135
  }
1114
1136
  }
1115
1137
  }
1116
- await saveAccounts(storage);
1138
+ try {
1139
+ await saveAccounts(storage);
1140
+ }
1141
+ catch (saveError) {
1142
+ logWarn("Failed to save account removal", { error: String(saveError) });
1143
+ return `Removed ${formatAccountLabel(account, targetIndex)} from memory but failed to persist. Changes may be lost on restart.`;
1144
+ }
1117
1145
  if (cachedAccountManager) {
1118
1146
  const managedAccounts = cachedAccountManager.getAccountsSnapshot();
1119
1147
  const managedAccount = managedAccounts.find((acc) => acc.refreshToken === account.refreshToken);
@@ -1132,13 +1160,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1132
1160
  ].join("\n");
1133
1161
  },
1134
1162
  }),
1135
- "openai-accounts-refresh": tool({
1163
+ "codex-refresh": tool({
1136
1164
  description: "Manually refresh OAuth tokens for all accounts to verify they're still valid.",
1137
1165
  args: {},
1138
1166
  async execute() {
1139
1167
  const storage = await loadAccounts();
1140
1168
  if (!storage || storage.accounts.length === 0) {
1141
- return "No OpenAI accounts configured. Run: opencode auth login";
1169
+ return "No Codex accounts configured. Run: opencode auth login";
1142
1170
  }
1143
1171
  const results = [
1144
1172
  `Refreshing ${storage.accounts.length} account(s):`,
@@ -1165,7 +1193,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1165
1193
  }
1166
1194
  catch (error) {
1167
1195
  const errorMsg = error instanceof Error ? error.message : String(error);
1168
- results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 50)}`);
1196
+ results.push(` ✗ ${label}: Error - ${errorMsg.slice(0, 120)}`);
1169
1197
  failedCount++;
1170
1198
  }
1171
1199
  }
@@ -1175,6 +1203,50 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1175
1203
  return results.join("\n");
1176
1204
  },
1177
1205
  }),
1206
+ "codex-export": tool({
1207
+ description: "Export accounts to a JSON file for backup or migration to another machine.",
1208
+ args: {
1209
+ path: tool.schema.string().describe("File path to export to (e.g., ~/codex-backup.json)"),
1210
+ force: tool.schema.boolean().optional().describe("Overwrite existing file (default: true)"),
1211
+ },
1212
+ async execute({ path: filePath, force }) {
1213
+ try {
1214
+ await exportAccounts(filePath, force ?? true);
1215
+ const storage = await loadAccounts();
1216
+ const count = storage?.accounts.length ?? 0;
1217
+ return `Exported ${count} account(s) to: ${filePath}`;
1218
+ }
1219
+ catch (error) {
1220
+ const msg = error instanceof Error ? error.message : String(error);
1221
+ return `Export failed: ${msg}`;
1222
+ }
1223
+ },
1224
+ }),
1225
+ "codex-import": tool({
1226
+ description: "Import accounts from a JSON file, merging with existing accounts.",
1227
+ args: {
1228
+ path: tool.schema.string().describe("File path to import from (e.g., ~/codex-backup.json)"),
1229
+ },
1230
+ async execute({ path: filePath }) {
1231
+ try {
1232
+ const result = await importAccounts(filePath);
1233
+ cachedAccountManager = null;
1234
+ const lines = [`Import complete.`, ``];
1235
+ if (result.imported > 0) {
1236
+ lines.push(`New accounts: ${result.imported}`);
1237
+ }
1238
+ if (result.skipped > 0) {
1239
+ lines.push(`Duplicates skipped: ${result.skipped}`);
1240
+ }
1241
+ lines.push(`Total accounts: ${result.total}`);
1242
+ return lines.join("\n");
1243
+ }
1244
+ catch (error) {
1245
+ const msg = error instanceof Error ? error.message : String(error);
1246
+ return `Import failed: ${msg}`;
1247
+ }
1248
+ },
1249
+ }),
1178
1250
  },
1179
1251
  };
1180
1252
  };