oc-codex-multi-auth 6.0.0 → 6.1.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 (270) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -18
  3. package/assets/readme-hero.svg +10 -10
  4. package/config/README.md +1 -1
  5. package/config/minimal-opencode.json +1 -1
  6. package/config/opencode-legacy.json +1 -1
  7. package/config/opencode-modern.json +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +98 -2520
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/accounts/persistence.d.ts +34 -0
  13. package/dist/lib/accounts/persistence.d.ts.map +1 -0
  14. package/dist/lib/accounts/persistence.js +153 -0
  15. package/dist/lib/accounts/persistence.js.map +1 -0
  16. package/dist/lib/accounts/recovery.d.ts +59 -0
  17. package/dist/lib/accounts/recovery.d.ts.map +1 -0
  18. package/dist/lib/accounts/recovery.js +229 -0
  19. package/dist/lib/accounts/recovery.js.map +1 -0
  20. package/dist/lib/accounts/rotation.d.ts +40 -0
  21. package/dist/lib/accounts/rotation.d.ts.map +1 -0
  22. package/dist/lib/accounts/rotation.js +204 -0
  23. package/dist/lib/accounts/rotation.js.map +1 -0
  24. package/dist/lib/accounts/state.d.ts +92 -0
  25. package/dist/lib/accounts/state.d.ts.map +1 -0
  26. package/dist/lib/accounts/state.js +366 -0
  27. package/dist/lib/accounts/state.js.map +1 -0
  28. package/dist/lib/accounts.d.ts +48 -91
  29. package/dist/lib/accounts.d.ts.map +1 -1
  30. package/dist/lib/accounts.js +121 -699
  31. package/dist/lib/accounts.js.map +1 -1
  32. package/dist/lib/auth/auth.d.ts +1 -1
  33. package/dist/lib/auth/auth.d.ts.map +1 -1
  34. package/dist/lib/auth/auth.js +5 -2
  35. package/dist/lib/auth/auth.js.map +1 -1
  36. package/dist/lib/auth/login-runner.d.ts +32 -0
  37. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  38. package/dist/lib/auth/login-runner.js +79 -64
  39. package/dist/lib/auth/login-runner.js.map +1 -1
  40. package/dist/lib/auth/server.d.ts.map +1 -1
  41. package/dist/lib/auth/server.js +3 -8
  42. package/dist/lib/auth/server.js.map +1 -1
  43. package/dist/lib/circuit-breaker.d.ts +53 -2
  44. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  45. package/dist/lib/circuit-breaker.js +65 -6
  46. package/dist/lib/circuit-breaker.js.map +1 -1
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +77 -32
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/error-sentinels.d.ts +20 -0
  51. package/dist/lib/error-sentinels.d.ts.map +1 -0
  52. package/dist/lib/error-sentinels.js +28 -0
  53. package/dist/lib/error-sentinels.js.map +1 -0
  54. package/dist/lib/errors.d.ts +100 -1
  55. package/dist/lib/errors.d.ts.map +1 -1
  56. package/dist/lib/errors.js +116 -1
  57. package/dist/lib/errors.js.map +1 -1
  58. package/dist/lib/logger.d.ts +14 -1
  59. package/dist/lib/logger.d.ts.map +1 -1
  60. package/dist/lib/logger.js +65 -5
  61. package/dist/lib/logger.js.map +1 -1
  62. package/dist/lib/oauth-constants.d.ts +18 -0
  63. package/dist/lib/oauth-constants.d.ts.map +1 -0
  64. package/dist/lib/oauth-constants.js +18 -0
  65. package/dist/lib/oauth-constants.js.map +1 -0
  66. package/dist/lib/oauth-success.d.ts +2 -0
  67. package/dist/lib/oauth-success.d.ts.map +1 -0
  68. package/dist/lib/oauth-success.html +711 -711
  69. package/dist/lib/oauth-success.js +714 -0
  70. package/dist/lib/oauth-success.js.map +1 -0
  71. package/dist/lib/prompts/codex.d.ts.map +1 -1
  72. package/dist/lib/prompts/codex.js +12 -3
  73. package/dist/lib/prompts/codex.js.map +1 -1
  74. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
  75. package/dist/lib/prompts/opencode-codex.js +3 -2
  76. package/dist/lib/prompts/opencode-codex.js.map +1 -1
  77. package/dist/lib/recovery/hook.d.ts +43 -0
  78. package/dist/lib/recovery/hook.d.ts.map +1 -0
  79. package/dist/lib/recovery/hook.js +325 -0
  80. package/dist/lib/recovery/hook.js.map +1 -0
  81. package/dist/lib/recovery/index.d.ts +1 -0
  82. package/dist/lib/recovery/index.d.ts.map +1 -1
  83. package/dist/lib/recovery/index.js +1 -0
  84. package/dist/lib/recovery/index.js.map +1 -1
  85. package/dist/lib/recovery/storage.d.ts.map +1 -1
  86. package/dist/lib/recovery/storage.js +5 -1
  87. package/dist/lib/recovery/storage.js.map +1 -1
  88. package/dist/lib/recovery.d.ts +15 -30
  89. package/dist/lib/recovery.d.ts.map +1 -1
  90. package/dist/lib/recovery.js +14 -312
  91. package/dist/lib/recovery.js.map +1 -1
  92. package/dist/lib/request/fetch-helpers.js +1 -1
  93. package/dist/lib/request/fetch-helpers.js.map +1 -1
  94. package/dist/lib/request/response-handler.d.ts.map +1 -1
  95. package/dist/lib/request/response-handler.js +8 -2
  96. package/dist/lib/request/response-handler.js.map +1 -1
  97. package/dist/lib/runtime.d.ts +101 -0
  98. package/dist/lib/runtime.d.ts.map +1 -0
  99. package/dist/lib/runtime.js +77 -0
  100. package/dist/lib/runtime.js.map +1 -0
  101. package/dist/lib/schemas.d.ts +164 -1
  102. package/dist/lib/schemas.d.ts.map +1 -1
  103. package/dist/lib/schemas.js +251 -0
  104. package/dist/lib/schemas.js.map +1 -1
  105. package/dist/lib/storage/atomic-write.d.ts +31 -0
  106. package/dist/lib/storage/atomic-write.d.ts.map +1 -0
  107. package/dist/lib/storage/atomic-write.js +75 -0
  108. package/dist/lib/storage/atomic-write.js.map +1 -0
  109. package/dist/lib/storage/backup.d.ts +13 -0
  110. package/dist/lib/storage/backup.d.ts.map +1 -0
  111. package/dist/lib/storage/backup.js +59 -0
  112. package/dist/lib/storage/backup.js.map +1 -0
  113. package/dist/lib/storage/errors.d.ts +17 -0
  114. package/dist/lib/storage/errors.d.ts.map +1 -0
  115. package/dist/lib/storage/errors.js +38 -0
  116. package/dist/lib/storage/errors.js.map +1 -0
  117. package/dist/lib/storage/export-import.d.ts +84 -0
  118. package/dist/lib/storage/export-import.d.ts.map +1 -0
  119. package/dist/lib/storage/export-import.js +222 -0
  120. package/dist/lib/storage/export-import.js.map +1 -0
  121. package/dist/lib/storage/flagged.d.ts +29 -0
  122. package/dist/lib/storage/flagged.d.ts.map +1 -0
  123. package/dist/lib/storage/flagged.js +207 -0
  124. package/dist/lib/storage/flagged.js.map +1 -0
  125. package/dist/lib/storage/identity.d.ts +72 -0
  126. package/dist/lib/storage/identity.d.ts.map +1 -0
  127. package/dist/lib/storage/identity.js +230 -0
  128. package/dist/lib/storage/identity.js.map +1 -0
  129. package/dist/lib/storage/keychain.d.ts +146 -0
  130. package/dist/lib/storage/keychain.d.ts.map +1 -0
  131. package/dist/lib/storage/keychain.js +278 -0
  132. package/dist/lib/storage/keychain.js.map +1 -0
  133. package/dist/lib/storage/load-save.d.ts +61 -0
  134. package/dist/lib/storage/load-save.d.ts.map +1 -0
  135. package/dist/lib/storage/load-save.js +606 -0
  136. package/dist/lib/storage/load-save.js.map +1 -0
  137. package/dist/lib/storage/migrations.d.ts +35 -0
  138. package/dist/lib/storage/migrations.d.ts.map +1 -1
  139. package/dist/lib/storage/migrations.js +37 -0
  140. package/dist/lib/storage/migrations.js.map +1 -1
  141. package/dist/lib/storage/normalize.d.ts +22 -0
  142. package/dist/lib/storage/normalize.d.ts.map +1 -0
  143. package/dist/lib/storage/normalize.js +114 -0
  144. package/dist/lib/storage/normalize.js.map +1 -0
  145. package/dist/lib/storage/paths.d.ts.map +1 -1
  146. package/dist/lib/storage/paths.js +2 -1
  147. package/dist/lib/storage/paths.js.map +1 -1
  148. package/dist/lib/storage/state.d.ts +46 -0
  149. package/dist/lib/storage/state.d.ts.map +1 -0
  150. package/dist/lib/storage/state.js +100 -0
  151. package/dist/lib/storage/state.js.map +1 -0
  152. package/dist/lib/storage/worktree-lock.d.ts +79 -0
  153. package/dist/lib/storage/worktree-lock.d.ts.map +1 -0
  154. package/dist/lib/storage/worktree-lock.js +265 -0
  155. package/dist/lib/storage/worktree-lock.js.map +1 -0
  156. package/dist/lib/storage.d.ts +21 -155
  157. package/dist/lib/storage.d.ts.map +1 -1
  158. package/dist/lib/storage.js +24 -1167
  159. package/dist/lib/storage.js.map +1 -1
  160. package/dist/lib/tools/_shared.d.ts +16 -0
  161. package/dist/lib/tools/_shared.d.ts.map +1 -0
  162. package/dist/lib/tools/_shared.js +21 -0
  163. package/dist/lib/tools/_shared.js.map +1 -0
  164. package/dist/lib/tools/codex-dashboard.d.ts +8 -0
  165. package/dist/lib/tools/codex-dashboard.d.ts.map +1 -0
  166. package/dist/lib/tools/codex-dashboard.js +176 -0
  167. package/dist/lib/tools/codex-dashboard.js.map +1 -0
  168. package/dist/lib/tools/codex-diag.d.ts +19 -0
  169. package/dist/lib/tools/codex-diag.d.ts.map +1 -0
  170. package/dist/lib/tools/codex-diag.js +139 -0
  171. package/dist/lib/tools/codex-diag.js.map +1 -0
  172. package/dist/lib/tools/codex-diff.d.ts +44 -0
  173. package/dist/lib/tools/codex-diff.d.ts.map +1 -0
  174. package/dist/lib/tools/codex-diff.js +284 -0
  175. package/dist/lib/tools/codex-diff.js.map +1 -0
  176. package/dist/lib/tools/codex-doctor.d.ts +8 -0
  177. package/dist/lib/tools/codex-doctor.d.ts.map +1 -0
  178. package/dist/lib/tools/codex-doctor.js +284 -0
  179. package/dist/lib/tools/codex-doctor.js.map +1 -0
  180. package/dist/lib/tools/codex-export.d.ts +8 -0
  181. package/dist/lib/tools/codex-export.d.ts.map +1 -0
  182. package/dist/lib/tools/codex-export.js +63 -0
  183. package/dist/lib/tools/codex-export.js.map +1 -0
  184. package/dist/lib/tools/codex-health.d.ts +8 -0
  185. package/dist/lib/tools/codex-health.d.ts.map +1 -0
  186. package/dist/lib/tools/codex-health.js +124 -0
  187. package/dist/lib/tools/codex-health.js.map +1 -0
  188. package/dist/lib/tools/codex-help.d.ts +8 -0
  189. package/dist/lib/tools/codex-help.d.ts.map +1 -0
  190. package/dist/lib/tools/codex-help.js +127 -0
  191. package/dist/lib/tools/codex-help.js.map +1 -0
  192. package/dist/lib/tools/codex-import.d.ts +8 -0
  193. package/dist/lib/tools/codex-import.d.ts.map +1 -0
  194. package/dist/lib/tools/codex-import.js +97 -0
  195. package/dist/lib/tools/codex-import.js.map +1 -0
  196. package/dist/lib/tools/codex-keychain.d.ts +36 -0
  197. package/dist/lib/tools/codex-keychain.d.ts.map +1 -0
  198. package/dist/lib/tools/codex-keychain.js +289 -0
  199. package/dist/lib/tools/codex-keychain.js.map +1 -0
  200. package/dist/lib/tools/codex-label.d.ts +8 -0
  201. package/dist/lib/tools/codex-label.d.ts.map +1 -0
  202. package/dist/lib/tools/codex-label.js +143 -0
  203. package/dist/lib/tools/codex-label.js.map +1 -0
  204. package/dist/lib/tools/codex-limits.d.ts +8 -0
  205. package/dist/lib/tools/codex-limits.d.ts.map +1 -0
  206. package/dist/lib/tools/codex-limits.js +497 -0
  207. package/dist/lib/tools/codex-limits.js.map +1 -0
  208. package/dist/lib/tools/codex-list.d.ts +8 -0
  209. package/dist/lib/tools/codex-list.d.ts.map +1 -0
  210. package/dist/lib/tools/codex-list.js +260 -0
  211. package/dist/lib/tools/codex-list.js.map +1 -0
  212. package/dist/lib/tools/codex-metrics.d.ts +8 -0
  213. package/dist/lib/tools/codex-metrics.d.ts.map +1 -0
  214. package/dist/lib/tools/codex-metrics.js +180 -0
  215. package/dist/lib/tools/codex-metrics.js.map +1 -0
  216. package/dist/lib/tools/codex-next.d.ts +8 -0
  217. package/dist/lib/tools/codex-next.d.ts.map +1 -0
  218. package/dist/lib/tools/codex-next.js +54 -0
  219. package/dist/lib/tools/codex-next.js.map +1 -0
  220. package/dist/lib/tools/codex-note.d.ts +8 -0
  221. package/dist/lib/tools/codex-note.d.ts.map +1 -0
  222. package/dist/lib/tools/codex-note.js +79 -0
  223. package/dist/lib/tools/codex-note.js.map +1 -0
  224. package/dist/lib/tools/codex-refresh.d.ts +8 -0
  225. package/dist/lib/tools/codex-refresh.d.ts.map +1 -0
  226. package/dist/lib/tools/codex-refresh.js +78 -0
  227. package/dist/lib/tools/codex-refresh.js.map +1 -0
  228. package/dist/lib/tools/codex-remove.d.ts +8 -0
  229. package/dist/lib/tools/codex-remove.d.ts.map +1 -0
  230. package/dist/lib/tools/codex-remove.js +184 -0
  231. package/dist/lib/tools/codex-remove.js.map +1 -0
  232. package/dist/lib/tools/codex-setup.d.ts +8 -0
  233. package/dist/lib/tools/codex-setup.d.ts.map +1 -0
  234. package/dist/lib/tools/codex-setup.js +26 -0
  235. package/dist/lib/tools/codex-setup.js.map +1 -0
  236. package/dist/lib/tools/codex-status.d.ts +8 -0
  237. package/dist/lib/tools/codex-status.d.ts.map +1 -0
  238. package/dist/lib/tools/codex-status.js +263 -0
  239. package/dist/lib/tools/codex-status.js.map +1 -0
  240. package/dist/lib/tools/codex-switch.d.ts +8 -0
  241. package/dist/lib/tools/codex-switch.d.ts.map +1 -0
  242. package/dist/lib/tools/codex-switch.js +122 -0
  243. package/dist/lib/tools/codex-switch.js.map +1 -0
  244. package/dist/lib/tools/codex-tag.d.ts +8 -0
  245. package/dist/lib/tools/codex-tag.d.ts.map +1 -0
  246. package/dist/lib/tools/codex-tag.js +101 -0
  247. package/dist/lib/tools/codex-tag.js.map +1 -0
  248. package/dist/lib/tools/index.d.ts +143 -0
  249. package/dist/lib/tools/index.d.ts.map +1 -0
  250. package/dist/lib/tools/index.js +67 -0
  251. package/dist/lib/tools/index.js.map +1 -0
  252. package/dist/lib/ui/format.d.ts.map +1 -1
  253. package/dist/lib/ui/format.js +5 -0
  254. package/dist/lib/ui/format.js.map +1 -1
  255. package/dist/lib/ui/runtime.d.ts +10 -0
  256. package/dist/lib/ui/runtime.d.ts.map +1 -1
  257. package/dist/lib/ui/runtime.js +16 -11
  258. package/dist/lib/ui/runtime.js.map +1 -1
  259. package/dist/lib/ui/select.d.ts.map +1 -1
  260. package/dist/lib/ui/select.js +12 -3
  261. package/dist/lib/ui/select.js.map +1 -1
  262. package/dist/lib/ui/theme.d.ts +19 -0
  263. package/dist/lib/ui/theme.d.ts.map +1 -1
  264. package/dist/lib/ui/theme.js +27 -0
  265. package/dist/lib/ui/theme.js.map +1 -1
  266. package/package.json +24 -16
  267. package/scripts/copy-oauth-success.js +16 -6
  268. package/scripts/install-oc-codex-multi-auth-core.js +462 -381
  269. package/scripts/install-oc-codex-multi-auth.js +15 -15
  270. package/scripts/test-all-models.sh +1 -1
package/dist/index.js CHANGED
@@ -18,15 +18,14 @@
18
18
  * For production applications, use the OpenAI Platform API: https://platform.openai.com/
19
19
  *
20
20
  * @license MIT (see LICENSE file)
21
- * @author numman-ali
21
+ * @author ndycode
22
22
  * @repository https://github.com/ndycode/oc-codex-multi-auth
23
23
 
24
24
  */
25
- import { tool } from "@opencode-ai/plugin/tool";
26
25
  import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./lib/auth/auth.js";
27
26
  import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCodeSession, } from "./lib/auth/device-code.js";
28
27
  import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
29
- import { queuedRefresh, getRefreshQueueMetrics } from "./lib/refresh-queue.js";
28
+ import { queuedRefresh } from "./lib/refresh-queue.js";
30
29
  import { openBrowserUrl } from "./lib/auth/browser.js";
31
30
  import { startLocalOAuthServer } from "./lib/auth/server.js";
32
31
  import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
@@ -35,34 +34,24 @@ import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PR
35
34
  import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
36
35
  import { checkAndNotify } from "./lib/auto-update-checker.js";
37
36
  import { handleContextOverflow } from "./lib/context-overflow.js";
38
- import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
39
- import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, previewImportAccounts, createTimestampedBackupPath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
37
+ import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
38
+ import { getStoragePath, loadAccounts, saveAccounts, clearAccounts, setStoragePath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
40
39
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
41
- import { createDeactivatedWorkspaceError, createUsageRequestTimeoutError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, } from "./lib/runtime-contracts.js";
40
+ import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, } from "./lib/error-sentinels.js";
42
41
  import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
43
42
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
44
43
  import { isEmptyResponse } from "./lib/request/response-handler.js";
44
+ import { getCircuitBreaker } from "./lib/circuit-breaker.js";
45
45
  import { RetryBudgetTracker, resolveRetryBudgetLimits, } from "./lib/request/retry-budget.js";
46
46
  import { addJitter } from "./lib/rotation.js";
47
- import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
48
47
  import { setUiRuntimeOptions } from "./lib/ui/runtime.js";
49
- import { paintUiText, formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
50
- import { buildBeginnerChecklist, buildBeginnerDoctorFindings, formatPromptCacheSnapshot, recommendBeginnerNextAction, summarizeBeginnerAccounts, } from "./lib/ui/beginner.js";
48
+ import { formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
49
+ import { buildBeginnerChecklist, recommendBeginnerNextAction, summarizeBeginnerAccounts, } from "./lib/ui/beginner.js";
51
50
  import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
52
51
  import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
53
52
  import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
54
- function matchesWorkspaceIdentity(account, identityKey) {
55
- return getWorkspaceIdentityKey(account) === identityKey;
56
- }
57
- function upsertFlaggedAccountRecord(accounts, record) {
58
- const identityKey = getWorkspaceIdentityKey(record);
59
- const existingIndex = accounts.findIndex((flagged) => matchesWorkspaceIdentity(flagged, identityKey));
60
- if (existingIndex >= 0) {
61
- accounts[existingIndex] = record;
62
- return;
63
- }
64
- accounts.push(record);
65
- }
53
+ import { matchesWorkspaceIdentity, upsertFlaggedAccountRecord, createRetryBudgetUsage, serializeSelectionExplainability, formatRoutingValue, formatExplainabilitySummary, } from "./lib/runtime.js";
54
+ import { createToolRegistry, } from "./lib/tools/index.js";
66
55
  /**
67
56
  * OpenAI Codex OAuth authentication plugin for opencode
68
57
  *
@@ -89,14 +78,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
89
78
  let startupPreflightShown = false;
90
79
  let beginnerSafeModeEnabled = false;
91
80
  const MIN_BACKOFF_MS = 100;
92
- const createRetryBudgetUsage = () => ({
93
- authRefresh: 0,
94
- network: 0,
95
- server: 0,
96
- rateLimitShort: 0,
97
- rateLimitGlobal: 0,
98
- emptyResponse: 0,
99
- });
100
81
  const runtimeMetrics = {
101
82
  startedAt: Date.now(),
102
83
  totalRequests: 0,
@@ -125,36 +106,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
125
106
  lastQuotaKey: null,
126
107
  lastSelectionSnapshot: null,
127
108
  };
128
- const toolOutputFormatSchema = () => tool.schema
129
- .string()
130
- .optional()
131
- .describe('Output format: "text" (default) or "json".');
132
- const toolSensitiveJsonSchema = () => tool.schema
133
- .boolean()
134
- .optional()
135
- .describe("Include raw account labels, emails, and account IDs in JSON output. Defaults to false.");
136
- const normalizeToolOutputFormat = (format) => {
137
- if (format === undefined)
138
- return "text";
139
- if (format === "text" || format === "json")
140
- return format;
141
- throw new Error(`Invalid format "${format}". Expected "text" or "json".`);
142
- };
143
- const renderJsonOutput = (payload) => JSON.stringify(payload, null, 2);
144
- const serializeSelectionExplainability = (entries) => entries.map((entry) => ({
145
- index: entry.index + 1,
146
- zeroBasedIndex: entry.index,
147
- enabled: entry.enabled,
148
- isCurrentForFamily: entry.isCurrentForFamily,
149
- eligible: entry.eligible,
150
- reasons: [...entry.reasons],
151
- healthScore: entry.healthScore,
152
- tokensAvailable: entry.tokensAvailable,
153
- rateLimitedUntil: typeof entry.rateLimitedUntil === "number" ? entry.rateLimitedUntil : null,
154
- coolingDownUntil: typeof entry.coolingDownUntil === "number" ? entry.coolingDownUntil : null,
155
- cooldownReason: entry.cooldownReason ?? null,
156
- lastUsed: entry.lastUsed,
157
- }));
158
109
  const buildRoutingVisibilitySnapshot = (options = {}) => {
159
110
  const snapshot = runtimeMetrics.lastSelectionSnapshot;
160
111
  const rawSelectedAccountIndex = options.selectedAccountIndex ??
@@ -177,14 +128,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
177
128
  selectionExplainability: serializeSelectionExplainability(options.selectionExplainability ?? snapshot?.explainability ?? []),
178
129
  };
179
130
  };
180
- const formatRoutingValue = (value) => {
181
- if (typeof value === "boolean")
182
- return value ? "yes" : "no";
183
- if (value === null || value === undefined || value === "")
184
- return "-";
185
- return String(value);
186
- };
187
- const formatExplainabilitySummary = (entry) => `Account ${entry.index}: ${entry.eligible ? "eligible" : "blocked"} | health=${Math.round(entry.healthScore)} | tokens=${entry.tokensAvailable.toFixed(1)} | ${entry.reasons.join(", ")}`;
188
131
  const buildJsonAccountIdentity = (index, options = {}) => ({
189
132
  index: index + 1,
190
133
  zeroBasedIndex: index,
@@ -453,10 +396,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
453
396
  const getStatusMarker = (ui, status) => {
454
397
  if (!ui.v2Enabled) {
455
398
  if (status === "ok")
456
- return "✓";
399
+ return "✓";
457
400
  if (status === "warning")
458
401
  return "!";
459
- return "✗";
402
+ return "✗";
460
403
  }
461
404
  if (status === "ok")
462
405
  return ui.theme.glyphs.check;
@@ -851,6 +794,56 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
851
794
  };
852
795
  // Initialize runtime UI settings once on plugin load; auth/tools refresh this dynamically.
853
796
  resolveUiRuntime();
797
+ // Build the shared ToolContext consumed by every codex-* tool factory.
798
+ // Mutable refs proxy plugin-closure `let` bindings so tool writes to
799
+ // `.current` propagate to the outer closure without exposing the raw
800
+ // variables (RC-1 Phase 2; see lib/tools/index.ts).
801
+ const ctx = {
802
+ cachedAccountManagerRef: {
803
+ get current() {
804
+ return cachedAccountManager;
805
+ },
806
+ set current(value) {
807
+ cachedAccountManager = value;
808
+ },
809
+ },
810
+ accountManagerPromiseRef: {
811
+ get current() {
812
+ return accountManagerPromise;
813
+ },
814
+ set current(value) {
815
+ accountManagerPromise = value;
816
+ },
817
+ },
818
+ runtimeMetrics,
819
+ beginnerSafeModeRef: {
820
+ get current() {
821
+ return beginnerSafeModeEnabled;
822
+ },
823
+ },
824
+ resolveUiRuntime,
825
+ getStatusMarker,
826
+ formatCommandAccountLabel,
827
+ normalizeAccountTags,
828
+ supportsInteractiveMenus,
829
+ promptAccountIndexSelection,
830
+ resolveActiveIndex,
831
+ getRateLimitResetTimeForFamily,
832
+ formatRateLimitEntry,
833
+ buildJsonAccountIdentity,
834
+ buildRoutingVisibilitySnapshot,
835
+ appendRoutingVisibilityText,
836
+ appendRoutingVisibilityUi,
837
+ toBeginnerAccountSnapshots,
838
+ getBeginnerRuntimeSnapshot,
839
+ formatDoctorSeverity,
840
+ formatDoctorSeverityText,
841
+ buildSetupChecklistState,
842
+ renderSetupChecklistOutput,
843
+ runSetupWizard,
844
+ invalidateAccountManagerCache,
845
+ upsertFlaggedAccountRecord,
846
+ };
854
847
  return {
855
848
  event: eventHandler,
856
849
  auth: {
@@ -1244,7 +1237,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1244
1237
  runtimeMetrics.accountRotations++;
1245
1238
  runtimeMetrics.lastError = err?.message ?? String(err);
1246
1239
  runtimeMetrics.lastErrorCategory = "auth-refresh";
1247
- const failures = accountManager.incrementAuthFailures(account);
1240
+ const failures = await accountManager.incrementAuthFailures(account);
1248
1241
  const accountLabel = formatAccountLabel(account, account.index);
1249
1242
  if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
1250
1243
  const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
@@ -1307,9 +1300,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1307
1300
  logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
1308
1301
  break;
1309
1302
  }
1303
+ // RC-8: per-(account, family) circuit-breaker key. The breaker gates
1304
+ // upstream calls so that repeated failures short-circuit to the
1305
+ // rotation path instead of hammering a degraded endpoint.
1306
+ const circuitBreakerKey = `${accountId}:${modelFamily}`;
1307
+ const circuitBreaker = getCircuitBreaker(circuitBreakerKey);
1310
1308
  while (true) {
1311
1309
  let response;
1312
1310
  const fetchStart = performance.now();
1311
+ // RC-8: consult the breaker BEFORE firing upstream. When the gate is
1312
+ // closed every call passes through unchanged. When the gate denies
1313
+ // (open within cooldown, or half-open with a probe already in
1314
+ // flight) we short-circuit to the rotation path instead of
1315
+ // retrying here. We classify the short-circuit as `circuit-open`
1316
+ // so observability traces and the runtime metrics agree with the
1317
+ // `CircuitOpenError` type exported from `lib/errors.ts`.
1318
+ const breakerCheck = circuitBreaker.canAttempt();
1319
+ if (!breakerCheck.allowed) {
1320
+ const shortCircuitMessage = `Circuit ${breakerCheck.state} for ${circuitBreakerKey}`;
1321
+ logWarn(`[circuit-breaker] ${shortCircuitMessage} (reason=${breakerCheck.reason ?? "denied"}). Rotating account.`);
1322
+ accountManager.refundToken(account, modelFamily, model);
1323
+ runtimeMetrics.accountRotations++;
1324
+ runtimeMetrics.lastError = shortCircuitMessage;
1325
+ runtimeMetrics.lastErrorCategory = "circuit-open";
1326
+ break;
1327
+ }
1313
1328
  // Merge user AbortSignal with timeout (Node 18 compatible - no AbortSignal.any)
1314
1329
  const fetchController = new AbortController();
1315
1330
  const requestTimeoutMs = fetchTimeoutMs;
@@ -1366,6 +1381,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1366
1381
  runtimeMetrics.lastErrorCategory = "network";
1367
1382
  accountManager.refundToken(account, modelFamily, model);
1368
1383
  accountManager.recordFailure(account, modelFamily, model);
1384
+ // RC-8: network failures feed the breaker so a degraded upstream
1385
+ // trips the gate for this (account, family) key after N hits
1386
+ // inside the failure window.
1387
+ circuitBreaker.recordFailure();
1369
1388
  break;
1370
1389
  }
1371
1390
  finally {
@@ -1573,6 +1592,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1573
1592
  runtimeMetrics.lastErrorCategory = "server";
1574
1593
  accountManager.refundToken(account, modelFamily, model);
1575
1594
  accountManager.recordFailure(account, modelFamily, model);
1595
+ // RC-8: 5xx responses are treated the same as network failures by
1596
+ // the breaker — they indicate an upstream fault rather than a
1597
+ // client-side classifier decision (401/403/404/429 are handled
1598
+ // upstream and do not feed the breaker).
1599
+ circuitBreaker.recordFailure();
1576
1600
  if (!consumeRetryBudget("server", `Server error ${response.status} on account ${account.index + 1}`)) {
1577
1601
  return errorResponse;
1578
1602
  }
@@ -1646,6 +1670,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1646
1670
  }
1647
1671
  }
1648
1672
  accountManager.recordSuccess(account, modelFamily, model);
1673
+ // RC-8: closes a half-open gate or prunes the failure window so a
1674
+ // sequence of successes keeps the breaker healthy.
1675
+ circuitBreaker.recordSuccess();
1649
1676
  runtimeMetrics.successfulRequests++;
1650
1677
  runtimeMetrics.lastError = null;
1651
1678
  runtimeMetrics.lastErrorCategory = null;
@@ -2551,2456 +2578,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2551
2578
  },
2552
2579
  ],
2553
2580
  },
2554
- tool: {
2555
- "codex-list": tool({
2556
- description: "List all Codex OAuth accounts and the current active index.",
2557
- args: {
2558
- tag: tool.schema
2559
- .string()
2560
- .optional()
2561
- .describe("Optional tag filter (e.g., work, personal, team-a)."),
2562
- format: toolOutputFormatSchema(),
2563
- includeSensitive: toolSensitiveJsonSchema(),
2564
- },
2565
- async execute({ tag, format, includeSensitive } = {}) {
2566
- const ui = resolveUiRuntime();
2567
- const storage = await loadAccounts();
2568
- const storePath = getStoragePath();
2569
- const outputFormat = normalizeToolOutputFormat(format);
2570
- const includeSensitiveOutput = includeSensitive === true;
2571
- const normalizedTag = tag?.trim().toLowerCase() ?? "";
2572
- const commandHints = [
2573
- "opencode auth login",
2574
- "codex-status",
2575
- "codex-dashboard",
2576
- "codex-metrics",
2577
- "codex-doctor",
2578
- "codex-setup",
2579
- "codex-next",
2580
- "codex-label",
2581
- "codex-tag",
2582
- "codex-note",
2583
- "codex-help",
2584
- ];
2585
- if (!storage || storage.accounts.length === 0) {
2586
- if (outputFormat === "json") {
2587
- return renderJsonOutput({
2588
- message: "No Codex accounts configured. Run: opencode auth login",
2589
- storagePath: storePath,
2590
- filterTag: normalizedTag || null,
2591
- totalAccounts: 0,
2592
- totalStoredAccounts: 0,
2593
- activeIndex: null,
2594
- accounts: [],
2595
- commands: commandHints,
2596
- });
2597
- }
2598
- if (ui.v2Enabled) {
2599
- return [
2600
- ...formatUiHeader(ui, "Codex accounts"),
2601
- "",
2602
- formatUiItem(ui, "No accounts configured.", "warning"),
2603
- formatUiItem(ui, "Run: opencode auth login", "accent"),
2604
- formatUiItem(ui, "Setup checklist: codex-setup"),
2605
- formatUiItem(ui, "Command guide: codex-help"),
2606
- formatUiKeyValue(ui, "Storage", storePath, "muted"),
2607
- ].join("\n");
2608
- }
2609
- return [
2610
- "No Codex accounts configured.",
2611
- "",
2612
- "Add accounts:",
2613
- " opencode auth login",
2614
- " codex-setup",
2615
- " codex-help",
2616
- "",
2617
- `Storage: ${storePath}`,
2618
- ].join("\n");
2619
- }
2620
- const now = Date.now();
2621
- const activeIndex = resolveActiveIndex(storage, "codex");
2622
- const filteredEntries = storage.accounts
2623
- .map((account, index) => ({ account, index }))
2624
- .filter(({ account }) => {
2625
- if (!normalizedTag)
2626
- return true;
2627
- const tags = Array.isArray(account.accountTags)
2628
- ? account.accountTags.map((entry) => entry.trim().toLowerCase())
2629
- : [];
2630
- return tags.includes(normalizedTag);
2631
- });
2632
- if (normalizedTag && filteredEntries.length === 0) {
2633
- if (outputFormat === "json") {
2634
- return renderJsonOutput({
2635
- message: `No accounts found for tag: ${normalizedTag}`,
2636
- storagePath: storePath,
2637
- filterTag: normalizedTag,
2638
- totalAccounts: 0,
2639
- totalStoredAccounts: storage.accounts.length,
2640
- activeIndex: activeIndex + 1,
2641
- accounts: [],
2642
- commands: commandHints,
2643
- });
2644
- }
2645
- if (ui.v2Enabled) {
2646
- return [
2647
- ...formatUiHeader(ui, "Codex accounts"),
2648
- "",
2649
- formatUiItem(ui, `No accounts found for tag: ${normalizedTag}`, "warning"),
2650
- formatUiItem(ui, "Use codex-tag index=2 tags=\"work,team-a\" to add tags.", "accent"),
2651
- ].join("\n");
2652
- }
2653
- return `No accounts found for tag: ${normalizedTag}\n\nUse codex-tag index=2 tags="work,team-a" to add tags.`;
2654
- }
2655
- if (outputFormat === "json") {
2656
- return renderJsonOutput({
2657
- totalAccounts: filteredEntries.length,
2658
- totalStoredAccounts: storage.accounts.length,
2659
- activeIndex: activeIndex + 1,
2660
- filterTag: normalizedTag || null,
2661
- storagePath: storePath,
2662
- accounts: filteredEntries.map(({ account, index }) => {
2663
- const rateLimit = formatRateLimitEntry(account, now);
2664
- const cooldown = formatCooldown(account, now);
2665
- const statuses = [];
2666
- if (index === activeIndex)
2667
- statuses.push("active");
2668
- if (account.enabled === false)
2669
- statuses.push("disabled");
2670
- if (rateLimit)
2671
- statuses.push("rate-limited");
2672
- if (cooldown)
2673
- statuses.push("cooldown");
2674
- if (statuses.length === 0)
2675
- statuses.push("ok");
2676
- return {
2677
- ...buildJsonAccountIdentity(index, {
2678
- includeSensitive: includeSensitiveOutput,
2679
- account,
2680
- }),
2681
- enabled: account.enabled !== false,
2682
- isActive: index === activeIndex,
2683
- rateLimit: rateLimit ?? null,
2684
- cooldown: cooldown ?? null,
2685
- tags: Array.isArray(account.accountTags) ? [...account.accountTags] : [],
2686
- note: account.accountNote ?? null,
2687
- statuses,
2688
- };
2689
- }),
2690
- commands: commandHints,
2691
- });
2692
- }
2693
- if (ui.v2Enabled) {
2694
- const lines = [
2695
- ...formatUiHeader(ui, "Codex accounts"),
2696
- formatUiKeyValue(ui, "Total", String(filteredEntries.length)),
2697
- normalizedTag
2698
- ? formatUiKeyValue(ui, "Filter tag", normalizedTag, "accent")
2699
- : formatUiKeyValue(ui, "Filter tag", "none", "muted"),
2700
- formatUiKeyValue(ui, "Storage", storePath, "muted"),
2701
- "",
2702
- ...formatUiSection(ui, "Accounts"),
2703
- ];
2704
- filteredEntries.forEach(({ account, index }) => {
2705
- const label = formatCommandAccountLabel(account, index);
2706
- const badges = [];
2707
- if (index === activeIndex)
2708
- badges.push(formatUiBadge(ui, "current", "accent"));
2709
- if (account.enabled === false)
2710
- badges.push(formatUiBadge(ui, "disabled", "danger"));
2711
- const rateLimit = formatRateLimitEntry(account, now);
2712
- if (rateLimit)
2713
- badges.push(formatUiBadge(ui, "rate-limited", "warning"));
2714
- if (typeof account.coolingDownUntil === "number" &&
2715
- account.coolingDownUntil > now) {
2716
- badges.push(formatUiBadge(ui, "cooldown", "warning"));
2717
- }
2718
- if (badges.length === 0) {
2719
- badges.push(formatUiBadge(ui, "ok", "success"));
2720
- }
2721
- lines.push(formatUiItem(ui, `${label} ${badges.join(" ")}`.trim()));
2722
- if (rateLimit) {
2723
- lines.push(` ${paintUiText(ui, `rate limit: ${rateLimit}`, "muted")}`);
2724
- }
2725
- });
2726
- lines.push("");
2727
- lines.push(...formatUiSection(ui, "Commands"));
2728
- lines.push(formatUiItem(ui, "Add account: opencode auth login", "accent"));
2729
- lines.push(formatUiItem(ui, "Switch account: codex-switch index=2"));
2730
- lines.push(formatUiItem(ui, "Detailed status: codex-status"));
2731
- lines.push(formatUiItem(ui, "Live dashboard: codex-dashboard"));
2732
- lines.push(formatUiItem(ui, "Runtime metrics: codex-metrics"));
2733
- lines.push(formatUiItem(ui, "Set account tags: codex-tag index=2 tags=\"work,team-a\""));
2734
- lines.push(formatUiItem(ui, "Set account note: codex-note index=2 note=\"weekday primary\""));
2735
- lines.push(formatUiItem(ui, "Doctor checks: codex-doctor"));
2736
- lines.push(formatUiItem(ui, "Onboarding checklist: codex-setup"));
2737
- lines.push(formatUiItem(ui, "Guided setup wizard: codex-setup --wizard"));
2738
- lines.push(formatUiItem(ui, "Best next action: codex-next"));
2739
- lines.push(formatUiItem(ui, "Rename account label: codex-label index=2 label=\"Work\""));
2740
- lines.push(formatUiItem(ui, "Command guide: codex-help"));
2741
- return lines.join("\n");
2742
- }
2743
- const listTableOptions = {
2744
- columns: [
2745
- { header: "#", width: 3 },
2746
- { header: "Label", width: 42 },
2747
- { header: "Status", width: 20 },
2748
- ],
2749
- };
2750
- const lines = [
2751
- `Codex Accounts (${filteredEntries.length}):`,
2752
- "",
2753
- ...buildTableHeader(listTableOptions),
2754
- ];
2755
- filteredEntries.forEach(({ account, index }) => {
2756
- const label = formatCommandAccountLabel(account, index);
2757
- const statuses = [];
2758
- const rateLimit = formatRateLimitEntry(account, now);
2759
- if (index === activeIndex)
2760
- statuses.push("active");
2761
- if (rateLimit)
2762
- statuses.push("rate-limited");
2763
- if (typeof account.coolingDownUntil ===
2764
- "number" &&
2765
- account.coolingDownUntil > now) {
2766
- statuses.push("cooldown");
2767
- }
2768
- const statusText = statuses.length > 0 ? statuses.join(", ") : "ok";
2769
- lines.push(buildTableRow([String(index + 1), label, statusText], listTableOptions));
2770
- });
2771
- lines.push("");
2772
- lines.push(`Storage: ${storePath}`);
2773
- if (normalizedTag) {
2774
- lines.push(`Filter tag: ${normalizedTag}`);
2775
- }
2776
- lines.push("");
2777
- lines.push("Commands:");
2778
- lines.push(" - Add account: opencode auth login");
2779
- lines.push(" - Switch account: codex-switch");
2780
- lines.push(" - Status details: codex-status");
2781
- lines.push(" - Live dashboard: codex-dashboard");
2782
- lines.push(" - Runtime metrics: codex-metrics");
2783
- lines.push(" - Set account tags: codex-tag");
2784
- lines.push(" - Set account note: codex-note");
2785
- lines.push(" - Doctor checks: codex-doctor");
2786
- lines.push(" - Setup checklist: codex-setup");
2787
- lines.push(" - Guided setup wizard: codex-setup --wizard");
2788
- lines.push(" - Best next action: codex-next");
2789
- lines.push(" - Rename account label: codex-label");
2790
- lines.push(" - Command guide: codex-help");
2791
- return lines.join("\n");
2792
- },
2793
- }),
2794
- "codex-switch": tool({
2795
- description: "Switch active Codex account by index (1-based) or interactive picker when index is omitted.",
2796
- args: {
2797
- index: tool.schema.number().optional().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
2798
- },
2799
- async execute({ index } = {}) {
2800
- const ui = resolveUiRuntime();
2801
- const storage = await loadAccounts();
2802
- if (!storage || storage.accounts.length === 0) {
2803
- if (ui.v2Enabled) {
2804
- return [
2805
- ...formatUiHeader(ui, "Switch account"),
2806
- "",
2807
- formatUiItem(ui, "No accounts configured.", "warning"),
2808
- formatUiItem(ui, "Run: opencode auth login", "accent"),
2809
- ].join("\n");
2810
- }
2811
- return "No Codex accounts configured. Run: opencode auth login";
2812
- }
2813
- let resolvedIndex = index;
2814
- if (resolvedIndex === undefined) {
2815
- const selectedIndex = await promptAccountIndexSelection(ui, storage, "Switch account");
2816
- if (selectedIndex === null) {
2817
- if (supportsInteractiveMenus()) {
2818
- if (ui.v2Enabled) {
2819
- return [
2820
- ...formatUiHeader(ui, "Switch account"),
2821
- "",
2822
- formatUiItem(ui, "No account selected.", "warning"),
2823
- formatUiItem(ui, "Run again and pick an account, or pass codex-switch index=2.", "muted"),
2824
- ].join("\n");
2825
- }
2826
- return "No account selected.";
2827
- }
2828
- if (ui.v2Enabled) {
2829
- return [
2830
- ...formatUiHeader(ui, "Switch account"),
2831
- "",
2832
- formatUiItem(ui, "Missing account number.", "warning"),
2833
- formatUiItem(ui, "Use: codex-switch index=2", "accent"),
2834
- ].join("\n");
2835
- }
2836
- return "Missing account number. Use: codex-switch index=2";
2837
- }
2838
- resolvedIndex = selectedIndex + 1;
2839
- }
2840
- const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
2841
- if (!Number.isFinite(targetIndex) ||
2842
- targetIndex < 0 ||
2843
- targetIndex >= storage.accounts.length) {
2844
- if (ui.v2Enabled) {
2845
- return [
2846
- ...formatUiHeader(ui, "Switch account"),
2847
- "",
2848
- formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
2849
- formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
2850
- ].join("\n");
2851
- }
2852
- return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
2853
- }
2854
- const now = Date.now();
2855
- const account = storage.accounts[targetIndex];
2856
- if (account) {
2857
- account.lastUsed = now;
2858
- account.lastSwitchReason = "rotation";
2859
- }
2860
- storage.activeIndex = targetIndex;
2861
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
2862
- for (const family of MODEL_FAMILIES) {
2863
- storage.activeIndexByFamily[family] = targetIndex;
2864
- }
2865
- try {
2866
- await saveAccounts(storage);
2867
- }
2868
- catch (saveError) {
2869
- logWarn("Failed to save account switch", { error: String(saveError) });
2870
- const label = formatCommandAccountLabel(account, targetIndex);
2871
- if (ui.v2Enabled) {
2872
- return [
2873
- ...formatUiHeader(ui, "Switch account"),
2874
- "",
2875
- formatUiItem(ui, `Switched to ${label}`, "warning"),
2876
- formatUiItem(ui, "Failed to persist change. It may be lost on restart.", "danger"),
2877
- ].join("\n");
2878
- }
2879
- return `Switched to ${label} but failed to persist. Changes may be lost on restart.`;
2880
- }
2881
- if (cachedAccountManager) {
2882
- const reloadedManager = await AccountManager.loadFromDisk();
2883
- cachedAccountManager = reloadedManager;
2884
- accountManagerPromise = Promise.resolve(reloadedManager);
2885
- }
2886
- const label = formatCommandAccountLabel(account, targetIndex);
2887
- if (ui.v2Enabled) {
2888
- return [
2889
- ...formatUiHeader(ui, "Switch account"),
2890
- "",
2891
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Switched to ${label}`, "success"),
2892
- ].join("\n");
2893
- }
2894
- return `Switched to account: ${label}`;
2895
- },
2896
- }),
2897
- "codex-status": tool({
2898
- description: "Show detailed status of Codex accounts and rate limits.",
2899
- args: {
2900
- format: toolOutputFormatSchema(),
2901
- includeSensitive: toolSensitiveJsonSchema(),
2902
- },
2903
- async execute({ format, includeSensitive } = {}) {
2904
- const ui = resolveUiRuntime();
2905
- const outputFormat = normalizeToolOutputFormat(format);
2906
- const includeSensitiveOutput = includeSensitive === true;
2907
- const storage = await loadAccounts();
2908
- if (!storage || storage.accounts.length === 0) {
2909
- if (outputFormat === "json") {
2910
- return renderJsonOutput({
2911
- message: "No Codex accounts configured. Run: opencode auth login",
2912
- totalAccounts: 0,
2913
- accounts: [],
2914
- activeIndexByFamily: {},
2915
- rateLimitsByModelFamily: [],
2916
- routingVisibility: buildRoutingVisibilitySnapshot(),
2917
- recommendedNextAction: "Run opencode auth login",
2918
- });
2919
- }
2920
- if (ui.v2Enabled) {
2921
- return [
2922
- ...formatUiHeader(ui, "Account status"),
2923
- "",
2924
- formatUiItem(ui, "No accounts configured.", "warning"),
2925
- formatUiItem(ui, "Run: opencode auth login", "accent"),
2926
- ].join("\n");
2927
- }
2928
- return "No Codex accounts configured. Run: opencode auth login";
2929
- }
2930
- const now = Date.now();
2931
- const activeIndex = resolveActiveIndex(storage, "codex");
2932
- const explainabilityFamily = runtimeMetrics.lastSelectionSnapshot?.family ?? "codex";
2933
- const explainabilityModel = runtimeMetrics.lastSelectionSnapshot?.effectiveModel ??
2934
- runtimeMetrics.lastSelectionSnapshot?.model ??
2935
- undefined;
2936
- const managerForExplainability = cachedAccountManager ?? (await AccountManager.loadFromDisk());
2937
- const explainability = managerForExplainability.getSelectionExplainability(explainabilityFamily, explainabilityModel, now);
2938
- const selectionQuotaKey = explainabilityModel
2939
- ? `${explainabilityFamily}:${explainabilityModel}`
2940
- : explainabilityFamily;
2941
- const routingVisibility = buildRoutingVisibilitySnapshot({
2942
- modelFamily: explainabilityFamily,
2943
- effectiveModel: explainabilityModel ?? null,
2944
- quotaKey: selectionQuotaKey,
2945
- selectedAccountIndex: activeIndex,
2946
- selectionExplainability: explainability,
2947
- });
2948
- const explainabilityByIndex = new Map(explainability.map((entry) => [entry.index, entry]));
2949
- const recommendedNextAction = recommendBeginnerNextAction({
2950
- accounts: toBeginnerAccountSnapshots(storage, activeIndex, now),
2951
- now,
2952
- runtime: getBeginnerRuntimeSnapshot(),
2953
- });
2954
- if (outputFormat === "json") {
2955
- return renderJsonOutput({
2956
- totalAccounts: storage.accounts.length,
2957
- selectionView: {
2958
- modelFamily: explainabilityFamily,
2959
- effectiveModel: explainabilityModel ?? null,
2960
- label: explainabilityModel
2961
- ? `${explainabilityFamily}:${explainabilityModel}`
2962
- : explainabilityFamily,
2963
- },
2964
- accounts: storage.accounts.map((account, index) => ({
2965
- ...buildJsonAccountIdentity(index, {
2966
- includeSensitive: includeSensitiveOutput,
2967
- account,
2968
- }),
2969
- enabled: account.enabled !== false,
2970
- isActive: index === activeIndex,
2971
- rateLimit: formatRateLimitEntry(account, now) ?? null,
2972
- cooldown: formatCooldown(account, now) ?? null,
2973
- lastUsedAgeMs: typeof account.lastUsed === "number" && account.lastUsed > 0
2974
- ? Math.max(0, now - account.lastUsed)
2975
- : null,
2976
- })),
2977
- activeIndexByFamily: Object.fromEntries(MODEL_FAMILIES.map((family) => [
2978
- family,
2979
- typeof storage.activeIndexByFamily?.[family] === "number"
2980
- ? (storage.activeIndexByFamily?.[family] ?? 0) + 1
2981
- : null,
2982
- ])),
2983
- rateLimitsByModelFamily: storage.accounts.map((account, index) => ({
2984
- ...buildJsonAccountIdentity(index, {
2985
- includeSensitive: includeSensitiveOutput,
2986
- account,
2987
- }),
2988
- families: Object.fromEntries(MODEL_FAMILIES.map((family) => {
2989
- const resetAt = getRateLimitResetTimeForFamily(account, now, family);
2990
- return [
2991
- family,
2992
- typeof resetAt === "number"
2993
- ? {
2994
- resetAtMs: resetAt,
2995
- wait: formatWaitTime(resetAt - now),
2996
- }
2997
- : null,
2998
- ];
2999
- })),
3000
- })),
3001
- routingVisibility,
3002
- recommendedNextAction,
3003
- });
3004
- }
3005
- if (ui.v2Enabled) {
3006
- const lines = [
3007
- ...formatUiHeader(ui, "Account status"),
3008
- formatUiKeyValue(ui, "Total", String(storage.accounts.length)),
3009
- formatUiKeyValue(ui, "Selection view", explainabilityModel
3010
- ? `${explainabilityFamily}:${explainabilityModel}`
3011
- : explainabilityFamily, "muted"),
3012
- "",
3013
- ...formatUiSection(ui, "Accounts"),
3014
- ];
3015
- storage.accounts.forEach((account, index) => {
3016
- const label = formatCommandAccountLabel(account, index);
3017
- const badges = [];
3018
- if (index === activeIndex)
3019
- badges.push(formatUiBadge(ui, "active", "accent"));
3020
- if (account.enabled === false)
3021
- badges.push(formatUiBadge(ui, "disabled", "danger"));
3022
- const rateLimit = formatRateLimitEntry(account, now) ?? "none";
3023
- const cooldown = formatCooldown(account, now) ?? "none";
3024
- if (rateLimit !== "none")
3025
- badges.push(formatUiBadge(ui, "rate-limited", "warning"));
3026
- if (cooldown !== "none")
3027
- badges.push(formatUiBadge(ui, "cooldown", "warning"));
3028
- if (badges.length === 0)
3029
- badges.push(formatUiBadge(ui, "ok", "success"));
3030
- lines.push(formatUiItem(ui, `${label} ${badges.join(" ")}`.trim()));
3031
- lines.push(` ${formatUiKeyValue(ui, "rate limit", rateLimit, rateLimit === "none" ? "muted" : "warning")}`);
3032
- lines.push(` ${formatUiKeyValue(ui, "cooldown", cooldown, cooldown === "none" ? "muted" : "warning")}`);
3033
- });
3034
- lines.push("");
3035
- lines.push(...formatUiSection(ui, "Active index by model family"));
3036
- for (const family of MODEL_FAMILIES) {
3037
- const idx = storage.activeIndexByFamily?.[family];
3038
- const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
3039
- lines.push(formatUiItem(ui, `${family}: ${familyIndexLabel}`));
3040
- }
3041
- lines.push("");
3042
- lines.push(...formatUiSection(ui, "Rate limits by model family (per account)"));
3043
- storage.accounts.forEach((account, index) => {
3044
- const statuses = MODEL_FAMILIES.map((family) => {
3045
- const resetAt = getRateLimitResetTimeForFamily(account, now, family);
3046
- if (typeof resetAt !== "number")
3047
- return `${family}=ok`;
3048
- return `${family}=${formatWaitTime(resetAt - now)}`;
3049
- });
3050
- lines.push(formatUiItem(ui, `Account ${index + 1}: ${statuses.join(" | ")}`));
3051
- });
3052
- lines.push("");
3053
- appendRoutingVisibilityUi(ui, lines, routingVisibility);
3054
- lines.push("");
3055
- lines.push(...formatUiSection(ui, "Selection explainability"));
3056
- for (const entry of explainability) {
3057
- const state = entry.eligible ? "eligible" : "blocked";
3058
- const reasons = entry.reasons.join(", ");
3059
- lines.push(formatUiItem(ui, `Account ${entry.index + 1}: ${state} | health=${Math.round(entry.healthScore)} | tokens=${entry.tokensAvailable.toFixed(1)} | ${reasons}`));
3060
- }
3061
- lines.push("");
3062
- lines.push(...formatUiSection(ui, "Recommended next step"));
3063
- lines.push(formatUiItem(ui, recommendedNextAction, "accent"));
3064
- return lines.join("\n");
3065
- }
3066
- const statusTableOptions = {
3067
- columns: [
3068
- { header: "#", width: 3 },
3069
- { header: "Label", width: 42 },
3070
- { header: "Active", width: 6 },
3071
- { header: "Rate Limit", width: 16 },
3072
- { header: "Cooldown", width: 16 },
3073
- { header: "Last Used", width: 16 },
3074
- ],
3075
- };
3076
- const lines = [
3077
- `Account Status (${storage.accounts.length} total):`,
3078
- "",
3079
- ...buildTableHeader(statusTableOptions),
3080
- ];
3081
- storage.accounts.forEach((account, index) => {
3082
- const label = formatCommandAccountLabel(account, index);
3083
- const active = index === activeIndex ? "Yes" : "No";
3084
- const rateLimit = formatRateLimitEntry(account, now) ?? "None";
3085
- const cooldown = formatCooldown(account, now) ?? "No";
3086
- const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
3087
- ? `${formatWaitTime(now - account.lastUsed)} ago`
3088
- : "-";
3089
- lines.push(buildTableRow([String(index + 1), label, active, rateLimit, cooldown, lastUsed], statusTableOptions));
3090
- });
3091
- lines.push("");
3092
- lines.push("Active index by model family:");
3093
- for (const family of MODEL_FAMILIES) {
3094
- const idx = storage.activeIndexByFamily?.[family];
3095
- const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
3096
- lines.push(` ${family}: ${familyIndexLabel}`);
3097
- }
3098
- lines.push("");
3099
- lines.push("Rate limits by model family (per account):");
3100
- storage.accounts.forEach((account, index) => {
3101
- const statuses = MODEL_FAMILIES.map((family) => {
3102
- const resetAt = getRateLimitResetTimeForFamily(account, now, family);
3103
- if (typeof resetAt !== "number")
3104
- return `${family}=ok`;
3105
- return `${family}=${formatWaitTime(resetAt - now)}`;
3106
- });
3107
- lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
3108
- });
3109
- lines.push("");
3110
- appendRoutingVisibilityText(lines, routingVisibility);
3111
- lines.push("");
3112
- lines.push(`Selection explainability (${explainabilityModel ? `${explainabilityFamily}:${explainabilityModel}` : explainabilityFamily}):`);
3113
- for (const [index] of storage.accounts.entries()) {
3114
- const details = explainabilityByIndex.get(index);
3115
- if (!details)
3116
- continue;
3117
- const state = details.eligible ? "eligible" : "blocked";
3118
- lines.push(` Account ${index + 1}: ${state} | health=${Math.round(details.healthScore)} | tokens=${details.tokensAvailable.toFixed(1)} | ${details.reasons.join(", ")}`);
3119
- }
3120
- lines.push("");
3121
- lines.push(`Recommended next step: ${recommendedNextAction}`);
3122
- return lines.join("\n");
3123
- },
3124
- }),
3125
- "codex-limits": tool({
3126
- description: "Show live 5-hour and weekly Codex usage limits for all accounts.",
3127
- args: {
3128
- format: toolOutputFormatSchema(),
3129
- includeSensitive: toolSensitiveJsonSchema(),
3130
- },
3131
- async execute({ format, includeSensitive } = {}) {
3132
- const ui = resolveUiRuntime();
3133
- const outputFormat = normalizeToolOutputFormat(format);
3134
- const includeSensitiveOutput = includeSensitive === true;
3135
- const storage = await loadAccounts();
3136
- if (!storage || storage.accounts.length === 0) {
3137
- if (outputFormat === "json") {
3138
- return renderJsonOutput({
3139
- message: "No Codex accounts configured. Run: opencode auth login",
3140
- totalAccounts: 0,
3141
- uniqueCredentialCount: 0,
3142
- activeIndex: null,
3143
- accounts: [],
3144
- });
3145
- }
3146
- if (ui.v2Enabled) {
3147
- return [
3148
- ...formatUiHeader(ui, "Codex limits"),
3149
- "",
3150
- formatUiItem(ui, "No accounts configured.", "warning"),
3151
- formatUiItem(ui, "Run: opencode auth login", "accent"),
3152
- ].join("\n");
3153
- }
3154
- return "No Codex accounts configured. Run: opencode auth login";
3155
- }
3156
- const formatWindowLabel = (windowMinutes) => {
3157
- if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
3158
- return "quota";
3159
- }
3160
- if (windowMinutes % 1440 === 0)
3161
- return `${windowMinutes / 1440}d`;
3162
- if (windowMinutes % 60 === 0)
3163
- return `${windowMinutes / 60}h`;
3164
- return `${windowMinutes}m`;
3165
- };
3166
- const formatReset = (resetAtMs) => {
3167
- if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
3168
- return undefined;
3169
- const date = new Date(resetAtMs);
3170
- if (!Number.isFinite(date.getTime()))
3171
- return undefined;
3172
- const now = new Date();
3173
- const sameDay = now.getFullYear() === date.getFullYear() &&
3174
- now.getMonth() === date.getMonth() &&
3175
- now.getDate() === date.getDate();
3176
- const time = date.toLocaleTimeString(undefined, {
3177
- hour: "2-digit",
3178
- minute: "2-digit",
3179
- hour12: false,
3180
- });
3181
- if (sameDay)
3182
- return time;
3183
- const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
3184
- return `${time} on ${day}`;
3185
- };
3186
- const mapWindow = (window) => {
3187
- if (!window)
3188
- return {};
3189
- return {
3190
- usedPercent: typeof window.used_percent === "number" && Number.isFinite(window.used_percent)
3191
- ? window.used_percent
3192
- : undefined,
3193
- windowMinutes: typeof window.limit_window_seconds === "number" && Number.isFinite(window.limit_window_seconds)
3194
- ? Math.max(1, Math.ceil(window.limit_window_seconds / 60))
3195
- : undefined,
3196
- resetAtMs: typeof window.reset_at === "number" && window.reset_at > 0
3197
- ? window.reset_at * 1000
3198
- : typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0
3199
- ? Date.now() + window.reset_after_seconds * 1000
3200
- : undefined,
3201
- };
3202
- };
3203
- const formatLimitTitle = (windowMinutes, fallback = "quota") => {
3204
- if (windowMinutes === 300)
3205
- return "5h limit";
3206
- if (windowMinutes === 10080)
3207
- return "Weekly limit";
3208
- if (fallback !== "quota")
3209
- return fallback;
3210
- return `${formatWindowLabel(windowMinutes)} limit`;
3211
- };
3212
- const formatLimitSummary = (window) => {
3213
- const used = window.usedPercent;
3214
- const left = typeof used === "number" && Number.isFinite(used)
3215
- ? Math.max(0, Math.min(100, Math.round(100 - used)))
3216
- : undefined;
3217
- const reset = formatReset(window.resetAtMs);
3218
- if (left !== undefined && reset)
3219
- return `${left}% left (resets ${reset})`;
3220
- if (left !== undefined)
3221
- return `${left}% left`;
3222
- if (reset)
3223
- return `resets ${reset}`;
3224
- return "unavailable";
3225
- };
3226
- const toLimitPayload = (name, window) => ({
3227
- name,
3228
- windowMinutes: window.windowMinutes ?? null,
3229
- usedPercent: typeof window.usedPercent === "number" ? window.usedPercent : null,
3230
- leftPercent: typeof window.usedPercent === "number"
3231
- ? Math.max(0, Math.min(100, Math.round(100 - window.usedPercent)))
3232
- : null,
3233
- resetAtMs: window.resetAtMs ?? null,
3234
- summary: formatLimitSummary(window),
3235
- });
3236
- const formatCredits = (credits) => {
3237
- if (!credits)
3238
- return undefined;
3239
- if (credits.unlimited)
3240
- return "unlimited";
3241
- if (typeof credits.balance === "string" && credits.balance.trim()) {
3242
- return credits.balance.trim();
3243
- }
3244
- if (credits.has_credits)
3245
- return "available";
3246
- return undefined;
3247
- };
3248
- const formatExtraName = (name) => {
3249
- if (!name)
3250
- return "Additional limit";
3251
- if (name === "code_review_rate_limit")
3252
- return "Code review";
3253
- return name.replace(/[_-]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
3254
- };
3255
- const sanitizeUsageErrorMessage = (status, bodyText) => {
3256
- const normalized = bodyText.replace(/\s+/g, " ").trim();
3257
- const redacted = normalized
3258
- .replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
3259
- .replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted-token]")
3260
- .replace(/\bsk-[A-Za-z0-9][A-Za-z0-9._:-]{19,}\b/gi, "[redacted-token]")
3261
- .replace(/\b[a-f0-9]{40,}\b/gi, "[redacted-token]");
3262
- return redacted ? `HTTP ${status}: ${redacted.slice(0, 200)}` : `HTTP ${status}`;
3263
- };
3264
- const isAbortError = (error) => (error instanceof Error && error.name === "AbortError") ||
3265
- (typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError");
3266
- const applyRefreshedCredentials = (target, result) => {
3267
- target.refreshToken = result.refresh;
3268
- target.accessToken = result.access;
3269
- target.expiresAt = result.expires;
3270
- };
3271
- const usageErrorBodyMaxChars = 4096;
3272
- const persistRefreshedCredentials = async (params) => {
3273
- return await withAccountStorageTransaction(async (current, persist) => {
3274
- const latestStorage = current ??
3275
- {
3276
- version: 3,
3277
- accounts: [],
3278
- activeIndex: 0,
3279
- activeIndexByFamily: {},
3280
- };
3281
- const uniqueMatch = (matches) => matches.length === 1 ? matches[0] : undefined;
3282
- let updated = false;
3283
- if (params.previousRefreshToken) {
3284
- for (const storedAccount of latestStorage.accounts) {
3285
- if (storedAccount.refreshToken === params.previousRefreshToken) {
3286
- applyRefreshedCredentials(storedAccount, params.refreshResult);
3287
- updated = true;
3288
- }
3289
- }
3290
- }
3291
- if (!updated) {
3292
- const normalizedOrganizationId = params.organizationId?.trim() ?? "";
3293
- const normalizedEmail = params.email?.trim().toLowerCase();
3294
- const orgScopedMatches = params.accountId
3295
- ? latestStorage.accounts.filter((storedAccount) => storedAccount.accountId === params.accountId &&
3296
- (storedAccount.organizationId?.trim() ?? "") === normalizedOrganizationId)
3297
- : [];
3298
- const accountIdMatches = params.accountId
3299
- ? latestStorage.accounts.filter((storedAccount) => storedAccount.accountId === params.accountId)
3300
- : [];
3301
- const emailMatches = normalizedEmail && !params.accountId
3302
- ? latestStorage.accounts.filter((storedAccount) => storedAccount.email?.trim().toLowerCase() === normalizedEmail)
3303
- : [];
3304
- const fallbackTarget = uniqueMatch(orgScopedMatches) ??
3305
- uniqueMatch(accountIdMatches) ??
3306
- uniqueMatch(emailMatches);
3307
- if (fallbackTarget) {
3308
- applyRefreshedCredentials(fallbackTarget, params.refreshResult);
3309
- updated = true;
3310
- }
3311
- }
3312
- if (updated) {
3313
- await persist(latestStorage);
3314
- }
3315
- if (!updated) {
3316
- logWarn(`[${PLUGIN_NAME}] persistRefreshedCredentials could not find a matching stored account. Refreshed credentials remain in-memory for this invocation only.`, {
3317
- accountId: params.accountId,
3318
- organizationId: params.organizationId,
3319
- });
3320
- }
3321
- return updated;
3322
- });
3323
- };
3324
- const usageFetchTimeoutMs = getFetchTimeoutMs(loadPluginConfig());
3325
- const fetchUsage = async (params) => {
3326
- const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
3327
- organizationId: params.organizationId,
3328
- });
3329
- headers.set("accept", "application/json");
3330
- const controller = new AbortController();
3331
- const timeout = setTimeout(() => controller.abort(), usageFetchTimeoutMs);
3332
- try {
3333
- const response = await fetch(`${CODEX_BASE_URL}/wham/usage`, {
3334
- method: "GET",
3335
- headers,
3336
- signal: controller.signal,
3337
- });
3338
- if (!response.ok) {
3339
- let bodyText = "";
3340
- try {
3341
- bodyText = (await response.text()).slice(0, usageErrorBodyMaxChars);
3342
- }
3343
- catch (error) {
3344
- if (isAbortError(error) || controller.signal.aborted) {
3345
- throw createUsageRequestTimeoutError();
3346
- }
3347
- throw error;
3348
- }
3349
- if (controller.signal.aborted) {
3350
- throw createUsageRequestTimeoutError();
3351
- }
3352
- throw new Error(sanitizeUsageErrorMessage(response.status, bodyText));
3353
- }
3354
- return (await response.json());
3355
- }
3356
- catch (error) {
3357
- if (isAbortError(error)) {
3358
- throw createUsageRequestTimeoutError();
3359
- }
3360
- throw error;
3361
- }
3362
- finally {
3363
- clearTimeout(timeout);
3364
- }
3365
- };
3366
- // Deduplicate accounts by refreshToken (same credential = same limits)
3367
- const seenTokens = new Set();
3368
- const uniqueIndices = [];
3369
- for (let i = 0; i < storage.accounts.length; i++) {
3370
- const acct = storage.accounts[i];
3371
- if (!acct)
3372
- continue;
3373
- const refreshToken = typeof acct.refreshToken === "string" ? acct.refreshToken.trim() : "";
3374
- if (refreshToken && seenTokens.has(refreshToken))
3375
- continue;
3376
- if (refreshToken)
3377
- seenTokens.add(refreshToken);
3378
- uniqueIndices.push(i);
3379
- }
3380
- const lines = ui.v2Enabled
3381
- ? [...formatUiHeader(ui, "Codex limits"), ""]
3382
- : [`Codex limits (${uniqueIndices.length} account${uniqueIndices.length === 1 ? "" : "s"}):`, ""];
3383
- const activeIndex = resolveActiveIndex(storage, "codex");
3384
- const activeRefreshToken = typeof activeIndex === "number" && activeIndex >= 0 && activeIndex < storage.accounts.length
3385
- ? storage.accounts[activeIndex]?.refreshToken?.trim() || undefined
3386
- : undefined;
3387
- let storageChanged = false;
3388
- const jsonAccounts = [];
3389
- for (const i of uniqueIndices) {
3390
- const account = storage.accounts[i];
3391
- if (!account)
3392
- continue;
3393
- const sharesActiveCredential = !!activeRefreshToken && account.refreshToken === activeRefreshToken;
3394
- const displayIndex = sharesActiveCredential && typeof activeIndex === "number" ? activeIndex : i;
3395
- const displayAccount = storage.accounts[displayIndex];
3396
- if (sharesActiveCredential && !displayAccount) {
3397
- logWarn(`[${PLUGIN_NAME}] active account entry missing for index ${displayIndex}, falling back to account ${i}`);
3398
- }
3399
- const effectiveDisplayAccount = displayAccount ?? account;
3400
- const label = formatCommandAccountLabel(effectiveDisplayAccount, displayIndex);
3401
- const isActive = i === activeIndex || sharesActiveCredential;
3402
- const activeSuffix = isActive ? (ui.v2Enabled ? ` ${formatUiBadge(ui, "active", "accent")}` : " [active]") : "";
3403
- try {
3404
- let accessToken = account.accessToken;
3405
- if (typeof accessToken !== "string" ||
3406
- !accessToken ||
3407
- typeof account.expiresAt !== "number" ||
3408
- account.expiresAt <= Date.now() + 30_000) {
3409
- const previousRefreshToken = account.refreshToken;
3410
- if (!previousRefreshToken) {
3411
- throw new Error("Cannot refresh: account has no refresh token");
3412
- }
3413
- const refreshResult = await queuedRefresh(previousRefreshToken);
3414
- if (refreshResult.type !== "success") {
3415
- throw new Error(refreshResult.message ?? refreshResult.reason);
3416
- }
3417
- let refreshedCount = 0;
3418
- for (const storedAccount of storage.accounts) {
3419
- if (!storedAccount)
3420
- continue;
3421
- if (storedAccount.refreshToken === previousRefreshToken) {
3422
- applyRefreshedCredentials(storedAccount, refreshResult);
3423
- refreshedCount += 1;
3424
- }
3425
- }
3426
- if (refreshedCount === 0) {
3427
- applyRefreshedCredentials(account, refreshResult);
3428
- }
3429
- const persistedRefresh = await persistRefreshedCredentials({
3430
- previousRefreshToken,
3431
- accountId: account.accountId,
3432
- organizationId: account.organizationId,
3433
- email: account.email,
3434
- refreshResult,
3435
- });
3436
- accessToken = refreshResult.access;
3437
- storageChanged = storageChanged || persistedRefresh;
3438
- }
3439
- const effectiveAccount = sharesActiveCredential ? effectiveDisplayAccount : account;
3440
- const accountId = effectiveAccount.accountId ?? extractAccountId(accessToken);
3441
- if (!accountId) {
3442
- throw new Error("Missing account id");
3443
- }
3444
- const payload = await fetchUsage({
3445
- accountId,
3446
- accessToken,
3447
- organizationId: effectiveAccount.organizationId,
3448
- });
3449
- const primary = mapWindow(payload.rate_limit?.primary_window ?? null);
3450
- const secondary = mapWindow(payload.rate_limit?.secondary_window ?? null);
3451
- const codeReviewRateLimit = payload.code_review_rate_limit ??
3452
- payload.additional_rate_limits?.find((entry) => entry.limit_name === "code_review_rate_limit")?.rate_limit ??
3453
- null;
3454
- const codeReview = mapWindow(codeReviewRateLimit?.primary_window ?? null);
3455
- const credits = formatCredits(payload.credits ?? null);
3456
- const additionalLimits = (payload.additional_rate_limits ?? []).filter((entry) => entry.limit_name !== "code_review_rate_limit");
3457
- const limits = [
3458
- toLimitPayload(formatLimitTitle(primary.windowMinutes), primary),
3459
- toLimitPayload(formatLimitTitle(secondary.windowMinutes), secondary),
3460
- ];
3461
- if (codeReview.windowMinutes ||
3462
- typeof codeReview.usedPercent === "number" ||
3463
- codeReview.resetAtMs) {
3464
- limits.push(toLimitPayload("Code review", codeReview));
3465
- }
3466
- for (const limit of additionalLimits) {
3467
- const extraWindow = mapWindow(limit.rate_limit?.primary_window ?? null);
3468
- limits.push(toLimitPayload(formatExtraName(limit.limit_name ?? limit.metered_feature), extraWindow));
3469
- }
3470
- jsonAccounts.push({
3471
- ...buildJsonAccountIdentity(displayIndex, {
3472
- includeSensitive: includeSensitiveOutput,
3473
- account: effectiveDisplayAccount,
3474
- label,
3475
- }),
3476
- isActive,
3477
- sharesActiveCredential,
3478
- planType: payload.plan_type ?? null,
3479
- credits: credits ?? null,
3480
- limits,
3481
- });
3482
- if (ui.v2Enabled) {
3483
- lines.push(formatUiItem(ui, `${label}${activeSuffix}`));
3484
- lines.push(` ${formatUiKeyValue(ui, formatLimitTitle(primary.windowMinutes), formatLimitSummary(primary), "muted")}`);
3485
- lines.push(` ${formatUiKeyValue(ui, formatLimitTitle(secondary.windowMinutes), formatLimitSummary(secondary), "muted")}`);
3486
- if (codeReview.windowMinutes || typeof codeReview.usedPercent === "number" || codeReview.resetAtMs) {
3487
- lines.push(` ${formatUiKeyValue(ui, "Code review", formatLimitSummary(codeReview), "muted")}`);
3488
- }
3489
- for (const limit of additionalLimits) {
3490
- const extraWindow = mapWindow(limit.rate_limit?.primary_window ?? null);
3491
- lines.push(` ${formatUiKeyValue(ui, formatExtraName(limit.limit_name ?? limit.metered_feature), formatLimitSummary(extraWindow), "muted")}`);
3492
- }
3493
- if (payload.plan_type) {
3494
- lines.push(` ${formatUiKeyValue(ui, "Plan", payload.plan_type, "muted")}`);
3495
- }
3496
- if (credits) {
3497
- lines.push(` ${formatUiKeyValue(ui, "Credits", credits, "muted")}`);
3498
- }
3499
- }
3500
- else {
3501
- lines.push(`${label}${activeSuffix}:`);
3502
- lines.push(` ${formatLimitTitle(primary.windowMinutes)}: ${formatLimitSummary(primary)}`);
3503
- lines.push(` ${formatLimitTitle(secondary.windowMinutes)}: ${formatLimitSummary(secondary)}`);
3504
- if (codeReview.windowMinutes || typeof codeReview.usedPercent === "number" || codeReview.resetAtMs) {
3505
- lines.push(` Code review: ${formatLimitSummary(codeReview)}`);
3506
- }
3507
- for (const limit of additionalLimits) {
3508
- const extraWindow = mapWindow(limit.rate_limit?.primary_window ?? null);
3509
- lines.push(` ${formatExtraName(limit.limit_name ?? limit.metered_feature)}: ${formatLimitSummary(extraWindow)}`);
3510
- }
3511
- if (payload.plan_type) {
3512
- lines.push(` Plan: ${payload.plan_type}`);
3513
- }
3514
- if (credits) {
3515
- lines.push(` Credits: ${credits}`);
3516
- }
3517
- }
3518
- }
3519
- catch (error) {
3520
- const message = error instanceof Error ? error.message : String(error);
3521
- jsonAccounts.push({
3522
- ...buildJsonAccountIdentity(displayIndex, {
3523
- includeSensitive: includeSensitiveOutput,
3524
- account: effectiveDisplayAccount,
3525
- label,
3526
- }),
3527
- isActive,
3528
- sharesActiveCredential,
3529
- error: message.slice(0, 160),
3530
- });
3531
- if (ui.v2Enabled) {
3532
- lines.push(formatUiItem(ui, `${label}${activeSuffix}`));
3533
- lines.push(` ${formatUiKeyValue(ui, "Error", message.slice(0, 160), "danger")}`);
3534
- }
3535
- else {
3536
- lines.push(`${label}${activeSuffix}:`);
3537
- lines.push(` Error: ${message.slice(0, 160)}`);
3538
- }
3539
- }
3540
- lines.push("");
3541
- }
3542
- if (storageChanged) {
3543
- invalidateAccountManagerCache();
3544
- }
3545
- if (outputFormat === "json") {
3546
- return renderJsonOutput({
3547
- totalAccounts: storage.accounts.length,
3548
- uniqueCredentialCount: uniqueIndices.length,
3549
- activeIndex: activeIndex + 1,
3550
- accounts: jsonAccounts,
3551
- });
3552
- }
3553
- while (lines.length > 0 && lines[lines.length - 1] === "") {
3554
- lines.pop();
3555
- }
3556
- return lines.join("\n");
3557
- },
3558
- }),
3559
- "codex-metrics": tool({
3560
- description: "Show runtime request metrics for this plugin process.",
3561
- args: {
3562
- format: toolOutputFormatSchema(),
3563
- },
3564
- execute({ format } = {}) {
3565
- const ui = resolveUiRuntime();
3566
- const outputFormat = normalizeToolOutputFormat(format);
3567
- const now = Date.now();
3568
- const uptimeMs = Math.max(0, now - runtimeMetrics.startedAt);
3569
- const total = runtimeMetrics.totalRequests;
3570
- const successful = runtimeMetrics.successfulRequests;
3571
- const refreshMetrics = getRefreshQueueMetrics();
3572
- const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : "0.0";
3573
- const avgLatencyMs = successful > 0
3574
- ? Math.round(runtimeMetrics.cumulativeLatencyMs / successful)
3575
- : 0;
3576
- const lastRequest = runtimeMetrics.lastRequestAt !== null
3577
- ? `${formatWaitTime(now - runtimeMetrics.lastRequestAt)} ago`
3578
- : "never";
3579
- const routingVisibility = buildRoutingVisibilitySnapshot();
3580
- if (outputFormat === "json") {
3581
- return Promise.resolve(renderJsonOutput({
3582
- uptimeMs,
3583
- totalRequests: total,
3584
- successfulResponses: successful,
3585
- failedResponses: runtimeMetrics.failedRequests,
3586
- successRatePercent: Number(successRate),
3587
- averageSuccessfulLatencyMs: avgLatencyMs,
3588
- rateLimitedResponses: runtimeMetrics.rateLimitedResponses,
3589
- serverErrors: runtimeMetrics.serverErrors,
3590
- networkErrors: runtimeMetrics.networkErrors,
3591
- authRefreshFailures: runtimeMetrics.authRefreshFailures,
3592
- accountRotations: runtimeMetrics.accountRotations,
3593
- emptyResponseRetries: runtimeMetrics.emptyResponseRetries,
3594
- retryProfile: runtimeMetrics.retryProfile,
3595
- beginnerSafeMode: beginnerSafeModeEnabled,
3596
- retryBudgetExhaustions: runtimeMetrics.retryBudgetExhaustions,
3597
- retryBudgetUsage: { ...runtimeMetrics.retryBudgetUsage },
3598
- retryBudgetLimits: { ...runtimeMetrics.retryBudgetLimits },
3599
- refreshQueue: { ...refreshMetrics },
3600
- lastRequestAt: runtimeMetrics.lastRequestAt,
3601
- lastRequestAgeMs: runtimeMetrics.lastRequestAt !== null
3602
- ? Math.max(0, now - runtimeMetrics.lastRequestAt)
3603
- : null,
3604
- lastError: runtimeMetrics.lastError,
3605
- lastErrorCategory: runtimeMetrics.lastErrorCategory,
3606
- lastSelectedAccountIndex: runtimeMetrics.lastSelectedAccountIndex === null
3607
- ? null
3608
- : runtimeMetrics.lastSelectedAccountIndex + 1,
3609
- lastQuotaKey: runtimeMetrics.lastQuotaKey,
3610
- lastBudgetExhaustion: runtimeMetrics.lastRetryBudgetExhaustedClass === null
3611
- ? null
3612
- : {
3613
- budgetClass: runtimeMetrics.lastRetryBudgetExhaustedClass,
3614
- reason: runtimeMetrics.lastRetryBudgetReason,
3615
- },
3616
- routingVisibility,
3617
- }));
3618
- }
3619
- const lines = [
3620
- "Codex Plugin Metrics:",
3621
- "",
3622
- `Uptime: ${formatWaitTime(uptimeMs)}`,
3623
- `Total upstream requests: ${total}`,
3624
- `Successful responses: ${successful}`,
3625
- `Failed responses: ${runtimeMetrics.failedRequests}`,
3626
- `Success rate: ${successRate}%`,
3627
- `Average successful latency: ${avgLatencyMs}ms`,
3628
- `Rate-limited responses: ${runtimeMetrics.rateLimitedResponses}`,
3629
- `Server errors (5xx): ${runtimeMetrics.serverErrors}`,
3630
- `Network errors: ${runtimeMetrics.networkErrors}`,
3631
- `Auth refresh failures: ${runtimeMetrics.authRefreshFailures}`,
3632
- `Account rotations: ${runtimeMetrics.accountRotations}`,
3633
- `Empty-response retries: ${runtimeMetrics.emptyResponseRetries}`,
3634
- `Retry profile: ${runtimeMetrics.retryProfile}`,
3635
- `Beginner safe mode: ${beginnerSafeModeEnabled ? "on" : "off"}`,
3636
- `Retry budget exhaustions: ${runtimeMetrics.retryBudgetExhaustions}`,
3637
- `Retry budget usage (auth/network/server/short/global/empty): ` +
3638
- `${runtimeMetrics.retryBudgetUsage.authRefresh}/` +
3639
- `${runtimeMetrics.retryBudgetUsage.network}/` +
3640
- `${runtimeMetrics.retryBudgetUsage.server}/` +
3641
- `${runtimeMetrics.retryBudgetUsage.rateLimitShort}/` +
3642
- `${runtimeMetrics.retryBudgetUsage.rateLimitGlobal}/` +
3643
- `${runtimeMetrics.retryBudgetUsage.emptyResponse}`,
3644
- `Refresh queue (started/success/failed/pending): ` +
3645
- `${refreshMetrics.started}/` +
3646
- `${refreshMetrics.succeeded}/` +
3647
- `${refreshMetrics.failed}/` +
3648
- `${refreshMetrics.pending}`,
3649
- `Last upstream request: ${lastRequest}`,
3650
- ];
3651
- if (runtimeMetrics.lastError) {
3652
- lines.push(`Last error: ${runtimeMetrics.lastError}`);
3653
- }
3654
- if (runtimeMetrics.lastErrorCategory) {
3655
- lines.push(`Last error category: ${runtimeMetrics.lastErrorCategory}`);
3656
- }
3657
- if (runtimeMetrics.lastSelectedAccountIndex !== null) {
3658
- lines.push(`Last selected account: ${runtimeMetrics.lastSelectedAccountIndex + 1}`);
3659
- }
3660
- if (runtimeMetrics.lastQuotaKey) {
3661
- lines.push(`Last quota key: ${runtimeMetrics.lastQuotaKey}`);
3662
- }
3663
- if (runtimeMetrics.lastRetryBudgetExhaustedClass) {
3664
- lines.push(`Last budget exhaustion: ${runtimeMetrics.lastRetryBudgetExhaustedClass}` +
3665
- (runtimeMetrics.lastRetryBudgetReason
3666
- ? ` (${runtimeMetrics.lastRetryBudgetReason})`
3667
- : ""));
3668
- }
3669
- lines.push("");
3670
- appendRoutingVisibilityText(lines, routingVisibility, {
3671
- includeExplainability: true,
3672
- });
3673
- if (ui.v2Enabled) {
3674
- const styled = [
3675
- ...formatUiHeader(ui, "Codex plugin metrics"),
3676
- formatUiKeyValue(ui, "Uptime", formatWaitTime(uptimeMs)),
3677
- formatUiKeyValue(ui, "Total upstream requests", String(total)),
3678
- formatUiKeyValue(ui, "Successful responses", String(successful), "success"),
3679
- formatUiKeyValue(ui, "Failed responses", String(runtimeMetrics.failedRequests), "danger"),
3680
- formatUiKeyValue(ui, "Success rate", `${successRate}%`, "accent"),
3681
- formatUiKeyValue(ui, "Average successful latency", `${avgLatencyMs}ms`),
3682
- formatUiKeyValue(ui, "Rate-limited responses", String(runtimeMetrics.rateLimitedResponses), "warning"),
3683
- formatUiKeyValue(ui, "Server errors (5xx)", String(runtimeMetrics.serverErrors), "danger"),
3684
- formatUiKeyValue(ui, "Network errors", String(runtimeMetrics.networkErrors), "danger"),
3685
- formatUiKeyValue(ui, "Auth refresh failures", String(runtimeMetrics.authRefreshFailures), "warning"),
3686
- formatUiKeyValue(ui, "Account rotations", String(runtimeMetrics.accountRotations), "accent"),
3687
- formatUiKeyValue(ui, "Empty-response retries", String(runtimeMetrics.emptyResponseRetries), "warning"),
3688
- formatUiKeyValue(ui, "Retry profile", runtimeMetrics.retryProfile, "muted"),
3689
- formatUiKeyValue(ui, "Beginner safe mode", beginnerSafeModeEnabled ? "on" : "off", beginnerSafeModeEnabled ? "accent" : "muted"),
3690
- formatUiKeyValue(ui, "Retry budget exhaustions", String(runtimeMetrics.retryBudgetExhaustions), "warning"),
3691
- formatUiKeyValue(ui, "Retry budget usage", `A${runtimeMetrics.retryBudgetUsage.authRefresh} N${runtimeMetrics.retryBudgetUsage.network} S${runtimeMetrics.retryBudgetUsage.server} RS${runtimeMetrics.retryBudgetUsage.rateLimitShort} RG${runtimeMetrics.retryBudgetUsage.rateLimitGlobal} E${runtimeMetrics.retryBudgetUsage.emptyResponse}`, "muted"),
3692
- formatUiKeyValue(ui, "Retry budget limits", `A${runtimeMetrics.retryBudgetLimits.authRefresh} N${runtimeMetrics.retryBudgetLimits.network} S${runtimeMetrics.retryBudgetLimits.server} RS${runtimeMetrics.retryBudgetLimits.rateLimitShort} RG${runtimeMetrics.retryBudgetLimits.rateLimitGlobal} E${runtimeMetrics.retryBudgetLimits.emptyResponse}`, "muted"),
3693
- formatUiKeyValue(ui, "Refresh queue", `started=${refreshMetrics.started} dedup=${refreshMetrics.deduplicated} reuse=${refreshMetrics.rotationReused} success=${refreshMetrics.succeeded} failed=${refreshMetrics.failed} pending=${refreshMetrics.pending}`, "muted"),
3694
- formatUiKeyValue(ui, "Last upstream request", lastRequest, "muted"),
3695
- ];
3696
- if (runtimeMetrics.lastError) {
3697
- styled.push(formatUiKeyValue(ui, "Last error", runtimeMetrics.lastError, "danger"));
3698
- }
3699
- if (runtimeMetrics.lastErrorCategory) {
3700
- styled.push(formatUiKeyValue(ui, "Last error category", runtimeMetrics.lastErrorCategory, "warning"));
3701
- }
3702
- if (runtimeMetrics.lastSelectedAccountIndex !== null) {
3703
- styled.push(formatUiKeyValue(ui, "Last selected account", String(runtimeMetrics.lastSelectedAccountIndex + 1), "accent"));
3704
- }
3705
- if (runtimeMetrics.lastQuotaKey) {
3706
- styled.push(formatUiKeyValue(ui, "Last quota key", runtimeMetrics.lastQuotaKey, "muted"));
3707
- }
3708
- if (runtimeMetrics.lastRetryBudgetExhaustedClass) {
3709
- styled.push(formatUiKeyValue(ui, "Last budget exhaustion", runtimeMetrics.lastRetryBudgetReason
3710
- ? `${runtimeMetrics.lastRetryBudgetExhaustedClass} (${runtimeMetrics.lastRetryBudgetReason})`
3711
- : runtimeMetrics.lastRetryBudgetExhaustedClass, "warning"));
3712
- }
3713
- styled.push("");
3714
- appendRoutingVisibilityUi(ui, styled, routingVisibility, {
3715
- includeExplainability: true,
3716
- });
3717
- return Promise.resolve(styled.join("\n"));
3718
- }
3719
- return Promise.resolve(lines.join("\n"));
3720
- },
3721
- }),
3722
- "codex-help": tool({
3723
- description: "Beginner-friendly command guide with quickstart and troubleshooting flows.",
3724
- args: {
3725
- topic: tool.schema
3726
- .string()
3727
- .optional()
3728
- .describe("Optional topic: setup, switch, health, backup, dashboard, metrics."),
3729
- },
3730
- async execute({ topic }) {
3731
- const ui = resolveUiRuntime();
3732
- await Promise.resolve();
3733
- const normalizedTopic = (topic ?? "").trim().toLowerCase();
3734
- const sections = [
3735
- {
3736
- key: "setup",
3737
- title: "Quickstart",
3738
- lines: [
3739
- "1) Add account: opencode auth login",
3740
- "2) Verify account health: codex-health",
3741
- "3) View account list: codex-list",
3742
- "4) Run checklist: codex-setup",
3743
- "5) Use guided wizard: codex-setup --wizard",
3744
- "6) Start requests and monitor: codex-dashboard",
3745
- ],
3746
- },
3747
- {
3748
- key: "switch",
3749
- title: "Daily account operations",
3750
- lines: [
3751
- "List accounts: codex-list",
3752
- "Switch active account: codex-switch index=2",
3753
- "Show detailed status: codex-status",
3754
- "Set account label: codex-label index=2 label=\"Work\"",
3755
- "Set account tags: codex-tag index=2 tags=\"work,team-a\"",
3756
- "Set account note: codex-note index=2 note=\"weekday primary\"",
3757
- "Filter by tag: codex-list tag=\"work\"",
3758
- "Remove account: codex-remove index=2",
3759
- ],
3760
- },
3761
- {
3762
- key: "health",
3763
- title: "Health and recovery",
3764
- lines: [
3765
- "Verify token health: codex-health",
3766
- "Refresh all tokens: codex-refresh",
3767
- "Run diagnostics: codex-doctor",
3768
- "Run diagnostics with fixes: codex-doctor --fix",
3769
- "Show best next action: codex-next",
3770
- "Run guided wizard: codex-setup --wizard",
3771
- ],
3772
- },
3773
- {
3774
- key: "dashboard",
3775
- title: "Monitoring",
3776
- lines: [
3777
- "Live dashboard: codex-dashboard",
3778
- "Runtime metrics: codex-metrics",
3779
- "Per-account status detail: codex-status",
3780
- ],
3781
- },
3782
- {
3783
- key: "backup",
3784
- title: "Backup and migration",
3785
- lines: [
3786
- "Export accounts: codex-export <path>",
3787
- "Auto backup export: codex-export",
3788
- "Import preview: codex-import <path> --dryRun",
3789
- "Import apply: codex-import <path>",
3790
- "Setup checklist: codex-setup",
3791
- ],
3792
- },
3793
- ];
3794
- const visibleSections = normalizedTopic.length === 0
3795
- ? sections
3796
- : sections.filter((section) => section.key.includes(normalizedTopic));
3797
- if (visibleSections.length === 0) {
3798
- const available = sections.map((section) => section.key).join(", ");
3799
- if (ui.v2Enabled) {
3800
- return [
3801
- ...formatUiHeader(ui, "Codex help"),
3802
- "",
3803
- formatUiItem(ui, `Unknown topic: ${normalizedTopic}`, "warning"),
3804
- formatUiItem(ui, `Available topics: ${available}`, "muted"),
3805
- ].join("\n");
3806
- }
3807
- return `Unknown topic: ${normalizedTopic}\n\nAvailable topics: ${available}`;
3808
- }
3809
- if (ui.v2Enabled) {
3810
- const lines = [...formatUiHeader(ui, "Codex help"), ""];
3811
- for (const section of visibleSections) {
3812
- lines.push(...formatUiSection(ui, section.title));
3813
- for (const line of section.lines) {
3814
- lines.push(formatUiItem(ui, line));
3815
- }
3816
- lines.push("");
3817
- }
3818
- lines.push(...formatUiSection(ui, "Tips"));
3819
- lines.push(formatUiItem(ui, "Run codex-setup after adding accounts."));
3820
- lines.push(formatUiItem(ui, "Use codex-setup --wizard for menu-driven onboarding."));
3821
- lines.push(formatUiItem(ui, "Use codex-doctor when request failures increase."));
3822
- return lines.join("\n").trimEnd();
3823
- }
3824
- const lines = ["Codex Help:", ""];
3825
- for (const section of visibleSections) {
3826
- lines.push(`${section.title}:`);
3827
- for (const line of section.lines) {
3828
- lines.push(` - ${line}`);
3829
- }
3830
- lines.push("");
3831
- }
3832
- lines.push("Tips:");
3833
- lines.push(" - Run codex-setup after adding accounts.");
3834
- lines.push(" - Use codex-setup --wizard for menu-driven onboarding.");
3835
- lines.push(" - Use codex-doctor when request failures increase.");
3836
- return lines.join("\n");
3837
- },
3838
- }),
3839
- "codex-setup": tool({
3840
- description: "Beginner checklist for first-time setup and account readiness.",
3841
- args: {
3842
- wizard: tool.schema
3843
- .boolean()
3844
- .optional()
3845
- .describe("Launch menu-driven setup wizard when terminal supports it."),
3846
- },
3847
- async execute({ wizard } = {}) {
3848
- const ui = resolveUiRuntime();
3849
- const state = await buildSetupChecklistState();
3850
- if (wizard) {
3851
- return runSetupWizard(ui, state);
3852
- }
3853
- return renderSetupChecklistOutput(ui, state);
3854
- },
3855
- }),
3856
- "codex-doctor": tool({
3857
- description: "Run beginner-friendly diagnostics with clear fixes.",
3858
- args: {
3859
- deep: tool.schema
3860
- .boolean()
3861
- .optional()
3862
- .describe("Include technical snapshot details (default: false)."),
3863
- fix: tool.schema
3864
- .boolean()
3865
- .optional()
3866
- .describe("Apply safe automated fixes (refresh tokens and switch to healthiest eligible account)."),
3867
- format: toolOutputFormatSchema(),
3868
- },
3869
- async execute({ deep, fix, format } = {}) {
3870
- const ui = resolveUiRuntime();
3871
- const outputFormat = normalizeToolOutputFormat(format);
3872
- const storage = await loadAccounts();
3873
- const now = Date.now();
3874
- const activeIndex = storage && storage.accounts.length > 0
3875
- ? resolveActiveIndex(storage, "codex")
3876
- : 0;
3877
- const snapshots = storage
3878
- ? toBeginnerAccountSnapshots(storage, activeIndex, now)
3879
- : [];
3880
- const runtime = getBeginnerRuntimeSnapshot();
3881
- const summary = summarizeBeginnerAccounts(snapshots, now);
3882
- const findings = buildBeginnerDoctorFindings({
3883
- accounts: snapshots,
3884
- now,
3885
- runtime,
3886
- });
3887
- const nextAction = recommendBeginnerNextAction({ accounts: snapshots, now, runtime });
3888
- let routingVisibility = null;
3889
- const appliedFixes = [];
3890
- const fixErrors = [];
3891
- if (fix && storage && storage.accounts.length > 0) {
3892
- let changedByRefresh = false;
3893
- let refreshedCount = 0;
3894
- for (const account of storage.accounts) {
3895
- try {
3896
- const refreshResult = await queuedRefresh(account.refreshToken);
3897
- if (refreshResult.type === "success") {
3898
- account.refreshToken = refreshResult.refresh;
3899
- account.accessToken = refreshResult.access;
3900
- account.expiresAt = refreshResult.expires;
3901
- changedByRefresh = true;
3902
- refreshedCount += 1;
3903
- }
3904
- }
3905
- catch (error) {
3906
- fixErrors.push(error instanceof Error ? error.message : String(error));
3907
- }
3908
- }
3909
- if (changedByRefresh) {
3910
- try {
3911
- await saveAccounts(storage);
3912
- appliedFixes.push(`Refreshed ${refreshedCount} account token(s).`);
3913
- }
3914
- catch (error) {
3915
- fixErrors.push(`Failed to persist refresh updates: ${error instanceof Error ? error.message : String(error)}`);
3916
- }
3917
- }
3918
- try {
3919
- const managerForFix = await AccountManager.loadFromDisk();
3920
- const explainability = managerForFix.getSelectionExplainability("codex", undefined, Date.now());
3921
- const eligible = explainability
3922
- .filter((entry) => entry.eligible)
3923
- .sort((a, b) => {
3924
- if (b.healthScore !== a.healthScore)
3925
- return b.healthScore - a.healthScore;
3926
- return b.tokensAvailable - a.tokensAvailable;
3927
- });
3928
- const best = eligible[0];
3929
- if (best) {
3930
- const currentActive = resolveActiveIndex(storage, "codex");
3931
- if (best.index !== currentActive) {
3932
- storage.activeIndex = best.index;
3933
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
3934
- for (const family of MODEL_FAMILIES) {
3935
- storage.activeIndexByFamily[family] = best.index;
3936
- }
3937
- await saveAccounts(storage);
3938
- appliedFixes.push(`Switched active account to ${best.index + 1} (best eligible).`);
3939
- }
3940
- }
3941
- else {
3942
- appliedFixes.push("No eligible account available for auto-switch.");
3943
- }
3944
- }
3945
- catch (error) {
3946
- fixErrors.push(`Auto-switch evaluation failed: ${error instanceof Error ? error.message : String(error)}`);
3947
- }
3948
- if (cachedAccountManager) {
3949
- const reloadedManager = await AccountManager.loadFromDisk();
3950
- cachedAccountManager = reloadedManager;
3951
- accountManagerPromise = Promise.resolve(reloadedManager);
3952
- }
3953
- }
3954
- if (deep) {
3955
- const managerForRouting = cachedAccountManager ?? (await AccountManager.loadFromDisk());
3956
- const routingFamily = runtimeMetrics.lastSelectionSnapshot?.family ?? "codex";
3957
- const routingModel = runtimeMetrics.lastSelectionSnapshot?.effectiveModel ??
3958
- runtimeMetrics.lastSelectionSnapshot?.model ??
3959
- null;
3960
- const routingExplainability = managerForRouting.getSelectionExplainability(routingFamily, routingModel ?? undefined, Date.now());
3961
- const routingActiveIndex = storage && storage.accounts.length > 0
3962
- ? resolveActiveIndex(storage, routingFamily)
3963
- : null;
3964
- routingVisibility = buildRoutingVisibilitySnapshot({
3965
- modelFamily: routingFamily,
3966
- effectiveModel: routingModel,
3967
- quotaKey: routingModel ? `${routingFamily}:${routingModel}` : routingFamily,
3968
- selectedAccountIndex: routingActiveIndex,
3969
- selectionExplainability: routingExplainability,
3970
- });
3971
- }
3972
- if (outputFormat === "json") {
3973
- return renderJsonOutput({
3974
- summary: {
3975
- totalAccounts: summary.total,
3976
- healthyAccounts: summary.healthy,
3977
- blockedAccounts: summary.blocked,
3978
- failureRatePercent: runtime.totalRequests > 0
3979
- ? Math.round((runtime.failedRequests / runtime.totalRequests) * 100)
3980
- : 0,
3981
- },
3982
- findings: findings.map((finding) => ({
3983
- severity: finding.severity,
3984
- summary: finding.summary,
3985
- action: finding.action,
3986
- })),
3987
- recommendedNextAction: nextAction,
3988
- autoFix: fix
3989
- ? {
3990
- appliedFixes,
3991
- errors: fixErrors,
3992
- }
3993
- : null,
3994
- technicalSnapshot: deep
3995
- ? {
3996
- storagePath: getStoragePath(),
3997
- runtimeFailures: {
3998
- failedRequests: runtime.failedRequests,
3999
- rateLimitedResponses: runtime.rateLimitedResponses,
4000
- authRefreshFailures: runtime.authRefreshFailures,
4001
- serverErrors: runtime.serverErrors,
4002
- networkErrors: runtime.networkErrors,
4003
- },
4004
- promptCache: {
4005
- enabledRequests: runtime.promptCacheEnabledRequests,
4006
- missingRequests: runtime.promptCacheMissingRequests,
4007
- lastPromptCacheKey: runtime.lastPromptCacheKey,
4008
- summary: formatPromptCacheSnapshot(runtime),
4009
- },
4010
- routingVisibility,
4011
- }
4012
- : null,
4013
- });
4014
- }
4015
- if (ui.v2Enabled) {
4016
- const lines = [
4017
- ...formatUiHeader(ui, "Codex doctor"),
4018
- formatUiKeyValue(ui, "Accounts", String(summary.total)),
4019
- formatUiKeyValue(ui, "Healthy", String(summary.healthy), summary.healthy > 0 ? "success" : "warning"),
4020
- formatUiKeyValue(ui, "Blocked", String(summary.blocked), summary.blocked > 0 ? "warning" : "muted"),
4021
- formatUiKeyValue(ui, "Failure rate", runtime.totalRequests > 0 ? `${Math.round((runtime.failedRequests / runtime.totalRequests) * 100)}%` : "0%"),
4022
- "",
4023
- ...formatUiSection(ui, "Findings"),
4024
- ];
4025
- for (const finding of findings) {
4026
- const tone = finding.severity === "ok"
4027
- ? "success"
4028
- : finding.severity === "warning"
4029
- ? "warning"
4030
- : "danger";
4031
- lines.push(formatUiItem(ui, `${formatDoctorSeverity(ui, finding.severity)} ${finding.summary}`, tone));
4032
- lines.push(` ${formatUiKeyValue(ui, "fix", finding.action, "muted")}`);
4033
- }
4034
- lines.push("");
4035
- lines.push(...formatUiSection(ui, "Recommended next step"));
4036
- lines.push(formatUiItem(ui, nextAction, "accent"));
4037
- if (fix) {
4038
- lines.push("");
4039
- lines.push(...formatUiSection(ui, "Auto-fix"));
4040
- if (appliedFixes.length === 0) {
4041
- lines.push(formatUiItem(ui, "No safe fixes were applied.", "muted"));
4042
- }
4043
- else {
4044
- for (const entry of appliedFixes) {
4045
- lines.push(formatUiItem(ui, entry, "success"));
4046
- }
4047
- }
4048
- for (const error of fixErrors) {
4049
- lines.push(formatUiItem(ui, error, "warning"));
4050
- }
4051
- }
4052
- if (deep) {
4053
- lines.push("");
4054
- lines.push(...formatUiSection(ui, "Technical snapshot"));
4055
- lines.push(formatUiKeyValue(ui, "Storage", getStoragePath(), "muted"));
4056
- lines.push(formatUiKeyValue(ui, "Runtime failures", `failed=${runtime.failedRequests}, rateLimited=${runtime.rateLimitedResponses}, authRefreshFailed=${runtime.authRefreshFailures}, server=${runtime.serverErrors}, network=${runtime.networkErrors}`, "muted"));
4057
- lines.push(formatUiKeyValue(ui, "Prompt cache", formatPromptCacheSnapshot(runtime), "muted"));
4058
- if (routingVisibility) {
4059
- lines.push("");
4060
- appendRoutingVisibilityUi(ui, lines, routingVisibility, {
4061
- includeExplainability: true,
4062
- });
4063
- }
4064
- }
4065
- return lines.join("\n");
4066
- }
4067
- const lines = [
4068
- "Codex Doctor:",
4069
- `Accounts: ${summary.total} (healthy=${summary.healthy}, blocked=${summary.blocked})`,
4070
- `Failure rate: ${runtime.totalRequests > 0 ? Math.round((runtime.failedRequests / runtime.totalRequests) * 100) : 0}%`,
4071
- "",
4072
- "Findings:",
4073
- ];
4074
- for (const finding of findings) {
4075
- lines.push(` ${formatDoctorSeverityText(finding.severity)} ${finding.summary}`);
4076
- lines.push(` fix: ${finding.action}`);
4077
- }
4078
- lines.push("");
4079
- lines.push(`Recommended next step: ${nextAction}`);
4080
- if (fix) {
4081
- lines.push("");
4082
- lines.push("Auto-fix:");
4083
- if (appliedFixes.length === 0) {
4084
- lines.push(" - No safe fixes were applied.");
4085
- }
4086
- else {
4087
- for (const entry of appliedFixes) {
4088
- lines.push(` - ${entry}`);
4089
- }
4090
- }
4091
- for (const error of fixErrors) {
4092
- lines.push(` - warning: ${error}`);
4093
- }
4094
- }
4095
- if (deep) {
4096
- lines.push("");
4097
- lines.push("Technical snapshot:");
4098
- lines.push(` Storage: ${getStoragePath()}`);
4099
- lines.push(` Runtime failures: failed=${runtime.failedRequests}, rateLimited=${runtime.rateLimitedResponses}, authRefreshFailed=${runtime.authRefreshFailures}, server=${runtime.serverErrors}, network=${runtime.networkErrors}`);
4100
- lines.push(` Prompt cache: ${formatPromptCacheSnapshot(runtime)}`);
4101
- if (routingVisibility) {
4102
- appendRoutingVisibilityText(lines, routingVisibility, {
4103
- includeExplainability: true,
4104
- });
4105
- }
4106
- }
4107
- return lines.join("\n");
4108
- },
4109
- }),
4110
- "codex-next": tool({
4111
- description: "Show the single most recommended next action for beginners.",
4112
- args: {
4113
- format: toolOutputFormatSchema(),
4114
- },
4115
- async execute({ format } = {}) {
4116
- const ui = resolveUiRuntime();
4117
- const outputFormat = normalizeToolOutputFormat(format);
4118
- const storage = await loadAccounts();
4119
- const now = Date.now();
4120
- const activeIndex = storage && storage.accounts.length > 0
4121
- ? resolveActiveIndex(storage, "codex")
4122
- : 0;
4123
- const snapshots = storage
4124
- ? toBeginnerAccountSnapshots(storage, activeIndex, now)
4125
- : [];
4126
- const action = recommendBeginnerNextAction({
4127
- accounts: snapshots,
4128
- now,
4129
- runtime: getBeginnerRuntimeSnapshot(),
4130
- });
4131
- if (outputFormat === "json") {
4132
- return renderJsonOutput({
4133
- recommendedNextAction: action,
4134
- totalAccounts: snapshots.length,
4135
- activeIndex: storage && storage.accounts.length > 0 ? activeIndex + 1 : null,
4136
- });
4137
- }
4138
- if (ui.v2Enabled) {
4139
- return [
4140
- ...formatUiHeader(ui, "Recommended next action"),
4141
- "",
4142
- formatUiItem(ui, action, "accent"),
4143
- ].join("\n");
4144
- }
4145
- return `Recommended next action:\n${action}`;
4146
- },
4147
- }),
4148
- "codex-label": tool({
4149
- description: "Set or clear a beginner-friendly display label for an account (interactive picker when index is omitted).",
4150
- args: {
4151
- index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
4152
- label: tool.schema.string().describe("Display label. Use an empty string to clear (e.g., Work, Personal, Team A)"),
4153
- },
4154
- async execute({ index, label }) {
4155
- const ui = resolveUiRuntime();
4156
- const storage = await loadAccounts();
4157
- if (!storage || storage.accounts.length === 0) {
4158
- if (ui.v2Enabled) {
4159
- return [
4160
- ...formatUiHeader(ui, "Set account label"),
4161
- "",
4162
- formatUiItem(ui, "No accounts configured.", "warning"),
4163
- formatUiItem(ui, "Run: opencode auth login", "accent"),
4164
- ].join("\n");
4165
- }
4166
- return "No Codex accounts configured. Run: opencode auth login";
4167
- }
4168
- let resolvedIndex = index;
4169
- if (resolvedIndex === undefined) {
4170
- const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account label");
4171
- if (selectedIndex === null) {
4172
- if (supportsInteractiveMenus()) {
4173
- if (ui.v2Enabled) {
4174
- return [
4175
- ...formatUiHeader(ui, "Set account label"),
4176
- "",
4177
- formatUiItem(ui, "No account selected.", "warning"),
4178
- formatUiItem(ui, "Run again and pick an account, or pass codex-label index=2 label=\"Work\".", "muted"),
4179
- ].join("\n");
4180
- }
4181
- return "No account selected.";
4182
- }
4183
- if (ui.v2Enabled) {
4184
- return [
4185
- ...formatUiHeader(ui, "Set account label"),
4186
- "",
4187
- formatUiItem(ui, "Missing account number.", "warning"),
4188
- formatUiItem(ui, "Use: codex-label index=2 label=\"Work\"", "accent"),
4189
- ].join("\n");
4190
- }
4191
- return "Missing account number. Use: codex-label index=2 label=\"Work\"";
4192
- }
4193
- resolvedIndex = selectedIndex + 1;
4194
- }
4195
- const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
4196
- if (!Number.isFinite(targetIndex) ||
4197
- targetIndex < 0 ||
4198
- targetIndex >= storage.accounts.length) {
4199
- if (ui.v2Enabled) {
4200
- return [
4201
- ...formatUiHeader(ui, "Set account label"),
4202
- "",
4203
- formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
4204
- formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
4205
- ].join("\n");
4206
- }
4207
- return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
4208
- }
4209
- const normalizedLabel = (label ?? "").trim().replace(/\s+/g, " ");
4210
- if (normalizedLabel.length > 60) {
4211
- if (ui.v2Enabled) {
4212
- return [
4213
- ...formatUiHeader(ui, "Set account label"),
4214
- "",
4215
- formatUiItem(ui, "Label is too long (max 60 characters).", "danger"),
4216
- ].join("\n");
4217
- }
4218
- return "Label is too long (max 60 characters).";
4219
- }
4220
- const account = storage.accounts[targetIndex];
4221
- if (!account) {
4222
- return `Account ${resolvedIndex} not found.`;
4223
- }
4224
- const previousLabel = account.accountLabel?.trim() ?? "";
4225
- if (normalizedLabel.length === 0) {
4226
- delete account.accountLabel;
4227
- }
4228
- else {
4229
- account.accountLabel = normalizedLabel;
4230
- }
4231
- try {
4232
- await saveAccounts(storage);
4233
- }
4234
- catch (saveError) {
4235
- logWarn("Failed to save account label update", { error: String(saveError) });
4236
- if (ui.v2Enabled) {
4237
- return [
4238
- ...formatUiHeader(ui, "Set account label"),
4239
- "",
4240
- formatUiItem(ui, "Label updated in memory but failed to persist.", "danger"),
4241
- ].join("\n");
4242
- }
4243
- return "Label updated in memory but failed to persist. Changes may be lost on restart.";
4244
- }
4245
- if (cachedAccountManager) {
4246
- const reloadedManager = await AccountManager.loadFromDisk();
4247
- cachedAccountManager = reloadedManager;
4248
- accountManagerPromise = Promise.resolve(reloadedManager);
4249
- }
4250
- const accountLabel = formatCommandAccountLabel(account, targetIndex);
4251
- if (ui.v2Enabled) {
4252
- const statusText = normalizedLabel.length === 0
4253
- ? `Cleared label for ${accountLabel}`
4254
- : `Set label for ${accountLabel} to "${normalizedLabel}"`;
4255
- const previousText = previousLabel.length > 0
4256
- ? formatUiKeyValue(ui, "Previous label", previousLabel, "muted")
4257
- : formatUiKeyValue(ui, "Previous label", "none", "muted");
4258
- return [
4259
- ...formatUiHeader(ui, "Set account label"),
4260
- "",
4261
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} ${statusText}`, "success"),
4262
- previousText,
4263
- ].join("\n");
4264
- }
4265
- if (normalizedLabel.length === 0) {
4266
- return `Cleared label for ${accountLabel}`;
4267
- }
4268
- return `Set label for ${accountLabel} to "${normalizedLabel}"`;
4269
- },
4270
- }),
4271
- "codex-tag": tool({
4272
- description: "Set or clear account tags for filtering and grouping.",
4273
- args: {
4274
- index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
4275
- tags: tool.schema.string().describe("Comma-separated tags (e.g., work,team-a). Empty string clears tags."),
4276
- },
4277
- async execute({ index, tags }) {
4278
- const ui = resolveUiRuntime();
4279
- const storage = await loadAccounts();
4280
- if (!storage || storage.accounts.length === 0) {
4281
- if (ui.v2Enabled) {
4282
- return [
4283
- ...formatUiHeader(ui, "Set account tags"),
4284
- "",
4285
- formatUiItem(ui, "No accounts configured.", "warning"),
4286
- formatUiItem(ui, "Run: opencode auth login", "accent"),
4287
- ].join("\n");
4288
- }
4289
- return "No Codex accounts configured. Run: opencode auth login";
4290
- }
4291
- let resolvedIndex = index;
4292
- if (resolvedIndex === undefined) {
4293
- const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account tags");
4294
- if (selectedIndex === null) {
4295
- if (supportsInteractiveMenus()) {
4296
- return ui.v2Enabled
4297
- ? [
4298
- ...formatUiHeader(ui, "Set account tags"),
4299
- "",
4300
- formatUiItem(ui, "No account selected.", "warning"),
4301
- ].join("\n")
4302
- : "No account selected.";
4303
- }
4304
- return "Missing account number. Use: codex-tag index=2 tags=\"work,team-a\"";
4305
- }
4306
- resolvedIndex = selectedIndex + 1;
4307
- }
4308
- const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
4309
- if (!Number.isFinite(targetIndex) ||
4310
- targetIndex < 0 ||
4311
- targetIndex >= storage.accounts.length) {
4312
- return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
4313
- }
4314
- const account = storage.accounts[targetIndex];
4315
- if (!account)
4316
- return `Account ${resolvedIndex} not found.`;
4317
- const normalizedTags = normalizeAccountTags(tags ?? "");
4318
- const previousTags = Array.isArray(account.accountTags)
4319
- ? [...account.accountTags]
4320
- : [];
4321
- if (normalizedTags.length === 0) {
4322
- delete account.accountTags;
4323
- }
4324
- else {
4325
- account.accountTags = normalizedTags;
4326
- }
4327
- try {
4328
- await saveAccounts(storage);
4329
- }
4330
- catch (error) {
4331
- logWarn("Failed to save account tag update", { error: String(error) });
4332
- return "Tag update failed to persist. Changes may be lost on restart.";
4333
- }
4334
- if (cachedAccountManager) {
4335
- const reloadedManager = await AccountManager.loadFromDisk();
4336
- cachedAccountManager = reloadedManager;
4337
- accountManagerPromise = Promise.resolve(reloadedManager);
4338
- }
4339
- const accountLabel = formatCommandAccountLabel(account, targetIndex);
4340
- const previousText = previousTags.length > 0 ? previousTags.join(", ") : "none";
4341
- const nextText = normalizedTags.length > 0 ? normalizedTags.join(", ") : "none";
4342
- if (ui.v2Enabled) {
4343
- return [
4344
- ...formatUiHeader(ui, "Set account tags"),
4345
- "",
4346
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Updated tags for ${accountLabel}`, "success"),
4347
- formatUiKeyValue(ui, "Previous tags", previousText, "muted"),
4348
- formatUiKeyValue(ui, "Current tags", nextText, normalizedTags.length > 0 ? "accent" : "muted"),
4349
- ].join("\n");
4350
- }
4351
- return `Updated tags for ${accountLabel}\nPrevious tags: ${previousText}\nCurrent tags: ${nextText}`;
4352
- },
4353
- }),
4354
- "codex-note": tool({
4355
- description: "Set or clear an account note for reminders.",
4356
- args: {
4357
- index: tool.schema.number().optional().describe("Account number to update (1-based, e.g., 1 for first account)"),
4358
- note: tool.schema.string().describe("Short note. Empty string clears the note."),
4359
- },
4360
- async execute({ index, note }) {
4361
- const ui = resolveUiRuntime();
4362
- const storage = await loadAccounts();
4363
- if (!storage || storage.accounts.length === 0) {
4364
- return "No Codex accounts configured. Run: opencode auth login";
4365
- }
4366
- let resolvedIndex = index;
4367
- if (resolvedIndex === undefined) {
4368
- const selectedIndex = await promptAccountIndexSelection(ui, storage, "Set account note");
4369
- if (selectedIndex === null) {
4370
- if (supportsInteractiveMenus())
4371
- return "No account selected.";
4372
- return "Missing account number. Use: codex-note index=2 note=\"weekday primary\"";
4373
- }
4374
- resolvedIndex = selectedIndex + 1;
4375
- }
4376
- const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
4377
- if (!Number.isFinite(targetIndex) ||
4378
- targetIndex < 0 ||
4379
- targetIndex >= storage.accounts.length) {
4380
- return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}`;
4381
- }
4382
- const account = storage.accounts[targetIndex];
4383
- if (!account)
4384
- return `Account ${resolvedIndex} not found.`;
4385
- const normalizedNote = (note ?? "").trim();
4386
- if (normalizedNote.length > 240) {
4387
- return "Note is too long (max 240 characters).";
4388
- }
4389
- if (normalizedNote.length === 0) {
4390
- delete account.accountNote;
4391
- }
4392
- else {
4393
- account.accountNote = normalizedNote;
4394
- }
4395
- try {
4396
- await saveAccounts(storage);
4397
- }
4398
- catch (error) {
4399
- logWarn("Failed to save account note update", { error: String(error) });
4400
- return "Note update failed to persist. Changes may be lost on restart.";
4401
- }
4402
- if (cachedAccountManager) {
4403
- const reloadedManager = await AccountManager.loadFromDisk();
4404
- cachedAccountManager = reloadedManager;
4405
- accountManagerPromise = Promise.resolve(reloadedManager);
4406
- }
4407
- const accountLabel = formatCommandAccountLabel(account, targetIndex);
4408
- if (normalizedNote.length === 0) {
4409
- return `Cleared note for ${accountLabel}`;
4410
- }
4411
- return `Saved note for ${accountLabel}: ${normalizedNote}`;
4412
- },
4413
- }),
4414
- "codex-dashboard": tool({
4415
- description: "Show a live Codex dashboard: account eligibility, retry budgets, and refresh queue health.",
4416
- args: {
4417
- format: toolOutputFormatSchema(),
4418
- includeSensitive: toolSensitiveJsonSchema(),
4419
- },
4420
- async execute({ format, includeSensitive } = {}) {
4421
- const ui = resolveUiRuntime();
4422
- const outputFormat = normalizeToolOutputFormat(format);
4423
- const includeSensitiveOutput = includeSensitive === true;
4424
- const storage = await loadAccounts();
4425
- if (!storage || storage.accounts.length === 0) {
4426
- if (outputFormat === "json") {
4427
- return renderJsonOutput({
4428
- message: "No Codex accounts configured. Run: opencode auth login",
4429
- accountCount: 0,
4430
- selectionLens: null,
4431
- retryProfile: runtimeMetrics.retryProfile,
4432
- beginnerSafeMode: beginnerSafeModeEnabled,
4433
- retryBudgetUsage: { ...runtimeMetrics.retryBudgetUsage },
4434
- refreshQueue: { ...getRefreshQueueMetrics() },
4435
- routingVisibility: buildRoutingVisibilitySnapshot(),
4436
- accountEligibility: [],
4437
- recommendedNextAction: "Run opencode auth login",
4438
- lastError: runtimeMetrics.lastError === null
4439
- ? null
4440
- : {
4441
- message: runtimeMetrics.lastError,
4442
- category: runtimeMetrics.lastErrorCategory,
4443
- },
4444
- });
4445
- }
4446
- if (ui.v2Enabled) {
4447
- return [
4448
- ...formatUiHeader(ui, "Codex dashboard"),
4449
- "",
4450
- formatUiItem(ui, "No accounts configured.", "warning"),
4451
- formatUiItem(ui, "Run: opencode auth login", "accent"),
4452
- ].join("\n");
4453
- }
4454
- return "No Codex accounts configured. Run: opencode auth login";
4455
- }
4456
- const now = Date.now();
4457
- const refreshMetrics = getRefreshQueueMetrics();
4458
- const family = runtimeMetrics.lastSelectionSnapshot?.family ?? "codex";
4459
- const model = runtimeMetrics.lastSelectionSnapshot?.effectiveModel ??
4460
- runtimeMetrics.lastSelectionSnapshot?.model ??
4461
- undefined;
4462
- const manager = cachedAccountManager ?? (await AccountManager.loadFromDisk());
4463
- const explainability = manager.getSelectionExplainability(family, model, now);
4464
- const selectionLabel = model ? `${family}:${model}` : family;
4465
- const routingVisibility = buildRoutingVisibilitySnapshot({
4466
- modelFamily: family,
4467
- effectiveModel: model ?? null,
4468
- quotaKey: model ? `${family}:${model}` : family,
4469
- selectedAccountIndex: resolveActiveIndex(storage, family),
4470
- selectionExplainability: explainability,
4471
- });
4472
- const recommendedNextAction = recommendBeginnerNextAction({
4473
- accounts: toBeginnerAccountSnapshots(storage, resolveActiveIndex(storage, "codex"), now),
4474
- now,
4475
- runtime: getBeginnerRuntimeSnapshot(),
4476
- });
4477
- if (outputFormat === "json") {
4478
- return renderJsonOutput({
4479
- accountCount: storage.accounts.length,
4480
- selectionLens: selectionLabel,
4481
- retryProfile: runtimeMetrics.retryProfile,
4482
- beginnerSafeMode: beginnerSafeModeEnabled,
4483
- retryBudgetUsage: { ...runtimeMetrics.retryBudgetUsage },
4484
- refreshQueue: { ...refreshMetrics },
4485
- routingVisibility,
4486
- accountEligibility: explainability.map((entry) => ({
4487
- ...buildJsonAccountIdentity(entry.index, {
4488
- includeSensitive: includeSensitiveOutput,
4489
- account: storage.accounts[entry.index],
4490
- }),
4491
- eligible: entry.eligible,
4492
- healthScore: entry.healthScore,
4493
- tokensAvailable: entry.tokensAvailable,
4494
- reasons: [...entry.reasons],
4495
- })),
4496
- recommendedNextAction,
4497
- lastError: runtimeMetrics.lastError === null
4498
- ? null
4499
- : {
4500
- message: runtimeMetrics.lastError,
4501
- category: runtimeMetrics.lastErrorCategory,
4502
- },
4503
- });
4504
- }
4505
- if (ui.v2Enabled) {
4506
- const lines = [
4507
- ...formatUiHeader(ui, "Codex dashboard"),
4508
- formatUiKeyValue(ui, "Accounts", String(storage.accounts.length)),
4509
- formatUiKeyValue(ui, "Selection lens", selectionLabel, "muted"),
4510
- formatUiKeyValue(ui, "Retry profile", runtimeMetrics.retryProfile, "muted"),
4511
- formatUiKeyValue(ui, "Beginner safe mode", beginnerSafeModeEnabled ? "on" : "off", beginnerSafeModeEnabled ? "accent" : "muted"),
4512
- formatUiKeyValue(ui, "Retry usage", `A${runtimeMetrics.retryBudgetUsage.authRefresh} N${runtimeMetrics.retryBudgetUsage.network} S${runtimeMetrics.retryBudgetUsage.server} RS${runtimeMetrics.retryBudgetUsage.rateLimitShort} RG${runtimeMetrics.retryBudgetUsage.rateLimitGlobal} E${runtimeMetrics.retryBudgetUsage.emptyResponse}`, "muted"),
4513
- formatUiKeyValue(ui, "Refresh queue", `pending=${refreshMetrics.pending}, success=${refreshMetrics.succeeded}, failed=${refreshMetrics.failed}`, "muted"),
4514
- "",
4515
- ];
4516
- appendRoutingVisibilityUi(ui, lines, routingVisibility);
4517
- lines.push("");
4518
- lines.push(...formatUiSection(ui, "Account eligibility"));
4519
- for (const entry of explainability) {
4520
- const label = formatCommandAccountLabel(storage.accounts[entry.index], entry.index);
4521
- const state = entry.eligible ? formatUiBadge(ui, "eligible", "success") : formatUiBadge(ui, "blocked", "warning");
4522
- lines.push(formatUiItem(ui, `${label} ${state} health=${Math.round(entry.healthScore)} tokens=${entry.tokensAvailable.toFixed(1)} reasons=${entry.reasons.join(", ")}`));
4523
- }
4524
- lines.push("");
4525
- lines.push(...formatUiSection(ui, "Recommended next step"));
4526
- lines.push(formatUiItem(ui, recommendedNextAction, "accent"));
4527
- if (runtimeMetrics.lastError) {
4528
- lines.push("");
4529
- lines.push(...formatUiSection(ui, "Last error"));
4530
- lines.push(formatUiItem(ui, runtimeMetrics.lastError, "danger"));
4531
- if (runtimeMetrics.lastErrorCategory) {
4532
- lines.push(formatUiKeyValue(ui, "Category", runtimeMetrics.lastErrorCategory, "warning"));
4533
- }
4534
- }
4535
- return lines.join("\n");
4536
- }
4537
- const lines = [
4538
- "Codex Dashboard:",
4539
- `Accounts: ${storage.accounts.length}`,
4540
- `Selection lens: ${selectionLabel}`,
4541
- `Retry profile: ${runtimeMetrics.retryProfile}`,
4542
- `Beginner safe mode: ${beginnerSafeModeEnabled ? "on" : "off"}`,
4543
- `Retry usage: auth=${runtimeMetrics.retryBudgetUsage.authRefresh}, network=${runtimeMetrics.retryBudgetUsage.network}, server=${runtimeMetrics.retryBudgetUsage.server}, short429=${runtimeMetrics.retryBudgetUsage.rateLimitShort}, global429=${runtimeMetrics.retryBudgetUsage.rateLimitGlobal}, empty=${runtimeMetrics.retryBudgetUsage.emptyResponse}`,
4544
- `Refresh queue: pending=${refreshMetrics.pending}, success=${refreshMetrics.succeeded}, failed=${refreshMetrics.failed}`,
4545
- ];
4546
- lines.push("");
4547
- appendRoutingVisibilityText(lines, routingVisibility);
4548
- lines.push("");
4549
- lines.push("Account eligibility:");
4550
- for (const entry of explainability) {
4551
- const label = formatCommandAccountLabel(storage.accounts[entry.index], entry.index);
4552
- lines.push(` - ${label}: ${entry.eligible ? "eligible" : "blocked"} | health=${Math.round(entry.healthScore)} | tokens=${entry.tokensAvailable.toFixed(1)} | reasons=${entry.reasons.join(", ")}`);
4553
- }
4554
- lines.push("");
4555
- lines.push(`Recommended next step: ${recommendedNextAction}`);
4556
- if (runtimeMetrics.lastError) {
4557
- lines.push("");
4558
- lines.push(`Last error: ${runtimeMetrics.lastError}`);
4559
- if (runtimeMetrics.lastErrorCategory) {
4560
- lines.push(`Category: ${runtimeMetrics.lastErrorCategory}`);
4561
- }
4562
- }
4563
- return lines.join("\n");
4564
- },
4565
- }),
4566
- "codex-health": tool({
4567
- description: "Check health of all Codex accounts by validating refresh tokens.",
4568
- args: {
4569
- format: toolOutputFormatSchema(),
4570
- includeSensitive: toolSensitiveJsonSchema(),
4571
- },
4572
- async execute({ format, includeSensitive } = {}) {
4573
- const ui = resolveUiRuntime();
4574
- const outputFormat = normalizeToolOutputFormat(format);
4575
- const includeSensitiveOutput = includeSensitive === true;
4576
- const storage = await loadAccounts();
4577
- if (!storage || storage.accounts.length === 0) {
4578
- if (outputFormat === "json") {
4579
- return renderJsonOutput({
4580
- message: "No Codex accounts configured. Run: opencode auth login",
4581
- totalAccounts: 0,
4582
- healthyCount: 0,
4583
- unhealthyCount: 0,
4584
- accounts: [],
4585
- });
4586
- }
4587
- if (ui.v2Enabled) {
4588
- return [
4589
- ...formatUiHeader(ui, "Health check"),
4590
- "",
4591
- formatUiItem(ui, "No accounts configured.", "warning"),
4592
- formatUiItem(ui, "Run: opencode auth login", "accent"),
4593
- ].join("\n");
4594
- }
4595
- return "No Codex accounts configured. Run: opencode auth login";
4596
- }
4597
- const results = ui.v2Enabled
4598
- ? []
4599
- : [`Health Check (${storage.accounts.length} accounts):`, ""];
4600
- const jsonAccounts = [];
4601
- let healthyCount = 0;
4602
- let unhealthyCount = 0;
4603
- for (let i = 0; i < storage.accounts.length; i++) {
4604
- const account = storage.accounts[i];
4605
- if (!account)
4606
- continue;
4607
- const label = formatCommandAccountLabel(account, i);
4608
- try {
4609
- const refreshResult = await queuedRefresh(account.refreshToken);
4610
- if (refreshResult.type === "success") {
4611
- jsonAccounts.push({
4612
- ...buildJsonAccountIdentity(i, {
4613
- includeSensitive: includeSensitiveOutput,
4614
- account,
4615
- label,
4616
- }),
4617
- status: "healthy",
4618
- });
4619
- results.push(` ${getStatusMarker(ui, "ok")} ${label}: Healthy`);
4620
- healthyCount++;
4621
- }
4622
- else {
4623
- jsonAccounts.push({
4624
- ...buildJsonAccountIdentity(i, {
4625
- includeSensitive: includeSensitiveOutput,
4626
- account,
4627
- label,
4628
- }),
4629
- status: "unhealthy",
4630
- error: refreshResult.message ?? refreshResult.reason,
4631
- });
4632
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Token refresh failed`);
4633
- unhealthyCount++;
4634
- }
4635
- }
4636
- catch (error) {
4637
- const errorMsg = error instanceof Error ? error.message : String(error);
4638
- jsonAccounts.push({
4639
- ...buildJsonAccountIdentity(i, {
4640
- includeSensitive: includeSensitiveOutput,
4641
- account,
4642
- label,
4643
- }),
4644
- status: "unhealthy",
4645
- error: errorMsg.slice(0, 120),
4646
- });
4647
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Error - ${errorMsg.slice(0, 120)}`);
4648
- unhealthyCount++;
4649
- }
4650
- }
4651
- results.push("");
4652
- results.push(`Summary: ${healthyCount} healthy, ${unhealthyCount} unhealthy`);
4653
- if (outputFormat === "json") {
4654
- return renderJsonOutput({
4655
- totalAccounts: storage.accounts.length,
4656
- healthyCount,
4657
- unhealthyCount,
4658
- accounts: jsonAccounts,
4659
- });
4660
- }
4661
- if (ui.v2Enabled) {
4662
- return [
4663
- ...formatUiHeader(ui, "Health check"),
4664
- "",
4665
- ...results.map((line) => paintUiText(ui, line, "normal")),
4666
- ].join("\n");
4667
- }
4668
- return results.join("\n");
4669
- },
4670
- }),
4671
- "codex-remove": tool({
4672
- description: "Remove one Codex account entry by index (1-based) or interactive picker when index is omitted.",
4673
- args: {
4674
- index: tool.schema.number().optional().describe("Account number to remove (1-based, e.g., 1 for first account)"),
4675
- },
4676
- async execute({ index } = {}) {
4677
- const ui = resolveUiRuntime();
4678
- const storage = await loadAccounts();
4679
- if (!storage || storage.accounts.length === 0) {
4680
- if (ui.v2Enabled) {
4681
- return [
4682
- ...formatUiHeader(ui, "Remove account"),
4683
- "",
4684
- formatUiItem(ui, "No accounts configured.", "warning"),
4685
- ].join("\n");
4686
- }
4687
- return "No Codex accounts configured. Nothing to remove.";
4688
- }
4689
- let resolvedIndex = index;
4690
- if (resolvedIndex === undefined) {
4691
- const selectedIndex = await promptAccountIndexSelection(ui, storage, "Remove account");
4692
- if (selectedIndex === null) {
4693
- if (supportsInteractiveMenus()) {
4694
- if (ui.v2Enabled) {
4695
- return [
4696
- ...formatUiHeader(ui, "Remove account"),
4697
- "",
4698
- formatUiItem(ui, "No account selected.", "warning"),
4699
- formatUiItem(ui, "Run again and pick an account, or pass codex-remove index=2.", "muted"),
4700
- ].join("\n");
4701
- }
4702
- return "No account selected.";
4703
- }
4704
- if (ui.v2Enabled) {
4705
- return [
4706
- ...formatUiHeader(ui, "Remove account"),
4707
- "",
4708
- formatUiItem(ui, "Missing account number.", "warning"),
4709
- formatUiItem(ui, "Use: codex-remove index=2", "accent"),
4710
- ].join("\n");
4711
- }
4712
- return "Missing account number. Use: codex-remove index=2";
4713
- }
4714
- resolvedIndex = selectedIndex + 1;
4715
- }
4716
- const targetIndex = Math.floor((resolvedIndex ?? 0) - 1);
4717
- if (!Number.isFinite(targetIndex) ||
4718
- targetIndex < 0 ||
4719
- targetIndex >= storage.accounts.length) {
4720
- if (ui.v2Enabled) {
4721
- return [
4722
- ...formatUiHeader(ui, "Remove account"),
4723
- "",
4724
- formatUiItem(ui, `Invalid account number: ${resolvedIndex}`, "danger"),
4725
- formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
4726
- formatUiItem(ui, "Use codex-list to list all accounts.", "accent"),
4727
- ].join("\n");
4728
- }
4729
- return `Invalid account number: ${resolvedIndex}\n\nValid range: 1-${storage.accounts.length}\n\nUse codex-list to list all accounts.`;
4730
- }
4731
- const account = storage.accounts[targetIndex];
4732
- if (!account) {
4733
- return `Account ${resolvedIndex} not found.`;
4734
- }
4735
- const label = formatCommandAccountLabel(account, targetIndex);
4736
- storage.accounts.splice(targetIndex, 1);
4737
- if (storage.accounts.length === 0) {
4738
- storage.activeIndex = 0;
4739
- storage.activeIndexByFamily = {};
4740
- }
4741
- else {
4742
- if (storage.activeIndex >= storage.accounts.length) {
4743
- storage.activeIndex = 0;
4744
- }
4745
- else if (storage.activeIndex > targetIndex) {
4746
- storage.activeIndex -= 1;
4747
- }
4748
- if (storage.activeIndexByFamily) {
4749
- for (const family of MODEL_FAMILIES) {
4750
- const idx = storage.activeIndexByFamily[family];
4751
- if (typeof idx === "number") {
4752
- if (idx >= storage.accounts.length) {
4753
- storage.activeIndexByFamily[family] = 0;
4754
- }
4755
- else if (idx > targetIndex) {
4756
- storage.activeIndexByFamily[family] = idx - 1;
4757
- }
4758
- }
4759
- }
4760
- }
4761
- }
4762
- try {
4763
- await saveAccounts(storage);
4764
- }
4765
- catch (saveError) {
4766
- logWarn("Failed to save account removal", { error: String(saveError) });
4767
- if (ui.v2Enabled) {
4768
- return [
4769
- ...formatUiHeader(ui, "Remove account"),
4770
- "",
4771
- formatUiItem(ui, `Removed selected entry: ${label}`, "warning"),
4772
- formatUiItem(ui, "Only the selected index was changed.", "muted"),
4773
- formatUiItem(ui, "Failed to persist. Change may be lost on restart.", "danger"),
4774
- ].join("\n");
4775
- }
4776
- return `Removed selected entry: ${label} from memory, but failed to persist. Only the selected index was changed and this may be lost on restart.`;
4777
- }
4778
- if (cachedAccountManager) {
4779
- const reloadedManager = await AccountManager.loadFromDisk();
4780
- cachedAccountManager = reloadedManager;
4781
- accountManagerPromise = Promise.resolve(reloadedManager);
4782
- }
4783
- const remaining = storage.accounts.length;
4784
- const matchingEmailRemaining = account.email?.trim()
4785
- ? storage.accounts.filter((entry) => entry.email === account.email).length
4786
- : 0;
4787
- if (ui.v2Enabled) {
4788
- const postRemoveHint = matchingEmailRemaining > 0 && account.email
4789
- ? formatUiItem(ui, `Other entries for ${account.email} remain: ${matchingEmailRemaining}`, "muted")
4790
- : formatUiItem(ui, "Only the selected entry was removed.", "muted");
4791
- return [
4792
- ...formatUiHeader(ui, "Remove account"),
4793
- "",
4794
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Removed selected entry: ${label}`, "success"),
4795
- postRemoveHint,
4796
- remaining > 0
4797
- ? formatUiKeyValue(ui, "Remaining accounts", String(remaining))
4798
- : formatUiItem(ui, "No accounts remaining. Run: opencode auth login", "warning"),
4799
- ].join("\n");
4800
- }
4801
- const postRemoveHint = matchingEmailRemaining > 0 && account.email
4802
- ? `Other entries for ${account.email} remain: ${matchingEmailRemaining}`
4803
- : "Only the selected entry was removed.";
4804
- return [
4805
- `Removed selected entry: ${label}`,
4806
- postRemoveHint,
4807
- "",
4808
- remaining > 0
4809
- ? `Remaining accounts: ${remaining}`
4810
- : "No accounts remaining. Run: opencode auth login",
4811
- ].join("\n");
4812
- },
4813
- }),
4814
- "codex-refresh": tool({
4815
- description: "Manually refresh OAuth tokens for all accounts to verify they're still valid.",
4816
- args: {},
4817
- async execute() {
4818
- const ui = resolveUiRuntime();
4819
- const storage = await loadAccounts();
4820
- if (!storage || storage.accounts.length === 0) {
4821
- if (ui.v2Enabled) {
4822
- return [
4823
- ...formatUiHeader(ui, "Refresh accounts"),
4824
- "",
4825
- formatUiItem(ui, "No accounts configured.", "warning"),
4826
- formatUiItem(ui, "Run: opencode auth login", "accent"),
4827
- ].join("\n");
4828
- }
4829
- return "No Codex accounts configured. Run: opencode auth login";
4830
- }
4831
- const results = ui.v2Enabled
4832
- ? []
4833
- : [`Refreshing ${storage.accounts.length} account(s):`, ""];
4834
- let refreshedCount = 0;
4835
- let failedCount = 0;
4836
- for (let i = 0; i < storage.accounts.length; i++) {
4837
- const account = storage.accounts[i];
4838
- if (!account)
4839
- continue;
4840
- const label = formatCommandAccountLabel(account, i);
4841
- try {
4842
- const refreshResult = await queuedRefresh(account.refreshToken);
4843
- if (refreshResult.type === "success") {
4844
- account.refreshToken = refreshResult.refresh;
4845
- account.accessToken = refreshResult.access;
4846
- account.expiresAt = refreshResult.expires;
4847
- results.push(` ${getStatusMarker(ui, "ok")} ${label}: Refreshed`);
4848
- refreshedCount++;
4849
- }
4850
- else {
4851
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Failed - ${refreshResult.message ?? refreshResult.reason}`);
4852
- failedCount++;
4853
- }
4854
- }
4855
- catch (error) {
4856
- const errorMsg = error instanceof Error ? error.message : String(error);
4857
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Error - ${errorMsg.slice(0, 120)}`);
4858
- failedCount++;
4859
- }
4860
- }
4861
- await saveAccounts(storage);
4862
- if (cachedAccountManager) {
4863
- const reloadedManager = await AccountManager.loadFromDisk();
4864
- cachedAccountManager = reloadedManager;
4865
- accountManagerPromise = Promise.resolve(reloadedManager);
4866
- }
4867
- results.push("");
4868
- results.push(`Summary: ${refreshedCount} refreshed, ${failedCount} failed`);
4869
- if (ui.v2Enabled) {
4870
- return [
4871
- ...formatUiHeader(ui, "Refresh accounts"),
4872
- "",
4873
- ...results.map((line) => paintUiText(ui, line, "normal")),
4874
- ].join("\n");
4875
- }
4876
- return results.join("\n");
4877
- },
4878
- }),
4879
- "codex-export": tool({
4880
- description: "Export accounts to a JSON file for backup or migration. Can auto-generate timestamped backup paths.",
4881
- args: {
4882
- path: tool.schema.string().optional().describe("File path to export to (e.g., ~/codex-backup.json). If omitted, a timestamped backup path is used."),
4883
- force: tool.schema.boolean().optional().describe("Overwrite existing file (default: true)"),
4884
- timestamped: tool.schema.boolean().optional().describe("When true (default), omitted paths use a timestamped backup filename."),
4885
- },
4886
- async execute({ path: filePath, force, timestamped, }) {
4887
- const ui = resolveUiRuntime();
4888
- const shouldTimestamp = timestamped ?? true;
4889
- const resolvedExportPath = filePath && filePath.trim().length > 0
4890
- ? filePath
4891
- : shouldTimestamp
4892
- ? createTimestampedBackupPath()
4893
- : "codex-backup.json";
4894
- try {
4895
- await exportAccounts(resolvedExportPath, force ?? true);
4896
- const storage = await loadAccounts();
4897
- const count = storage?.accounts.length ?? 0;
4898
- if (ui.v2Enabled) {
4899
- return [
4900
- ...formatUiHeader(ui, "Export accounts"),
4901
- "",
4902
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Exported ${count} account(s)`, "success"),
4903
- formatUiKeyValue(ui, "Path", resolvedExportPath, "muted"),
4904
- ].join("\n");
4905
- }
4906
- return `Exported ${count} account(s) to: ${resolvedExportPath}`;
4907
- }
4908
- catch (error) {
4909
- const msg = error instanceof Error ? error.message : String(error);
4910
- if (ui.v2Enabled) {
4911
- return [
4912
- ...formatUiHeader(ui, "Export accounts"),
4913
- "",
4914
- formatUiItem(ui, `${getStatusMarker(ui, "error")} Export failed`, "danger"),
4915
- formatUiKeyValue(ui, "Error", msg, "danger"),
4916
- ].join("\n");
4917
- }
4918
- return `Export failed: ${msg}`;
4919
- }
4920
- },
4921
- }),
4922
- "codex-import": tool({
4923
- description: "Import accounts from a JSON file, with dry-run preview and automatic timestamped backup before apply.",
4924
- args: {
4925
- path: tool.schema.string().describe("File path to import from (e.g., ~/codex-backup.json)"),
4926
- dryRun: tool.schema.boolean().optional().describe("Preview import impact without applying changes."),
4927
- },
4928
- async execute({ path: filePath, dryRun }) {
4929
- const ui = resolveUiRuntime();
4930
- try {
4931
- const preview = await previewImportAccounts(filePath);
4932
- if (dryRun) {
4933
- if (ui.v2Enabled) {
4934
- return [
4935
- ...formatUiHeader(ui, "Import preview"),
4936
- "",
4937
- formatUiItem(ui, "No changes applied (dry run).", "warning"),
4938
- formatUiKeyValue(ui, "Path", filePath, "muted"),
4939
- formatUiKeyValue(ui, "New accounts", String(preview.imported), preview.imported > 0 ? "success" : "muted"),
4940
- formatUiKeyValue(ui, "Duplicates skipped", String(preview.skipped), preview.skipped > 0 ? "warning" : "muted"),
4941
- formatUiKeyValue(ui, "Resulting total", String(preview.total), "accent"),
4942
- ].join("\n");
4943
- }
4944
- return [
4945
- "Import preview (dry run):",
4946
- `Path: ${filePath}`,
4947
- `New accounts: ${preview.imported}`,
4948
- `Duplicates skipped: ${preview.skipped}`,
4949
- `Resulting total: ${preview.total}`,
4950
- ].join("\n");
4951
- }
4952
- const result = await importAccounts(filePath, {
4953
- preImportBackupPrefix: "codex-pre-import-backup",
4954
- backupMode: "required",
4955
- });
4956
- const backupSummary = result.backupStatus === "created"
4957
- ? result.backupPath ?? "created"
4958
- : result.backupStatus === "failed"
4959
- ? `failed (${result.backupError ?? "unknown error"})`
4960
- : "skipped (no existing accounts)";
4961
- const backupStatus = result.backupStatus === "created" ? "ok" : "warning";
4962
- invalidateAccountManagerCache();
4963
- const lines = [`Import complete.`, ``];
4964
- lines.push(`Preview: +${preview.imported} new, ${preview.skipped} skipped, ${preview.total} total`);
4965
- lines.push(`Auto-backup: ${backupSummary}`);
4966
- if (result.imported > 0) {
4967
- lines.push(`New accounts: ${result.imported}`);
4968
- }
4969
- if (result.skipped > 0) {
4970
- lines.push(`Duplicates skipped: ${result.skipped}`);
4971
- }
4972
- lines.push(`Total accounts: ${result.total}`);
4973
- if (ui.v2Enabled) {
4974
- const styled = [
4975
- ...formatUiHeader(ui, "Import accounts"),
4976
- "",
4977
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Import complete`, "success"),
4978
- formatUiKeyValue(ui, "Path", filePath, "muted"),
4979
- formatUiKeyValue(ui, "Auto-backup", backupSummary, backupStatus === "ok" ? "muted" : "warning"),
4980
- formatUiKeyValue(ui, "Preview", `+${preview.imported}, skipped=${preview.skipped}, total=${preview.total}`, "muted"),
4981
- formatUiKeyValue(ui, "New accounts", String(result.imported), result.imported > 0 ? "success" : "muted"),
4982
- formatUiKeyValue(ui, "Duplicates skipped", String(result.skipped), result.skipped > 0 ? "warning" : "muted"),
4983
- formatUiKeyValue(ui, "Total accounts", String(result.total), "accent"),
4984
- ];
4985
- return styled.join("\n");
4986
- }
4987
- return lines.join("\n");
4988
- }
4989
- catch (error) {
4990
- const msg = error instanceof Error ? error.message : String(error);
4991
- if (ui.v2Enabled) {
4992
- return [
4993
- ...formatUiHeader(ui, "Import accounts"),
4994
- "",
4995
- formatUiItem(ui, `${getStatusMarker(ui, "error")} Import failed`, "danger"),
4996
- formatUiKeyValue(ui, "Error", msg, "danger"),
4997
- ].join("\n");
4998
- }
4999
- return `Import failed: ${msg}`;
5000
- }
5001
- },
5002
- }),
5003
- },
2581
+ tool: createToolRegistry(ctx),
5004
2582
  };
5005
2583
  };
5006
2584
  export const OpenAIAuthPlugin = OpenAIOAuthPlugin;