codex-multi-auth 0.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 (327) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +162 -0
  3. package/assets/opencode-logo-ornate-dark.svg +18 -0
  4. package/assets/readme-hero.svg +31 -0
  5. package/config/README.md +87 -0
  6. package/config/minimal-opencode.json +13 -0
  7. package/config/opencode-legacy.json +571 -0
  8. package/config/opencode-modern.json +239 -0
  9. package/dist/index.d.ts +45 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +3160 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  14. package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
  15. package/dist/lib/accounts/rate-limits.js +63 -0
  16. package/dist/lib/accounts/rate-limits.js.map +1 -0
  17. package/dist/lib/accounts.d.ts +95 -0
  18. package/dist/lib/accounts.d.ts.map +1 -0
  19. package/dist/lib/accounts.js +668 -0
  20. package/dist/lib/accounts.js.map +1 -0
  21. package/dist/lib/audit.d.ts +45 -0
  22. package/dist/lib/audit.d.ts.map +1 -0
  23. package/dist/lib/audit.js +131 -0
  24. package/dist/lib/audit.js.map +1 -0
  25. package/dist/lib/auth/auth.d.ts +56 -0
  26. package/dist/lib/auth/auth.d.ts.map +1 -0
  27. package/dist/lib/auth/auth.js +214 -0
  28. package/dist/lib/auth/auth.js.map +1 -0
  29. package/dist/lib/auth/browser.d.ts +34 -0
  30. package/dist/lib/auth/browser.d.ts.map +1 -0
  31. package/dist/lib/auth/browser.js +185 -0
  32. package/dist/lib/auth/browser.js.map +1 -0
  33. package/dist/lib/auth/server.d.ts +24 -0
  34. package/dist/lib/auth/server.d.ts.map +1 -0
  35. package/dist/lib/auth/server.js +116 -0
  36. package/dist/lib/auth/server.js.map +1 -0
  37. package/dist/lib/auth/token-utils.d.ts +59 -0
  38. package/dist/lib/auth/token-utils.d.ts.map +1 -0
  39. package/dist/lib/auth/token-utils.js +331 -0
  40. package/dist/lib/auth/token-utils.js.map +1 -0
  41. package/dist/lib/auth-rate-limit.d.ts +20 -0
  42. package/dist/lib/auth-rate-limit.d.ts.map +1 -0
  43. package/dist/lib/auth-rate-limit.js +91 -0
  44. package/dist/lib/auth-rate-limit.js.map +1 -0
  45. package/dist/lib/auto-update-checker.d.ts +10 -0
  46. package/dist/lib/auto-update-checker.d.ts.map +1 -0
  47. package/dist/lib/auto-update-checker.js +216 -0
  48. package/dist/lib/auto-update-checker.js.map +1 -0
  49. package/dist/lib/capability-policy.d.ts +18 -0
  50. package/dist/lib/capability-policy.d.ts.map +1 -0
  51. package/dist/lib/capability-policy.js +150 -0
  52. package/dist/lib/capability-policy.js.map +1 -0
  53. package/dist/lib/circuit-breaker.d.ts +34 -0
  54. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  55. package/dist/lib/circuit-breaker.js +124 -0
  56. package/dist/lib/circuit-breaker.js.map +1 -0
  57. package/dist/lib/cli.d.ts +64 -0
  58. package/dist/lib/cli.d.ts.map +1 -0
  59. package/dist/lib/cli.js +274 -0
  60. package/dist/lib/cli.js.map +1 -0
  61. package/dist/lib/codex-cli/observability.d.ts +22 -0
  62. package/dist/lib/codex-cli/observability.d.ts.map +1 -0
  63. package/dist/lib/codex-cli/observability.js +36 -0
  64. package/dist/lib/codex-cli/observability.js.map +1 -0
  65. package/dist/lib/codex-cli/state.d.ts +86 -0
  66. package/dist/lib/codex-cli/state.d.ts.map +1 -0
  67. package/dist/lib/codex-cli/state.js +470 -0
  68. package/dist/lib/codex-cli/state.js.map +1 -0
  69. package/dist/lib/codex-cli/sync.d.ts +27 -0
  70. package/dist/lib/codex-cli/sync.d.ts.map +1 -0
  71. package/dist/lib/codex-cli/sync.js +325 -0
  72. package/dist/lib/codex-cli/sync.js.map +1 -0
  73. package/dist/lib/codex-cli/writer.d.ts +12 -0
  74. package/dist/lib/codex-cli/writer.d.ts.map +1 -0
  75. package/dist/lib/codex-cli/writer.js +388 -0
  76. package/dist/lib/codex-cli/writer.js.map +1 -0
  77. package/dist/lib/codex-manager.d.ts +2 -0
  78. package/dist/lib/codex-manager.d.ts.map +1 -0
  79. package/dist/lib/codex-manager.js +4841 -0
  80. package/dist/lib/codex-manager.js.map +1 -0
  81. package/dist/lib/config.d.ts +269 -0
  82. package/dist/lib/config.d.ts.map +1 -0
  83. package/dist/lib/config.js +789 -0
  84. package/dist/lib/config.js.map +1 -0
  85. package/dist/lib/constants.d.ts +78 -0
  86. package/dist/lib/constants.d.ts.map +1 -0
  87. package/dist/lib/constants.js +78 -0
  88. package/dist/lib/constants.js.map +1 -0
  89. package/dist/lib/context-overflow.d.ts +27 -0
  90. package/dist/lib/context-overflow.d.ts.map +1 -0
  91. package/dist/lib/context-overflow.js +124 -0
  92. package/dist/lib/context-overflow.js.map +1 -0
  93. package/dist/lib/dashboard-settings.d.ts +90 -0
  94. package/dist/lib/dashboard-settings.d.ts.map +1 -0
  95. package/dist/lib/dashboard-settings.js +327 -0
  96. package/dist/lib/dashboard-settings.js.map +1 -0
  97. package/dist/lib/entitlement-cache.d.ts +41 -0
  98. package/dist/lib/entitlement-cache.d.ts.map +1 -0
  99. package/dist/lib/entitlement-cache.js +137 -0
  100. package/dist/lib/entitlement-cache.js.map +1 -0
  101. package/dist/lib/errors.d.ts +113 -0
  102. package/dist/lib/errors.d.ts.map +1 -0
  103. package/dist/lib/errors.js +103 -0
  104. package/dist/lib/errors.js.map +1 -0
  105. package/dist/lib/forecast.d.ts +42 -0
  106. package/dist/lib/forecast.d.ts.map +1 -0
  107. package/dist/lib/forecast.js +256 -0
  108. package/dist/lib/forecast.js.map +1 -0
  109. package/dist/lib/health.d.ts +33 -0
  110. package/dist/lib/health.d.ts.map +1 -0
  111. package/dist/lib/health.js +70 -0
  112. package/dist/lib/health.js.map +1 -0
  113. package/dist/lib/index.d.ts +32 -0
  114. package/dist/lib/index.d.ts.map +1 -0
  115. package/dist/lib/index.js +32 -0
  116. package/dist/lib/index.js.map +1 -0
  117. package/dist/lib/live-account-sync.d.ts +39 -0
  118. package/dist/lib/live-account-sync.d.ts.map +1 -0
  119. package/dist/lib/live-account-sync.js +196 -0
  120. package/dist/lib/live-account-sync.js.map +1 -0
  121. package/dist/lib/logger.d.ts +40 -0
  122. package/dist/lib/logger.d.ts.map +1 -0
  123. package/dist/lib/logger.js +364 -0
  124. package/dist/lib/logger.js.map +1 -0
  125. package/dist/lib/oauth-success.html +338 -0
  126. package/dist/lib/parallel-probe.d.ts +28 -0
  127. package/dist/lib/parallel-probe.d.ts.map +1 -0
  128. package/dist/lib/parallel-probe.js +97 -0
  129. package/dist/lib/parallel-probe.js.map +1 -0
  130. package/dist/lib/preemptive-quota-scheduler.d.ts +53 -0
  131. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -0
  132. package/dist/lib/preemptive-quota-scheduler.js +220 -0
  133. package/dist/lib/preemptive-quota-scheduler.js.map +1 -0
  134. package/dist/lib/proactive-refresh.d.ts +66 -0
  135. package/dist/lib/proactive-refresh.d.ts.map +1 -0
  136. package/dist/lib/proactive-refresh.js +143 -0
  137. package/dist/lib/proactive-refresh.js.map +1 -0
  138. package/dist/lib/prompts/codex-opencode-bridge.d.ts +19 -0
  139. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
  140. package/dist/lib/prompts/codex-opencode-bridge.js +169 -0
  141. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
  142. package/dist/lib/prompts/codex.d.ts +41 -0
  143. package/dist/lib/prompts/codex.d.ts.map +1 -0
  144. package/dist/lib/prompts/codex.js +383 -0
  145. package/dist/lib/prompts/codex.js.map +1 -0
  146. package/dist/lib/prompts/opencode-codex.d.ts +25 -0
  147. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
  148. package/dist/lib/prompts/opencode-codex.js +270 -0
  149. package/dist/lib/prompts/opencode-codex.js.map +1 -0
  150. package/dist/lib/quota-cache.d.ts +68 -0
  151. package/dist/lib/quota-cache.d.ts.map +1 -0
  152. package/dist/lib/quota-cache.js +224 -0
  153. package/dist/lib/quota-cache.js.map +1 -0
  154. package/dist/lib/quota-probe.d.ts +49 -0
  155. package/dist/lib/quota-probe.d.ts.map +1 -0
  156. package/dist/lib/quota-probe.js +368 -0
  157. package/dist/lib/quota-probe.js.map +1 -0
  158. package/dist/lib/recovery/constants.d.ts +12 -0
  159. package/dist/lib/recovery/constants.d.ts.map +1 -0
  160. package/dist/lib/recovery/constants.js +31 -0
  161. package/dist/lib/recovery/constants.js.map +1 -0
  162. package/dist/lib/recovery/index.d.ts +12 -0
  163. package/dist/lib/recovery/index.d.ts.map +1 -0
  164. package/dist/lib/recovery/index.js +12 -0
  165. package/dist/lib/recovery/index.js.map +1 -0
  166. package/dist/lib/recovery/storage.d.ts +24 -0
  167. package/dist/lib/recovery/storage.d.ts.map +1 -0
  168. package/dist/lib/recovery/storage.js +362 -0
  169. package/dist/lib/recovery/storage.js.map +1 -0
  170. package/dist/lib/recovery/types.d.ts +116 -0
  171. package/dist/lib/recovery/types.d.ts.map +1 -0
  172. package/dist/lib/recovery/types.js +7 -0
  173. package/dist/lib/recovery/types.js.map +1 -0
  174. package/dist/lib/recovery.d.ts +31 -0
  175. package/dist/lib/recovery.d.ts.map +1 -0
  176. package/dist/lib/recovery.js +313 -0
  177. package/dist/lib/recovery.js.map +1 -0
  178. package/dist/lib/refresh-guardian.d.ts +31 -0
  179. package/dist/lib/refresh-guardian.d.ts.map +1 -0
  180. package/dist/lib/refresh-guardian.js +151 -0
  181. package/dist/lib/refresh-guardian.js.map +1 -0
  182. package/dist/lib/refresh-lease.d.ts +37 -0
  183. package/dist/lib/refresh-lease.d.ts.map +1 -0
  184. package/dist/lib/refresh-lease.js +335 -0
  185. package/dist/lib/refresh-lease.js.map +1 -0
  186. package/dist/lib/refresh-queue.d.ts +117 -0
  187. package/dist/lib/refresh-queue.d.ts.map +1 -0
  188. package/dist/lib/refresh-queue.js +297 -0
  189. package/dist/lib/refresh-queue.js.map +1 -0
  190. package/dist/lib/request/failure-policy.d.ts +42 -0
  191. package/dist/lib/request/failure-policy.d.ts.map +1 -0
  192. package/dist/lib/request/failure-policy.js +133 -0
  193. package/dist/lib/request/failure-policy.js.map +1 -0
  194. package/dist/lib/request/fetch-helpers.d.ts +152 -0
  195. package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
  196. package/dist/lib/request/fetch-helpers.js +704 -0
  197. package/dist/lib/request/fetch-helpers.js.map +1 -0
  198. package/dist/lib/request/helpers/input-utils.d.ts +7 -0
  199. package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
  200. package/dist/lib/request/helpers/input-utils.js +214 -0
  201. package/dist/lib/request/helpers/input-utils.js.map +1 -0
  202. package/dist/lib/request/helpers/model-map.d.ts +28 -0
  203. package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
  204. package/dist/lib/request/helpers/model-map.js +133 -0
  205. package/dist/lib/request/helpers/model-map.js.map +1 -0
  206. package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
  207. package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
  208. package/dist/lib/request/helpers/tool-utils.js +117 -0
  209. package/dist/lib/request/helpers/tool-utils.js.map +1 -0
  210. package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
  211. package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
  212. package/dist/lib/request/rate-limit-backoff.js +83 -0
  213. package/dist/lib/request/rate-limit-backoff.js.map +1 -0
  214. package/dist/lib/request/request-transformer.d.ts +107 -0
  215. package/dist/lib/request/request-transformer.d.ts.map +1 -0
  216. package/dist/lib/request/request-transformer.js +814 -0
  217. package/dist/lib/request/request-transformer.js.map +1 -0
  218. package/dist/lib/request/response-handler.d.ts +23 -0
  219. package/dist/lib/request/response-handler.d.ts.map +1 -0
  220. package/dist/lib/request/response-handler.js +155 -0
  221. package/dist/lib/request/response-handler.js.map +1 -0
  222. package/dist/lib/request/stream-failover.d.ts +21 -0
  223. package/dist/lib/request/stream-failover.d.ts.map +1 -0
  224. package/dist/lib/request/stream-failover.js +204 -0
  225. package/dist/lib/request/stream-failover.js.map +1 -0
  226. package/dist/lib/rotation.d.ts +146 -0
  227. package/dist/lib/rotation.d.ts.map +1 -0
  228. package/dist/lib/rotation.js +321 -0
  229. package/dist/lib/rotation.js.map +1 -0
  230. package/dist/lib/runtime-paths.d.ts +58 -0
  231. package/dist/lib/runtime-paths.d.ts.map +1 -0
  232. package/dist/lib/runtime-paths.js +164 -0
  233. package/dist/lib/runtime-paths.js.map +1 -0
  234. package/dist/lib/schemas.d.ts +435 -0
  235. package/dist/lib/schemas.d.ts.map +1 -0
  236. package/dist/lib/schemas.js +268 -0
  237. package/dist/lib/schemas.js.map +1 -0
  238. package/dist/lib/session-affinity.d.ts +23 -0
  239. package/dist/lib/session-affinity.d.ts.map +1 -0
  240. package/dist/lib/session-affinity.js +127 -0
  241. package/dist/lib/session-affinity.js.map +1 -0
  242. package/dist/lib/shutdown.d.ts +7 -0
  243. package/dist/lib/shutdown.d.ts.map +1 -0
  244. package/dist/lib/shutdown.js +43 -0
  245. package/dist/lib/shutdown.js.map +1 -0
  246. package/dist/lib/storage/migrations.d.ts +59 -0
  247. package/dist/lib/storage/migrations.d.ts.map +1 -0
  248. package/dist/lib/storage/migrations.js +41 -0
  249. package/dist/lib/storage/migrations.js.map +1 -0
  250. package/dist/lib/storage/paths.d.ts +51 -0
  251. package/dist/lib/storage/paths.d.ts.map +1 -0
  252. package/dist/lib/storage/paths.js +152 -0
  253. package/dist/lib/storage/paths.js.map +1 -0
  254. package/dist/lib/storage.d.ts +106 -0
  255. package/dist/lib/storage.d.ts.map +1 -0
  256. package/dist/lib/storage.js +896 -0
  257. package/dist/lib/storage.js.map +1 -0
  258. package/dist/lib/table-formatter.d.ts +32 -0
  259. package/dist/lib/table-formatter.d.ts.map +1 -0
  260. package/dist/lib/table-formatter.js +44 -0
  261. package/dist/lib/table-formatter.js.map +1 -0
  262. package/dist/lib/tools/hashline-tools.d.ts +51 -0
  263. package/dist/lib/tools/hashline-tools.d.ts.map +1 -0
  264. package/dist/lib/tools/hashline-tools.js +456 -0
  265. package/dist/lib/tools/hashline-tools.js.map +1 -0
  266. package/dist/lib/types.d.ts +130 -0
  267. package/dist/lib/types.d.ts.map +1 -0
  268. package/dist/lib/types.js +2 -0
  269. package/dist/lib/types.js.map +1 -0
  270. package/dist/lib/ui/ansi.d.ts +40 -0
  271. package/dist/lib/ui/ansi.d.ts.map +1 -0
  272. package/dist/lib/ui/ansi.js +68 -0
  273. package/dist/lib/ui/ansi.js.map +1 -0
  274. package/dist/lib/ui/auth-menu.d.ts +76 -0
  275. package/dist/lib/ui/auth-menu.d.ts.map +1 -0
  276. package/dist/lib/ui/auth-menu.js +590 -0
  277. package/dist/lib/ui/auth-menu.js.map +1 -0
  278. package/dist/lib/ui/confirm.d.ts +11 -0
  279. package/dist/lib/ui/confirm.d.ts.map +1 -0
  280. package/dist/lib/ui/confirm.js +29 -0
  281. package/dist/lib/ui/confirm.js.map +1 -0
  282. package/dist/lib/ui/copy.d.ts +123 -0
  283. package/dist/lib/ui/copy.d.ts.map +1 -0
  284. package/dist/lib/ui/copy.js +127 -0
  285. package/dist/lib/ui/copy.js.map +1 -0
  286. package/dist/lib/ui/format.d.ts +62 -0
  287. package/dist/lib/ui/format.d.ts.map +1 -0
  288. package/dist/lib/ui/format.js +205 -0
  289. package/dist/lib/ui/format.js.map +1 -0
  290. package/dist/lib/ui/runtime.d.ts +43 -0
  291. package/dist/lib/ui/runtime.d.ts.map +1 -0
  292. package/dist/lib/ui/runtime.js +69 -0
  293. package/dist/lib/ui/runtime.js.map +1 -0
  294. package/dist/lib/ui/select.d.ts +60 -0
  295. package/dist/lib/ui/select.d.ts.map +1 -0
  296. package/dist/lib/ui/select.js +467 -0
  297. package/dist/lib/ui/select.js.map +1 -0
  298. package/dist/lib/ui/theme.d.ts +56 -0
  299. package/dist/lib/ui/theme.d.ts.map +1 -0
  300. package/dist/lib/ui/theme.js +186 -0
  301. package/dist/lib/ui/theme.js.map +1 -0
  302. package/dist/lib/unified-settings.d.ts +71 -0
  303. package/dist/lib/unified-settings.d.ts.map +1 -0
  304. package/dist/lib/unified-settings.js +299 -0
  305. package/dist/lib/unified-settings.js.map +1 -0
  306. package/dist/lib/utils.d.ts +29 -0
  307. package/dist/lib/utils.d.ts.map +1 -0
  308. package/dist/lib/utils.js +54 -0
  309. package/dist/lib/utils.js.map +1 -0
  310. package/package.json +115 -0
  311. package/scripts/audit-dev-allowlist.js +128 -0
  312. package/scripts/bench-format/hashline-v2.mjs +642 -0
  313. package/scripts/bench-format/models.mjs +105 -0
  314. package/scripts/bench-format/opencode.mjs +205 -0
  315. package/scripts/bench-format/render.mjs +496 -0
  316. package/scripts/bench-format/stats.mjs +54 -0
  317. package/scripts/bench-format/tasks.mjs +151 -0
  318. package/scripts/benchmark-edit-formats.mjs +1161 -0
  319. package/scripts/benchmark-render-dashboard.mjs +49 -0
  320. package/scripts/codex-multi-auth.js +6 -0
  321. package/scripts/codex-routing.js +34 -0
  322. package/scripts/codex.js +122 -0
  323. package/scripts/copy-oauth-success.js +37 -0
  324. package/scripts/install-opencode-codex-auth.js +193 -0
  325. package/scripts/test-all-models.sh +7 -0
  326. package/scripts/test-model-matrix.js +424 -0
  327. package/scripts/validate-model-map.sh +7 -0
@@ -0,0 +1,668 @@
1
+ import { createLogger } from "./logger.js";
2
+ import { loadAccounts, saveAccounts, } from "./storage.js";
3
+ import { MODEL_FAMILIES } from "./prompts/codex.js";
4
+ import { getHealthTracker, getTokenTracker, selectHybridAccount, } from "./rotation.js";
5
+ import { nowMs } from "./utils.js";
6
+ import { loadCodexCliState, } from "./codex-cli/state.js";
7
+ import { syncAccountStorageFromCodexCli } from "./codex-cli/sync.js";
8
+ import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
9
+ export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
10
+ export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
11
+ export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
12
+ import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
13
+ import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
14
+ const log = createLogger("accounts");
15
+ function initFamilyState(defaultValue) {
16
+ return Object.fromEntries(MODEL_FAMILIES.map((family) => [family, defaultValue]));
17
+ }
18
+ export class AccountManager {
19
+ accounts = [];
20
+ cursorByFamily = initFamilyState(0);
21
+ currentAccountIndexByFamily = initFamilyState(-1);
22
+ lastToastAccountIndex = -1;
23
+ lastToastTime = 0;
24
+ saveDebounceTimer = null;
25
+ pendingSave = null;
26
+ static async loadFromDisk(authFallback) {
27
+ const stored = await loadAccounts();
28
+ const synced = await syncAccountStorageFromCodexCli(stored);
29
+ const sourceOfTruthStorage = synced.storage ?? stored;
30
+ if (synced.changed && sourceOfTruthStorage) {
31
+ try {
32
+ await saveAccounts(sourceOfTruthStorage);
33
+ }
34
+ catch (error) {
35
+ log.debug("Failed to persist Codex CLI source-of-truth sync", {
36
+ error: String(error),
37
+ });
38
+ }
39
+ }
40
+ const manager = new AccountManager(authFallback, sourceOfTruthStorage);
41
+ await manager.hydrateFromCodexCli();
42
+ return manager;
43
+ }
44
+ hasRefreshToken(refreshToken) {
45
+ return this.accounts.some((account) => account.refreshToken === refreshToken);
46
+ }
47
+ async hydrateFromCodexCli() {
48
+ const state = await loadCodexCliState();
49
+ if (!state || state.accounts.length === 0)
50
+ return;
51
+ const cache = new Map();
52
+ for (const snapshot of state.accounts) {
53
+ const email = sanitizeEmail(snapshot.email);
54
+ if (!email || !snapshot.accessToken)
55
+ continue;
56
+ cache.set(email, {
57
+ accessToken: snapshot.accessToken,
58
+ expiresAt: snapshot.expiresAt,
59
+ refreshToken: snapshot.refreshToken,
60
+ accountId: snapshot.accountId,
61
+ });
62
+ }
63
+ if (cache.size === 0)
64
+ return;
65
+ const now = nowMs();
66
+ let changed = false;
67
+ for (const account of this.accounts) {
68
+ const email = sanitizeEmail(account.email);
69
+ if (!email)
70
+ continue;
71
+ const cached = cache.get(email);
72
+ if (!cached)
73
+ continue;
74
+ if (typeof cached.expiresAt === "number" && cached.expiresAt <= now) {
75
+ continue;
76
+ }
77
+ const missingOrExpired = !account.access || account.expires === undefined || account.expires <= now;
78
+ if (missingOrExpired) {
79
+ account.access = cached.accessToken;
80
+ if (typeof cached.expiresAt === "number") {
81
+ account.expires = cached.expiresAt;
82
+ }
83
+ changed = true;
84
+ }
85
+ if (!account.accountId &&
86
+ cached.accountId &&
87
+ shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId)) {
88
+ account.accountId = cached.accountId;
89
+ account.accountIdSource = account.accountIdSource ?? "token";
90
+ changed = true;
91
+ }
92
+ }
93
+ if (!changed)
94
+ return;
95
+ try {
96
+ await this.saveToDisk();
97
+ }
98
+ catch (error) {
99
+ log.debug("Failed to persist Codex CLI cache hydration", { error: String(error) });
100
+ }
101
+ }
102
+ constructor(authFallback, stored) {
103
+ const fallbackAccountId = extractAccountId(authFallback?.access);
104
+ const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
105
+ if (stored && stored.accounts.length > 0) {
106
+ const baseNow = nowMs();
107
+ this.accounts = stored.accounts
108
+ .map((account, index) => {
109
+ if (!account.refreshToken || typeof account.refreshToken !== "string") {
110
+ return null;
111
+ }
112
+ const matchesFallback = !!authFallback &&
113
+ ((fallbackAccountId && account.accountId === fallbackAccountId) ||
114
+ account.refreshToken === authFallback.refresh ||
115
+ (!!fallbackAccountEmail &&
116
+ sanitizeEmail(account.email) === fallbackAccountEmail));
117
+ const refreshToken = matchesFallback && authFallback ? authFallback.refresh : account.refreshToken;
118
+ return {
119
+ index,
120
+ accountId: matchesFallback ? fallbackAccountId ?? account.accountId : account.accountId,
121
+ accountIdSource: account.accountIdSource,
122
+ accountLabel: account.accountLabel,
123
+ email: matchesFallback
124
+ ? fallbackAccountEmail ?? sanitizeEmail(account.email)
125
+ : sanitizeEmail(account.email),
126
+ refreshToken,
127
+ enabled: account.enabled !== false,
128
+ access: matchesFallback && authFallback ? authFallback.access : account.accessToken,
129
+ expires: matchesFallback && authFallback ? authFallback.expires : account.expiresAt,
130
+ addedAt: clampNonNegativeInt(account.addedAt, baseNow),
131
+ lastUsed: clampNonNegativeInt(account.lastUsed, 0),
132
+ lastSwitchReason: account.lastSwitchReason,
133
+ rateLimitResetTimes: account.rateLimitResetTimes ?? {},
134
+ coolingDownUntil: account.coolingDownUntil,
135
+ cooldownReason: account.cooldownReason,
136
+ };
137
+ })
138
+ .filter((account) => account !== null);
139
+ const hasMatchingFallback = !!authFallback &&
140
+ this.accounts.some((account) => account.refreshToken === authFallback.refresh ||
141
+ (fallbackAccountId && account.accountId === fallbackAccountId) ||
142
+ (!!fallbackAccountEmail && account.email === fallbackAccountEmail));
143
+ if (authFallback && !hasMatchingFallback) {
144
+ const now = nowMs();
145
+ this.accounts.push({
146
+ index: this.accounts.length,
147
+ accountId: fallbackAccountId,
148
+ accountIdSource: fallbackAccountId ? "token" : undefined,
149
+ email: fallbackAccountEmail,
150
+ refreshToken: authFallback.refresh,
151
+ enabled: true,
152
+ access: authFallback.access,
153
+ expires: authFallback.expires,
154
+ addedAt: now,
155
+ lastUsed: now,
156
+ lastSwitchReason: "initial",
157
+ rateLimitResetTimes: {},
158
+ });
159
+ }
160
+ if (this.accounts.length > 0) {
161
+ const defaultIndex = clampNonNegativeInt(stored.activeIndex, 0) % this.accounts.length;
162
+ for (const family of MODEL_FAMILIES) {
163
+ const rawIndex = stored.activeIndexByFamily?.[family];
164
+ const nextIndex = clampNonNegativeInt(rawIndex, defaultIndex) % this.accounts.length;
165
+ this.currentAccountIndexByFamily[family] = nextIndex;
166
+ this.cursorByFamily[family] = nextIndex;
167
+ }
168
+ }
169
+ return;
170
+ }
171
+ if (authFallback) {
172
+ const now = nowMs();
173
+ this.accounts = [
174
+ {
175
+ index: 0,
176
+ accountId: fallbackAccountId,
177
+ accountIdSource: fallbackAccountId ? "token" : undefined,
178
+ email: fallbackAccountEmail,
179
+ refreshToken: authFallback.refresh,
180
+ enabled: true,
181
+ access: authFallback.access,
182
+ expires: authFallback.expires,
183
+ addedAt: now,
184
+ lastUsed: 0,
185
+ lastSwitchReason: "initial",
186
+ rateLimitResetTimes: {},
187
+ },
188
+ ];
189
+ for (const family of MODEL_FAMILIES) {
190
+ this.currentAccountIndexByFamily[family] = 0;
191
+ this.cursorByFamily[family] = 0;
192
+ }
193
+ }
194
+ }
195
+ getAccountCount() {
196
+ return this.accounts.length;
197
+ }
198
+ getActiveIndex() {
199
+ return this.getActiveIndexForFamily("codex");
200
+ }
201
+ getActiveIndexForFamily(family) {
202
+ const index = this.currentAccountIndexByFamily[family];
203
+ if (index < 0 || index >= this.accounts.length) {
204
+ return this.accounts.length > 0 ? 0 : -1;
205
+ }
206
+ return index;
207
+ }
208
+ getAccountsSnapshot() {
209
+ return this.accounts.map((account) => ({
210
+ ...account,
211
+ rateLimitResetTimes: { ...account.rateLimitResetTimes },
212
+ }));
213
+ }
214
+ getAccountByIndex(index) {
215
+ if (!Number.isFinite(index))
216
+ return null;
217
+ if (index < 0 || index >= this.accounts.length)
218
+ return null;
219
+ const account = this.accounts[index];
220
+ return account ?? null;
221
+ }
222
+ isAccountAvailableForFamily(index, family, model) {
223
+ const account = this.getAccountByIndex(index);
224
+ if (!account)
225
+ return false;
226
+ if (account.enabled === false)
227
+ return false;
228
+ clearExpiredRateLimits(account);
229
+ return !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
230
+ }
231
+ setActiveIndex(index) {
232
+ if (!Number.isFinite(index))
233
+ return null;
234
+ if (index < 0 || index >= this.accounts.length)
235
+ return null;
236
+ const account = this.accounts[index];
237
+ if (!account)
238
+ return null;
239
+ if (account.enabled === false)
240
+ return null;
241
+ for (const family of MODEL_FAMILIES) {
242
+ this.currentAccountIndexByFamily[family] = index;
243
+ this.cursorByFamily[family] = index;
244
+ }
245
+ account.lastUsed = nowMs();
246
+ account.lastSwitchReason = "rotation";
247
+ void this.syncCodexCliActiveSelectionForIndex(account.index);
248
+ return account;
249
+ }
250
+ async syncCodexCliActiveSelectionForIndex(index) {
251
+ if (!Number.isFinite(index))
252
+ return;
253
+ if (index < 0 || index >= this.accounts.length)
254
+ return;
255
+ const account = this.accounts[index];
256
+ if (!account)
257
+ return;
258
+ await setCodexCliActiveSelection({
259
+ accountId: account.accountId,
260
+ email: account.email,
261
+ accessToken: account.access,
262
+ refreshToken: account.refreshToken,
263
+ expiresAt: account.expires,
264
+ });
265
+ }
266
+ getCurrentAccount() {
267
+ return this.getCurrentAccountForFamily("codex");
268
+ }
269
+ getCurrentAccountForFamily(family) {
270
+ const index = this.currentAccountIndexByFamily[family];
271
+ if (index < 0 || index >= this.accounts.length) {
272
+ return null;
273
+ }
274
+ const account = this.accounts[index];
275
+ if (!account || account.enabled === false) {
276
+ return null;
277
+ }
278
+ return account;
279
+ }
280
+ getCurrentOrNext() {
281
+ return this.getCurrentOrNextForFamily("codex");
282
+ }
283
+ getCurrentOrNextForFamily(family, model) {
284
+ const count = this.accounts.length;
285
+ if (count === 0)
286
+ return null;
287
+ const cursor = this.cursorByFamily[family];
288
+ for (let i = 0; i < count; i++) {
289
+ const idx = (cursor + i) % count;
290
+ const account = this.accounts[idx];
291
+ if (!account)
292
+ continue;
293
+ if (account.enabled === false)
294
+ continue;
295
+ clearExpiredRateLimits(account);
296
+ if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
297
+ continue;
298
+ }
299
+ this.cursorByFamily[family] = (idx + 1) % count;
300
+ this.currentAccountIndexByFamily[family] = idx;
301
+ account.lastUsed = nowMs();
302
+ return account;
303
+ }
304
+ return null;
305
+ }
306
+ getNextForFamily(family, model) {
307
+ const count = this.accounts.length;
308
+ if (count === 0)
309
+ return null;
310
+ const cursor = this.cursorByFamily[family];
311
+ for (let i = 0; i < count; i++) {
312
+ const idx = (cursor + i) % count;
313
+ const account = this.accounts[idx];
314
+ if (!account)
315
+ continue;
316
+ if (account.enabled === false)
317
+ continue;
318
+ clearExpiredRateLimits(account);
319
+ if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
320
+ continue;
321
+ }
322
+ this.cursorByFamily[family] = (idx + 1) % count;
323
+ account.lastUsed = nowMs();
324
+ return account;
325
+ }
326
+ return null;
327
+ }
328
+ getCurrentOrNextForFamilyHybrid(family, model, options) {
329
+ const count = this.accounts.length;
330
+ if (count === 0)
331
+ return null;
332
+ const currentIndex = this.currentAccountIndexByFamily[family];
333
+ if (currentIndex >= 0 && currentIndex < count) {
334
+ const currentAccount = this.accounts[currentIndex];
335
+ if (currentAccount) {
336
+ if (currentAccount.enabled === false) {
337
+ // Fall through to hybrid selection.
338
+ }
339
+ else {
340
+ clearExpiredRateLimits(currentAccount);
341
+ if (!isRateLimitedForFamily(currentAccount, family, model) &&
342
+ !this.isAccountCoolingDown(currentAccount)) {
343
+ currentAccount.lastUsed = nowMs();
344
+ return currentAccount;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ const quotaKey = model ? `${family}:${model}` : family;
350
+ const healthTracker = getHealthTracker();
351
+ const tokenTracker = getTokenTracker();
352
+ const accountsWithMetrics = this.accounts
353
+ .map((account) => {
354
+ if (!account)
355
+ return null;
356
+ if (account.enabled === false)
357
+ return null;
358
+ clearExpiredRateLimits(account);
359
+ const isAvailable = !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
360
+ return {
361
+ index: account.index,
362
+ isAvailable,
363
+ lastUsed: account.lastUsed,
364
+ };
365
+ })
366
+ .filter((a) => a !== null);
367
+ const selected = selectHybridAccount(accountsWithMetrics, healthTracker, tokenTracker, quotaKey, {}, options);
368
+ if (!selected)
369
+ return null;
370
+ const account = this.accounts[selected.index];
371
+ if (!account)
372
+ return null;
373
+ this.currentAccountIndexByFamily[family] = account.index;
374
+ this.cursorByFamily[family] = (account.index + 1) % count;
375
+ account.lastUsed = nowMs();
376
+ return account;
377
+ }
378
+ recordSuccess(account, family, model) {
379
+ const quotaKey = model ? `${family}:${model}` : family;
380
+ const healthTracker = getHealthTracker();
381
+ healthTracker.recordSuccess(account.index, quotaKey);
382
+ }
383
+ recordRateLimit(account, family, model) {
384
+ const quotaKey = model ? `${family}:${model}` : family;
385
+ const healthTracker = getHealthTracker();
386
+ const tokenTracker = getTokenTracker();
387
+ healthTracker.recordRateLimit(account.index, quotaKey);
388
+ tokenTracker.drain(account.index, quotaKey);
389
+ }
390
+ recordFailure(account, family, model) {
391
+ const quotaKey = model ? `${family}:${model}` : family;
392
+ const healthTracker = getHealthTracker();
393
+ healthTracker.recordFailure(account.index, quotaKey);
394
+ }
395
+ consumeToken(account, family, model) {
396
+ const quotaKey = model ? `${family}:${model}` : family;
397
+ const tokenTracker = getTokenTracker();
398
+ return tokenTracker.tryConsume(account.index, quotaKey);
399
+ }
400
+ /**
401
+ * Refund a token consumed within the refund window (30 seconds).
402
+ * Use this when a request fails due to network errors (not rate limits).
403
+ * @returns true if refund was successful, false if no valid consumption found
404
+ */
405
+ refundToken(account, family, model) {
406
+ const quotaKey = model ? `${family}:${model}` : family;
407
+ const tokenTracker = getTokenTracker();
408
+ return tokenTracker.refundToken(account.index, quotaKey);
409
+ }
410
+ markSwitched(account, reason, family) {
411
+ account.lastSwitchReason = reason;
412
+ this.currentAccountIndexByFamily[family] = account.index;
413
+ }
414
+ markRateLimited(account, retryAfterMs, family, model) {
415
+ this.markRateLimitedWithReason(account, retryAfterMs, family, "unknown", model);
416
+ }
417
+ markRateLimitedWithReason(account, retryAfterMs, family, reason, model) {
418
+ const retryMs = Math.max(0, Math.floor(retryAfterMs));
419
+ const resetAt = nowMs() + retryMs;
420
+ const baseKey = getQuotaKey(family);
421
+ account.rateLimitResetTimes[baseKey] = resetAt;
422
+ if (model) {
423
+ const modelKey = getQuotaKey(family, model);
424
+ account.rateLimitResetTimes[modelKey] = resetAt;
425
+ }
426
+ account.lastRateLimitReason = reason;
427
+ }
428
+ markAccountCoolingDown(account, cooldownMs, reason) {
429
+ const ms = Math.max(0, Math.floor(cooldownMs));
430
+ account.coolingDownUntil = nowMs() + ms;
431
+ account.cooldownReason = reason;
432
+ }
433
+ isAccountCoolingDown(account) {
434
+ if (account.coolingDownUntil === undefined)
435
+ return false;
436
+ if (nowMs() >= account.coolingDownUntil) {
437
+ this.clearAccountCooldown(account);
438
+ return false;
439
+ }
440
+ return true;
441
+ }
442
+ clearAccountCooldown(account) {
443
+ delete account.coolingDownUntil;
444
+ delete account.cooldownReason;
445
+ }
446
+ incrementAuthFailures(account) {
447
+ account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
448
+ return account.consecutiveAuthFailures;
449
+ }
450
+ clearAuthFailures(account) {
451
+ account.consecutiveAuthFailures = 0;
452
+ }
453
+ shouldShowAccountToast(accountIndex, debounceMs = 30000) {
454
+ const now = nowMs();
455
+ if (accountIndex === this.lastToastAccountIndex && now - this.lastToastTime < debounceMs) {
456
+ return false;
457
+ }
458
+ return true;
459
+ }
460
+ markToastShown(accountIndex) {
461
+ this.lastToastAccountIndex = accountIndex;
462
+ this.lastToastTime = nowMs();
463
+ }
464
+ updateFromAuth(account, auth) {
465
+ account.refreshToken = auth.refresh;
466
+ account.access = auth.access;
467
+ account.expires = auth.expires;
468
+ const tokenAccountId = extractAccountId(auth.access);
469
+ if (tokenAccountId &&
470
+ (shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId))) {
471
+ account.accountId = tokenAccountId;
472
+ account.accountIdSource = "token";
473
+ }
474
+ account.email = sanitizeEmail(extractAccountEmail(auth.access)) ?? account.email;
475
+ }
476
+ toAuthDetails(account) {
477
+ return {
478
+ type: "oauth",
479
+ access: account.access ?? "",
480
+ refresh: account.refreshToken,
481
+ expires: account.expires ?? 0,
482
+ };
483
+ }
484
+ getMinWaitTime() {
485
+ return this.getMinWaitTimeForFamily("codex");
486
+ }
487
+ getMinWaitTimeForFamily(family, model) {
488
+ const now = nowMs();
489
+ const enabledAccounts = this.accounts.filter((account) => account.enabled !== false);
490
+ const available = enabledAccounts.filter((account) => {
491
+ clearExpiredRateLimits(account);
492
+ return !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
493
+ });
494
+ if (available.length > 0)
495
+ return 0;
496
+ if (enabledAccounts.length === 0)
497
+ return 0;
498
+ const waitTimes = [];
499
+ const baseKey = getQuotaKey(family);
500
+ const modelKey = model ? getQuotaKey(family, model) : null;
501
+ for (const account of enabledAccounts) {
502
+ const baseResetAt = account.rateLimitResetTimes[baseKey];
503
+ if (typeof baseResetAt === "number") {
504
+ waitTimes.push(Math.max(0, baseResetAt - now));
505
+ }
506
+ if (modelKey) {
507
+ const modelResetAt = account.rateLimitResetTimes[modelKey];
508
+ if (typeof modelResetAt === "number") {
509
+ waitTimes.push(Math.max(0, modelResetAt - now));
510
+ }
511
+ }
512
+ if (typeof account.coolingDownUntil === "number") {
513
+ waitTimes.push(Math.max(0, account.coolingDownUntil - now));
514
+ }
515
+ }
516
+ return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
517
+ }
518
+ removeAccount(account) {
519
+ const idx = this.accounts.indexOf(account);
520
+ if (idx < 0) {
521
+ return false;
522
+ }
523
+ this.accounts.splice(idx, 1);
524
+ this.accounts.forEach((acc, index) => {
525
+ acc.index = index;
526
+ });
527
+ if (this.accounts.length === 0) {
528
+ for (const family of MODEL_FAMILIES) {
529
+ this.cursorByFamily[family] = 0;
530
+ this.currentAccountIndexByFamily[family] = -1;
531
+ }
532
+ return true;
533
+ }
534
+ for (const family of MODEL_FAMILIES) {
535
+ if (this.cursorByFamily[family] > idx) {
536
+ this.cursorByFamily[family] = Math.max(0, this.cursorByFamily[family] - 1);
537
+ }
538
+ }
539
+ for (const family of MODEL_FAMILIES) {
540
+ this.cursorByFamily[family] = this.cursorByFamily[family] % this.accounts.length;
541
+ }
542
+ for (const family of MODEL_FAMILIES) {
543
+ if (this.currentAccountIndexByFamily[family] > idx) {
544
+ this.currentAccountIndexByFamily[family] -= 1;
545
+ }
546
+ if (this.currentAccountIndexByFamily[family] >= this.accounts.length) {
547
+ this.currentAccountIndexByFamily[family] = -1;
548
+ }
549
+ }
550
+ return true;
551
+ }
552
+ removeAccountByIndex(index) {
553
+ if (!Number.isFinite(index))
554
+ return false;
555
+ if (index < 0 || index >= this.accounts.length)
556
+ return false;
557
+ const account = this.accounts[index];
558
+ if (!account)
559
+ return false;
560
+ return this.removeAccount(account);
561
+ }
562
+ setAccountEnabled(index, enabled) {
563
+ if (!Number.isFinite(index))
564
+ return null;
565
+ if (index < 0 || index >= this.accounts.length)
566
+ return null;
567
+ const account = this.accounts[index];
568
+ if (!account)
569
+ return null;
570
+ account.enabled = enabled;
571
+ return account;
572
+ }
573
+ async saveToDisk() {
574
+ const activeIndexByFamily = {};
575
+ for (const family of MODEL_FAMILIES) {
576
+ const raw = this.currentAccountIndexByFamily[family];
577
+ activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
578
+ }
579
+ const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
580
+ const storage = {
581
+ version: 3,
582
+ accounts: this.accounts.map((account) => ({
583
+ accountId: account.accountId,
584
+ accountIdSource: account.accountIdSource,
585
+ accountLabel: account.accountLabel,
586
+ email: account.email,
587
+ refreshToken: account.refreshToken,
588
+ accessToken: account.access,
589
+ expiresAt: account.expires,
590
+ enabled: account.enabled === false ? false : undefined,
591
+ addedAt: account.addedAt,
592
+ lastUsed: account.lastUsed,
593
+ lastSwitchReason: account.lastSwitchReason,
594
+ rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
595
+ coolingDownUntil: account.coolingDownUntil,
596
+ cooldownReason: account.cooldownReason,
597
+ })),
598
+ activeIndex,
599
+ activeIndexByFamily,
600
+ };
601
+ await saveAccounts(storage);
602
+ }
603
+ saveToDiskDebounced(delayMs = 500) {
604
+ if (this.saveDebounceTimer) {
605
+ clearTimeout(this.saveDebounceTimer);
606
+ }
607
+ this.saveDebounceTimer = setTimeout(() => {
608
+ this.saveDebounceTimer = null;
609
+ const doSave = async () => {
610
+ try {
611
+ if (this.pendingSave) {
612
+ await this.pendingSave;
613
+ }
614
+ this.pendingSave = this.saveToDisk().finally(() => {
615
+ this.pendingSave = null;
616
+ });
617
+ await this.pendingSave;
618
+ }
619
+ catch (error) {
620
+ log.warn("Debounced save failed", { error: error instanceof Error ? error.message : String(error) });
621
+ }
622
+ };
623
+ void doSave();
624
+ }, delayMs);
625
+ }
626
+ async flushPendingSave() {
627
+ if (this.saveDebounceTimer) {
628
+ clearTimeout(this.saveDebounceTimer);
629
+ this.saveDebounceTimer = null;
630
+ await this.saveToDisk();
631
+ }
632
+ if (this.pendingSave) {
633
+ await this.pendingSave;
634
+ }
635
+ }
636
+ }
637
+ export function formatAccountLabel(account, index) {
638
+ const accountLabel = account?.accountLabel?.trim();
639
+ const email = account?.email?.trim();
640
+ const accountId = account?.accountId?.trim();
641
+ const idSuffix = accountId ? (accountId.length > 6 ? accountId.slice(-6) : accountId) : null;
642
+ if (accountLabel && email && idSuffix) {
643
+ return `Account ${index + 1} (${accountLabel}, ${email}, id:${idSuffix})`;
644
+ }
645
+ if (accountLabel && email)
646
+ return `Account ${index + 1} (${accountLabel}, ${email})`;
647
+ if (accountLabel && idSuffix)
648
+ return `Account ${index + 1} (${accountLabel}, id:${idSuffix})`;
649
+ if (accountLabel)
650
+ return `Account ${index + 1} (${accountLabel})`;
651
+ if (email && idSuffix)
652
+ return `Account ${index + 1} (${email}, id:${idSuffix})`;
653
+ if (email)
654
+ return `Account ${index + 1} (${email})`;
655
+ if (idSuffix)
656
+ return `Account ${index + 1} (${idSuffix})`;
657
+ return `Account ${index + 1}`;
658
+ }
659
+ export function formatCooldown(account, now = nowMs()) {
660
+ if (typeof account.coolingDownUntil !== "number")
661
+ return null;
662
+ const remaining = account.coolingDownUntil - now;
663
+ if (remaining <= 0)
664
+ return null;
665
+ const reason = account.cooldownReason ? ` (${account.cooldownReason})` : "";
666
+ return `${formatWaitTime(remaining)}${reason}`;
667
+ }
668
+ //# sourceMappingURL=accounts.js.map