codex-multi-auth 1.2.0 → 1.2.2

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 (651) hide show
  1. package/README.md +49 -32
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +929 -1712
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts +10 -0
  6. package/dist/lib/accounts.d.ts.map +1 -1
  7. package/dist/lib/accounts.js +312 -69
  8. package/dist/lib/accounts.js.map +1 -1
  9. package/dist/lib/auth/index.d.ts +2 -0
  10. package/dist/lib/auth/index.d.ts.map +1 -0
  11. package/dist/lib/auth/index.js +2 -0
  12. package/dist/lib/auth/index.js.map +1 -0
  13. package/dist/lib/circuit-breaker.d.ts +4 -0
  14. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  15. package/dist/lib/circuit-breaker.js +33 -1
  16. package/dist/lib/circuit-breaker.js.map +1 -1
  17. package/dist/lib/codex-cli/index.d.ts +5 -0
  18. package/dist/lib/codex-cli/index.d.ts.map +1 -0
  19. package/dist/lib/codex-cli/index.js +5 -0
  20. package/dist/lib/codex-cli/index.js.map +1 -0
  21. package/dist/lib/codex-manager/auth-commands.d.ts +72 -0
  22. package/dist/lib/codex-manager/auth-commands.d.ts.map +1 -0
  23. package/dist/lib/codex-manager/auth-commands.js +643 -0
  24. package/dist/lib/codex-manager/auth-commands.js.map +1 -0
  25. package/dist/lib/codex-manager/backend-category-entry.d.ts +103 -0
  26. package/dist/lib/codex-manager/backend-category-entry.d.ts.map +1 -0
  27. package/dist/lib/codex-manager/backend-category-entry.js +23 -0
  28. package/dist/lib/codex-manager/backend-category-entry.js.map +1 -0
  29. package/dist/lib/codex-manager/backend-category-helpers.d.ts +10 -0
  30. package/dist/lib/codex-manager/backend-category-helpers.d.ts.map +1 -0
  31. package/dist/lib/codex-manager/backend-category-helpers.js +29 -0
  32. package/dist/lib/codex-manager/backend-category-helpers.js.map +1 -0
  33. package/dist/lib/codex-manager/backend-category-prompt.d.ts +55 -0
  34. package/dist/lib/codex-manager/backend-category-prompt.d.ts.map +1 -0
  35. package/dist/lib/codex-manager/backend-category-prompt.js +178 -0
  36. package/dist/lib/codex-manager/backend-category-prompt.js.map +1 -0
  37. package/dist/lib/codex-manager/backend-settings-controller.d.ts +11 -0
  38. package/dist/lib/codex-manager/backend-settings-controller.d.ts.map +1 -0
  39. package/dist/lib/codex-manager/backend-settings-controller.js +14 -0
  40. package/dist/lib/codex-manager/backend-settings-controller.js.map +1 -0
  41. package/dist/lib/codex-manager/backend-settings-entry.d.ts +20 -0
  42. package/dist/lib/codex-manager/backend-settings-entry.d.ts.map +1 -0
  43. package/dist/lib/codex-manager/backend-settings-entry.js +12 -0
  44. package/dist/lib/codex-manager/backend-settings-entry.js.map +1 -0
  45. package/dist/lib/codex-manager/backend-settings-helpers.d.ts +16 -0
  46. package/dist/lib/codex-manager/backend-settings-helpers.d.ts.map +1 -0
  47. package/dist/lib/codex-manager/backend-settings-helpers.js +102 -0
  48. package/dist/lib/codex-manager/backend-settings-helpers.js.map +1 -0
  49. package/dist/lib/codex-manager/backend-settings-prompt.d.ts +49 -0
  50. package/dist/lib/codex-manager/backend-settings-prompt.d.ts.map +1 -0
  51. package/dist/lib/codex-manager/backend-settings-prompt.js +121 -0
  52. package/dist/lib/codex-manager/backend-settings-prompt.js.map +1 -0
  53. package/dist/lib/codex-manager/backend-settings-schema.d.ts +55 -0
  54. package/dist/lib/codex-manager/backend-settings-schema.d.ts.map +1 -0
  55. package/dist/lib/codex-manager/backend-settings-schema.js +264 -0
  56. package/dist/lib/codex-manager/backend-settings-schema.js.map +1 -0
  57. package/dist/lib/codex-manager/behavior-settings-panel.d.ts +33 -0
  58. package/dist/lib/codex-manager/behavior-settings-panel.d.ts.map +1 -0
  59. package/dist/lib/codex-manager/behavior-settings-panel.js +180 -0
  60. package/dist/lib/codex-manager/behavior-settings-panel.js.map +1 -0
  61. package/dist/lib/codex-manager/commands/best.d.ts +81 -0
  62. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -0
  63. package/dist/lib/codex-manager/commands/best.js +223 -0
  64. package/dist/lib/codex-manager/commands/best.js.map +1 -0
  65. package/dist/lib/codex-manager/commands/check.d.ts +7 -0
  66. package/dist/lib/codex-manager/commands/check.d.ts.map +1 -0
  67. package/dist/lib/codex-manager/commands/check.js +5 -0
  68. package/dist/lib/codex-manager/commands/check.js.map +1 -0
  69. package/dist/lib/codex-manager/commands/config-explain.d.ts +7 -0
  70. package/dist/lib/codex-manager/commands/config-explain.d.ts.map +1 -0
  71. package/dist/lib/codex-manager/commands/config-explain.js +31 -0
  72. package/dist/lib/codex-manager/commands/config-explain.js.map +1 -0
  73. package/dist/lib/codex-manager/commands/debug-bundle.d.ts +28 -0
  74. package/dist/lib/codex-manager/commands/debug-bundle.d.ts.map +1 -0
  75. package/dist/lib/codex-manager/commands/debug-bundle.js +62 -0
  76. package/dist/lib/codex-manager/commands/debug-bundle.js.map +1 -0
  77. package/dist/lib/codex-manager/commands/doctor.d.ts +85 -0
  78. package/dist/lib/codex-manager/commands/doctor.d.ts.map +1 -0
  79. package/dist/lib/codex-manager/commands/doctor.js +424 -0
  80. package/dist/lib/codex-manager/commands/doctor.js.map +1 -0
  81. package/dist/lib/codex-manager/commands/fix.d.ts +92 -0
  82. package/dist/lib/codex-manager/commands/fix.d.ts.map +1 -0
  83. package/dist/lib/codex-manager/commands/fix.js +347 -0
  84. package/dist/lib/codex-manager/commands/fix.js.map +1 -0
  85. package/dist/lib/codex-manager/commands/forecast.d.ts +71 -0
  86. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -0
  87. package/dist/lib/codex-manager/commands/forecast.js +371 -0
  88. package/dist/lib/codex-manager/commands/forecast.js.map +1 -0
  89. package/dist/lib/codex-manager/commands/init-config.d.ts +15 -0
  90. package/dist/lib/codex-manager/commands/init-config.d.ts.map +1 -0
  91. package/dist/lib/codex-manager/commands/init-config.js +95 -0
  92. package/dist/lib/codex-manager/commands/init-config.js.map +1 -0
  93. package/dist/lib/codex-manager/commands/report.d.ts +25 -0
  94. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -0
  95. package/dist/lib/codex-manager/commands/report.js +399 -0
  96. package/dist/lib/codex-manager/commands/report.js.map +1 -0
  97. package/dist/lib/codex-manager/commands/status.d.ts +23 -0
  98. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -0
  99. package/dist/lib/codex-manager/commands/status.js +50 -0
  100. package/dist/lib/codex-manager/commands/status.js.map +1 -0
  101. package/dist/lib/codex-manager/commands/switch.d.ts +22 -0
  102. package/dist/lib/codex-manager/commands/switch.d.ts.map +1 -0
  103. package/dist/lib/codex-manager/commands/switch.js +41 -0
  104. package/dist/lib/codex-manager/commands/switch.js.map +1 -0
  105. package/dist/lib/codex-manager/commands/verify-flagged.d.ts +69 -0
  106. package/dist/lib/codex-manager/commands/verify-flagged.d.ts.map +1 -0
  107. package/dist/lib/codex-manager/commands/verify-flagged.js +251 -0
  108. package/dist/lib/codex-manager/commands/verify-flagged.js.map +1 -0
  109. package/dist/lib/codex-manager/dashboard-display-panel.d.ts +40 -0
  110. package/dist/lib/codex-manager/dashboard-display-panel.d.ts.map +1 -0
  111. package/dist/lib/codex-manager/dashboard-display-panel.js +187 -0
  112. package/dist/lib/codex-manager/dashboard-display-panel.js.map +1 -0
  113. package/dist/lib/codex-manager/dashboard-formatters.d.ts +6 -0
  114. package/dist/lib/codex-manager/dashboard-formatters.d.ts.map +1 -0
  115. package/dist/lib/codex-manager/dashboard-formatters.js +19 -0
  116. package/dist/lib/codex-manager/dashboard-formatters.js.map +1 -0
  117. package/dist/lib/codex-manager/dashboard-settings-controller.d.ts +12 -0
  118. package/dist/lib/codex-manager/dashboard-settings-controller.d.ts.map +1 -0
  119. package/dist/lib/codex-manager/dashboard-settings-controller.js +17 -0
  120. package/dist/lib/codex-manager/dashboard-settings-controller.js.map +1 -0
  121. package/dist/lib/codex-manager/dashboard-settings-data.d.ts +10 -0
  122. package/dist/lib/codex-manager/dashboard-settings-data.d.ts.map +1 -0
  123. package/dist/lib/codex-manager/dashboard-settings-data.js +95 -0
  124. package/dist/lib/codex-manager/dashboard-settings-data.js.map +1 -0
  125. package/dist/lib/codex-manager/dashboard-settings-entry.d.ts +22 -0
  126. package/dist/lib/codex-manager/dashboard-settings-entry.d.ts.map +1 -0
  127. package/dist/lib/codex-manager/dashboard-settings-entry.js +13 -0
  128. package/dist/lib/codex-manager/dashboard-settings-entry.js.map +1 -0
  129. package/dist/lib/codex-manager/experimental-settings-entry.d.ts +7 -0
  130. package/dist/lib/codex-manager/experimental-settings-entry.d.ts.map +1 -0
  131. package/dist/lib/codex-manager/experimental-settings-entry.js +30 -0
  132. package/dist/lib/codex-manager/experimental-settings-entry.js.map +1 -0
  133. package/dist/lib/codex-manager/experimental-settings-prompt.d.ts +66 -0
  134. package/dist/lib/codex-manager/experimental-settings-prompt.d.ts.map +1 -0
  135. package/dist/lib/codex-manager/experimental-settings-prompt.js +221 -0
  136. package/dist/lib/codex-manager/experimental-settings-prompt.js.map +1 -0
  137. package/dist/lib/codex-manager/experimental-settings-schema.d.ts +23 -0
  138. package/dist/lib/codex-manager/experimental-settings-schema.d.ts.map +1 -0
  139. package/dist/lib/codex-manager/experimental-settings-schema.js +34 -0
  140. package/dist/lib/codex-manager/experimental-settings-schema.js.map +1 -0
  141. package/dist/lib/codex-manager/experimental-sync-target-entry.d.ts +30 -0
  142. package/dist/lib/codex-manager/experimental-sync-target-entry.d.ts.map +1 -0
  143. package/dist/lib/codex-manager/experimental-sync-target-entry.js +16 -0
  144. package/dist/lib/codex-manager/experimental-sync-target-entry.js.map +1 -0
  145. package/dist/lib/codex-manager/experimental-sync-target.d.ts +23 -0
  146. package/dist/lib/codex-manager/experimental-sync-target.d.ts.map +1 -0
  147. package/dist/lib/codex-manager/experimental-sync-target.js +39 -0
  148. package/dist/lib/codex-manager/experimental-sync-target.js.map +1 -0
  149. package/dist/lib/codex-manager/forecast-report-commands.d.ts +54 -0
  150. package/dist/lib/codex-manager/forecast-report-commands.d.ts.map +1 -0
  151. package/dist/lib/codex-manager/forecast-report-commands.js +443 -0
  152. package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -0
  153. package/dist/lib/codex-manager/help.d.ts +36 -0
  154. package/dist/lib/codex-manager/help.d.ts.map +1 -0
  155. package/dist/lib/codex-manager/help.js +125 -0
  156. package/dist/lib/codex-manager/help.js.map +1 -0
  157. package/dist/lib/codex-manager/repair-commands.d.ts +70 -0
  158. package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -0
  159. package/dist/lib/codex-manager/repair-commands.js +1671 -0
  160. package/dist/lib/codex-manager/repair-commands.js.map +1 -0
  161. package/dist/lib/codex-manager/settings-hub-entry.d.ts +30 -0
  162. package/dist/lib/codex-manager/settings-hub-entry.d.ts.map +1 -0
  163. package/dist/lib/codex-manager/settings-hub-entry.js +11 -0
  164. package/dist/lib/codex-manager/settings-hub-entry.js.map +1 -0
  165. package/dist/lib/codex-manager/settings-hub-menu.d.ts +30 -0
  166. package/dist/lib/codex-manager/settings-hub-menu.d.ts.map +1 -0
  167. package/dist/lib/codex-manager/settings-hub-menu.js +37 -0
  168. package/dist/lib/codex-manager/settings-hub-menu.js.map +1 -0
  169. package/dist/lib/codex-manager/settings-hub-prompt.d.ts +25 -0
  170. package/dist/lib/codex-manager/settings-hub-prompt.d.ts.map +1 -0
  171. package/dist/lib/codex-manager/settings-hub-prompt.js +23 -0
  172. package/dist/lib/codex-manager/settings-hub-prompt.js.map +1 -0
  173. package/dist/lib/codex-manager/settings-hub.d.ts +10 -29
  174. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  175. package/dist/lib/codex-manager/settings-hub.js +260 -2168
  176. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  177. package/dist/lib/codex-manager/settings-panels.d.ts +56 -0
  178. package/dist/lib/codex-manager/settings-panels.d.ts.map +1 -0
  179. package/dist/lib/codex-manager/settings-panels.js +73 -0
  180. package/dist/lib/codex-manager/settings-panels.js.map +1 -0
  181. package/dist/lib/codex-manager/settings-persist-utils.d.ts +9 -0
  182. package/dist/lib/codex-manager/settings-persist-utils.d.ts.map +1 -0
  183. package/dist/lib/codex-manager/settings-persist-utils.js +33 -0
  184. package/dist/lib/codex-manager/settings-persist-utils.js.map +1 -0
  185. package/dist/lib/codex-manager/settings-preview.d.ts +12 -0
  186. package/dist/lib/codex-manager/settings-preview.d.ts.map +1 -0
  187. package/dist/lib/codex-manager/settings-preview.js +118 -0
  188. package/dist/lib/codex-manager/settings-preview.js.map +1 -0
  189. package/dist/lib/codex-manager/settings-write-queue.d.ts +14 -0
  190. package/dist/lib/codex-manager/settings-write-queue.d.ts.map +1 -0
  191. package/dist/lib/codex-manager/settings-write-queue.js +88 -0
  192. package/dist/lib/codex-manager/settings-write-queue.js.map +1 -0
  193. package/dist/lib/codex-manager/statusline-order.d.ts +3 -0
  194. package/dist/lib/codex-manager/statusline-order.d.ts.map +1 -0
  195. package/dist/lib/codex-manager/statusline-order.js +17 -0
  196. package/dist/lib/codex-manager/statusline-order.js.map +1 -0
  197. package/dist/lib/codex-manager/statusline-settings-panel.d.ts +40 -0
  198. package/dist/lib/codex-manager/statusline-settings-panel.d.ts.map +1 -0
  199. package/dist/lib/codex-manager/statusline-settings-panel.js +167 -0
  200. package/dist/lib/codex-manager/statusline-settings-panel.js.map +1 -0
  201. package/dist/lib/codex-manager/theme-settings-panel.d.ts +26 -0
  202. package/dist/lib/codex-manager/theme-settings-panel.d.ts.map +1 -0
  203. package/dist/lib/codex-manager/theme-settings-panel.js +131 -0
  204. package/dist/lib/codex-manager/theme-settings-panel.js.map +1 -0
  205. package/dist/lib/codex-manager/unified-settings-controller.d.ts +27 -0
  206. package/dist/lib/codex-manager/unified-settings-controller.d.ts.map +1 -0
  207. package/dist/lib/codex-manager/unified-settings-controller.js +50 -0
  208. package/dist/lib/codex-manager/unified-settings-controller.js.map +1 -0
  209. package/dist/lib/codex-manager/unified-settings-entry.d.ts +6 -0
  210. package/dist/lib/codex-manager/unified-settings-entry.d.ts.map +1 -0
  211. package/dist/lib/codex-manager/unified-settings-entry.js +23 -0
  212. package/dist/lib/codex-manager/unified-settings-entry.js.map +1 -0
  213. package/dist/lib/codex-manager.d.ts.map +1 -1
  214. package/dist/lib/codex-manager.js +441 -2199
  215. package/dist/lib/codex-manager.js.map +1 -1
  216. package/dist/lib/config.d.ts +38 -0
  217. package/dist/lib/config.d.ts.map +1 -1
  218. package/dist/lib/config.js +371 -9
  219. package/dist/lib/config.js.map +1 -1
  220. package/dist/lib/errors.d.ts +9 -0
  221. package/dist/lib/errors.d.ts.map +1 -1
  222. package/dist/lib/errors.js +13 -0
  223. package/dist/lib/errors.js.map +1 -1
  224. package/dist/lib/forecast.d.ts +17 -0
  225. package/dist/lib/forecast.d.ts.map +1 -1
  226. package/dist/lib/forecast.js +33 -6
  227. package/dist/lib/forecast.js.map +1 -1
  228. package/dist/lib/health.d.ts +1 -0
  229. package/dist/lib/health.d.ts.map +1 -1
  230. package/dist/lib/health.js +6 -2
  231. package/dist/lib/health.js.map +1 -1
  232. package/dist/lib/named-backup-export.js +2 -2
  233. package/dist/lib/named-backup-export.js.map +1 -1
  234. package/dist/lib/parallel-probe.d.ts +1 -1
  235. package/dist/lib/parallel-probe.d.ts.map +1 -1
  236. package/dist/lib/parallel-probe.js +5 -2
  237. package/dist/lib/parallel-probe.js.map +1 -1
  238. package/dist/lib/proactive-refresh.d.ts +1 -1
  239. package/dist/lib/proactive-refresh.d.ts.map +1 -1
  240. package/dist/lib/proactive-refresh.js +14 -5
  241. package/dist/lib/proactive-refresh.js.map +1 -1
  242. package/dist/lib/prompts/codex.d.ts +8 -3
  243. package/dist/lib/prompts/codex.d.ts.map +1 -1
  244. package/dist/lib/prompts/codex.js +16 -26
  245. package/dist/lib/prompts/codex.js.map +1 -1
  246. package/dist/lib/refresh-guardian.d.ts +3 -0
  247. package/dist/lib/refresh-guardian.d.ts.map +1 -1
  248. package/dist/lib/refresh-guardian.js +142 -52
  249. package/dist/lib/refresh-guardian.js.map +1 -1
  250. package/dist/lib/request/failover-config.d.ts +4 -0
  251. package/dist/lib/request/failover-config.d.ts.map +1 -0
  252. package/dist/lib/request/failover-config.js +15 -0
  253. package/dist/lib/request/failover-config.js.map +1 -0
  254. package/dist/lib/request/fetch-helpers.d.ts +10 -4
  255. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  256. package/dist/lib/request/fetch-helpers.js +77 -21
  257. package/dist/lib/request/fetch-helpers.js.map +1 -1
  258. package/dist/lib/request/helpers/model-map.d.ts +49 -12
  259. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  260. package/dist/lib/request/helpers/model-map.js +303 -108
  261. package/dist/lib/request/helpers/model-map.js.map +1 -1
  262. package/dist/lib/request/helpers/tool-utils.d.ts +2 -15
  263. package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -1
  264. package/dist/lib/request/helpers/tool-utils.js +40 -27
  265. package/dist/lib/request/helpers/tool-utils.js.map +1 -1
  266. package/dist/lib/request/index.d.ts +5 -0
  267. package/dist/lib/request/index.d.ts.map +1 -0
  268. package/dist/lib/request/index.js +5 -0
  269. package/dist/lib/request/index.js.map +1 -0
  270. package/dist/lib/request/request-init.d.ts +3 -0
  271. package/dist/lib/request/request-init.d.ts.map +1 -0
  272. package/dist/lib/request/request-init.js +50 -0
  273. package/dist/lib/request/request-init.js.map +1 -0
  274. package/dist/lib/request/request-transformer.d.ts +19 -6
  275. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  276. package/dist/lib/request/request-transformer.js +210 -193
  277. package/dist/lib/request/request-transformer.js.map +1 -1
  278. package/dist/lib/request/response-compaction.d.ts +20 -0
  279. package/dist/lib/request/response-compaction.d.ts.map +1 -0
  280. package/dist/lib/request/response-compaction.js +124 -0
  281. package/dist/lib/request/response-compaction.js.map +1 -0
  282. package/dist/lib/request/response-handler.d.ts +2 -0
  283. package/dist/lib/request/response-handler.d.ts.map +1 -1
  284. package/dist/lib/request/response-handler.js +567 -11
  285. package/dist/lib/request/response-handler.js.map +1 -1
  286. package/dist/lib/request/response-metadata.d.ts +3 -0
  287. package/dist/lib/request/response-metadata.d.ts.map +1 -0
  288. package/dist/lib/request/response-metadata.js +61 -0
  289. package/dist/lib/request/response-metadata.js.map +1 -0
  290. package/dist/lib/request/wait-utils.d.ts +14 -0
  291. package/dist/lib/request/wait-utils.d.ts.map +1 -0
  292. package/dist/lib/request/wait-utils.js +44 -0
  293. package/dist/lib/request/wait-utils.js.map +1 -0
  294. package/dist/lib/rotation.d.ts +14 -11
  295. package/dist/lib/rotation.d.ts.map +1 -1
  296. package/dist/lib/rotation.js +36 -30
  297. package/dist/lib/rotation.js.map +1 -1
  298. package/dist/lib/runtime/account-check-helpers.d.ts +8 -0
  299. package/dist/lib/runtime/account-check-helpers.d.ts.map +1 -0
  300. package/dist/lib/runtime/account-check-helpers.js +30 -0
  301. package/dist/lib/runtime/account-check-helpers.js.map +1 -0
  302. package/dist/lib/runtime/account-check-types.d.ts +18 -0
  303. package/dist/lib/runtime/account-check-types.d.ts.map +1 -0
  304. package/dist/lib/runtime/account-check-types.js +12 -0
  305. package/dist/lib/runtime/account-check-types.js.map +1 -0
  306. package/dist/lib/runtime/account-check.d.ts +48 -0
  307. package/dist/lib/runtime/account-check.d.ts.map +1 -0
  308. package/dist/lib/runtime/account-check.js +202 -0
  309. package/dist/lib/runtime/account-check.js.map +1 -0
  310. package/dist/lib/runtime/account-health-check.d.ts +8 -0
  311. package/dist/lib/runtime/account-health-check.d.ts.map +1 -0
  312. package/dist/lib/runtime/account-health-check.js +30 -0
  313. package/dist/lib/runtime/account-health-check.js.map +1 -0
  314. package/dist/lib/runtime/account-manager-cache-entry.d.ts +25 -0
  315. package/dist/lib/runtime/account-manager-cache-entry.d.ts.map +1 -0
  316. package/dist/lib/runtime/account-manager-cache-entry.js +17 -0
  317. package/dist/lib/runtime/account-manager-cache-entry.js.map +1 -0
  318. package/dist/lib/runtime/account-manager-cache.d.ts +25 -0
  319. package/dist/lib/runtime/account-manager-cache.d.ts.map +1 -0
  320. package/dist/lib/runtime/account-manager-cache.js +44 -0
  321. package/dist/lib/runtime/account-manager-cache.js.map +1 -0
  322. package/dist/lib/runtime/account-pool.d.ts +23 -0
  323. package/dist/lib/runtime/account-pool.d.ts.map +1 -0
  324. package/dist/lib/runtime/account-pool.js +138 -0
  325. package/dist/lib/runtime/account-pool.js.map +1 -0
  326. package/dist/lib/runtime/account-scope.d.ts +10 -0
  327. package/dist/lib/runtime/account-scope.d.ts.map +1 -0
  328. package/dist/lib/runtime/account-scope.js +13 -0
  329. package/dist/lib/runtime/account-scope.js.map +1 -0
  330. package/dist/lib/runtime/account-select-event.d.ts +19 -0
  331. package/dist/lib/runtime/account-select-event.d.ts.map +1 -0
  332. package/dist/lib/runtime/account-select-event.js +53 -0
  333. package/dist/lib/runtime/account-select-event.js.map +1 -0
  334. package/dist/lib/runtime/account-selection.d.ts +28 -0
  335. package/dist/lib/runtime/account-selection.d.ts.map +1 -0
  336. package/dist/lib/runtime/account-selection.js +46 -0
  337. package/dist/lib/runtime/account-selection.js.map +1 -0
  338. package/dist/lib/runtime/account-state.d.ts +13 -0
  339. package/dist/lib/runtime/account-state.d.ts.map +1 -0
  340. package/dist/lib/runtime/account-state.js +38 -0
  341. package/dist/lib/runtime/account-state.js.map +1 -0
  342. package/dist/lib/runtime/account-status.d.ts +13 -0
  343. package/dist/lib/runtime/account-status.d.ts.map +1 -0
  344. package/dist/lib/runtime/account-status.js +37 -0
  345. package/dist/lib/runtime/account-status.js.map +1 -0
  346. package/dist/lib/runtime/account-storage-scope-entry.d.ts +26 -0
  347. package/dist/lib/runtime/account-storage-scope-entry.d.ts.map +1 -0
  348. package/dist/lib/runtime/account-storage-scope-entry.js +15 -0
  349. package/dist/lib/runtime/account-storage-scope-entry.js.map +1 -0
  350. package/dist/lib/runtime/auth-facade.d.ts +51 -0
  351. package/dist/lib/runtime/auth-facade.d.ts.map +1 -0
  352. package/dist/lib/runtime/auth-facade.js +48 -0
  353. package/dist/lib/runtime/auth-facade.js.map +1 -0
  354. package/dist/lib/runtime/browser-oauth-flow.d.ts +32 -0
  355. package/dist/lib/runtime/browser-oauth-flow.d.ts.map +1 -0
  356. package/dist/lib/runtime/browser-oauth-flow.js +31 -0
  357. package/dist/lib/runtime/browser-oauth-flow.js.map +1 -0
  358. package/dist/lib/runtime/capability-boost.d.ts +25 -0
  359. package/dist/lib/runtime/capability-boost.d.ts.map +1 -0
  360. package/dist/lib/runtime/capability-boost.js +26 -0
  361. package/dist/lib/runtime/capability-boost.js.map +1 -0
  362. package/dist/lib/runtime/event-handler.d.ts +53 -0
  363. package/dist/lib/runtime/event-handler.d.ts.map +1 -0
  364. package/dist/lib/runtime/event-handler.js +46 -0
  365. package/dist/lib/runtime/event-handler.js.map +1 -0
  366. package/dist/lib/runtime/flagged-verify-types.d.ts +9 -0
  367. package/dist/lib/runtime/flagged-verify-types.d.ts.map +1 -0
  368. package/dist/lib/runtime/flagged-verify-types.js +7 -0
  369. package/dist/lib/runtime/flagged-verify-types.js.map +1 -0
  370. package/dist/lib/runtime/hydrate-emails.d.ts +13 -0
  371. package/dist/lib/runtime/hydrate-emails.d.ts.map +1 -0
  372. package/dist/lib/runtime/hydrate-emails.js +56 -0
  373. package/dist/lib/runtime/hydrate-emails.js.map +1 -0
  374. package/dist/lib/runtime/live-sync-entry.d.ts +36 -0
  375. package/dist/lib/runtime/live-sync-entry.d.ts.map +1 -0
  376. package/dist/lib/runtime/live-sync-entry.js +14 -0
  377. package/dist/lib/runtime/live-sync-entry.js.map +1 -0
  378. package/dist/lib/runtime/live-sync.d.ts +35 -0
  379. package/dist/lib/runtime/live-sync.d.ts.map +1 -0
  380. package/dist/lib/runtime/live-sync.js +65 -0
  381. package/dist/lib/runtime/live-sync.js.map +1 -0
  382. package/dist/lib/runtime/loader-setup.d.ts +9 -0
  383. package/dist/lib/runtime/loader-setup.d.ts.map +1 -0
  384. package/dist/lib/runtime/loader-setup.js +8 -0
  385. package/dist/lib/runtime/loader-setup.js.map +1 -0
  386. package/dist/lib/runtime/login-menu-accounts.d.ts +29 -0
  387. package/dist/lib/runtime/login-menu-accounts.d.ts.map +1 -0
  388. package/dist/lib/runtime/login-menu-accounts.js +33 -0
  389. package/dist/lib/runtime/login-menu-accounts.js.map +1 -0
  390. package/dist/lib/runtime/manual-oauth-flow.d.ts +40 -0
  391. package/dist/lib/runtime/manual-oauth-flow.d.ts.map +1 -0
  392. package/dist/lib/runtime/manual-oauth-flow.js +71 -0
  393. package/dist/lib/runtime/manual-oauth-flow.js.map +1 -0
  394. package/dist/lib/runtime/metrics.d.ts +29 -0
  395. package/dist/lib/runtime/metrics.d.ts.map +1 -0
  396. package/dist/lib/runtime/metrics.js +97 -0
  397. package/dist/lib/runtime/metrics.js.map +1 -0
  398. package/dist/lib/runtime/oauth-browser-flow.d.ts +9 -0
  399. package/dist/lib/runtime/oauth-browser-flow.d.ts.map +1 -0
  400. package/dist/lib/runtime/oauth-browser-flow.js +34 -0
  401. package/dist/lib/runtime/oauth-browser-flow.js.map +1 -0
  402. package/dist/lib/runtime/preemptive-quota.d.ts +13 -0
  403. package/dist/lib/runtime/preemptive-quota.d.ts.map +1 -0
  404. package/dist/lib/runtime/preemptive-quota.js +9 -0
  405. package/dist/lib/runtime/preemptive-quota.js.map +1 -0
  406. package/dist/lib/runtime/quota-headers.d.ts +12 -0
  407. package/dist/lib/runtime/quota-headers.d.ts.map +1 -0
  408. package/dist/lib/runtime/quota-headers.js +131 -0
  409. package/dist/lib/runtime/quota-headers.js.map +1 -0
  410. package/dist/lib/runtime/quota-probe.d.ts +18 -0
  411. package/dist/lib/runtime/quota-probe.d.ts.map +1 -0
  412. package/dist/lib/runtime/quota-probe.js +82 -0
  413. package/dist/lib/runtime/quota-probe.js.map +1 -0
  414. package/dist/lib/runtime/quota-settings.d.ts +14 -0
  415. package/dist/lib/runtime/quota-settings.d.ts.map +1 -0
  416. package/dist/lib/runtime/quota-settings.js +12 -0
  417. package/dist/lib/runtime/quota-settings.js.map +1 -0
  418. package/dist/lib/runtime/refresh-guardian-entry.d.ts +32 -0
  419. package/dist/lib/runtime/refresh-guardian-entry.d.ts.map +1 -0
  420. package/dist/lib/runtime/refresh-guardian-entry.js +12 -0
  421. package/dist/lib/runtime/refresh-guardian-entry.js.map +1 -0
  422. package/dist/lib/runtime/refresh-guardian.d.ts +24 -0
  423. package/dist/lib/runtime/refresh-guardian.d.ts.map +1 -0
  424. package/dist/lib/runtime/refresh-guardian.js +32 -0
  425. package/dist/lib/runtime/refresh-guardian.js.map +1 -0
  426. package/dist/lib/runtime/request-init.d.ts +5 -0
  427. package/dist/lib/runtime/request-init.d.ts.map +1 -0
  428. package/dist/lib/runtime/request-init.js +50 -0
  429. package/dist/lib/runtime/request-init.js.map +1 -0
  430. package/dist/lib/runtime/runtime-services.d.ts +58 -0
  431. package/dist/lib/runtime/runtime-services.d.ts.map +1 -0
  432. package/dist/lib/runtime/runtime-services.js +87 -0
  433. package/dist/lib/runtime/runtime-services.js.map +1 -0
  434. package/dist/lib/runtime/session-affinity-entry.d.ts +30 -0
  435. package/dist/lib/runtime/session-affinity-entry.d.ts.map +1 -0
  436. package/dist/lib/runtime/session-affinity-entry.js +11 -0
  437. package/dist/lib/runtime/session-affinity-entry.js.map +1 -0
  438. package/dist/lib/runtime/session-affinity.d.ts +13 -0
  439. package/dist/lib/runtime/session-affinity.d.ts.map +1 -0
  440. package/dist/lib/runtime/session-affinity.js +17 -0
  441. package/dist/lib/runtime/session-affinity.js.map +1 -0
  442. package/dist/lib/runtime/session-recovery.d.ts +8 -0
  443. package/dist/lib/runtime/session-recovery.d.ts.map +1 -0
  444. package/dist/lib/runtime/session-recovery.js +7 -0
  445. package/dist/lib/runtime/session-recovery.js.map +1 -0
  446. package/dist/lib/runtime/status-marker.d.ts +3 -0
  447. package/dist/lib/runtime/status-marker.d.ts.map +1 -0
  448. package/dist/lib/runtime/status-marker.js +15 -0
  449. package/dist/lib/runtime/status-marker.js.map +1 -0
  450. package/dist/lib/runtime/storage-scope.d.ts +13 -0
  451. package/dist/lib/runtime/storage-scope.d.ts.map +1 -0
  452. package/dist/lib/runtime/storage-scope.js +14 -0
  453. package/dist/lib/runtime/storage-scope.js.map +1 -0
  454. package/dist/lib/runtime/toast.d.ts +16 -0
  455. package/dist/lib/runtime/toast.d.ts.map +1 -0
  456. package/dist/lib/runtime/toast.js +16 -0
  457. package/dist/lib/runtime/toast.js.map +1 -0
  458. package/dist/lib/runtime/ui-runtime-entry.d.ts +7 -0
  459. package/dist/lib/runtime/ui-runtime-entry.d.ts.map +1 -0
  460. package/dist/lib/runtime/ui-runtime-entry.js +4 -0
  461. package/dist/lib/runtime/ui-runtime-entry.js.map +1 -0
  462. package/dist/lib/runtime/ui-runtime.d.ts +9 -0
  463. package/dist/lib/runtime/ui-runtime.d.ts.map +1 -0
  464. package/dist/lib/runtime/ui-runtime.js +23 -0
  465. package/dist/lib/runtime/ui-runtime.js.map +1 -0
  466. package/dist/lib/runtime/verify-flagged.d.ts +44 -0
  467. package/dist/lib/runtime/verify-flagged.d.ts.map +1 -0
  468. package/dist/lib/runtime/verify-flagged.js +84 -0
  469. package/dist/lib/runtime/verify-flagged.js.map +1 -0
  470. package/dist/lib/schemas.d.ts +2 -0
  471. package/dist/lib/schemas.d.ts.map +1 -1
  472. package/dist/lib/schemas.js +2 -0
  473. package/dist/lib/schemas.js.map +1 -1
  474. package/dist/lib/session-affinity.d.ts +10 -0
  475. package/dist/lib/session-affinity.d.ts.map +1 -1
  476. package/dist/lib/session-affinity.js +53 -6
  477. package/dist/lib/session-affinity.js.map +1 -1
  478. package/dist/lib/storage/account-clear-entry.d.ts +16 -0
  479. package/dist/lib/storage/account-clear-entry.d.ts.map +1 -0
  480. package/dist/lib/storage/account-clear-entry.js +12 -0
  481. package/dist/lib/storage/account-clear-entry.js.map +1 -0
  482. package/dist/lib/storage/account-clear.d.ts +8 -0
  483. package/dist/lib/storage/account-clear.d.ts.map +1 -0
  484. package/dist/lib/storage/account-clear.js +29 -0
  485. package/dist/lib/storage/account-clear.js.map +1 -0
  486. package/dist/lib/storage/account-match-utils.d.ts +9 -0
  487. package/dist/lib/storage/account-match-utils.d.ts.map +1 -0
  488. package/dist/lib/storage/account-match-utils.js +45 -0
  489. package/dist/lib/storage/account-match-utils.js.map +1 -0
  490. package/dist/lib/storage/account-persistence.d.ts +3 -0
  491. package/dist/lib/storage/account-persistence.d.ts.map +1 -0
  492. package/dist/lib/storage/account-persistence.js +12 -0
  493. package/dist/lib/storage/account-persistence.js.map +1 -0
  494. package/dist/lib/storage/account-port.d.ts +50 -0
  495. package/dist/lib/storage/account-port.d.ts.map +1 -0
  496. package/dist/lib/storage/account-port.js +43 -0
  497. package/dist/lib/storage/account-port.js.map +1 -0
  498. package/dist/lib/storage/account-save-entry.d.ts +7 -0
  499. package/dist/lib/storage/account-save-entry.d.ts.map +1 -0
  500. package/dist/lib/storage/account-save-entry.js +6 -0
  501. package/dist/lib/storage/account-save-entry.js.map +1 -0
  502. package/dist/lib/storage/account-save.d.ts +30 -0
  503. package/dist/lib/storage/account-save.d.ts.map +1 -0
  504. package/dist/lib/storage/account-save.js +49 -0
  505. package/dist/lib/storage/account-save.js.map +1 -0
  506. package/dist/lib/storage/account-snapshot.d.ts +25 -0
  507. package/dist/lib/storage/account-snapshot.d.ts.map +1 -0
  508. package/dist/lib/storage/account-snapshot.js +78 -0
  509. package/dist/lib/storage/account-snapshot.js.map +1 -0
  510. package/dist/lib/storage/backup-metadata-builder.d.ts +32 -0
  511. package/dist/lib/storage/backup-metadata-builder.d.ts.map +1 -0
  512. package/dist/lib/storage/backup-metadata-builder.js +77 -0
  513. package/dist/lib/storage/backup-metadata-builder.js.map +1 -0
  514. package/dist/lib/storage/backup-metadata.d.ts +29 -0
  515. package/dist/lib/storage/backup-metadata.d.ts.map +1 -0
  516. package/dist/lib/storage/backup-metadata.js +16 -0
  517. package/dist/lib/storage/backup-metadata.js.map +1 -0
  518. package/dist/lib/storage/backup-paths.d.ts +10 -0
  519. package/dist/lib/storage/backup-paths.d.ts.map +1 -0
  520. package/dist/lib/storage/backup-paths.js +27 -0
  521. package/dist/lib/storage/backup-paths.js.map +1 -0
  522. package/dist/lib/storage/backup-restore.d.ts +11 -0
  523. package/dist/lib/storage/backup-restore.d.ts.map +1 -0
  524. package/dist/lib/storage/backup-restore.js +52 -0
  525. package/dist/lib/storage/backup-restore.js.map +1 -0
  526. package/dist/lib/storage/cache-artifacts.d.ts +2 -0
  527. package/dist/lib/storage/cache-artifacts.d.ts.map +1 -0
  528. package/dist/lib/storage/cache-artifacts.js +4 -0
  529. package/dist/lib/storage/cache-artifacts.js.map +1 -0
  530. package/dist/lib/storage/error-hints.d.ts +10 -0
  531. package/dist/lib/storage/error-hints.d.ts.map +1 -0
  532. package/dist/lib/storage/error-hints.js +36 -0
  533. package/dist/lib/storage/error-hints.js.map +1 -0
  534. package/dist/lib/storage/file-paths.d.ts +6 -0
  535. package/dist/lib/storage/file-paths.d.ts.map +1 -0
  536. package/dist/lib/storage/file-paths.js +17 -0
  537. package/dist/lib/storage/file-paths.js.map +1 -0
  538. package/dist/lib/storage/fixture-guards.d.ts +4 -0
  539. package/dist/lib/storage/fixture-guards.d.ts.map +1 -0
  540. package/dist/lib/storage/fixture-guards.js +27 -0
  541. package/dist/lib/storage/fixture-guards.js.map +1 -0
  542. package/dist/lib/storage/flagged-entry.d.ts +15 -0
  543. package/dist/lib/storage/flagged-entry.d.ts.map +1 -0
  544. package/dist/lib/storage/flagged-entry.js +12 -0
  545. package/dist/lib/storage/flagged-entry.js.map +1 -0
  546. package/dist/lib/storage/flagged-load-entry.d.ts +20 -0
  547. package/dist/lib/storage/flagged-load-entry.d.ts.map +1 -0
  548. package/dist/lib/storage/flagged-load-entry.js +13 -0
  549. package/dist/lib/storage/flagged-load-entry.js.map +1 -0
  550. package/dist/lib/storage/flagged-save-entry.d.ts +7 -0
  551. package/dist/lib/storage/flagged-save-entry.d.ts.map +1 -0
  552. package/dist/lib/storage/flagged-save-entry.js +6 -0
  553. package/dist/lib/storage/flagged-save-entry.js.map +1 -0
  554. package/dist/lib/storage/flagged-storage-file.d.ts +7 -0
  555. package/dist/lib/storage/flagged-storage-file.d.ts.map +1 -0
  556. package/dist/lib/storage/flagged-storage-file.js +25 -0
  557. package/dist/lib/storage/flagged-storage-file.js.map +1 -0
  558. package/dist/lib/storage/flagged-storage-io.d.ts +28 -0
  559. package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -0
  560. package/dist/lib/storage/flagged-storage-io.js +135 -0
  561. package/dist/lib/storage/flagged-storage-io.js.map +1 -0
  562. package/dist/lib/storage/flagged-storage.d.ts +6 -0
  563. package/dist/lib/storage/flagged-storage.d.ts.map +1 -0
  564. package/dist/lib/storage/flagged-storage.js +90 -0
  565. package/dist/lib/storage/flagged-storage.js.map +1 -0
  566. package/dist/lib/storage/gitignore.d.ts +7 -0
  567. package/dist/lib/storage/gitignore.d.ts.map +1 -0
  568. package/dist/lib/storage/gitignore.js +34 -0
  569. package/dist/lib/storage/gitignore.js.map +1 -0
  570. package/dist/lib/storage/hash.d.ts +2 -0
  571. package/dist/lib/storage/hash.d.ts.map +1 -0
  572. package/dist/lib/storage/hash.js +5 -0
  573. package/dist/lib/storage/hash.js.map +1 -0
  574. package/dist/lib/storage/identity.d.ts +21 -0
  575. package/dist/lib/storage/identity.d.ts.map +1 -0
  576. package/dist/lib/storage/identity.js +59 -0
  577. package/dist/lib/storage/identity.js.map +1 -0
  578. package/dist/lib/storage/import-export.d.ts +24 -0
  579. package/dist/lib/storage/import-export.d.ts.map +1 -0
  580. package/dist/lib/storage/import-export.js +108 -0
  581. package/dist/lib/storage/import-export.js.map +1 -0
  582. package/dist/lib/storage/metadata-section.d.ts +16 -0
  583. package/dist/lib/storage/metadata-section.d.ts.map +1 -0
  584. package/dist/lib/storage/metadata-section.js +16 -0
  585. package/dist/lib/storage/metadata-section.js.map +1 -0
  586. package/dist/lib/storage/named-backup-entry.d.ts +15 -0
  587. package/dist/lib/storage/named-backup-entry.d.ts.map +1 -0
  588. package/dist/lib/storage/named-backup-entry.js +7 -0
  589. package/dist/lib/storage/named-backup-entry.js.map +1 -0
  590. package/dist/lib/storage/named-backups-entry.d.ts +19 -0
  591. package/dist/lib/storage/named-backups-entry.d.ts.map +1 -0
  592. package/dist/lib/storage/named-backups-entry.js +7 -0
  593. package/dist/lib/storage/named-backups-entry.js.map +1 -0
  594. package/dist/lib/storage/named-backups.d.ts +15 -0
  595. package/dist/lib/storage/named-backups.d.ts.map +1 -0
  596. package/dist/lib/storage/named-backups.js +68 -0
  597. package/dist/lib/storage/named-backups.js.map +1 -0
  598. package/dist/lib/storage/path-state.d.ts +9 -0
  599. package/dist/lib/storage/path-state.d.ts.map +1 -0
  600. package/dist/lib/storage/path-state.js +20 -0
  601. package/dist/lib/storage/path-state.js.map +1 -0
  602. package/dist/lib/storage/project-migration.d.ts +10 -0
  603. package/dist/lib/storage/project-migration.d.ts.map +1 -0
  604. package/dist/lib/storage/project-migration.js +38 -0
  605. package/dist/lib/storage/project-migration.js.map +1 -0
  606. package/dist/lib/storage/record-utils.d.ts +3 -0
  607. package/dist/lib/storage/record-utils.d.ts.map +1 -0
  608. package/dist/lib/storage/record-utils.js +9 -0
  609. package/dist/lib/storage/record-utils.js.map +1 -0
  610. package/dist/lib/storage/restore-assessment.d.ts +7 -0
  611. package/dist/lib/storage/restore-assessment.d.ts.map +1 -0
  612. package/dist/lib/storage/restore-assessment.js +42 -0
  613. package/dist/lib/storage/restore-assessment.js.map +1 -0
  614. package/dist/lib/storage/restore-backup-entry.d.ts +24 -0
  615. package/dist/lib/storage/restore-backup-entry.d.ts.map +1 -0
  616. package/dist/lib/storage/restore-backup-entry.js +10 -0
  617. package/dist/lib/storage/restore-backup-entry.js.map +1 -0
  618. package/dist/lib/storage/restore-metadata.d.ts +10 -0
  619. package/dist/lib/storage/restore-metadata.d.ts.map +1 -0
  620. package/dist/lib/storage/restore-metadata.js +18 -0
  621. package/dist/lib/storage/restore-metadata.js.map +1 -0
  622. package/dist/lib/storage/restore.d.ts +14 -0
  623. package/dist/lib/storage/restore.d.ts.map +1 -0
  624. package/dist/lib/storage/restore.js +53 -0
  625. package/dist/lib/storage/restore.js.map +1 -0
  626. package/dist/lib/storage/snapshot-inspectors.d.ts +22 -0
  627. package/dist/lib/storage/snapshot-inspectors.d.ts.map +1 -0
  628. package/dist/lib/storage/snapshot-inspectors.js +95 -0
  629. package/dist/lib/storage/snapshot-inspectors.js.map +1 -0
  630. package/dist/lib/storage/storage-parser.d.ts +15 -0
  631. package/dist/lib/storage/storage-parser.d.ts.map +1 -0
  632. package/dist/lib/storage/storage-parser.js +16 -0
  633. package/dist/lib/storage/storage-parser.js.map +1 -0
  634. package/dist/lib/storage/transactions.d.ts +23 -0
  635. package/dist/lib/storage/transactions.d.ts.map +1 -0
  636. package/dist/lib/storage/transactions.js +64 -0
  637. package/dist/lib/storage/transactions.js.map +1 -0
  638. package/dist/lib/storage.d.ts +8 -47
  639. package/dist/lib/storage.d.ts.map +1 -1
  640. package/dist/lib/storage.js +482 -1044
  641. package/dist/lib/storage.js.map +1 -1
  642. package/dist/lib/types.d.ts +80 -1
  643. package/dist/lib/types.d.ts.map +1 -1
  644. package/package.json +41 -4
  645. package/scripts/check-pack-budget-lib.js +114 -0
  646. package/scripts/check-pack-budget.mjs +10 -0
  647. package/scripts/codex-multi-auth.js +34 -10
  648. package/scripts/install-codex-auth-utils.js +54 -14
  649. package/scripts/install-codex-auth.js +62 -33
  650. package/scripts/update-vendor-provenance.mjs +96 -0
  651. package/scripts/verify-vendor-provenance.mjs +53 -0
package/dist/index.js CHANGED
@@ -23,39 +23,69 @@
23
23
 
24
24
  */
25
25
  import { tool } from "@codex-ai/plugin/tool";
26
+ import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, isCodexCliSyncEnabled, lookupCodexCliTokensByEmail, parseRateLimitReason, resolveRequestAccountId, resolveRuntimeRequestIdentity, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./lib/accounts.js";
26
27
  import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, redactOAuthUrlForLog, REDIRECT_URI, } from "./lib/auth/auth.js";
27
- import { queuedRefresh } from "./lib/refresh-queue.js";
28
28
  import { isBrowserLaunchSuppressed, openBrowserUrl } from "./lib/auth/browser.js";
29
29
  import { startLocalOAuthServer } from "./lib/auth/server.js";
30
- import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
31
- import { getCodexMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getLiveAccountSync, getLiveAccountSyncDebounceMs, getLiveAccountSyncPollMs, getSessionAffinity, getSessionAffinityTtlMs, getSessionAffinityMaxEntries, getProactiveRefreshGuardian, getProactiveRefreshIntervalMs, getProactiveRefreshBufferMs, getNetworkErrorCooldownMs, getServerErrorCooldownMs, getStorageBackupEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getPreemptiveQuotaMaxDeferralMs, loadPluginConfig, } from "./lib/config.js";
32
- import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
33
- import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
34
30
  import { checkAndNotify } from "./lib/auto-update-checker.js";
31
+ import { CapabilityPolicyStore } from "./lib/capability-policy.js";
32
+ import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
33
+ import { getAutoResume, getCodexMode, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getFallbackToGpt52OnUnsupportedGpt53, getFastSession, getFastSessionMaxInputItems, getFastSessionStrategy, getFetchTimeoutMs, getLiveAccountSync, getLiveAccountSyncDebounceMs, getLiveAccountSyncPollMs, getNetworkErrorCooldownMs, getPerProjectAccounts, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getProactiveRefreshBufferMs, getProactiveRefreshGuardian, getProactiveRefreshIntervalMs, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getBackgroundResponses, getResponseContinuation, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getSessionRecovery, getStorageBackupEnabled, getStreamStallTimeoutMs, getToastDurationMs, getTokenRefreshSkewMs, getUnsupportedCodexFallbackChain, getUnsupportedCodexPolicy, loadPluginConfig, } from "./lib/config.js";
34
+ import { ACCOUNT_LIMITS, AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, } from "./lib/constants.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
- import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, resolveRuntimeRequestIdentity, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, findMatchingAccountIndex, StorageError, formatStorageErrorHint, setStorageBackupEnabled, } from "./lib/storage.js";
38
- import { applyProxyCompatibleInit, createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, isWorkspaceDisabledError, } from "./lib/request/fetch-helpers.js";
39
- import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
40
- import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
41
- import { isEmptyResponse } from "./lib/request/response-handler.js";
42
- import { addJitter } from "./lib/rotation.js";
43
- import { SessionAffinityStore } from "./lib/session-affinity.js";
36
+ import { EntitlementCache, resolveEntitlementAccountKey, } from "./lib/entitlement-cache.js";
44
37
  import { LiveAccountSync } from "./lib/live-account-sync.js";
38
+ import { clearCorrelationId, initLogger, logDebug, logError, logInfo, logRequest, logWarn, setCorrelationId, } from "./lib/logger.js";
39
+ import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./lib/preemptive-quota-scheduler.js";
40
+ import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
41
+ import { prewarmHostCodexPrompt } from "./lib/prompts/host-codex-prompt.js";
42
+ import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./lib/quota-probe.js";
43
+ import { detectErrorType, getRecoveryToastContent, isRecoverableError, } from "./lib/recovery.js";
45
44
  import { RefreshGuardian } from "./lib/refresh-guardian.js";
45
+ import { queuedRefresh } from "./lib/refresh-queue.js";
46
+ import { parseEnvInt, parseFailoverMode, } from "./lib/request/failover-config.js";
46
47
  import { evaluateFailurePolicy, } from "./lib/request/failure-policy.js";
47
- import { EntitlementCache, resolveEntitlementAccountKey, } from "./lib/entitlement-cache.js";
48
- import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./lib/preemptive-quota-scheduler.js";
49
- import { CapabilityPolicyStore } from "./lib/capability-policy.js";
48
+ import { applyProxyCompatibleInit, createCodexHeaders, extractRequestUrl, getUnsupportedCodexModelInfo, handleErrorResponse, handleSuccessResponse, isWorkspaceDisabledError, refreshAndUpdateToken, resolveUnsupportedCodexFallbackModel, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
49
+ import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
50
+ import { normalizeRequestInit, parseRequestBodyFromInit, } from "./lib/request/request-init.js";
51
+ import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
52
+ import { applyResponseCompaction } from "./lib/request/response-compaction.js";
53
+ import { isEmptyResponse } from "./lib/request/response-handler.js";
54
+ import { CodexAuthError } from "./lib/errors.js";
55
+ import { parseRetryAfterHintMs, sanitizeResponseHeadersForLog, } from "./lib/request/response-metadata.js";
50
56
  import { withStreamingFailover } from "./lib/request/stream-failover.js";
51
- import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
52
- import { setUiRuntimeOptions } from "./lib/ui/runtime.js";
53
- import { paintUiText, formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
54
- import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
55
- import { prewarmHostCodexPrompt } from "./lib/prompts/host-codex-prompt.js";
56
- import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
57
- import { createHashlineEditTool, createHashlineReadTool, } from "./lib/tools/hashline-tools.js";
57
+ import { createAbortableSleep, sleepWithCountdown, } from "./lib/request/wait-utils.js";
58
+ import { addJitter } from "./lib/rotation.js";
59
+ import { clampActiveIndices, isFlaggableFailure, } from "./lib/runtime/account-check-helpers.js";
60
+ import { runRuntimeAccountCheck } from "./lib/runtime/account-check.js";
61
+ import { createAccountCheckWorkingState } from "./lib/runtime/account-check-types.js";
62
+ import { invalidateRuntimeAccountManagerCache, reloadRuntimeAccountManager, } from "./lib/runtime/account-manager-cache.js";
63
+ import { persistAccountPoolResults, } from "./lib/runtime/account-pool.js";
64
+ import { resolveAccountSelection } from "./lib/runtime/account-selection.js";
65
+ import { formatRateLimitEntry, getRateLimitResetTimeForFamily, resolveActiveIndex, } from "./lib/runtime/account-status.js";
66
+ import { createPersistAccounts, runRuntimeOAuthFlow, } from "./lib/runtime/auth-facade.js";
67
+ import { applyAccountStorageScopeEntry } from "./lib/runtime/account-storage-scope-entry.js";
68
+ import { runBrowserOAuthFlow } from "./lib/runtime/browser-oauth-flow.js";
69
+ import { handleRuntimeEvent } from "./lib/runtime/event-handler.js";
70
+ import { hydrateRuntimeEmails } from "./lib/runtime/hydrate-emails.js";
71
+ import { buildLoginMenuAccounts } from "./lib/runtime/login-menu-accounts.js";
72
+ import { ensureLiveAccountSyncEntry } from "./lib/runtime/live-sync-entry.js";
73
+ import { applyLoaderRuntimeSetup } from "./lib/runtime/loader-setup.js";
74
+ import { buildManualOAuthFlow } from "./lib/runtime/manual-oauth-flow.js";
75
+ import { applyPreemptiveQuotaSettingsFromConfig, } from "./lib/runtime/quota-settings.js";
76
+ import { ensureLiveAccountSyncState, ensureRefreshGuardianState, ensureSessionAffinityState, } from "./lib/runtime/runtime-services.js";
77
+ import { applyAccountStorageScopeFromConfig } from "./lib/runtime/storage-scope.js";
78
+ import { showRuntimeToast } from "./lib/runtime/toast.js";
79
+ import { createRuntimeSessionRecoveryHook } from "./lib/runtime/session-recovery.js";
80
+ import { applyUiRuntimeFromConfig, getStatusMarker, } from "./lib/runtime/ui-runtime.js";
81
+ import { verifyRuntimeFlaggedAccounts } from "./lib/runtime/verify-flagged.js";
82
+ import { SessionAffinityStore } from "./lib/session-affinity.js";
58
83
  import { registerCleanup } from "./lib/shutdown.js";
84
+ import { clearAccounts, clearFlaggedAccounts, exportAccounts, findMatchingAccountIndex, formatStorageErrorHint, getStoragePath, importAccounts, loadAccounts, loadFlaggedAccounts, StorageError, saveAccounts, saveFlaggedAccounts, setStorageBackupEnabled, setStoragePath, withAccountStorageTransaction, } from "./lib/storage.js";
85
+ import { buildTableHeader, buildTableRow, } from "./lib/table-formatter.js";
86
+ import { createHashlineEditTool, createHashlineReadTool, } from "./lib/tools/hashline-tools.js";
87
+ import { formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection, paintUiText, } from "./lib/ui/format.js";
88
+ import { setUiRuntimeOptions, } from "./lib/ui/runtime.js";
59
89
  /**
60
90
  * OpenAI Codex OAuth authentication plugin for Codex CLI host runtime
61
91
  *
@@ -85,6 +115,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
85
115
  let liveAccountSyncPath = null;
86
116
  let refreshGuardian = null;
87
117
  let refreshGuardianConfigKey = null;
118
+ let refreshGuardianCleanupRegistered = false;
88
119
  let sessionAffinityStore = new SessionAffinityStore();
89
120
  let sessionAffinityConfigKey = null;
90
121
  const entitlementCache = new EntitlementCache();
@@ -103,80 +134,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
103
134
  balanced: 15_000,
104
135
  conservative: 20_000,
105
136
  };
106
- const parseFailoverMode = (value) => {
107
- const normalized = (value ?? "").trim().toLowerCase();
108
- if (normalized === "aggressive")
109
- return "aggressive";
110
- if (normalized === "conservative")
111
- return "conservative";
112
- return "balanced";
113
- };
114
- const parseEnvInt = (value) => {
115
- if (value === undefined)
116
- return undefined;
117
- const parsed = Number.parseInt(value, 10);
118
- return Number.isFinite(parsed) ? parsed : undefined;
119
- };
120
- const MAX_RETRY_HINT_MS = 5 * 60 * 1000;
121
- const clampRetryHintMs = (value) => {
122
- if (!Number.isFinite(value))
123
- return null;
124
- const normalized = Math.floor(value);
125
- if (normalized <= 0)
126
- return null;
127
- return Math.min(normalized, MAX_RETRY_HINT_MS);
128
- };
129
- const parseRetryAfterHintMs = (headers) => {
130
- const retryAfterMsHeader = headers.get("retry-after-ms")?.trim();
131
- if (retryAfterMsHeader && /^\d+$/.test(retryAfterMsHeader)) {
132
- return clampRetryHintMs(Number.parseInt(retryAfterMsHeader, 10));
133
- }
134
- const retryAfterHeader = headers.get("retry-after")?.trim();
135
- if (retryAfterHeader && /^\d+$/.test(retryAfterHeader)) {
136
- return clampRetryHintMs(Number.parseInt(retryAfterHeader, 10) * 1000);
137
- }
138
- if (retryAfterHeader) {
139
- const retryAtMs = Date.parse(retryAfterHeader);
140
- if (Number.isFinite(retryAtMs)) {
141
- return clampRetryHintMs(retryAtMs - Date.now());
142
- }
143
- }
144
- const resetAtHeader = headers.get("x-ratelimit-reset")?.trim();
145
- if (resetAtHeader && /^\d+$/.test(resetAtHeader)) {
146
- const resetRaw = Number.parseInt(resetAtHeader, 10);
147
- const resetAtMs = resetRaw < 10_000_000_000 ? resetRaw * 1000 : resetRaw;
148
- return clampRetryHintMs(resetAtMs - Date.now());
149
- }
150
- return null;
151
- };
152
- const sanitizeResponseHeadersForLog = (headers) => {
153
- const allowed = new Set([
154
- "content-type",
155
- "x-request-id",
156
- "x-openai-request-id",
157
- "x-codex-plan-type",
158
- "x-codex-active-limit",
159
- "x-codex-primary-used-percent",
160
- "x-codex-primary-window-minutes",
161
- "x-codex-primary-reset-at",
162
- "x-codex-primary-reset-after-seconds",
163
- "x-codex-secondary-used-percent",
164
- "x-codex-secondary-window-minutes",
165
- "x-codex-secondary-reset-at",
166
- "x-codex-secondary-reset-after-seconds",
167
- "retry-after",
168
- "x-ratelimit-reset",
169
- "x-ratelimit-reset-requests",
170
- ]);
171
- const sanitized = {};
172
- for (const [rawName, rawValue] of headers.entries()) {
173
- const name = rawName.toLowerCase();
174
- if (!allowed.has(name))
175
- continue;
176
- sanitized[name] = rawValue;
177
- }
178
- return sanitized;
179
- };
180
137
  const runtimeMetrics = {
181
138
  startedAt: Date.now(),
182
139
  totalRequests: 0,
@@ -197,557 +154,145 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
197
154
  lastRequestAt: null,
198
155
  lastError: null,
199
156
  };
200
- const resolveAccountSelection = (tokens) => {
201
- const override = (process.env.CODEX_AUTH_ACCOUNT_ID ?? "").trim();
202
- if (override) {
203
- const suffix = override.length > 6 ? override.slice(-6) : override;
204
- logInfo(`Using account override from CODEX_AUTH_ACCOUNT_ID (id:${suffix}).`);
205
- return {
206
- ...tokens,
207
- accountIdOverride: override,
208
- accountIdSource: "manual",
209
- accountLabel: `Override [id:${suffix}]`,
210
- };
211
- }
212
- const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
213
- if (candidates.length === 0) {
214
- return tokens;
215
- }
216
- // Convert candidates to workspaces
217
- const workspaces = candidates.map((c) => ({
218
- id: c.accountId,
219
- name: c.label,
220
- enabled: true,
221
- isDefault: c.isDefault,
222
- }));
223
- if (candidates.length === 1) {
224
- const [candidate] = candidates;
225
- if (candidate) {
226
- return {
227
- ...tokens,
228
- accountIdOverride: candidate.accountId,
229
- accountIdSource: candidate.source,
230
- accountLabel: candidate.label,
231
- workspaces,
232
- };
233
- }
234
- }
235
- // Auto-select the best workspace candidate without prompting.
236
- // This honors org/default/id-token signals and avoids forcing personal token IDs.
237
- const choice = selectBestAccountCandidate(candidates);
238
- if (!choice)
239
- return tokens;
240
- return {
241
- ...tokens,
242
- accountIdOverride: choice.accountId,
243
- accountIdSource: choice.source ?? "token",
244
- accountLabel: choice.label,
245
- workspaces,
246
- };
157
+ const resolveTokenSuccessAccount = (tokens) => resolveAccountSelection(tokens, {
158
+ envAccountId: process.env.CODEX_AUTH_ACCOUNT_ID,
159
+ logInfo,
160
+ getAccountIdCandidates,
161
+ selectBestAccountCandidate,
162
+ });
163
+ const persistAccountPool = createPersistAccounts({
164
+ persistAccountPoolResults,
165
+ withAccountStorageTransaction,
166
+ extractAccountId,
167
+ extractAccountEmail,
168
+ sanitizeEmail,
169
+ findMatchingAccountIndex,
170
+ modelFamilies: MODEL_FAMILIES,
171
+ });
172
+ const accountManagerCacheInvalidationDeps = {
173
+ setCachedAccountManager: (value) => {
174
+ cachedAccountManager = value;
175
+ },
176
+ setAccountManagerPromise: (value) => {
177
+ accountManagerPromise = value;
178
+ },
247
179
  };
248
- const buildManualOAuthFlow = (pkce, url, expectedState, onSuccess) => ({
249
- url,
250
- method: "code",
251
- instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
252
- validate: (input) => {
253
- const parsed = parseAuthorizationInput(input);
254
- if (!parsed.code) {
255
- return `No authorization code found. Paste the full callback URL (e.g., ${REDIRECT_URI}?code=...)`;
256
- }
257
- if (!parsed.state) {
258
- return "Missing OAuth state. Paste the full callback URL including both code and state parameters.";
259
- }
260
- if (parsed.state !== expectedState) {
261
- return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt.";
262
- }
263
- return undefined;
180
+ const makeReloadAccountManagerDeps = (authFallback) => ({
181
+ currentReloadInFlight: accountReloadInFlight,
182
+ loadFromDisk: (fallback) => AccountManager.loadFromDisk(fallback),
183
+ setCachedAccountManager: (value) => {
184
+ cachedAccountManager = value;
264
185
  },
265
- callback: async (input) => {
266
- const parsed = parseAuthorizationInput(input);
267
- if (!parsed.code || !parsed.state) {
268
- return {
269
- type: "failed",
270
- reason: "invalid_response",
271
- message: "Missing authorization code or OAuth state",
272
- };
273
- }
274
- if (parsed.state !== expectedState) {
275
- return {
276
- type: "failed",
277
- reason: "invalid_response",
278
- message: "OAuth state mismatch. Restart login and try again.",
279
- };
280
- }
281
- const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
282
- if (tokens?.type === "success") {
283
- const resolved = resolveAccountSelection(tokens);
284
- if (onSuccess) {
285
- await onSuccess(resolved);
286
- }
287
- return resolved;
288
- }
289
- return tokens?.type === "failed"
290
- ? tokens
291
- : { type: "failed" };
186
+ setAccountManagerPromise: (value) => {
187
+ accountManagerPromise = value;
188
+ },
189
+ setReloadInFlight: (value) => {
190
+ accountReloadInFlight = value;
292
191
  },
192
+ authFallback,
193
+ });
194
+ const reloadAccountManagerFromDisk = async (authFallback) => reloadRuntimeAccountManager(makeReloadAccountManagerDeps(authFallback));
195
+ const applyAccountStorageScope = (pluginConfig) => applyAccountStorageScopeEntry({
196
+ pluginConfig,
197
+ getPerProjectAccounts,
198
+ getStorageBackupEnabled,
199
+ setStorageBackupEnabled,
200
+ isCodexCliSyncEnabled,
201
+ getWarningShown: () => perProjectStorageWarningShown,
202
+ setWarningShown: (shown) => {
203
+ perProjectStorageWarningShown = shown;
204
+ },
205
+ logWarn,
206
+ pluginName: PLUGIN_NAME,
207
+ setStoragePath,
208
+ cwd: () => process.cwd(),
209
+ applyAccountStorageScopeFromConfig,
293
210
  });
294
- const runOAuthFlow = async (forceNewLogin = false) => {
295
- const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
296
- logInfo(`OAuth URL: ${redactOAuthUrlForLog(url)}`);
297
- let serverInfo = null;
298
- try {
299
- serverInfo = await startLocalOAuthServer({ state });
300
- }
301
- catch (err) {
302
- logDebug(`[${PLUGIN_NAME}] Failed to start OAuth server: ${err?.message ?? String(err)}`);
303
- serverInfo = null;
304
- }
305
- openBrowserUrl(url);
306
- if (!serverInfo || !serverInfo.ready) {
307
- serverInfo?.close();
308
- const message = `\n[${PLUGIN_NAME}] OAuth callback server failed to start. ` +
309
- `Please retry with "${AUTH_LABELS.OAUTH_MANUAL}".\n`;
310
- logWarn(message);
311
- return { type: "failed" };
312
- }
313
- const result = await serverInfo.waitForCode(state);
314
- serverInfo.close();
315
- if (!result) {
316
- return { type: "failed", reason: "unknown", message: "OAuth callback timeout or cancelled" };
317
- }
318
- return await exchangeAuthorizationCode(result.code, pkce.verifier, REDIRECT_URI);
319
- };
320
- const persistAccountPool = async (results, replaceAll = false) => {
321
- if (results.length === 0)
322
- return;
323
- await withAccountStorageTransaction(async (loadedStorage, persist) => {
324
- const now = Date.now();
325
- const stored = replaceAll ? null : loadedStorage;
326
- const accounts = stored?.accounts ? [...stored.accounts] : [];
327
- for (const result of results) {
328
- const accountId = result.accountIdOverride ?? extractAccountId(result.access);
329
- const accountIdSource = accountId
330
- ? result.accountIdSource ??
331
- (result.accountIdOverride ? "manual" : "token")
332
- : undefined;
333
- const accountLabel = result.accountLabel;
334
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
335
- const existingIndex = findMatchingAccountIndex(accounts, {
336
- accountId,
337
- email: accountEmail,
338
- refreshToken: result.refresh,
339
- }, {
340
- allowUniqueAccountIdFallbackWithoutEmail: true,
341
- });
342
- if (existingIndex === undefined) {
343
- const initialWorkspaceIndex = result.workspaces && result.workspaces.length > 0
344
- ? (() => {
345
- if (accountId) {
346
- const matchingWorkspaceIndex = result.workspaces.findIndex((workspace) => workspace.id === accountId);
347
- if (matchingWorkspaceIndex >= 0) {
348
- return matchingWorkspaceIndex;
349
- }
350
- }
351
- const firstEnabledWorkspaceIndex = result.workspaces.findIndex((workspace) => workspace.enabled !== false);
352
- return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
353
- })()
354
- : undefined;
355
- accounts.push({
356
- accountId,
357
- accountIdSource,
358
- accountLabel,
359
- email: accountEmail,
360
- refreshToken: result.refresh,
361
- accessToken: result.access,
362
- expiresAt: result.expires,
363
- addedAt: now,
364
- lastUsed: now,
365
- workspaces: result.workspaces,
366
- currentWorkspaceIndex: initialWorkspaceIndex,
367
- });
368
- continue;
369
- }
370
- const existing = accounts[existingIndex];
371
- if (!existing)
372
- continue;
373
- const nextEmail = accountEmail ?? sanitizeEmail(existing.email);
374
- const nextAccountId = accountId ?? existing.accountId;
375
- const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
376
- const nextAccountLabel = accountLabel ?? existing.accountLabel;
377
- // Preserve tracked workspace state when auth refreshes do not return workspace metadata.
378
- const mergedWorkspaces = result.workspaces
379
- ? result.workspaces.map((newWs) => {
380
- const existingWs = existing.workspaces?.find((w) => w.id === newWs.id);
381
- return existingWs
382
- ? {
383
- ...newWs,
384
- enabled: existingWs.enabled,
385
- disabledAt: existingWs.disabledAt,
386
- }
387
- : newWs;
388
- })
389
- : existing.workspaces;
390
- const currentWorkspaceId = existing.workspaces?.[typeof existing.currentWorkspaceIndex === "number"
391
- ? existing.currentWorkspaceIndex
392
- : 0]?.id;
393
- const nextCurrentWorkspaceIndex = mergedWorkspaces && mergedWorkspaces.length > 0
394
- ? (() => {
395
- if (currentWorkspaceId) {
396
- const matchingWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.id === currentWorkspaceId);
397
- if (matchingWorkspaceIndex >= 0) {
398
- return matchingWorkspaceIndex;
399
- }
400
- }
401
- const defaultWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.isDefault === true);
402
- if (defaultWorkspaceIndex >= 0) {
403
- return defaultWorkspaceIndex;
404
- }
405
- const firstEnabledWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.enabled !== false);
406
- return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
407
- })()
408
- : existing.currentWorkspaceIndex;
409
- accounts[existingIndex] = {
410
- ...existing,
411
- accountId: nextAccountId,
412
- accountIdSource: nextAccountIdSource,
413
- accountLabel: nextAccountLabel,
414
- email: nextEmail,
415
- refreshToken: result.refresh,
416
- accessToken: result.access,
417
- expiresAt: result.expires,
418
- lastUsed: now,
419
- workspaces: mergedWorkspaces,
420
- currentWorkspaceIndex: nextCurrentWorkspaceIndex,
421
- };
422
- }
423
- if (accounts.length === 0)
424
- return;
425
- const activeIndex = replaceAll
426
- ? 0
427
- : typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
428
- ? stored.activeIndex
429
- : 0;
430
- const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
431
- const activeIndexByFamily = {};
432
- for (const family of MODEL_FAMILIES) {
433
- const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
434
- const rawFamilyIndex = replaceAll
435
- ? 0
436
- : typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
437
- ? storedFamilyIndex
438
- : clampedActiveIndex;
439
- activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
440
- }
441
- await persist({
442
- version: 3,
443
- accounts,
444
- activeIndex: clampedActiveIndex,
445
- activeIndexByFamily,
446
- });
447
- });
448
- };
449
- const showToast = async (message, variant = "success", options) => {
450
- try {
451
- await client.tui.showToast({
452
- body: {
453
- message,
454
- variant,
455
- ...(options?.title && { title: options.title }),
456
- ...(options?.duration && { duration: options.duration }),
457
- },
458
- });
459
- }
460
- catch {
461
- // Ignore when TUI is not available.
462
- }
463
- };
464
- const resolveActiveIndex = (storage, family = "codex") => {
465
- const total = storage.accounts.length;
466
- if (total === 0)
467
- return 0;
468
- const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
469
- const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
470
- return Math.max(0, Math.min(raw, total - 1));
471
- };
472
- const hydrateEmails = async (storage) => {
473
- if (!storage)
474
- return storage;
475
- const skipHydrate = process.env.VITEST_WORKER_ID !== undefined ||
476
- process.env.NODE_ENV === "test" ||
477
- process.env.CODEX_SKIP_EMAIL_HYDRATE === "1";
478
- if (skipHydrate)
479
- return storage;
480
- const accountsCopy = storage.accounts.map((account) => account ? { ...account } : account);
481
- const accountsToHydrate = accountsCopy.filter((account) => account && !account.email);
482
- if (accountsToHydrate.length === 0)
483
- return storage;
484
- let changed = false;
485
- await Promise.all(accountsToHydrate.map(async (account) => {
486
- try {
487
- const refreshed = await queuedRefresh(account.refreshToken);
488
- if (refreshed.type !== "success")
489
- return;
490
- const id = extractAccountId(refreshed.access);
491
- const email = sanitizeEmail(extractAccountEmail(refreshed.access, refreshed.idToken));
492
- if (id &&
493
- id !== account.accountId &&
494
- shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId)) {
495
- account.accountId = id;
496
- account.accountIdSource = "token";
497
- changed = true;
498
- }
499
- if (email && email !== account.email) {
500
- account.email = email;
501
- changed = true;
502
- }
503
- if (refreshed.access && refreshed.access !== account.accessToken) {
504
- account.accessToken = refreshed.access;
505
- changed = true;
506
- }
507
- if (typeof refreshed.expires === "number" && refreshed.expires !== account.expiresAt) {
508
- account.expiresAt = refreshed.expires;
509
- changed = true;
510
- }
511
- if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
512
- account.refreshToken = refreshed.refresh;
513
- changed = true;
514
- }
515
- }
516
- catch {
517
- logWarn(`[${PLUGIN_NAME}] Failed to hydrate email for account`);
518
- }
519
- }));
520
- if (changed) {
521
- storage.accounts = accountsCopy;
522
- await saveAccounts(storage);
523
- }
524
- return storage;
525
- };
526
- const getRateLimitResetTimeForFamily = (account, now, family) => {
527
- const times = account.rateLimitResetTimes;
528
- if (!times)
529
- return null;
530
- let minReset = null;
531
- const prefix = `${family}:`;
532
- for (const [key, value] of Object.entries(times)) {
533
- if (typeof value !== "number")
534
- continue;
535
- if (value <= now)
536
- continue;
537
- if (key !== family && !key.startsWith(prefix))
538
- continue;
539
- if (minReset === null || value < minReset) {
540
- minReset = value;
541
- }
542
- }
543
- return minReset;
544
- };
545
- const formatRateLimitEntry = (account, now, family = "codex") => {
546
- const resetAt = getRateLimitResetTimeForFamily(account, now, family);
547
- if (typeof resetAt !== "number")
548
- return null;
549
- const remaining = resetAt - now;
550
- if (remaining <= 0)
551
- return null;
552
- return `resets in ${formatWaitTime(remaining)}`;
553
- };
554
- const applyUiRuntimeFromConfig = (pluginConfig) => {
555
- return setUiRuntimeOptions({
556
- v2Enabled: getCodexTuiV2(pluginConfig),
557
- colorProfile: getCodexTuiColorProfile(pluginConfig),
558
- glyphMode: getCodexTuiGlyphMode(pluginConfig),
559
- });
560
- };
561
- const resolveUiRuntime = () => {
562
- return applyUiRuntimeFromConfig(loadPluginConfig());
563
- };
564
- const getStatusMarker = (ui, status) => {
565
- if (!ui.v2Enabled) {
566
- if (status === "ok")
567
- return "✓";
568
- if (status === "warning")
569
- return "!";
570
- return "✗";
571
- }
572
- if (status === "ok")
573
- return ui.theme.glyphs.check;
574
- if (status === "warning")
575
- return "!";
576
- return ui.theme.glyphs.cross;
577
- };
578
- const invalidateAccountManagerCache = () => {
579
- cachedAccountManager = null;
580
- accountManagerPromise = null;
581
- };
582
- const reloadAccountManagerFromDisk = async (authFallback) => {
583
- if (accountReloadInFlight) {
584
- return accountReloadInFlight;
585
- }
586
- accountReloadInFlight = (async () => {
587
- const reloaded = await AccountManager.loadFromDisk(authFallback);
588
- cachedAccountManager = reloaded;
589
- accountManagerPromise = Promise.resolve(reloaded);
590
- return reloaded;
591
- })();
592
- try {
593
- return await accountReloadInFlight;
594
- }
595
- finally {
596
- accountReloadInFlight = null;
597
- }
598
- };
599
- const applyAccountStorageScope = (pluginConfig) => {
600
- const perProjectAccounts = getPerProjectAccounts(pluginConfig);
601
- setStorageBackupEnabled(getStorageBackupEnabled(pluginConfig));
602
- if (isCodexCliSyncEnabled()) {
603
- if (perProjectAccounts && !perProjectStorageWarningShown) {
604
- perProjectStorageWarningShown = true;
605
- logWarn(`[${PLUGIN_NAME}] CODEX_AUTH_PER_PROJECT_ACCOUNTS is ignored while Codex CLI sync is enabled. Using global account storage.`);
606
- }
607
- setStoragePath(null);
608
- return;
609
- }
610
- setStoragePath(perProjectAccounts ? process.cwd() : null);
611
- };
612
211
  const ensureLiveAccountSync = async (pluginConfig, authFallback) => {
613
- if (!getLiveAccountSync(pluginConfig)) {
614
- if (liveAccountSync) {
615
- liveAccountSync.stop();
616
- liveAccountSync = null;
617
- liveAccountSyncPath = null;
618
- }
619
- return;
620
- }
621
- const targetPath = getStoragePath();
622
- if (!liveAccountSync) {
623
- liveAccountSync = new LiveAccountSync(async () => {
624
- await reloadAccountManagerFromDisk(authFallback);
212
+ const next = await ensureLiveAccountSyncEntry({
213
+ pluginConfig,
214
+ authFallback,
215
+ currentSync: liveAccountSync,
216
+ currentPath: liveAccountSyncPath,
217
+ getLiveAccountSync,
218
+ getStoragePath,
219
+ createSync: (oauthFallback) => new LiveAccountSync(async () => {
220
+ await reloadRuntimeAccountManager(makeReloadAccountManagerDeps(oauthFallback));
625
221
  }, {
626
222
  debounceMs: getLiveAccountSyncDebounceMs(pluginConfig),
627
223
  pollIntervalMs: getLiveAccountSyncPollMs(pluginConfig),
628
- });
629
- registerCleanup(() => {
630
- liveAccountSync?.stop();
631
- });
632
- }
633
- if (liveAccountSyncPath !== targetPath) {
634
- let switched = false;
635
- for (let attempt = 0; attempt < 3; attempt += 1) {
636
- try {
637
- await liveAccountSync.syncToPath(targetPath);
638
- liveAccountSyncPath = targetPath;
639
- switched = true;
640
- break;
641
- }
642
- catch (error) {
643
- const code = error?.code;
644
- if (code !== "EBUSY" && code !== "EPERM") {
645
- throw error;
646
- }
647
- await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt));
648
- }
649
- }
650
- if (!switched) {
651
- logWarn(`[${PLUGIN_NAME}] Live account sync path switch failed due to transient filesystem locks; keeping previous watcher.`);
652
- }
653
- }
224
+ }),
225
+ registerCleanup,
226
+ logWarn,
227
+ pluginName: PLUGIN_NAME,
228
+ ensureLiveAccountSyncState,
229
+ });
230
+ liveAccountSync = next.liveAccountSync;
231
+ liveAccountSyncPath = next.liveAccountSyncPath;
654
232
  };
655
233
  const ensureRefreshGuardian = (pluginConfig) => {
656
- if (!getProactiveRefreshGuardian(pluginConfig)) {
657
- if (refreshGuardian) {
658
- refreshGuardian.stop();
659
- refreshGuardian = null;
660
- refreshGuardianConfigKey = null;
661
- }
662
- return;
663
- }
664
- const intervalMs = getProactiveRefreshIntervalMs(pluginConfig);
665
- const bufferMs = getProactiveRefreshBufferMs(pluginConfig);
666
- const configKey = `${intervalMs}:${bufferMs}`;
667
- if (refreshGuardian && refreshGuardianConfigKey === configKey)
668
- return;
669
- if (refreshGuardian) {
670
- refreshGuardian.stop();
671
- }
672
- refreshGuardian = new RefreshGuardian(() => cachedAccountManager, { intervalMs, bufferMs });
673
- refreshGuardianConfigKey = configKey;
674
- refreshGuardian.start();
675
- registerCleanup(() => {
676
- refreshGuardian?.stop();
234
+ const next = ensureRefreshGuardianState({
235
+ enabled: getProactiveRefreshGuardian(pluginConfig),
236
+ intervalMs: getProactiveRefreshIntervalMs(pluginConfig),
237
+ bufferMs: getProactiveRefreshBufferMs(pluginConfig),
238
+ currentGuardian: refreshGuardian,
239
+ currentConfigKey: refreshGuardianConfigKey,
240
+ currentCleanupRegistered: refreshGuardianCleanupRegistered,
241
+ getCurrentGuardian: () => refreshGuardian,
242
+ createGuardian: ({ intervalMs, bufferMs }) => new RefreshGuardian(() => cachedAccountManager, {
243
+ intervalMs,
244
+ bufferMs,
245
+ }),
246
+ registerCleanup,
677
247
  });
248
+ refreshGuardian = next.refreshGuardian;
249
+ refreshGuardianConfigKey = next.refreshGuardianConfigKey;
250
+ refreshGuardianCleanupRegistered =
251
+ next.refreshGuardianCleanupRegistered;
678
252
  };
679
253
  const ensureSessionAffinity = (pluginConfig) => {
680
- if (!getSessionAffinity(pluginConfig)) {
681
- sessionAffinityStore = null;
682
- sessionAffinityConfigKey = null;
683
- return;
684
- }
685
- const ttlMs = getSessionAffinityTtlMs(pluginConfig);
686
- const maxEntries = getSessionAffinityMaxEntries(pluginConfig);
687
- const configKey = `${ttlMs}:${maxEntries}`;
688
- if (sessionAffinityStore && sessionAffinityConfigKey === configKey)
689
- return;
690
- sessionAffinityStore = new SessionAffinityStore({ ttlMs, maxEntries });
691
- sessionAffinityConfigKey = configKey;
692
- };
693
- const applyPreemptiveQuotaSettings = (pluginConfig) => {
694
- preemptiveQuotaScheduler.configure({
695
- enabled: getPreemptiveQuotaEnabled(pluginConfig),
696
- remainingPercentThresholdPrimary: getPreemptiveQuotaRemainingPercent5h(pluginConfig),
697
- remainingPercentThresholdSecondary: getPreemptiveQuotaRemainingPercent7d(pluginConfig),
698
- maxDeferralMs: getPreemptiveQuotaMaxDeferralMs(pluginConfig),
254
+ const next = ensureSessionAffinityState({
255
+ enabled: getSessionAffinity(pluginConfig) ||
256
+ getResponseContinuation(pluginConfig),
257
+ ttlMs: getSessionAffinityTtlMs(pluginConfig),
258
+ maxEntries: getSessionAffinityMaxEntries(pluginConfig),
259
+ currentStore: sessionAffinityStore,
260
+ currentConfigKey: sessionAffinityConfigKey,
261
+ createStore: ({ ttlMs, maxEntries }) => new SessionAffinityStore({ ttlMs, maxEntries }),
699
262
  });
263
+ sessionAffinityStore = next.sessionAffinityStore;
264
+ sessionAffinityConfigKey = next.sessionAffinityConfigKey;
700
265
  };
266
+ const applyPreemptiveQuotaSettings = (pluginConfig) => applyPreemptiveQuotaSettingsFromConfig(pluginConfig, {
267
+ configure: (options) => preemptiveQuotaScheduler.configure(options),
268
+ getPreemptiveQuotaEnabled,
269
+ getPreemptiveQuotaRemainingPercent5h,
270
+ getPreemptiveQuotaRemainingPercent7d,
271
+ getPreemptiveQuotaMaxDeferralMs,
272
+ });
701
273
  // Event handler for session recovery and account selection
702
- const eventHandler = async (input) => {
703
- try {
704
- const { event } = input;
705
- // Handle TUI account selection events
706
- // Accepts generic selection events with an index property
707
- if (event.type === "account.select" ||
708
- event.type === "openai.account.select") {
709
- const props = event.properties;
710
- // Filter by provider if specified
711
- if (props.provider && props.provider !== "openai" && props.provider !== PROVIDER_ID) {
712
- return;
713
- }
714
- const index = props.index ?? props.accountIndex;
715
- if (typeof index === "number") {
716
- const storage = await loadAccounts();
717
- if (!storage || index < 0 || index >= storage.accounts.length) {
718
- return;
719
- }
720
- const now = Date.now();
721
- const account = storage.accounts[index];
722
- if (account) {
723
- account.lastUsed = now;
724
- account.lastSwitchReason = "rotation";
725
- }
726
- storage.activeIndex = index;
727
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
728
- for (const family of MODEL_FAMILIES) {
729
- storage.activeIndexByFamily[family] = index;
730
- }
731
- await saveAccounts(storage);
732
- if (cachedAccountManager) {
733
- await cachedAccountManager.syncCodexCliActiveSelectionForIndex(index);
734
- }
735
- lastCodexCliActiveSyncIndex = index;
736
- // Reload manager from disk so we don't overwrite newer rotated
737
- // refresh tokens with stale in-memory state.
738
- if (cachedAccountManager) {
739
- await reloadAccountManagerFromDisk();
740
- }
741
- await showToast(`Switched to account ${index + 1}`, "info");
742
- }
743
- }
744
- }
745
- catch (error) {
746
- logDebug(`[${PLUGIN_NAME}] Event handler error: ${error instanceof Error ? error.message : String(error)}`);
747
- }
748
- };
274
+ const eventHandler = async (input) => handleRuntimeEvent({
275
+ input,
276
+ providerId: PROVIDER_ID,
277
+ modelFamilies: MODEL_FAMILIES,
278
+ loadAccounts,
279
+ saveAccounts,
280
+ hasCachedAccountManager: () => !!cachedAccountManager,
281
+ syncCodexCliActiveSelectionForIndex: async (index) => {
282
+ if (!cachedAccountManager)
283
+ return;
284
+ await cachedAccountManager.syncCodexCliActiveSelectionForIndex(index);
285
+ },
286
+ setLastCodexCliActiveSyncIndex: (index) => {
287
+ lastCodexCliActiveSyncIndex = index;
288
+ },
289
+ reloadAccountManagerFromDisk: () => reloadAccountManagerFromDisk(),
290
+ showToast: (message, variant) => showRuntimeToast(client, message, variant),
291
+ logDebug,
292
+ pluginName: PLUGIN_NAME,
293
+ });
749
294
  // Initialize runtime UI settings once on plugin load; auth/tools refresh this dynamically.
750
- resolveUiRuntime();
295
+ applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
751
296
  return {
752
297
  event: eventHandler,
753
298
  auth: {
@@ -756,11 +301,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
756
301
  * Loader function that configures OAuth authentication and request handling
757
302
  *
758
303
  * This function:
759
- * 1. Validates OAuth authentication
760
- * 2. Loads multi-account pool from disk (fallback to current auth)
761
- * 3. Loads user configuration from runtime model config
762
- * 4. Fetches Codex system instructions from GitHub (cached)
763
- * 5. Returns SDK configuration with custom fetch implementation
304
+ * 1. Validates OAuth authentication
305
+ * 2. Loads multi-account pool from disk (fallback to current auth)
306
+ * 3. Loads user configuration from runtime model config
307
+ * 4. Fetches Codex system instructions from GitHub (cached)
308
+ * 5. Returns SDK configuration with custom fetch implementation
764
309
  *
765
310
  * @param getAuth - Function to retrieve current auth state
766
311
  * @param provider - Provider configuration from runtime model config
@@ -769,11 +314,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
769
314
  async loader(getAuth, provider) {
770
315
  const auth = await getAuth();
771
316
  const pluginConfig = loadPluginConfig();
772
- applyUiRuntimeFromConfig(pluginConfig);
773
- applyAccountStorageScope(pluginConfig);
774
- ensureSessionAffinity(pluginConfig);
775
- ensureRefreshGuardian(pluginConfig);
776
- applyPreemptiveQuotaSettings(pluginConfig);
317
+ applyLoaderRuntimeSetup({
318
+ pluginConfig,
319
+ applyUiRuntimeFromConfig: (config) => applyUiRuntimeFromConfig(config, setUiRuntimeOptions),
320
+ applyAccountStorageScope,
321
+ ensureSessionAffinity,
322
+ ensureRefreshGuardian,
323
+ applyPreemptiveQuotaSettings,
324
+ });
777
325
  // Only handle OAuth auth type, skip API key auth
778
326
  if (auth.type !== "oauth") {
779
327
  return {};
@@ -797,15 +345,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
797
345
  try {
798
346
  await ensureLiveAccountSync(pluginConfig, auth);
799
347
  if (!accountManagerPromise) {
800
- await reloadAccountManagerFromDisk(auth);
348
+ await reloadRuntimeAccountManager(makeReloadAccountManagerDeps(auth));
801
349
  }
802
350
  const managerPromise = accountManagerPromise ??
803
- reloadAccountManagerFromDisk(auth);
351
+ reloadRuntimeAccountManager(makeReloadAccountManagerDeps(auth));
804
352
  let accountManager = await managerPromise;
805
353
  cachedAccountManager = accountManager;
806
354
  const refreshToken = auth.type === "oauth" ? auth.refresh : "";
807
- const needsPersist = refreshToken &&
808
- !accountManager.hasRefreshToken(refreshToken);
355
+ const needsPersist = refreshToken && !accountManager.hasRefreshToken(refreshToken);
809
356
  if (needsPersist) {
810
357
  await accountManager.saveToDisk();
811
358
  }
@@ -846,7 +393,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
846
393
  STREAM_FAILOVER_SOFT_TIMEOUT_BY_MODE[failoverMode]);
847
394
  const streamFailoverHardTimeoutMs = Math.max(streamFailoverSoftTimeoutMs, parseEnvInt(process.env.CODEX_AUTH_STREAM_STALL_HARD_TIMEOUT_MS) ??
848
395
  streamStallTimeoutMs);
849
- const maxSameAccountRetries = failoverMode === "conservative" ? 2 : failoverMode === "balanced" ? 1 : 0;
396
+ const maxSameAccountRetries = failoverMode === "conservative"
397
+ ? 2
398
+ : failoverMode === "balanced"
399
+ ? 1
400
+ : 0;
850
401
  const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
851
402
  const autoResumeEnabled = getAutoResume(pluginConfig);
852
403
  const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
@@ -875,11 +426,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
875
426
  prewarmHostCodexPrompt();
876
427
  }
877
428
  }
878
- const recoveryHook = sessionRecoveryEnabled
879
- ? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
880
- : null;
429
+ const recoveryHook = createRuntimeSessionRecoveryHook({
430
+ enabled: sessionRecoveryEnabled,
431
+ client,
432
+ directory: process.cwd(),
433
+ autoResume: autoResumeEnabled,
434
+ });
881
435
  checkAndNotify(async (message, variant) => {
882
- await showToast(message, variant);
436
+ await showRuntimeToast(client, message, variant);
883
437
  }).catch((err) => {
884
438
  logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
885
439
  });
@@ -904,7 +458,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
904
458
  */
905
459
  async fetch(input, init) {
906
460
  try {
907
- if (cachedAccountManager && cachedAccountManager !== accountManager) {
461
+ if (cachedAccountManager &&
462
+ cachedAccountManager !== accountManager) {
908
463
  accountManager = cachedAccountManager;
909
464
  }
910
465
  // Step 1: Extract and rewrite URL for Codex backend
@@ -914,118 +469,58 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
914
469
  // Instructions are fetched per model family (codex-max, codex, gpt-5.1)
915
470
  // Capture original stream value before transformation
916
471
  // generateText() sends no stream field, streamText() sends stream=true
917
- const normalizeRequestInit = async (requestInput, requestInit) => {
918
- if (requestInit)
919
- return requestInit;
920
- if (!(requestInput instanceof Request))
921
- return requestInit;
922
- const method = requestInput.method || "GET";
923
- const normalized = {
924
- method,
925
- headers: new Headers(requestInput.headers),
926
- };
927
- if (method !== "GET" && method !== "HEAD") {
928
- try {
929
- const bodyText = await requestInput.clone().text();
930
- if (bodyText) {
931
- normalized.body = bodyText;
932
- }
933
- }
934
- catch {
935
- // Body may be unreadable; proceed without it.
936
- }
937
- }
938
- return normalized;
939
- };
940
- const parseRequestBodyFromInit = async (body) => {
941
- if (!body)
942
- return {};
943
- try {
944
- if (typeof body === "string") {
945
- return JSON.parse(body);
946
- }
947
- if (body instanceof Uint8Array) {
948
- return JSON.parse(new TextDecoder().decode(body));
949
- }
950
- if (body instanceof ArrayBuffer) {
951
- return JSON.parse(new TextDecoder().decode(new Uint8Array(body)));
952
- }
953
- if (ArrayBuffer.isView(body)) {
954
- const view = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
955
- return JSON.parse(new TextDecoder().decode(view));
956
- }
957
- if (typeof Blob !== "undefined" && body instanceof Blob) {
958
- return JSON.parse(await body.text());
959
- }
960
- }
961
- catch {
962
- logWarn("Failed to parse request body, using empty object");
963
- }
964
- return {};
965
- };
966
472
  const baseInit = await normalizeRequestInit(input, init);
967
- const originalBody = await parseRequestBodyFromInit(baseInit?.body);
473
+ const originalBody = await parseRequestBodyFromInit(baseInit?.body, logWarn);
968
474
  const isStreaming = originalBody.stream === true;
969
- const parsedBody = Object.keys(originalBody).length > 0 ? originalBody : undefined;
475
+ const parsedBody = Object.keys(originalBody).length > 0
476
+ ? { ...originalBody }
477
+ : undefined;
970
478
  const transformation = await transformRequestForCodex(baseInit, url, effectiveUserConfig, codexMode, parsedBody, {
971
479
  fastSession: fastSessionEnabled,
972
480
  fastSessionStrategy,
973
481
  fastSessionMaxInputItems,
482
+ deferFastSessionInputTrimming: fastSessionEnabled,
483
+ allowBackgroundResponses: getBackgroundResponses(pluginConfig),
974
484
  });
975
485
  let requestInit = transformation?.updatedInit ?? baseInit;
976
486
  let transformedBody = transformation?.body;
487
+ let pendingFastSessionInputTrim = transformation?.deferredFastSessionInputTrim;
977
488
  const promptCacheKey = transformedBody?.prompt_cache_key;
978
489
  let model = transformedBody?.model;
979
490
  let modelFamily = model ? getModelFamily(model) : "gpt-5.1";
980
491
  let quotaKey = model ? `${modelFamily}:${model}` : modelFamily;
492
+ const responseContinuationEnabled = getResponseContinuation(pluginConfig);
981
493
  const threadIdCandidate = (process.env.CODEX_THREAD_ID ?? promptCacheKey ?? "")
982
494
  .toString()
983
495
  .trim() || undefined;
984
496
  const sessionAffinityKey = threadIdCandidate ?? promptCacheKey ?? null;
985
- const effectivePromptCacheKey = (sessionAffinityKey ?? promptCacheKey ?? "").toString().trim() || undefined;
497
+ const effectivePromptCacheKey = (sessionAffinityKey ?? promptCacheKey ?? "")
498
+ .toString()
499
+ .trim() || undefined;
500
+ const shouldUseResponseContinuation = Boolean(transformedBody) &&
501
+ responseContinuationEnabled &&
502
+ !transformedBody?.previous_response_id;
503
+ if (shouldUseResponseContinuation && transformedBody) {
504
+ const lastResponseId = sessionAffinityStore?.getLastResponseId(sessionAffinityKey);
505
+ if (lastResponseId) {
506
+ transformedBody = {
507
+ ...transformedBody,
508
+ previous_response_id: lastResponseId,
509
+ };
510
+ requestInit = {
511
+ ...requestInit,
512
+ body: JSON.stringify(transformedBody),
513
+ };
514
+ }
515
+ }
986
516
  const preferredSessionAccountIndex = sessionAffinityStore?.getPreferredAccountIndex(sessionAffinityKey);
987
517
  sessionAffinityStore?.prune();
988
- const requestCorrelationId = setCorrelationId(threadIdCandidate ? `${threadIdCandidate}:${Date.now()}` : undefined);
518
+ const requestCorrelationId = setCorrelationId(threadIdCandidate
519
+ ? `${threadIdCandidate}:${Date.now()}`
520
+ : undefined);
989
521
  runtimeMetrics.lastRequestAt = Date.now();
990
522
  const abortSignal = requestInit?.signal ?? init?.signal ?? null;
991
- const sleep = (ms) => new Promise((resolve, reject) => {
992
- if (abortSignal?.aborted) {
993
- reject(new Error("Aborted"));
994
- return;
995
- }
996
- const timeout = setTimeout(() => {
997
- cleanup();
998
- resolve();
999
- }, ms);
1000
- const onAbort = () => {
1001
- cleanup();
1002
- reject(new Error("Aborted"));
1003
- };
1004
- const cleanup = () => {
1005
- clearTimeout(timeout);
1006
- abortSignal?.removeEventListener("abort", onAbort);
1007
- };
1008
- abortSignal?.addEventListener("abort", onAbort, { once: true });
1009
- });
1010
- const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
1011
- const startTime = Date.now();
1012
- const endTime = startTime + totalMs;
1013
- while (Date.now() < endTime) {
1014
- if (abortSignal?.aborted) {
1015
- throw new Error("Aborted");
1016
- }
1017
- const remaining = Math.max(0, endTime - Date.now());
1018
- const waitLabel = formatWaitTime(remaining);
1019
- await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
1020
- const sleepTime = Math.min(intervalMs, remaining);
1021
- if (sleepTime > 0) {
1022
- await sleep(sleepTime);
1023
- }
1024
- else {
1025
- break;
1026
- }
1027
- }
1028
- };
523
+ const sleep = createAbortableSleep(abortSignal);
1029
524
  let allRateLimitedRetries = 0;
1030
525
  let emptyResponseRetries = 0;
1031
526
  const attemptedUnsupportedFallbackModels = new Set();
@@ -1040,11 +535,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1040
535
  let usedPreferredSessionAccount = false;
1041
536
  const capabilityBoostByAccount = {};
1042
537
  const accountSnapshotSource = accountManager;
1043
- const accountSnapshotList = typeof accountSnapshotSource.getAccountsSnapshot === "function"
1044
- ? accountSnapshotSource.getAccountsSnapshot() ?? []
538
+ const accountSnapshotList = typeof accountSnapshotSource.getAccountsSnapshot ===
539
+ "function"
540
+ ? (accountSnapshotSource.getAccountsSnapshot() ?? [])
1045
541
  : [];
1046
542
  if (accountSnapshotList.length === 0 &&
1047
- typeof accountSnapshotSource.getAccountByIndex === "function") {
543
+ typeof accountSnapshotSource.getAccountByIndex ===
544
+ "function") {
1048
545
  for (let accountSnapshotIndex = 0; accountSnapshotIndex < accountCount; accountSnapshotIndex += 1) {
1049
546
  const candidate = accountSnapshotSource.getAccountByIndex(accountSnapshotIndex);
1050
547
  if (candidate) {
@@ -1054,7 +551,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1054
551
  }
1055
552
  for (const candidate of accountSnapshotList) {
1056
553
  const accountKey = resolveEntitlementAccountKey(candidate);
1057
- capabilityBoostByAccount[candidate.index] = capabilityPolicyStore.getBoost(accountKey, model ?? modelFamily);
554
+ capabilityBoostByAccount[candidate.index] =
555
+ capabilityPolicyStore.getBoost(accountKey, model ?? modelFamily);
1058
556
  }
1059
557
  accountAttemptLoop: while (attempted.size < Math.max(1, accountCount)) {
1060
558
  let account = null;
@@ -1088,9 +586,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1088
586
  try {
1089
587
  if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
1090
588
  accountAuth = (await refreshAndUpdateToken(accountAuth, client));
1091
- accountManager.updateFromAuth(account, accountAuth);
1092
- accountManager.clearAuthFailures(account);
1093
- accountManager.saveToDiskDebounced();
589
+ await accountManager.commitRefreshedAuth(account, accountAuth);
1094
590
  }
1095
591
  }
1096
592
  catch (err) {
@@ -1098,21 +594,36 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1098
594
  runtimeMetrics.authRefreshFailures++;
1099
595
  runtimeMetrics.failedRequests++;
1100
596
  runtimeMetrics.accountRotations++;
1101
- runtimeMetrics.lastError = err?.message ?? String(err);
1102
- const failures = accountManager.incrementAuthFailures(account);
597
+ runtimeMetrics.lastError =
598
+ err?.message ?? String(err);
599
+ const isRetryableRefreshError = err instanceof CodexAuthError && err.retryable;
1103
600
  const accountLabel = formatAccountLabel(account, account.index);
601
+ sessionAffinityStore?.forgetSession(sessionAffinityKey);
602
+ if (isRetryableRefreshError) {
603
+ runtimeMetrics.networkErrors++;
604
+ const retryableRefreshPolicy = evaluateFailurePolicy({ kind: "network", failoverMode }, { networkCooldownMs: networkErrorCooldownMs });
605
+ if (retryableRefreshPolicy.recordFailure) {
606
+ accountManager.recordFailure(account, modelFamily, model);
607
+ }
608
+ if (typeof retryableRefreshPolicy.cooldownMs === "number" &&
609
+ retryableRefreshPolicy.cooldownReason) {
610
+ accountManager.markAccountCoolingDown(account, retryableRefreshPolicy.cooldownMs, retryableRefreshPolicy.cooldownReason);
611
+ accountManager.saveToDiskDebounced();
612
+ }
613
+ continue;
614
+ }
615
+ const failures = accountManager.incrementAuthFailures(account);
1104
616
  const authFailurePolicy = evaluateFailurePolicy({
1105
617
  kind: "auth-refresh",
1106
618
  consecutiveAuthFailures: failures,
1107
619
  });
1108
- sessionAffinityStore?.forgetSession(sessionAffinityKey);
1109
620
  if (authFailurePolicy.removeAccount) {
1110
621
  const removedIndex = account.index;
1111
622
  sessionAffinityStore?.forgetAccount(removedIndex);
1112
623
  accountManager.removeAccount(account);
1113
624
  sessionAffinityStore?.reindexAfterRemoval(removedIndex);
1114
625
  accountManager.saveToDiskDebounced();
1115
- await showToast(`Removed ${accountLabel} after ${failures} consecutive auth failures. Run 'codex login' to re-add.`, "error", { duration: toastDurationMs * 2 });
626
+ await showRuntimeToast(client, `Removed ${accountLabel} after ${failures} consecutive auth failures. Run 'codex login' to re-add.`, "error", { duration: toastDurationMs * 2 });
1116
627
  continue;
1117
628
  }
1118
629
  if (typeof authFailurePolicy.cooldownMs === "number" &&
@@ -1158,8 +669,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1158
669
  continue;
1159
670
  }
1160
671
  account.accountId = accountId;
1161
- if (!hadAccountId && tokenAccountId && accountId === tokenAccountId) {
1162
- account.accountIdSource = storedAccountIdSource ?? "token";
672
+ if (!hadAccountId &&
673
+ tokenAccountId &&
674
+ accountId === tokenAccountId) {
675
+ account.accountIdSource =
676
+ storedAccountIdSource ?? "token";
1163
677
  }
1164
678
  if (resolvedEmail) {
1165
679
  account.email = resolvedEmail;
@@ -1167,13 +681,38 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1167
681
  if (accountCount > 1 &&
1168
682
  accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1169
683
  const accountLabel = formatAccountLabel(account, account.index);
1170
- await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
684
+ await showRuntimeToast(client, `Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
1171
685
  accountManager.markToastShown(account.index);
1172
686
  }
1173
687
  const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
1174
688
  model,
1175
689
  promptCacheKey: effectivePromptCacheKey,
1176
690
  });
691
+ if (transformedBody && pendingFastSessionInputTrim) {
692
+ const activeFastSessionInputTrim = pendingFastSessionInputTrim;
693
+ pendingFastSessionInputTrim = undefined;
694
+ const compactionResult = await applyResponseCompaction({
695
+ body: transformedBody,
696
+ requestUrl: url,
697
+ headers,
698
+ trim: activeFastSessionInputTrim,
699
+ fetchImpl: async (requestUrl, requestInit) => {
700
+ const normalizedCompactionUrl = typeof requestUrl === "string"
701
+ ? requestUrl
702
+ : String(requestUrl);
703
+ return fetch(normalizedCompactionUrl, applyProxyCompatibleInit(normalizedCompactionUrl, requestInit));
704
+ },
705
+ signal: abortSignal,
706
+ timeoutMs: Math.min(fetchTimeoutMs, 4_000),
707
+ });
708
+ if (compactionResult.mode !== "unchanged") {
709
+ transformedBody = compactionResult.body;
710
+ requestInit = {
711
+ ...(requestInit ?? {}),
712
+ body: JSON.stringify(transformedBody),
713
+ };
714
+ }
715
+ }
1177
716
  const quotaScheduleKey = `${entitlementAccountKey}:${model ?? modelFamily}`;
1178
717
  const capabilityModelKey = model ?? modelFamily;
1179
718
  const quotaDeferral = preemptiveQuotaScheduler.getDeferral(quotaScheduleKey);
@@ -1190,8 +729,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1190
729
  if (!tokenConsumed) {
1191
730
  accountManager.recordRateLimit(account, modelFamily, model);
1192
731
  runtimeMetrics.accountRotations++;
1193
- runtimeMetrics.lastError =
1194
- `Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
732
+ runtimeMetrics.lastError = `Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
1195
733
  logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
1196
734
  continue;
1197
735
  }
@@ -1211,14 +749,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1211
749
  fetchController.abort(timeoutReason);
1212
750
  }, requestTimeoutMs);
1213
751
  const onUserAbort = abortSignal
1214
- ? () => fetchController.abort(abortSignal.reason ?? new Error("Aborted by user"))
752
+ ? () => fetchController.abort(abortSignal.reason ??
753
+ new Error("Aborted by user"))
1215
754
  : null;
1216
755
  if (abortSignal?.aborted) {
1217
756
  clearTimeout(fetchTimeoutId);
1218
757
  fetchController.abort(abortSignal.reason ?? new Error("Aborted by user"));
1219
758
  }
1220
759
  else if (abortSignal && onUserAbort) {
1221
- abortSignal.addEventListener("abort", onUserAbort, { once: true });
760
+ abortSignal.addEventListener("abort", onUserAbort, {
761
+ once: true,
762
+ });
1222
763
  }
1223
764
  try {
1224
765
  runtimeMetrics.totalRequests++;
@@ -1239,11 +780,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1239
780
  runtimeMetrics.userAborts++;
1240
781
  runtimeMetrics.lastError = "request aborted by user";
1241
782
  sessionAffinityStore?.forgetSession(sessionAffinityKey);
1242
- throw (fetchAbortReason instanceof Error
783
+ throw fetchAbortReason instanceof Error
1243
784
  ? fetchAbortReason
1244
- : new Error("Aborted by user"));
785
+ : new Error("Aborted by user");
1245
786
  }
1246
- const errorMsg = networkError instanceof Error ? networkError.message : String(networkError);
787
+ const errorMsg = networkError instanceof Error
788
+ ? networkError.message
789
+ : String(networkError);
1247
790
  logWarn(`Network error for account ${account.index + 1}: ${errorMsg}`);
1248
791
  runtimeMetrics.failedRequests++;
1249
792
  runtimeMetrics.networkErrors++;
@@ -1296,18 +839,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1296
839
  if (contextOverflowResult.handled) {
1297
840
  return contextOverflowResult.response;
1298
841
  }
1299
- const { response: errorResponse, rateLimit, errorBody } = await handleErrorResponse(response, {
842
+ const { response: errorResponse, rateLimit, errorBody, } = await handleErrorResponse(response, {
1300
843
  requestCorrelationId,
1301
844
  threadId: threadIdCandidate,
1302
845
  });
1303
846
  const unsupportedModelInfo = getUnsupportedCodexModelInfo(errorBody);
1304
847
  const hasRemainingAccounts = attempted.size < Math.max(1, accountCount);
1305
- const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
848
+ const blockedModel = unsupportedModelInfo.unsupportedModel ??
849
+ model ??
850
+ "requested model";
1306
851
  const blockedModelNormalized = blockedModel.toLowerCase();
1307
852
  const shouldForceSparkFallback = unsupportedModelInfo.isUnsupported &&
1308
853
  (blockedModelNormalized === "gpt-5.3-codex-spark" ||
1309
854
  blockedModelNormalized.includes("gpt-5.3-codex-spark"));
1310
- const allowUnsupportedFallback = fallbackOnUnsupportedCodexModel || shouldForceSparkFallback;
855
+ const allowUnsupportedFallback = fallbackOnUnsupportedCodexModel ||
856
+ shouldForceSparkFallback;
1311
857
  // Entitlements can differ by account/workspace, so try remaining
1312
858
  // accounts before degrading the model via fallback.
1313
859
  // Spark entitlement is commonly unavailable on non-Pro/Business workspaces;
@@ -1352,12 +898,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1352
898
  model = fallbackModel;
1353
899
  modelFamily = getModelFamily(model);
1354
900
  quotaKey = `${modelFamily}:${model}`;
1355
- if (transformedBody && typeof transformedBody === "object") {
901
+ if (transformedBody &&
902
+ typeof transformedBody === "object") {
1356
903
  transformedBody = { ...transformedBody, model };
1357
904
  }
1358
905
  else {
1359
- let fallbackBody = { model };
1360
- if (requestInit?.body && typeof requestInit.body === "string") {
906
+ let fallbackBody = {
907
+ model,
908
+ };
909
+ if (requestInit?.body &&
910
+ typeof requestInit.body === "string") {
1361
911
  try {
1362
912
  const parsed = JSON.parse(requestInit.body);
1363
913
  fallbackBody = { ...parsed, model };
@@ -1380,11 +930,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1380
930
  fallbackApplied: true,
1381
931
  fallbackReason: "unsupported-model-entitlement",
1382
932
  });
1383
- await showToast(`Model ${previousModel} is not available for this account. Retrying with ${model}.`, "warning", { duration: toastDurationMs });
933
+ await showRuntimeToast(client, `Model ${previousModel} is not available for this account. Retrying with ${model}.`, "warning", { duration: toastDurationMs });
1384
934
  restartAccountTraversalWithFallback = true;
1385
935
  break;
1386
936
  }
1387
- if (unsupportedModelInfo.isUnsupported && !allowUnsupportedFallback) {
937
+ if (unsupportedModelInfo.isUnsupported &&
938
+ !allowUnsupportedFallback) {
1388
939
  entitlementCache.markBlocked(entitlementAccountKey, blockedModel, "unsupported-model");
1389
940
  capabilityPolicyStore.recordUnsupported(entitlementAccountKey, blockedModel);
1390
941
  runtimeMetrics.lastError = `Unsupported model (strict): ${blockedModel}`;
@@ -1395,7 +946,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1395
946
  fallbackApplied: false,
1396
947
  fallbackReason: "unsupported-model-entitlement",
1397
948
  });
1398
- await showToast(`Model ${blockedModel} is not available for this account. Strict policy blocked automatic fallback.`, "warning", { duration: toastDurationMs });
949
+ await showRuntimeToast(client, `Model ${blockedModel} is not available for this account. Strict policy blocked automatic fallback.`, "warning", { duration: toastDurationMs });
1399
950
  }
1400
951
  if (unsupportedModelInfo.isUnsupported &&
1401
952
  allowUnsupportedFallback &&
@@ -1412,7 +963,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1412
963
  if (isDisabledWorkspaceError) {
1413
964
  runtimeMetrics.failedRequests++;
1414
965
  runtimeMetrics.lastError = `Workspace disabled for account ${account.index + 1}`;
1415
- if (!account.workspaces || account.workspaces.length === 0) {
966
+ if (!account.workspaces ||
967
+ account.workspaces.length === 0) {
1416
968
  logWarn(`Workspace disabled/expired for account ${account.index + 1} without tracked workspaces. Leaving account enabled.`, { errorCode: workspaceErrorCode });
1417
969
  if (hasRemainingAccounts) {
1418
970
  continue accountAttemptLoop;
@@ -1421,7 +973,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1421
973
  }
1422
974
  else {
1423
975
  const currentWorkspace = accountManager.getCurrentWorkspace(account);
1424
- const workspaceName = currentWorkspace?.name ?? currentWorkspace?.id ?? "unknown";
976
+ const workspaceName = currentWorkspace?.name ??
977
+ currentWorkspace?.id ??
978
+ "unknown";
1425
979
  logWarn(`Workspace disabled/expired for account ${account.index + 1} - workspace: ${workspaceName}. Rotating to next workspace.`, { errorCode: workspaceErrorCode });
1426
980
  const disabledWorkspace = currentWorkspace
1427
981
  ? accountManager.disableCurrentWorkspace(account, currentWorkspace.id)
@@ -1429,13 +983,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1429
983
  let nextWorkspace = disabledWorkspace
1430
984
  ? accountManager.rotateToNextWorkspace(account)
1431
985
  : accountManager.getCurrentWorkspace(account);
1432
- if (!disabledWorkspace && (!nextWorkspace || nextWorkspace.enabled === false)) {
1433
- nextWorkspace = accountManager.rotateToNextWorkspace(account);
986
+ if (!disabledWorkspace &&
987
+ (!nextWorkspace ||
988
+ nextWorkspace.enabled === false)) {
989
+ nextWorkspace =
990
+ accountManager.rotateToNextWorkspace(account);
1434
991
  }
1435
992
  if (nextWorkspace) {
1436
993
  accountManager.saveToDiskDebounced();
1437
994
  const newWorkspaceName = nextWorkspace.name ?? nextWorkspace.id;
1438
- await showToast(`Workspace ${workspaceName} disabled. Switched to ${newWorkspaceName}.`, "warning", { duration: toastDurationMs });
995
+ await showRuntimeToast(client, `Workspace ${workspaceName} disabled. Switched to ${newWorkspaceName}.`, "warning", { duration: toastDurationMs });
1439
996
  logInfo(`Rotated to workspace ${newWorkspaceName} for account ${account.index + 1}`);
1440
997
  // Allow the same account to be selected again with fresh request state.
1441
998
  attempted.delete(account.index);
@@ -1444,21 +1001,25 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1444
1001
  logWarn(`All workspaces disabled for account ${account.index + 1}. Disabling account.`);
1445
1002
  accountManager.setAccountEnabled(account.index, false);
1446
1003
  accountManager.saveToDiskDebounced();
1447
- await showToast(`All workspaces disabled for account ${account.index + 1}. Switching to another account.`, "warning", { duration: toastDurationMs });
1004
+ await showRuntimeToast(client, `All workspaces disabled for account ${account.index + 1}. Switching to another account.`, "warning", { duration: toastDurationMs });
1448
1005
  // Forget session affinity and continue the outer loop so another
1449
1006
  // enabled account can service the request.
1450
1007
  sessionAffinityStore?.forgetSession(sessionAffinityKey);
1451
1008
  continue accountAttemptLoop;
1452
1009
  }
1453
1010
  }
1454
- if (errorResponse.status === 403 && !unsupportedModelInfo.isUnsupported && !isDisabledWorkspaceError) {
1011
+ if (errorResponse.status === 403 &&
1012
+ !unsupportedModelInfo.isUnsupported &&
1013
+ !isDisabledWorkspaceError) {
1455
1014
  entitlementCache.markBlocked(entitlementAccountKey, model ?? modelFamily, "plan-entitlement");
1456
1015
  capabilityPolicyStore.recordFailure(entitlementAccountKey, capabilityModelKey);
1457
1016
  }
1458
- if (recoveryHook && errorBody && isRecoverableError(errorBody)) {
1017
+ if (recoveryHook &&
1018
+ errorBody &&
1019
+ isRecoverableError(errorBody)) {
1459
1020
  const errorType = detectErrorType(errorBody);
1460
1021
  const toastContent = getRecoveryToastContent(errorType);
1461
- await showToast(`${toastContent.title}: ${toastContent.message}`, "warning", { duration: toastDurationMs });
1022
+ await showRuntimeToast(client, `${toastContent.title}: ${toastContent.message}`, "warning", { duration: toastDurationMs });
1462
1023
  logDebug(`[${PLUGIN_NAME}] Recoverable error detected: ${errorType}`);
1463
1024
  }
1464
1025
  // Handle 5xx server errors by rotating to another account
@@ -1469,7 +1030,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1469
1030
  runtimeMetrics.accountRotations++;
1470
1031
  runtimeMetrics.lastError = `HTTP ${response.status}`;
1471
1032
  const serverRetryAfterMs = parseRetryAfterHintMs(response.headers);
1472
- const policy = evaluateFailurePolicy({ kind: "server", failoverMode, serverRetryAfterMs: serverRetryAfterMs ?? undefined }, { serverCooldownMs: serverErrorCooldownMs });
1033
+ const policy = evaluateFailurePolicy({
1034
+ kind: "server",
1035
+ failoverMode,
1036
+ serverRetryAfterMs: serverRetryAfterMs ?? undefined,
1037
+ }, { serverCooldownMs: serverErrorCooldownMs });
1473
1038
  if (policy.refundToken) {
1474
1039
  accountManager.refundToken(account, modelFamily, model);
1475
1040
  }
@@ -1500,7 +1065,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1500
1065
  const waitLabel = formatWaitTime(delayMs);
1501
1066
  if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) {
1502
1067
  if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1503
- await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
1068
+ await showRuntimeToast(client, `Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
1504
1069
  accountManager.markToastShown(account.index);
1505
1070
  }
1506
1071
  await sleep(addJitter(Math.max(MIN_BACKOFF_MS, delayMs), 0.2));
@@ -1515,7 +1080,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1515
1080
  logWarn(`Rate limited. Rotating account ${account.index + 1} (${account.email ?? "unknown"}).`);
1516
1081
  if (accountManager.getAccountCount() > 1 &&
1517
1082
  accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1518
- await showToast(`Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning", { duration: toastDurationMs });
1083
+ await showRuntimeToast(client, `Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning", { duration: toastDurationMs });
1519
1084
  accountManager.markToastShown(account.index);
1520
1085
  }
1521
1086
  break;
@@ -1559,9 +1124,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1559
1124
  try {
1560
1125
  if (shouldRefreshToken(fallbackAuth, tokenRefreshSkewMs)) {
1561
1126
  fallbackAuth = (await refreshAndUpdateToken(fallbackAuth, client));
1562
- accountManager.updateFromAuth(fallbackAccount, fallbackAuth);
1563
- accountManager.clearAuthFailures(fallbackAccount);
1564
- accountManager.saveToDiskDebounced();
1127
+ await accountManager.commitRefreshedAuth(fallbackAccount, fallbackAuth);
1565
1128
  }
1566
1129
  }
1567
1130
  catch (refreshError) {
@@ -1590,7 +1153,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1590
1153
  }
1591
1154
  const fallbackResolvedEmail = fallbackRuntimeIdentity.email;
1592
1155
  const fallbackEntitlementAccountKey = resolveEntitlementAccountKey({
1593
- accountId: fallbackStoredAccountId ?? fallbackAccountId,
1156
+ accountId: fallbackStoredAccountId ??
1157
+ fallbackAccountId,
1594
1158
  email: fallbackResolvedEmail,
1595
1159
  refreshToken: fallbackAccount.refreshToken,
1596
1160
  index: fallbackAccount.index,
@@ -1598,8 +1162,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1598
1162
  const fallbackEntitlementBlock = entitlementCache.isBlocked(fallbackEntitlementAccountKey, model ?? modelFamily);
1599
1163
  if (fallbackEntitlementBlock.blocked) {
1600
1164
  runtimeMetrics.accountRotations++;
1601
- runtimeMetrics.lastError =
1602
- `Entitlement cached block for account ${fallbackAccount.index + 1}`;
1165
+ runtimeMetrics.lastError = `Entitlement cached block for account ${fallbackAccount.index + 1}`;
1603
1166
  logWarn(`Skipping account ${fallbackAccount.index + 1} due to cached entitlement block (${formatWaitTime(fallbackEntitlementBlock.waitMs)} remaining).`);
1604
1167
  continue;
1605
1168
  }
@@ -1623,7 +1186,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1623
1186
  const fallbackController = new AbortController();
1624
1187
  const fallbackTimeoutId = setTimeout(() => fallbackController.abort(new Error("Request timeout")), fetchTimeoutMs);
1625
1188
  const onFallbackAbort = abortSignal
1626
- ? () => fallbackController.abort(abortSignal.reason ?? new Error("Aborted by user"))
1189
+ ? () => fallbackController.abort(abortSignal.reason ??
1190
+ new Error("Aborted by user"))
1627
1191
  : null;
1628
1192
  if (abortSignal && onFallbackAbort) {
1629
1193
  abortSignal.addEventListener("abort", onFallbackAbort, {
@@ -1660,12 +1224,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1660
1224
  continue;
1661
1225
  }
1662
1226
  successAccountForResponse = fallbackAccount;
1663
- successEntitlementAccountKey = fallbackEntitlementAccountKey;
1227
+ successEntitlementAccountKey =
1228
+ fallbackEntitlementAccountKey;
1664
1229
  runtimeMetrics.streamFailoverRecoveries += 1;
1665
1230
  if (fallbackAccount.index !== account.index) {
1666
1231
  runtimeMetrics.streamFailoverCrossAccountRecoveries += 1;
1667
1232
  runtimeMetrics.accountRotations += 1;
1668
- sessionAffinityStore?.remember(sessionAffinityKey, fallbackAccount.index);
1233
+ if (!responseContinuationEnabled) {
1234
+ sessionAffinityStore?.remember(sessionAffinityKey, fallbackAccount.index);
1235
+ }
1669
1236
  }
1670
1237
  logInfo(`Recovered stream via failover attempt ${failoverAttempt} using account ${fallbackAccount.index + 1}.`, { emittedBytes });
1671
1238
  return fallbackResponse;
@@ -1680,7 +1247,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1680
1247
  ? streamFailoverError.message
1681
1248
  : String(streamFailoverError),
1682
1249
  });
1683
- continue;
1684
1250
  }
1685
1251
  finally {
1686
1252
  clearTimeout(fallbackTimeoutId);
@@ -1697,20 +1263,30 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1697
1263
  requestInstanceId: requestCorrelationId ?? undefined,
1698
1264
  });
1699
1265
  }
1266
+ let storedResponseIdForSuccess = false;
1700
1267
  const successResponse = await handleSuccessResponse(responseForSuccess, isStreaming, {
1268
+ onResponseId: (responseId) => {
1269
+ if (!responseContinuationEnabled)
1270
+ return;
1271
+ sessionAffinityStore?.remember(sessionAffinityKey, successAccountForResponse.index);
1272
+ sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId);
1273
+ storedResponseIdForSuccess = true;
1274
+ },
1701
1275
  streamStallTimeoutMs,
1702
1276
  });
1703
1277
  if (!isStreaming && emptyResponseMaxRetries > 0) {
1704
1278
  const clonedResponse = successResponse.clone();
1705
1279
  try {
1706
1280
  const bodyText = await clonedResponse.text();
1707
- const parsedBody = bodyText ? JSON.parse(bodyText) : null;
1281
+ const parsedBody = bodyText
1282
+ ? JSON.parse(bodyText)
1283
+ : null;
1708
1284
  if (isEmptyResponse(parsedBody)) {
1709
1285
  if (emptyResponseRetries < emptyResponseMaxRetries) {
1710
1286
  emptyResponseRetries++;
1711
1287
  runtimeMetrics.emptyResponseRetries++;
1712
1288
  logWarn(`Empty response received (attempt ${emptyResponseRetries}/${emptyResponseMaxRetries}). Retrying...`);
1713
- await showToast(`Empty response. Retrying (${emptyResponseRetries}/${emptyResponseMaxRetries})...`, "warning", { duration: toastDurationMs });
1289
+ await showRuntimeToast(client, `Empty response. Retrying (${emptyResponseRetries}/${emptyResponseMaxRetries})...`, "warning", { duration: toastDurationMs });
1714
1290
  accountManager.refundToken(account, modelFamily, model);
1715
1291
  accountManager.recordFailure(account, modelFamily, model);
1716
1292
  capabilityPolicyStore.recordFailure(entitlementAccountKey, capabilityModelKey);
@@ -1722,7 +1298,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1722
1298
  sameAccountRetryCount < maxSameAccountRetries) {
1723
1299
  sameAccountRetryCount += 1;
1724
1300
  runtimeMetrics.sameAccountRetries += 1;
1725
- const retryDelayMs = Math.max(0, Math.floor(emptyPolicy.retryDelayMs ?? emptyResponseRetryDelayMs));
1301
+ const retryDelayMs = Math.max(0, Math.floor(emptyPolicy.retryDelayMs ??
1302
+ emptyResponseRetryDelayMs));
1726
1303
  if (retryDelayMs > 0) {
1727
1304
  await sleep(addJitter(retryDelayMs, 0.2));
1728
1305
  }
@@ -1746,12 +1323,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1746
1323
  accountManager.recordSuccess(successAccountForResponse, modelFamily, model);
1747
1324
  capabilityPolicyStore.recordSuccess(successAccountKey, capabilityModelKey);
1748
1325
  entitlementCache.clear(successAccountKey, capabilityModelKey);
1749
- sessionAffinityStore?.remember(sessionAffinityKey, successAccountForResponse.index);
1326
+ if (!responseContinuationEnabled ||
1327
+ (!isStreaming && !storedResponseIdForSuccess)) {
1328
+ sessionAffinityStore?.remember(sessionAffinityKey, successAccountForResponse.index);
1329
+ }
1750
1330
  runtimeMetrics.successfulRequests++;
1751
1331
  runtimeMetrics.lastError = null;
1752
- if (lastCodexCliActiveSyncIndex !== successAccountForResponse.index) {
1332
+ if (lastCodexCliActiveSyncIndex !==
1333
+ successAccountForResponse.index) {
1753
1334
  void accountManager.syncCodexCliActiveSelectionForIndex(successAccountForResponse.index);
1754
- lastCodexCliActiveSyncIndex = successAccountForResponse.index;
1335
+ lastCodexCliActiveSyncIndex =
1336
+ successAccountForResponse.index;
1755
1337
  }
1756
1338
  return successResponse;
1757
1339
  }
@@ -1775,7 +1357,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1775
1357
  waitMs <= retryAllAccountsMaxWaitMs) &&
1776
1358
  allRateLimitedRetries < retryAllAccountsMaxRetries) {
1777
1359
  const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
1778
- await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
1360
+ await sleepWithCountdown({
1361
+ totalMs: addJitter(waitMs, 0.2),
1362
+ message: countdownMessage,
1363
+ sleep,
1364
+ showToast: (message, variant, options) => showRuntimeToast(client, message, variant, options),
1365
+ formatWaitTime,
1366
+ toastDurationMs,
1367
+ abortSignal,
1368
+ });
1779
1369
  allRateLimitedRetries++;
1780
1370
  continue;
1781
1371
  }
@@ -1812,8 +1402,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1812
1402
  type: "oauth",
1813
1403
  authorize: async (inputs) => {
1814
1404
  const authPluginConfig = loadPluginConfig();
1815
- applyUiRuntimeFromConfig(authPluginConfig);
1816
- applyAccountStorageScope(authPluginConfig);
1405
+ applyUiRuntimeFromConfig(authPluginConfig, setUiRuntimeOptions);
1406
+ applyAccountStorageScopeFromConfig(authPluginConfig, {
1407
+ getPerProjectAccounts,
1408
+ getStorageBackupEnabled,
1409
+ setStorageBackupEnabled,
1410
+ isCodexCliSyncEnabled,
1411
+ getWarningShown: () => perProjectStorageWarningShown,
1412
+ setWarningShown: (shown) => {
1413
+ perProjectStorageWarningShown = shown;
1414
+ },
1415
+ logWarn,
1416
+ pluginName: PLUGIN_NAME,
1417
+ setStoragePath,
1418
+ cwd: () => process.cwd(),
1419
+ });
1817
1420
  const accounts = [];
1818
1421
  const noBrowser = inputs?.manual === "true" ||
1819
1422
  inputs?.noBrowser === "true" ||
@@ -1824,569 +1427,45 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1824
1427
  : null;
1825
1428
  let startFresh = explicitLoginMode === "fresh";
1826
1429
  let refreshAccountIndex;
1827
- const clampActiveIndices = (storage) => {
1828
- const count = storage.accounts.length;
1829
- if (count === 0) {
1830
- storage.activeIndex = 0;
1831
- storage.activeIndexByFamily = {};
1832
- return;
1833
- }
1834
- storage.activeIndex = Math.max(0, Math.min(storage.activeIndex, count - 1));
1835
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
1836
- for (const family of MODEL_FAMILIES) {
1837
- const raw = storage.activeIndexByFamily[family];
1838
- const candidate = typeof raw === "number" && Number.isFinite(raw) ? raw : storage.activeIndex;
1839
- storage.activeIndexByFamily[family] = Math.max(0, Math.min(candidate, count - 1));
1840
- }
1841
- };
1842
- const isFlaggableFailure = (failure) => {
1843
- if (failure.reason === "missing_refresh")
1844
- return true;
1845
- if (failure.statusCode === 401)
1846
- return true;
1847
- if (failure.statusCode !== 400)
1848
- return false;
1849
- const message = (failure.message ?? "").toLowerCase();
1850
- return (message.includes("invalid_grant") ||
1851
- message.includes("invalid refresh") ||
1852
- message.includes("token has been revoked"));
1853
- };
1854
- const parseFiniteNumberHeader = (headers, name) => {
1855
- const raw = headers.get(name);
1856
- if (!raw)
1857
- return undefined;
1858
- const parsed = Number(raw);
1859
- return Number.isFinite(parsed) ? parsed : undefined;
1860
- };
1861
- const parseFiniteIntHeader = (headers, name) => {
1862
- const raw = headers.get(name);
1863
- if (!raw)
1864
- return undefined;
1865
- const parsed = Number.parseInt(raw, 10);
1866
- return Number.isFinite(parsed) ? parsed : undefined;
1867
- };
1868
- const parseResetAtMs = (headers, prefix) => {
1869
- const resetAfterSeconds = parseFiniteIntHeader(headers, `${prefix}-reset-after-seconds`);
1870
- if (typeof resetAfterSeconds === "number" &&
1871
- Number.isFinite(resetAfterSeconds) &&
1872
- resetAfterSeconds > 0) {
1873
- return Date.now() + resetAfterSeconds * 1000;
1874
- }
1875
- const resetAtRaw = headers.get(`${prefix}-reset-at`);
1876
- if (!resetAtRaw)
1877
- return undefined;
1878
- const trimmed = resetAtRaw.trim();
1879
- if (/^\d+$/.test(trimmed)) {
1880
- const parsedNumber = Number.parseInt(trimmed, 10);
1881
- if (Number.isFinite(parsedNumber) && parsedNumber > 0) {
1882
- // Upstream sometimes returns seconds since epoch.
1883
- return parsedNumber < 10_000_000_000 ? parsedNumber * 1000 : parsedNumber;
1884
- }
1885
- }
1886
- const parsedDate = Date.parse(trimmed);
1887
- return Number.isFinite(parsedDate) ? parsedDate : undefined;
1888
- };
1889
- const hasCodexQuotaHeaders = (headers) => {
1890
- const keys = [
1891
- "x-codex-primary-used-percent",
1892
- "x-codex-primary-window-minutes",
1893
- "x-codex-primary-reset-at",
1894
- "x-codex-primary-reset-after-seconds",
1895
- "x-codex-secondary-used-percent",
1896
- "x-codex-secondary-window-minutes",
1897
- "x-codex-secondary-reset-at",
1898
- "x-codex-secondary-reset-after-seconds",
1899
- ];
1900
- return keys.some((key) => headers.get(key) !== null);
1901
- };
1902
- const parseCodexQuotaSnapshot = (headers, status) => {
1903
- if (!hasCodexQuotaHeaders(headers))
1904
- return null;
1905
- const primaryPrefix = "x-codex-primary";
1906
- const secondaryPrefix = "x-codex-secondary";
1907
- const primary = {
1908
- usedPercent: parseFiniteNumberHeader(headers, `${primaryPrefix}-used-percent`),
1909
- windowMinutes: parseFiniteIntHeader(headers, `${primaryPrefix}-window-minutes`),
1910
- resetAtMs: parseResetAtMs(headers, primaryPrefix),
1911
- };
1912
- const secondary = {
1913
- usedPercent: parseFiniteNumberHeader(headers, `${secondaryPrefix}-used-percent`),
1914
- windowMinutes: parseFiniteIntHeader(headers, `${secondaryPrefix}-window-minutes`),
1915
- resetAtMs: parseResetAtMs(headers, secondaryPrefix),
1916
- };
1917
- const planTypeRaw = headers.get("x-codex-plan-type");
1918
- const planType = planTypeRaw && planTypeRaw.trim() ? planTypeRaw.trim() : undefined;
1919
- const activeLimit = parseFiniteIntHeader(headers, "x-codex-active-limit");
1920
- return { status, planType, activeLimit, primary, secondary };
1921
- };
1922
- const formatQuotaWindowLabel = (windowMinutes) => {
1923
- if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
1924
- return "quota";
1925
- }
1926
- if (windowMinutes % 1440 === 0)
1927
- return `${windowMinutes / 1440}d`;
1928
- if (windowMinutes % 60 === 0)
1929
- return `${windowMinutes / 60}h`;
1930
- return `${windowMinutes}m`;
1931
- };
1932
- const formatResetAt = (resetAtMs) => {
1933
- if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
1934
- return undefined;
1935
- const date = new Date(resetAtMs);
1936
- if (!Number.isFinite(date.getTime()))
1937
- return undefined;
1938
- const now = new Date();
1939
- const sameDay = now.getFullYear() === date.getFullYear() &&
1940
- now.getMonth() === date.getMonth() &&
1941
- now.getDate() === date.getDate();
1942
- const time = date.toLocaleTimeString(undefined, {
1943
- hour: "2-digit",
1944
- minute: "2-digit",
1945
- hour12: false,
1946
- });
1947
- if (sameDay)
1948
- return time;
1949
- const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
1950
- return `${time} on ${day}`;
1951
- };
1952
- const formatCodexQuotaLine = (snapshot) => {
1953
- const summarizeWindow = (label, window) => {
1954
- const used = window.usedPercent;
1955
- const left = typeof used === "number" && Number.isFinite(used)
1956
- ? Math.max(0, Math.min(100, Math.round(100 - used)))
1957
- : undefined;
1958
- const reset = formatResetAt(window.resetAtMs);
1959
- let summary = label;
1960
- if (left !== undefined)
1961
- summary = `${summary} ${left}% left`;
1962
- if (reset)
1963
- summary = `${summary} (resets ${reset})`;
1964
- return summary;
1965
- };
1966
- const primaryLabel = formatQuotaWindowLabel(snapshot.primary.windowMinutes);
1967
- const secondaryLabel = formatQuotaWindowLabel(snapshot.secondary.windowMinutes);
1968
- const parts = [
1969
- summarizeWindow(primaryLabel, snapshot.primary),
1970
- summarizeWindow(secondaryLabel, snapshot.secondary),
1971
- ];
1972
- if (snapshot.planType)
1973
- parts.push(`plan:${snapshot.planType}`);
1974
- if (typeof snapshot.activeLimit === "number" && Number.isFinite(snapshot.activeLimit)) {
1975
- parts.push(`active:${snapshot.activeLimit}`);
1976
- }
1977
- if (snapshot.status === 429)
1978
- parts.push("rate-limited");
1979
- return parts.join(", ");
1980
- };
1981
- const fetchCodexQuotaSnapshot = async (params) => {
1982
- const QUOTA_PROBE_MODELS = ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
1983
- let lastError = null;
1984
- for (const model of QUOTA_PROBE_MODELS) {
1985
- try {
1986
- const instructions = await getCodexInstructions(model);
1987
- const probeBody = {
1988
- model,
1989
- stream: true,
1990
- store: false,
1991
- include: ["reasoning.encrypted_content"],
1992
- instructions,
1993
- input: [
1994
- {
1995
- type: "message",
1996
- role: "user",
1997
- content: [{ type: "input_text", text: "quota ping" }],
1998
- },
1999
- ],
2000
- reasoning: { effort: "none", summary: "auto" },
2001
- text: { verbosity: "low" },
2002
- };
2003
- const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
2004
- model,
2005
- });
2006
- headers.set("content-type", "application/json; charset=utf-8");
2007
- const controller = new AbortController();
2008
- const timeout = setTimeout(() => controller.abort(), 15_000);
2009
- let response;
2010
- try {
2011
- response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
2012
- method: "POST",
2013
- headers,
2014
- body: JSON.stringify(probeBody),
2015
- signal: controller.signal,
2016
- });
2017
- }
2018
- finally {
2019
- clearTimeout(timeout);
2020
- }
2021
- const snapshot = parseCodexQuotaSnapshot(response.headers, response.status);
2022
- if (snapshot) {
2023
- // We only need headers; cancel the SSE stream immediately.
2024
- try {
2025
- await response.body?.cancel();
2026
- }
2027
- catch {
2028
- // Ignore cancellation failures.
2029
- }
2030
- return snapshot;
2031
- }
2032
- if (!response.ok) {
2033
- const bodyText = await response.text().catch(() => "");
2034
- let errorBody = undefined;
2035
- try {
2036
- errorBody = bodyText ? JSON.parse(bodyText) : undefined;
2037
- }
2038
- catch {
2039
- errorBody = { error: { message: bodyText } };
2040
- }
2041
- const unsupportedInfo = getUnsupportedCodexModelInfo(errorBody);
2042
- if (unsupportedInfo.isUnsupported) {
2043
- lastError = new Error(unsupportedInfo.message ?? `Model '${model}' unsupported for this account`);
2044
- continue;
2045
- }
2046
- const message = (typeof errorBody?.error?.message === "string"
2047
- ? errorBody.error?.message
2048
- : bodyText) || `HTTP ${response.status}`;
2049
- throw new Error(message);
2050
- }
2051
- lastError = new Error("Codex response did not include quota headers");
2052
- }
2053
- catch (error) {
2054
- lastError = error instanceof Error ? error : new Error(String(error));
2055
- }
2056
- }
2057
- throw lastError ?? new Error("Failed to fetch quotas");
2058
- };
2059
- const runAccountCheck = async (deepProbe) => {
2060
- const loadedStorage = await hydrateEmails(await loadAccounts());
2061
- const workingStorage = loadedStorage
2062
- ? {
2063
- ...loadedStorage,
2064
- accounts: loadedStorage.accounts.map((account) => ({ ...account })),
2065
- activeIndexByFamily: loadedStorage.activeIndexByFamily
2066
- ? { ...loadedStorage.activeIndexByFamily }
2067
- : {},
2068
- }
2069
- : { version: 3, accounts: [], activeIndex: 0, activeIndexByFamily: {} };
2070
- if (workingStorage.accounts.length === 0) {
2071
- console.log("\nNo accounts to check.\n");
2072
- return;
2073
- }
2074
- const flaggedStorage = await loadFlaggedAccounts();
2075
- let storageChanged = false;
2076
- let flaggedChanged = false;
2077
- const removeFromActive = new Set();
2078
- const total = workingStorage.accounts.length;
2079
- let ok = 0;
2080
- let disabled = 0;
2081
- let errors = 0;
2082
- console.log(`\nChecking ${deepProbe ? "full account health" : "quotas"} for all accounts...\n`);
2083
- for (let i = 0; i < total; i += 1) {
2084
- const account = workingStorage.accounts[i];
2085
- if (!account)
2086
- continue;
2087
- const label = account.email ?? account.accountLabel ?? `Account ${i + 1}`;
2088
- if (account.enabled === false) {
2089
- disabled += 1;
2090
- console.log(`[${i + 1}/${total}] ${label}: DISABLED`);
2091
- continue;
2092
- }
2093
- try {
2094
- // If we already have a valid cached access token, don't force-refresh.
2095
- // This avoids flagging accounts where the refresh token has been burned
2096
- // but the access token is still valid (same behavior as Codex CLI).
2097
- const nowMs = Date.now();
2098
- let accessToken = null;
2099
- let tokenAccountId = undefined;
2100
- let authDetail = "OK";
2101
- if (account.accessToken &&
2102
- (typeof account.expiresAt !== "number" ||
2103
- !Number.isFinite(account.expiresAt) ||
2104
- account.expiresAt > nowMs)) {
2105
- accessToken = account.accessToken;
2106
- authDetail = "OK (cached access)";
2107
- tokenAccountId = extractAccountId(account.accessToken);
2108
- if (tokenAccountId &&
2109
- shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
2110
- tokenAccountId !== account.accountId) {
2111
- account.accountId = tokenAccountId;
2112
- account.accountIdSource = "token";
2113
- storageChanged = true;
2114
- }
2115
- }
2116
- // If Codex CLI has a valid cached access token for this email, use it
2117
- // instead of forcing a refresh.
2118
- if (!accessToken) {
2119
- const cached = await lookupCodexCliTokensByEmail(account.email);
2120
- if (cached &&
2121
- (typeof cached.expiresAt !== "number" ||
2122
- !Number.isFinite(cached.expiresAt) ||
2123
- cached.expiresAt > nowMs)) {
2124
- accessToken = cached.accessToken;
2125
- authDetail = "OK (Codex CLI cache)";
2126
- if (cached.refreshToken && cached.refreshToken !== account.refreshToken) {
2127
- account.refreshToken = cached.refreshToken;
2128
- storageChanged = true;
2129
- }
2130
- if (cached.accessToken && cached.accessToken !== account.accessToken) {
2131
- account.accessToken = cached.accessToken;
2132
- storageChanged = true;
2133
- }
2134
- if (cached.expiresAt !== account.expiresAt) {
2135
- account.expiresAt = cached.expiresAt;
2136
- storageChanged = true;
2137
- }
2138
- const hydratedEmail = sanitizeEmail(extractAccountEmail(cached.accessToken));
2139
- if (hydratedEmail && hydratedEmail !== account.email) {
2140
- account.email = hydratedEmail;
2141
- storageChanged = true;
2142
- }
2143
- tokenAccountId = extractAccountId(cached.accessToken);
2144
- if (tokenAccountId &&
2145
- shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
2146
- tokenAccountId !== account.accountId) {
2147
- account.accountId = tokenAccountId;
2148
- account.accountIdSource = "token";
2149
- storageChanged = true;
2150
- }
2151
- }
2152
- }
2153
- if (!accessToken) {
2154
- const refreshResult = await queuedRefresh(account.refreshToken);
2155
- if (refreshResult.type !== "success") {
2156
- errors += 1;
2157
- const message = refreshResult.message ?? refreshResult.reason ?? "refresh failed";
2158
- console.log(`[${i + 1}/${total}] ${label}: ERROR (${message})`);
2159
- if (deepProbe && isFlaggableFailure(refreshResult)) {
2160
- const existingIndex = flaggedStorage.accounts.findIndex((flagged) => flagged.refreshToken === account.refreshToken);
2161
- const flaggedRecord = {
2162
- ...account,
2163
- flaggedAt: Date.now(),
2164
- flaggedReason: "token-invalid",
2165
- lastError: message,
2166
- };
2167
- if (existingIndex >= 0) {
2168
- flaggedStorage.accounts[existingIndex] = flaggedRecord;
2169
- }
2170
- else {
2171
- flaggedStorage.accounts.push(flaggedRecord);
2172
- }
2173
- removeFromActive.add(account.refreshToken);
2174
- flaggedChanged = true;
2175
- }
2176
- continue;
2177
- }
2178
- accessToken = refreshResult.access;
2179
- authDetail = "OK";
2180
- if (refreshResult.refresh !== account.refreshToken) {
2181
- account.refreshToken = refreshResult.refresh;
2182
- storageChanged = true;
2183
- }
2184
- if (refreshResult.access && refreshResult.access !== account.accessToken) {
2185
- account.accessToken = refreshResult.access;
2186
- storageChanged = true;
2187
- }
2188
- if (typeof refreshResult.expires === "number" &&
2189
- refreshResult.expires !== account.expiresAt) {
2190
- account.expiresAt = refreshResult.expires;
2191
- storageChanged = true;
2192
- }
2193
- const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
2194
- if (hydratedEmail && hydratedEmail !== account.email) {
2195
- account.email = hydratedEmail;
2196
- storageChanged = true;
2197
- }
2198
- tokenAccountId = extractAccountId(refreshResult.access);
2199
- if (tokenAccountId &&
2200
- shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
2201
- tokenAccountId !== account.accountId) {
2202
- account.accountId = tokenAccountId;
2203
- account.accountIdSource = "token";
2204
- storageChanged = true;
2205
- }
2206
- }
2207
- if (!accessToken) {
2208
- throw new Error("Missing access token after refresh");
2209
- }
2210
- if (deepProbe) {
2211
- ok += 1;
2212
- const detail = tokenAccountId
2213
- ? `${authDetail} (id:${tokenAccountId.slice(-6)})`
2214
- : authDetail;
2215
- console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
2216
- continue;
2217
- }
2218
- try {
2219
- const requestAccountId = resolveRequestAccountId(account.accountId, account.accountIdSource, tokenAccountId) ??
2220
- tokenAccountId ??
2221
- account.accountId;
2222
- if (!requestAccountId) {
2223
- throw new Error("Missing accountId for quota probe");
2224
- }
2225
- const snapshot = await fetchCodexQuotaSnapshot({
2226
- accountId: requestAccountId,
2227
- accessToken,
2228
- });
2229
- ok += 1;
2230
- console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`);
2231
- }
2232
- catch (error) {
2233
- errors += 1;
2234
- const message = error instanceof Error ? error.message : String(error);
2235
- console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
2236
- }
2237
- }
2238
- catch (error) {
2239
- errors += 1;
2240
- const message = error instanceof Error ? error.message : String(error);
2241
- console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 120)})`);
2242
- }
2243
- }
2244
- if (removeFromActive.size > 0) {
2245
- workingStorage.accounts = workingStorage.accounts.filter((account) => !removeFromActive.has(account.refreshToken));
2246
- clampActiveIndices(workingStorage);
2247
- storageChanged = true;
2248
- }
2249
- if (storageChanged) {
2250
- await saveAccounts(workingStorage);
2251
- invalidateAccountManagerCache();
2252
- }
2253
- if (flaggedChanged) {
2254
- await saveFlaggedAccounts(flaggedStorage);
2255
- }
2256
- console.log("");
2257
- console.log(`Results: ${ok} ok, ${errors} error, ${disabled} disabled`);
2258
- if (removeFromActive.size > 0) {
2259
- console.log(`Moved ${removeFromActive.size} account(s) to flagged pool (invalid refresh token).`);
2260
- }
2261
- console.log("");
2262
- };
2263
- const verifyFlaggedAccounts = async () => {
2264
- const flaggedStorage = await loadFlaggedAccounts();
2265
- if (flaggedStorage.accounts.length === 0) {
2266
- console.log("\nNo flagged accounts to verify.\n");
2267
- return;
2268
- }
2269
- console.log("\nVerifying flagged accounts...\n");
2270
- const remaining = [];
2271
- const restored = [];
2272
- for (let i = 0; i < flaggedStorage.accounts.length; i += 1) {
2273
- const flagged = flaggedStorage.accounts[i];
2274
- if (!flagged)
2275
- continue;
2276
- const label = flagged.email ?? flagged.accountLabel ?? `Flagged ${i + 1}`;
2277
- try {
2278
- const cached = await lookupCodexCliTokensByEmail(flagged.email);
2279
- const now = Date.now();
2280
- if (cached &&
2281
- typeof cached.expiresAt === "number" &&
2282
- Number.isFinite(cached.expiresAt) &&
2283
- cached.expiresAt > now) {
2284
- const refreshToken = typeof cached.refreshToken === "string" && cached.refreshToken.trim()
2285
- ? cached.refreshToken.trim()
2286
- : flagged.refreshToken;
2287
- const resolved = resolveAccountSelection({
2288
- type: "success",
2289
- access: cached.accessToken,
2290
- refresh: refreshToken,
2291
- expires: cached.expiresAt,
2292
- multiAccount: true,
2293
- });
2294
- if (!resolved.accountIdOverride && flagged.accountId) {
2295
- resolved.accountIdOverride = flagged.accountId;
2296
- resolved.accountIdSource = flagged.accountIdSource ?? "manual";
2297
- }
2298
- if (!resolved.accountLabel && flagged.accountLabel) {
2299
- resolved.accountLabel = flagged.accountLabel;
2300
- }
2301
- restored.push(resolved);
2302
- console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: RESTORED (Codex CLI cache)`);
2303
- continue;
2304
- }
2305
- const refreshResult = await queuedRefresh(flagged.refreshToken);
2306
- if (refreshResult.type !== "success") {
2307
- console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (${refreshResult.message ?? refreshResult.reason ?? "refresh failed"})`);
2308
- remaining.push(flagged);
2309
- continue;
2310
- }
2311
- const resolved = resolveAccountSelection(refreshResult);
2312
- if (!resolved.accountIdOverride && flagged.accountId) {
2313
- resolved.accountIdOverride = flagged.accountId;
2314
- resolved.accountIdSource = flagged.accountIdSource ?? "manual";
2315
- }
2316
- if (!resolved.accountLabel && flagged.accountLabel) {
2317
- resolved.accountLabel = flagged.accountLabel;
2318
- }
2319
- restored.push(resolved);
2320
- console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: RESTORED`);
2321
- }
2322
- catch (error) {
2323
- const message = error instanceof Error ? error.message : String(error);
2324
- console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: ERROR (${message.slice(0, 120)})`);
2325
- remaining.push({
2326
- ...flagged,
2327
- lastError: message,
2328
- });
2329
- }
2330
- }
2331
- if (restored.length > 0) {
2332
- await persistAccountPool(restored, false);
2333
- invalidateAccountManagerCache();
2334
- }
2335
- await saveFlaggedAccounts({
2336
- version: 1,
2337
- accounts: remaining,
2338
- });
2339
- console.log("");
2340
- console.log(`Results: ${restored.length} restored, ${remaining.length} still flagged`);
2341
- console.log("");
2342
- };
2343
1430
  if (!explicitLoginMode) {
2344
1431
  while (true) {
2345
- const loadedStorage = await hydrateEmails(await loadAccounts());
1432
+ const loadedStorage = await hydrateRuntimeEmails(await loadAccounts(), {
1433
+ queuedRefresh,
1434
+ extractAccountId,
1435
+ sanitizeEmail,
1436
+ extractAccountEmail,
1437
+ shouldUpdateAccountIdFromToken,
1438
+ saveAccounts,
1439
+ logWarn,
1440
+ pluginName: PLUGIN_NAME,
1441
+ });
2346
1442
  const workingStorage = loadedStorage
2347
1443
  ? {
2348
1444
  ...loadedStorage,
2349
- accounts: loadedStorage.accounts.map((account) => ({ ...account })),
1445
+ accounts: loadedStorage.accounts.map((account) => ({
1446
+ ...account,
1447
+ })),
2350
1448
  activeIndexByFamily: loadedStorage.activeIndexByFamily
2351
1449
  ? { ...loadedStorage.activeIndexByFamily }
2352
1450
  : {},
2353
1451
  }
2354
- : { version: 3, accounts: [], activeIndex: 0, activeIndexByFamily: {} };
1452
+ : {
1453
+ version: 3,
1454
+ accounts: [],
1455
+ activeIndex: 0,
1456
+ activeIndexByFamily: {},
1457
+ };
2355
1458
  const flaggedStorage = await loadFlaggedAccounts();
2356
- if (workingStorage.accounts.length === 0 && flaggedStorage.accounts.length === 0) {
1459
+ if (workingStorage.accounts.length === 0 &&
1460
+ flaggedStorage.accounts.length === 0) {
2357
1461
  break;
2358
1462
  }
2359
1463
  const now = Date.now();
2360
1464
  const activeIndex = resolveActiveIndex(workingStorage, "codex");
2361
- const existingAccounts = workingStorage.accounts.map((account, index) => {
2362
- let status;
2363
- if (account.enabled === false) {
2364
- status = "disabled";
2365
- }
2366
- else if (typeof account.coolingDownUntil === "number" &&
2367
- account.coolingDownUntil > now) {
2368
- status = "cooldown";
2369
- }
2370
- else if (formatRateLimitEntry(account, now)) {
2371
- status = "rate-limited";
2372
- }
2373
- else if (index === activeIndex) {
2374
- status = "active";
2375
- }
2376
- else {
2377
- status = "ok";
2378
- }
2379
- return {
2380
- accountId: account.accountId,
2381
- accountLabel: account.accountLabel,
2382
- email: account.email,
2383
- index,
2384
- addedAt: account.addedAt,
2385
- lastUsed: account.lastUsed,
2386
- status,
2387
- isCurrentAccount: index === activeIndex,
2388
- enabled: account.enabled !== false,
2389
- };
1465
+ const existingAccounts = buildLoginMenuAccounts(workingStorage.accounts, {
1466
+ now,
1467
+ activeIndex,
1468
+ formatRateLimitEntry: (account, currentNow) => formatRateLimitEntry(account, currentNow, formatWaitTime),
2390
1469
  });
2391
1470
  const menuResult = await promptLoginMode(existingAccounts, {
2392
1471
  flaggedCount: flaggedStorage.accounts.length,
@@ -2401,16 +1480,62 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2401
1480
  }),
2402
1481
  };
2403
1482
  }
1483
+ const accountCheckDeps = {
1484
+ hydrateEmails: (storage) => hydrateRuntimeEmails(storage, {
1485
+ queuedRefresh,
1486
+ extractAccountId,
1487
+ sanitizeEmail,
1488
+ extractAccountEmail,
1489
+ shouldUpdateAccountIdFromToken,
1490
+ saveAccounts,
1491
+ logWarn,
1492
+ pluginName: PLUGIN_NAME,
1493
+ }),
1494
+ loadAccounts,
1495
+ createEmptyStorage: () => ({
1496
+ version: 3,
1497
+ accounts: [],
1498
+ activeIndex: 0,
1499
+ activeIndexByFamily: {},
1500
+ }),
1501
+ loadFlaggedAccounts,
1502
+ createAccountCheckWorkingState,
1503
+ lookupCodexCliTokensByEmail,
1504
+ extractAccountId,
1505
+ shouldUpdateAccountIdFromToken,
1506
+ sanitizeEmail,
1507
+ extractAccountEmail,
1508
+ queuedRefresh,
1509
+ isRuntimeFlaggableFailure: isFlaggableFailure,
1510
+ fetchCodexQuotaSnapshot,
1511
+ resolveRequestAccountId,
1512
+ formatCodexQuotaLine: formatQuotaSnapshotLine,
1513
+ clampRuntimeActiveIndices: clampActiveIndices,
1514
+ MODEL_FAMILIES,
1515
+ saveAccounts,
1516
+ invalidateAccountManagerCache: () => invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps),
1517
+ saveFlaggedAccounts,
1518
+ showLine: (message) => console.log(message),
1519
+ };
2404
1520
  if (menuResult.mode === "check") {
2405
- await runAccountCheck(false);
1521
+ await runRuntimeAccountCheck(false, accountCheckDeps);
2406
1522
  continue;
2407
1523
  }
2408
1524
  if (menuResult.mode === "deep-check") {
2409
- await runAccountCheck(true);
1525
+ await runRuntimeAccountCheck(true, accountCheckDeps);
2410
1526
  continue;
2411
1527
  }
2412
1528
  if (menuResult.mode === "verify-flagged") {
2413
- await verifyFlaggedAccounts();
1529
+ await verifyRuntimeFlaggedAccounts({
1530
+ loadFlaggedAccounts,
1531
+ lookupCodexCliTokensByEmail,
1532
+ queuedRefresh,
1533
+ resolveTokenSuccessAccount,
1534
+ persistAccounts: persistAccountPool,
1535
+ invalidateAccountManagerCache: () => invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps),
1536
+ saveFlaggedAccounts,
1537
+ showLine: (message) => console.log(message),
1538
+ });
2414
1539
  continue;
2415
1540
  }
2416
1541
  if (menuResult.mode === "manage") {
@@ -2418,13 +1543,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2418
1543
  const target = workingStorage.accounts[menuResult.deleteAccountIndex];
2419
1544
  if (target) {
2420
1545
  workingStorage.accounts.splice(menuResult.deleteAccountIndex, 1);
2421
- clampActiveIndices(workingStorage);
1546
+ clampActiveIndices(workingStorage, MODEL_FAMILIES);
2422
1547
  await saveAccounts(workingStorage);
2423
1548
  await saveFlaggedAccounts({
2424
1549
  version: 1,
2425
1550
  accounts: flaggedStorage.accounts.filter((flagged) => flagged.refreshToken !== target.refreshToken),
2426
1551
  });
2427
- invalidateAccountManagerCache();
1552
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
2428
1553
  console.log(`\nDeleted ${target.email ?? `Account ${menuResult.deleteAccountIndex + 1}`}.\n`);
2429
1554
  }
2430
1555
  continue;
@@ -2434,7 +1559,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2434
1559
  if (target) {
2435
1560
  target.enabled = target.enabled === false ? true : false;
2436
1561
  await saveAccounts(workingStorage);
2437
- invalidateAccountManagerCache();
1562
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
2438
1563
  console.log(`\n${target.email ?? `Account ${menuResult.toggleAccountIndex + 1}`} ${target.enabled === false ? "disabled" : "enabled"}.\n`);
2439
1564
  }
2440
1565
  continue;
@@ -2451,7 +1576,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2451
1576
  if (menuResult.deleteAll) {
2452
1577
  await clearAccounts();
2453
1578
  await clearFlaggedAccounts();
2454
- invalidateAccountManagerCache();
1579
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
2455
1580
  console.log("\nCleared saved accounts from active storage. Recovery snapshots remain available. Starting fresh.\n");
2456
1581
  }
2457
1582
  break;
@@ -2463,7 +1588,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2463
1588
  const latestStorage = await loadAccounts();
2464
1589
  const existingCount = latestStorage?.accounts.length ?? 0;
2465
1590
  const requestedCount = Number.parseInt(inputs?.accountCount ?? "1", 10);
2466
- const normalizedRequested = Number.isFinite(requestedCount) ? requestedCount : 1;
1591
+ const normalizedRequested = Number.isFinite(requestedCount)
1592
+ ? requestedCount
1593
+ : 1;
2467
1594
  const availableSlots = refreshAccountIndex !== undefined
2468
1595
  ? 1
2469
1596
  : startFresh
@@ -2488,39 +1615,72 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2488
1615
  }
2489
1616
  if (useManualMode) {
2490
1617
  const { pkce, state, url } = await createAuthorizationFlow();
2491
- return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
2492
- try {
2493
- await persistAccountPool([tokens], startFresh);
2494
- invalidateAccountManagerCache();
2495
- }
2496
- catch (err) {
2497
- const storagePath = getStoragePath();
2498
- const errorCode = err?.code || "UNKNOWN";
2499
- const hint = err instanceof StorageError
2500
- ? err.hint
2501
- : formatStorageErrorHint(err, storagePath);
2502
- logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
2503
- await showToast(hint, "error", {
2504
- title: "Account Persistence Failed",
2505
- duration: 10000,
2506
- });
2507
- }
1618
+ return buildManualOAuthFlow({
1619
+ pkce,
1620
+ url,
1621
+ expectedState: state,
1622
+ redirectUri: REDIRECT_URI,
1623
+ parseAuthorizationInput,
1624
+ exchangeAuthorizationCode,
1625
+ resolveTokenSuccess: resolveTokenSuccessAccount,
1626
+ instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
1627
+ onSuccess: async (tokens) => {
1628
+ try {
1629
+ await persistAccountPool([tokens], startFresh);
1630
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
1631
+ }
1632
+ catch (err) {
1633
+ const storagePath = getStoragePath();
1634
+ const errorCode = err?.code || "UNKNOWN";
1635
+ const hint = err instanceof StorageError
1636
+ ? err.hint
1637
+ : formatStorageErrorHint(err, storagePath);
1638
+ logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
1639
+ await showRuntimeToast(client, hint, "error", {
1640
+ title: "Account Persistence Failed",
1641
+ duration: 10000,
1642
+ });
1643
+ }
1644
+ },
2508
1645
  });
2509
1646
  }
2510
- const explicitCountProvided = typeof inputs?.accountCount === "string" && inputs.accountCount.trim().length > 0;
1647
+ const explicitCountProvided = typeof inputs?.accountCount === "string" &&
1648
+ inputs.accountCount.trim().length > 0;
2511
1649
  while (accounts.length < targetCount) {
2512
1650
  logInfo(`=== OpenAI OAuth (Account ${accounts.length + 1}) ===`);
2513
1651
  const forceNewLogin = accounts.length > 0 || refreshAccountIndex !== undefined;
2514
- const result = await runOAuthFlow(forceNewLogin);
1652
+ const result = await runRuntimeOAuthFlow(forceNewLogin, {
1653
+ runBrowserOAuthFlow: (input) => runBrowserOAuthFlow({
1654
+ ...input,
1655
+ createAuthorizationFlow,
1656
+ redactOAuthUrlForLog,
1657
+ startLocalOAuthServer,
1658
+ openBrowserUrl,
1659
+ pluginName: PLUGIN_NAME,
1660
+ authManualLabel: AUTH_LABELS.OAUTH_MANUAL,
1661
+ exchangeAuthorizationCode,
1662
+ redirectUri: REDIRECT_URI,
1663
+ }),
1664
+ manualModeLabel: AUTH_LABELS.OAUTH_MANUAL,
1665
+ logInfo,
1666
+ logDebug,
1667
+ logWarn,
1668
+ pluginName: PLUGIN_NAME,
1669
+ });
2515
1670
  let resolved = null;
2516
1671
  if (result.type === "success") {
2517
- resolved = resolveAccountSelection(result);
1672
+ resolved = resolveTokenSuccessAccount(result);
2518
1673
  const email = extractAccountEmail(resolved.access, resolved.idToken);
2519
- const accountId = resolved.accountIdOverride ?? extractAccountId(resolved.access);
2520
- const label = resolved.accountLabel ?? email ?? accountId ?? "Unknown account";
1674
+ const accountId = resolved.accountIdOverride ??
1675
+ extractAccountId(resolved.access);
1676
+ const label = resolved.accountLabel ??
1677
+ email ??
1678
+ accountId ??
1679
+ "Unknown account";
2521
1680
  logInfo(`Authenticated as: ${label}`);
2522
1681
  const isDuplicate = findMatchingAccountIndex(accounts.map((account) => ({
2523
- accountId: account.accountIdOverride ?? extractAccountId(account.access),
1682
+ accountId: account.accountIdOverride ??
1683
+ extractAccountId(account.access),
2524
1684
  email: sanitizeEmail(extractAccountEmail(account.access, account.idToken)),
2525
1685
  refreshToken: account.refresh,
2526
1686
  })), {
@@ -2550,11 +1710,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2550
1710
  continue;
2551
1711
  }
2552
1712
  accounts.push(resolved);
2553
- await showToast(`Account ${accounts.length} authenticated`, "success");
1713
+ await showRuntimeToast(client, `Account ${accounts.length} authenticated`, "success");
2554
1714
  try {
2555
1715
  const isFirstAccount = accounts.length === 1;
2556
1716
  await persistAccountPool([resolved], isFirstAccount && startFresh);
2557
- invalidateAccountManagerCache();
1717
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
2558
1718
  }
2559
1719
  catch (err) {
2560
1720
  const storagePath = getStoragePath();
@@ -2563,7 +1723,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2563
1723
  ? err.hint
2564
1724
  : formatStorageErrorHint(err, storagePath);
2565
1725
  logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
2566
- await showToast(hint, "error", {
1726
+ await showRuntimeToast(client, hint, "error", {
2567
1727
  title: "Account Persistence Failed",
2568
1728
  duration: 10000,
2569
1729
  });
@@ -2619,20 +1779,48 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2619
1779
  // Initialize storage path for manual OAuth flow
2620
1780
  // Must happen BEFORE persistAccountPool to ensure correct storage location
2621
1781
  const manualPluginConfig = loadPluginConfig();
2622
- applyUiRuntimeFromConfig(manualPluginConfig);
2623
- applyAccountStorageScope(manualPluginConfig);
1782
+ applyUiRuntimeFromConfig(manualPluginConfig, setUiRuntimeOptions);
1783
+ applyAccountStorageScopeFromConfig(manualPluginConfig, {
1784
+ getPerProjectAccounts,
1785
+ getStorageBackupEnabled,
1786
+ setStorageBackupEnabled,
1787
+ isCodexCliSyncEnabled,
1788
+ getWarningShown: () => perProjectStorageWarningShown,
1789
+ setWarningShown: (shown) => {
1790
+ perProjectStorageWarningShown = shown;
1791
+ },
1792
+ logWarn,
1793
+ pluginName: PLUGIN_NAME,
1794
+ setStoragePath,
1795
+ cwd: () => process.cwd(),
1796
+ });
2624
1797
  const { pkce, state, url } = await createAuthorizationFlow();
2625
- return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
2626
- try {
2627
- await persistAccountPool([tokens], false);
2628
- }
2629
- catch (err) {
2630
- const storagePath = getStoragePath();
2631
- const errorCode = err?.code || "UNKNOWN";
2632
- const hint = err instanceof StorageError ? err.hint : formatStorageErrorHint(err, storagePath);
2633
- logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
2634
- await showToast(hint, "error", { title: "Account Persistence Failed", duration: 10000 });
2635
- }
1798
+ return buildManualOAuthFlow({
1799
+ pkce,
1800
+ url,
1801
+ expectedState: state,
1802
+ redirectUri: REDIRECT_URI,
1803
+ parseAuthorizationInput,
1804
+ exchangeAuthorizationCode,
1805
+ resolveTokenSuccess: resolveTokenSuccessAccount,
1806
+ instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
1807
+ onSuccess: async (tokens) => {
1808
+ try {
1809
+ await persistAccountPool([tokens], false);
1810
+ }
1811
+ catch (err) {
1812
+ const storagePath = getStoragePath();
1813
+ const errorCode = err?.code || "UNKNOWN";
1814
+ const hint = err instanceof StorageError
1815
+ ? err.hint
1816
+ : formatStorageErrorHint(err, storagePath);
1817
+ logError(`[${PLUGIN_NAME}] Failed to persist account: [${errorCode}] ${err?.message ?? String(err)}`);
1818
+ await showRuntimeToast(client, hint, "error", {
1819
+ title: "Account Persistence Failed",
1820
+ duration: 10000,
1821
+ });
1822
+ }
1823
+ },
2636
1824
  });
2637
1825
  },
2638
1826
  },
@@ -2648,7 +1836,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2648
1836
  description: "List all Codex OAuth accounts and the current active index.",
2649
1837
  args: {},
2650
1838
  async execute() {
2651
- const ui = resolveUiRuntime();
1839
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2652
1840
  const storage = await loadAccounts();
2653
1841
  const storePath = getStoragePath();
2654
1842
  if (!storage || storage.accounts.length === 0) {
@@ -2687,7 +1875,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2687
1875
  badges.push(formatUiBadge(ui, "current", "accent"));
2688
1876
  if (account.enabled === false)
2689
1877
  badges.push(formatUiBadge(ui, "disabled", "danger"));
2690
- const rateLimit = formatRateLimitEntry(account, now);
1878
+ const rateLimit = formatRateLimitEntry(account, now, formatWaitTime);
2691
1879
  if (rateLimit)
2692
1880
  badges.push(formatUiBadge(ui, "rate-limited", "warning"));
2693
1881
  if (typeof account.coolingDownUntil === "number" &&
@@ -2725,13 +1913,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2725
1913
  storage.accounts.forEach((account, index) => {
2726
1914
  const label = formatAccountLabel(account, index);
2727
1915
  const statuses = [];
2728
- const rateLimit = formatRateLimitEntry(account, now);
1916
+ const rateLimit = formatRateLimitEntry(account, now, formatWaitTime);
2729
1917
  if (index === activeIndex)
2730
1918
  statuses.push("active");
2731
1919
  if (rateLimit)
2732
1920
  statuses.push("rate-limited");
2733
- if (typeof account.coolingDownUntil ===
2734
- "number" &&
1921
+ if (typeof account.coolingDownUntil === "number" &&
2735
1922
  account.coolingDownUntil > now) {
2736
1923
  statuses.push("cooldown");
2737
1924
  }
@@ -2752,10 +1939,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2752
1939
  "codex-switch": tool({
2753
1940
  description: "Switch active Codex account by index (1-based).",
2754
1941
  args: {
2755
- index: tool.schema.number().describe("Account number to switch to (1-based, e.g., 1 for first account)"),
1942
+ index: tool.schema
1943
+ .number()
1944
+ .describe("Account number to switch to (1-based, e.g., 1 for first account)"),
2756
1945
  },
2757
1946
  async execute({ index }) {
2758
- const ui = resolveUiRuntime();
1947
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2759
1948
  const storage = await loadAccounts();
2760
1949
  if (!storage || storage.accounts.length === 0) {
2761
1950
  if (ui.v2Enabled) {
@@ -2797,7 +1986,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2797
1986
  await saveAccounts(storage);
2798
1987
  }
2799
1988
  catch (saveError) {
2800
- logWarn("Failed to save account switch", { error: String(saveError) });
1989
+ logWarn("Failed to save account switch", {
1990
+ error: String(saveError),
1991
+ });
2801
1992
  if (ui.v2Enabled) {
2802
1993
  return [
2803
1994
  ...formatUiHeader(ui, "Switch account"),
@@ -2809,7 +2000,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2809
2000
  return `Switched to ${formatAccountLabel(account, targetIndex)} but failed to persist. Changes may be lost on restart.`;
2810
2001
  }
2811
2002
  if (cachedAccountManager) {
2812
- await reloadAccountManagerFromDisk();
2003
+ await reloadRuntimeAccountManager(makeReloadAccountManagerDeps());
2813
2004
  }
2814
2005
  const label = formatAccountLabel(account, targetIndex);
2815
2006
  if (ui.v2Enabled) {
@@ -2826,7 +2017,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2826
2017
  description: "Show detailed status of Codex accounts and rate limits.",
2827
2018
  args: {},
2828
2019
  async execute() {
2829
- const ui = resolveUiRuntime();
2020
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2830
2021
  const storage = await loadAccounts();
2831
2022
  if (!storage || storage.accounts.length === 0) {
2832
2023
  if (ui.v2Enabled) {
@@ -2855,7 +2046,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2855
2046
  badges.push(formatUiBadge(ui, "active", "accent"));
2856
2047
  if (account.enabled === false)
2857
2048
  badges.push(formatUiBadge(ui, "disabled", "danger"));
2858
- const rateLimit = formatRateLimitEntry(account, now) ?? "none";
2049
+ const rateLimit = formatRateLimitEntry(account, now, formatWaitTime) ?? "none";
2859
2050
  const cooldown = formatCooldown(account, now) ?? "none";
2860
2051
  if (rateLimit !== "none")
2861
2052
  badges.push(formatUiBadge(ui, "rate-limited", "warning"));
@@ -2871,7 +2062,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2871
2062
  lines.push(...formatUiSection(ui, "Active index by model family"));
2872
2063
  for (const family of MODEL_FAMILIES) {
2873
2064
  const idx = storage.activeIndexByFamily?.[family];
2874
- const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
2065
+ const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx)
2066
+ ? String(idx + 1)
2067
+ : "-";
2875
2068
  lines.push(formatUiItem(ui, `${family}: ${familyIndexLabel}`));
2876
2069
  }
2877
2070
  lines.push("");
@@ -2905,18 +2098,27 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2905
2098
  storage.accounts.forEach((account, index) => {
2906
2099
  const label = formatAccountLabel(account, index);
2907
2100
  const active = index === activeIndex ? "Yes" : "No";
2908
- const rateLimit = formatRateLimitEntry(account, now) ?? "None";
2101
+ const rateLimit = formatRateLimitEntry(account, now, formatWaitTime) ?? "None";
2909
2102
  const cooldown = formatCooldown(account, now) ?? "No";
2910
2103
  const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
2911
2104
  ? `${formatWaitTime(now - account.lastUsed)} ago`
2912
2105
  : "-";
2913
- lines.push(buildTableRow([String(index + 1), label, active, rateLimit, cooldown, lastUsed], statusTableOptions));
2106
+ lines.push(buildTableRow([
2107
+ String(index + 1),
2108
+ label,
2109
+ active,
2110
+ rateLimit,
2111
+ cooldown,
2112
+ lastUsed,
2113
+ ], statusTableOptions));
2914
2114
  });
2915
2115
  lines.push("");
2916
2116
  lines.push("Active index by model family:");
2917
2117
  for (const family of MODEL_FAMILIES) {
2918
2118
  const idx = storage.activeIndexByFamily?.[family];
2919
- const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
2119
+ const familyIndexLabel = typeof idx === "number" && Number.isFinite(idx)
2120
+ ? String(idx + 1)
2121
+ : "-";
2920
2122
  lines.push(` ${family}: ${familyIndexLabel}`);
2921
2123
  }
2922
2124
  lines.push("");
@@ -2933,95 +2135,97 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2933
2135
  return lines.join("\n");
2934
2136
  },
2935
2137
  }),
2936
- ...(exposeAdvancedCodexTools ? {
2937
- "codex-metrics": tool({
2938
- description: "Show runtime request metrics for this plugin process.",
2939
- args: {},
2940
- execute() {
2941
- const ui = resolveUiRuntime();
2942
- const now = Date.now();
2943
- const uptimeMs = Math.max(0, now - runtimeMetrics.startedAt);
2944
- const total = runtimeMetrics.totalRequests;
2945
- const successful = runtimeMetrics.successfulRequests;
2946
- const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : "0.0";
2947
- const avgLatencyMs = successful > 0
2948
- ? Math.round(runtimeMetrics.cumulativeLatencyMs / successful)
2949
- : 0;
2950
- const liveSyncSnapshot = liveAccountSync?.getSnapshot();
2951
- const guardianStats = refreshGuardian?.getStats();
2952
- const sessionAffinityEntries = sessionAffinityStore?.size() ?? 0;
2953
- const lastRequest = runtimeMetrics.lastRequestAt !== null
2954
- ? `${formatWaitTime(now - runtimeMetrics.lastRequestAt)} ago`
2955
- : "never";
2956
- const lines = [
2957
- "Codex Plugin Metrics:",
2958
- "",
2959
- `Uptime: ${formatWaitTime(uptimeMs)}`,
2960
- `Total upstream requests: ${total}`,
2961
- `Successful responses: ${successful}`,
2962
- `Failed responses: ${runtimeMetrics.failedRequests}`,
2963
- `Success rate: ${successRate}%`,
2964
- `Average successful latency: ${avgLatencyMs}ms`,
2965
- `Rate-limited responses: ${runtimeMetrics.rateLimitedResponses}`,
2966
- `Server errors (5xx): ${runtimeMetrics.serverErrors}`,
2967
- `Network errors: ${runtimeMetrics.networkErrors}`,
2968
- `User aborts: ${runtimeMetrics.userAborts}`,
2969
- `Auth refresh failures: ${runtimeMetrics.authRefreshFailures}`,
2970
- `Account rotations: ${runtimeMetrics.accountRotations}`,
2971
- `Same-account retries: ${runtimeMetrics.sameAccountRetries}`,
2972
- `Stream failover attempts: ${runtimeMetrics.streamFailoverAttempts}`,
2973
- `Stream failover recoveries: ${runtimeMetrics.streamFailoverRecoveries}`,
2974
- `Stream failover cross-account recoveries: ${runtimeMetrics.streamFailoverCrossAccountRecoveries}`,
2975
- `Empty-response retries: ${runtimeMetrics.emptyResponseRetries}`,
2976
- `Session affinity entries: ${sessionAffinityEntries}`,
2977
- `Live sync: ${liveSyncSnapshot?.running ? "on" : "off"} (${liveSyncSnapshot?.reloadCount ?? 0} reloads)`,
2978
- `Refresh guardian: ${guardianStats ? "on" : "off"} (${guardianStats?.refreshed ?? 0} refreshed, ${guardianStats?.failed ?? 0} failed)`,
2979
- `Last upstream request: ${lastRequest}`,
2980
- ];
2981
- if (runtimeMetrics.lastError) {
2982
- lines.push(`Last error: ${runtimeMetrics.lastError}`);
2983
- }
2984
- if (ui.v2Enabled) {
2985
- const styled = [
2986
- ...formatUiHeader(ui, "Codex plugin metrics"),
2987
- formatUiKeyValue(ui, "Uptime", formatWaitTime(uptimeMs)),
2988
- formatUiKeyValue(ui, "Total upstream requests", String(total)),
2989
- formatUiKeyValue(ui, "Successful responses", String(successful), "success"),
2990
- formatUiKeyValue(ui, "Failed responses", String(runtimeMetrics.failedRequests), "danger"),
2991
- formatUiKeyValue(ui, "Success rate", `${successRate}%`, "accent"),
2992
- formatUiKeyValue(ui, "Average successful latency", `${avgLatencyMs}ms`),
2993
- formatUiKeyValue(ui, "Rate-limited responses", String(runtimeMetrics.rateLimitedResponses), "warning"),
2994
- formatUiKeyValue(ui, "Server errors (5xx)", String(runtimeMetrics.serverErrors), "danger"),
2995
- formatUiKeyValue(ui, "Network errors", String(runtimeMetrics.networkErrors), "danger"),
2996
- formatUiKeyValue(ui, "User aborts", String(runtimeMetrics.userAborts), "muted"),
2997
- formatUiKeyValue(ui, "Auth refresh failures", String(runtimeMetrics.authRefreshFailures), "warning"),
2998
- formatUiKeyValue(ui, "Account rotations", String(runtimeMetrics.accountRotations), "accent"),
2999
- formatUiKeyValue(ui, "Same-account retries", String(runtimeMetrics.sameAccountRetries), "warning"),
3000
- formatUiKeyValue(ui, "Stream failover attempts", String(runtimeMetrics.streamFailoverAttempts), "muted"),
3001
- formatUiKeyValue(ui, "Stream failover recoveries", String(runtimeMetrics.streamFailoverRecoveries), "success"),
3002
- formatUiKeyValue(ui, "Stream failover cross-account recoveries", String(runtimeMetrics.streamFailoverCrossAccountRecoveries), "accent"),
3003
- formatUiKeyValue(ui, "Empty-response retries", String(runtimeMetrics.emptyResponseRetries), "warning"),
3004
- formatUiKeyValue(ui, "Session affinity entries", String(sessionAffinityEntries), "muted"),
3005
- formatUiKeyValue(ui, "Live sync", `${liveSyncSnapshot?.running ? "on" : "off"} (${liveSyncSnapshot?.reloadCount ?? 0} reloads)`, liveSyncSnapshot?.running ? "success" : "muted"),
3006
- formatUiKeyValue(ui, "Refresh guardian", guardianStats
3007
- ? `on (${guardianStats.refreshed} refreshed, ${guardianStats.failed} failed)`
3008
- : "off", guardianStats ? "success" : "muted"),
3009
- formatUiKeyValue(ui, "Last upstream request", lastRequest, "muted"),
2138
+ ...(exposeAdvancedCodexTools
2139
+ ? {
2140
+ "codex-metrics": tool({
2141
+ description: "Show runtime request metrics for this plugin process.",
2142
+ args: {},
2143
+ execute() {
2144
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2145
+ const now = Date.now();
2146
+ const uptimeMs = Math.max(0, now - runtimeMetrics.startedAt);
2147
+ const total = runtimeMetrics.totalRequests;
2148
+ const successful = runtimeMetrics.successfulRequests;
2149
+ const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : "0.0";
2150
+ const avgLatencyMs = successful > 0
2151
+ ? Math.round(runtimeMetrics.cumulativeLatencyMs / successful)
2152
+ : 0;
2153
+ const liveSyncSnapshot = liveAccountSync?.getSnapshot();
2154
+ const guardianStats = refreshGuardian?.getStats();
2155
+ const sessionAffinityEntries = sessionAffinityStore?.size() ?? 0;
2156
+ const lastRequest = runtimeMetrics.lastRequestAt !== null
2157
+ ? `${formatWaitTime(now - runtimeMetrics.lastRequestAt)} ago`
2158
+ : "never";
2159
+ const lines = [
2160
+ "Codex Plugin Metrics:",
2161
+ "",
2162
+ `Uptime: ${formatWaitTime(uptimeMs)}`,
2163
+ `Total upstream requests: ${total}`,
2164
+ `Successful responses: ${successful}`,
2165
+ `Failed responses: ${runtimeMetrics.failedRequests}`,
2166
+ `Success rate: ${successRate}%`,
2167
+ `Average successful latency: ${avgLatencyMs}ms`,
2168
+ `Rate-limited responses: ${runtimeMetrics.rateLimitedResponses}`,
2169
+ `Server errors (5xx): ${runtimeMetrics.serverErrors}`,
2170
+ `Network errors: ${runtimeMetrics.networkErrors}`,
2171
+ `User aborts: ${runtimeMetrics.userAborts}`,
2172
+ `Auth refresh failures: ${runtimeMetrics.authRefreshFailures}`,
2173
+ `Account rotations: ${runtimeMetrics.accountRotations}`,
2174
+ `Same-account retries: ${runtimeMetrics.sameAccountRetries}`,
2175
+ `Stream failover attempts: ${runtimeMetrics.streamFailoverAttempts}`,
2176
+ `Stream failover recoveries: ${runtimeMetrics.streamFailoverRecoveries}`,
2177
+ `Stream failover cross-account recoveries: ${runtimeMetrics.streamFailoverCrossAccountRecoveries}`,
2178
+ `Empty-response retries: ${runtimeMetrics.emptyResponseRetries}`,
2179
+ `Session affinity entries: ${sessionAffinityEntries}`,
2180
+ `Live sync: ${liveSyncSnapshot?.running ? "on" : "off"} (${liveSyncSnapshot?.reloadCount ?? 0} reloads)`,
2181
+ `Refresh guardian: ${guardianStats ? "on" : "off"} (${guardianStats?.refreshed ?? 0} refreshed, ${guardianStats?.failed ?? 0} failed)`,
2182
+ `Last upstream request: ${lastRequest}`,
3010
2183
  ];
3011
2184
  if (runtimeMetrics.lastError) {
3012
- styled.push(formatUiKeyValue(ui, "Last error", runtimeMetrics.lastError, "danger"));
2185
+ lines.push(`Last error: ${runtimeMetrics.lastError}`);
3013
2186
  }
3014
- return Promise.resolve(styled.join("\n"));
3015
- }
3016
- return Promise.resolve(lines.join("\n"));
3017
- },
3018
- }),
3019
- } : {}),
2187
+ if (ui.v2Enabled) {
2188
+ const styled = [
2189
+ ...formatUiHeader(ui, "Codex plugin metrics"),
2190
+ formatUiKeyValue(ui, "Uptime", formatWaitTime(uptimeMs)),
2191
+ formatUiKeyValue(ui, "Total upstream requests", String(total)),
2192
+ formatUiKeyValue(ui, "Successful responses", String(successful), "success"),
2193
+ formatUiKeyValue(ui, "Failed responses", String(runtimeMetrics.failedRequests), "danger"),
2194
+ formatUiKeyValue(ui, "Success rate", `${successRate}%`, "accent"),
2195
+ formatUiKeyValue(ui, "Average successful latency", `${avgLatencyMs}ms`),
2196
+ formatUiKeyValue(ui, "Rate-limited responses", String(runtimeMetrics.rateLimitedResponses), "warning"),
2197
+ formatUiKeyValue(ui, "Server errors (5xx)", String(runtimeMetrics.serverErrors), "danger"),
2198
+ formatUiKeyValue(ui, "Network errors", String(runtimeMetrics.networkErrors), "danger"),
2199
+ formatUiKeyValue(ui, "User aborts", String(runtimeMetrics.userAborts), "muted"),
2200
+ formatUiKeyValue(ui, "Auth refresh failures", String(runtimeMetrics.authRefreshFailures), "warning"),
2201
+ formatUiKeyValue(ui, "Account rotations", String(runtimeMetrics.accountRotations), "accent"),
2202
+ formatUiKeyValue(ui, "Same-account retries", String(runtimeMetrics.sameAccountRetries), "warning"),
2203
+ formatUiKeyValue(ui, "Stream failover attempts", String(runtimeMetrics.streamFailoverAttempts), "muted"),
2204
+ formatUiKeyValue(ui, "Stream failover recoveries", String(runtimeMetrics.streamFailoverRecoveries), "success"),
2205
+ formatUiKeyValue(ui, "Stream failover cross-account recoveries", String(runtimeMetrics.streamFailoverCrossAccountRecoveries), "accent"),
2206
+ formatUiKeyValue(ui, "Empty-response retries", String(runtimeMetrics.emptyResponseRetries), "warning"),
2207
+ formatUiKeyValue(ui, "Session affinity entries", String(sessionAffinityEntries), "muted"),
2208
+ formatUiKeyValue(ui, "Live sync", `${liveSyncSnapshot?.running ? "on" : "off"} (${liveSyncSnapshot?.reloadCount ?? 0} reloads)`, liveSyncSnapshot?.running ? "success" : "muted"),
2209
+ formatUiKeyValue(ui, "Refresh guardian", guardianStats
2210
+ ? `on (${guardianStats.refreshed} refreshed, ${guardianStats.failed} failed)`
2211
+ : "off", guardianStats ? "success" : "muted"),
2212
+ formatUiKeyValue(ui, "Last upstream request", lastRequest, "muted"),
2213
+ ];
2214
+ if (runtimeMetrics.lastError) {
2215
+ styled.push(formatUiKeyValue(ui, "Last error", runtimeMetrics.lastError, "danger"));
2216
+ }
2217
+ return Promise.resolve(styled.join("\n"));
2218
+ }
2219
+ return Promise.resolve(lines.join("\n"));
2220
+ },
2221
+ }),
2222
+ }
2223
+ : {}),
3020
2224
  "codex-health": tool({
3021
2225
  description: "Check health of all Codex accounts by validating refresh tokens.",
3022
2226
  args: {},
3023
2227
  async execute() {
3024
- const ui = resolveUiRuntime();
2228
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
3025
2229
  const storage = await loadAccounts();
3026
2230
  if (!storage || storage.accounts.length === 0) {
3027
2231
  if (ui.v2Enabled) {
@@ -3073,255 +2277,268 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3073
2277
  return results.join("\n");
3074
2278
  },
3075
2279
  }),
3076
- ...(exposeAdvancedCodexTools ? {
3077
- "codex-remove": tool({
3078
- description: "Remove a Codex account by index (1-based). Use codex-list to list accounts first.",
3079
- args: {
3080
- index: tool.schema.number().describe("Account number to remove (1-based, e.g., 1 for first account)"),
3081
- },
3082
- async execute({ index }) {
3083
- const ui = resolveUiRuntime();
3084
- const storage = await loadAccounts();
3085
- if (!storage || storage.accounts.length === 0) {
3086
- if (ui.v2Enabled) {
3087
- return [
3088
- ...formatUiHeader(ui, "Remove account"),
3089
- "",
3090
- formatUiItem(ui, "No accounts configured.", "warning"),
3091
- ].join("\n");
2280
+ ...(exposeAdvancedCodexTools
2281
+ ? {
2282
+ "codex-remove": tool({
2283
+ description: "Remove a Codex account by index (1-based). Use codex-list to list accounts first.",
2284
+ args: {
2285
+ index: tool.schema
2286
+ .number()
2287
+ .describe("Account number to remove (1-based, e.g., 1 for first account)"),
2288
+ },
2289
+ async execute({ index }) {
2290
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2291
+ const storage = await loadAccounts();
2292
+ if (!storage || storage.accounts.length === 0) {
2293
+ if (ui.v2Enabled) {
2294
+ return [
2295
+ ...formatUiHeader(ui, "Remove account"),
2296
+ "",
2297
+ formatUiItem(ui, "No accounts configured.", "warning"),
2298
+ ].join("\n");
2299
+ }
2300
+ return "No Codex accounts configured. Nothing to remove.";
3092
2301
  }
3093
- return "No Codex accounts configured. Nothing to remove.";
3094
- }
3095
- const targetIndex = Math.floor((index ?? 0) - 1);
3096
- if (!Number.isFinite(targetIndex) ||
3097
- targetIndex < 0 ||
3098
- targetIndex >= storage.accounts.length) {
3099
- if (ui.v2Enabled) {
3100
- return [
3101
- ...formatUiHeader(ui, "Remove account"),
3102
- "",
3103
- formatUiItem(ui, `Invalid account number: ${index}`, "danger"),
3104
- formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
3105
- formatUiItem(ui, "Use codex-list to list all accounts.", "accent"),
3106
- ].join("\n");
2302
+ const targetIndex = Math.floor((index ?? 0) - 1);
2303
+ if (!Number.isFinite(targetIndex) ||
2304
+ targetIndex < 0 ||
2305
+ targetIndex >= storage.accounts.length) {
2306
+ if (ui.v2Enabled) {
2307
+ return [
2308
+ ...formatUiHeader(ui, "Remove account"),
2309
+ "",
2310
+ formatUiItem(ui, `Invalid account number: ${index}`, "danger"),
2311
+ formatUiKeyValue(ui, "Valid range", `1-${storage.accounts.length}`, "muted"),
2312
+ formatUiItem(ui, "Use codex-list to list all accounts.", "accent"),
2313
+ ].join("\n");
2314
+ }
2315
+ return `Invalid account number: ${index}\n\nValid range: 1-${storage.accounts.length}\n\nUse codex-list to list all accounts.`;
3107
2316
  }
3108
- return `Invalid account number: ${index}\n\nValid range: 1-${storage.accounts.length}\n\nUse codex-list to list all accounts.`;
3109
- }
3110
- const account = storage.accounts[targetIndex];
3111
- if (!account) {
3112
- return `Account ${index} not found.`;
3113
- }
3114
- const label = formatAccountLabel(account, targetIndex);
3115
- storage.accounts.splice(targetIndex, 1);
3116
- if (storage.accounts.length === 0) {
3117
- storage.activeIndex = 0;
3118
- storage.activeIndexByFamily = {};
3119
- }
3120
- else {
3121
- if (storage.activeIndex >= storage.accounts.length) {
3122
- storage.activeIndex = 0;
2317
+ const account = storage.accounts[targetIndex];
2318
+ if (!account) {
2319
+ return `Account ${index} not found.`;
3123
2320
  }
3124
- else if (storage.activeIndex > targetIndex) {
3125
- storage.activeIndex -= 1;
2321
+ const label = formatAccountLabel(account, targetIndex);
2322
+ storage.accounts.splice(targetIndex, 1);
2323
+ if (storage.accounts.length === 0) {
2324
+ storage.activeIndex = 0;
2325
+ storage.activeIndexByFamily = {};
3126
2326
  }
3127
- if (storage.activeIndexByFamily) {
3128
- for (const family of MODEL_FAMILIES) {
3129
- const idx = storage.activeIndexByFamily[family];
3130
- if (typeof idx === "number") {
3131
- if (idx >= storage.accounts.length) {
3132
- storage.activeIndexByFamily[family] = 0;
3133
- }
3134
- else if (idx > targetIndex) {
3135
- storage.activeIndexByFamily[family] = idx - 1;
2327
+ else {
2328
+ if (storage.activeIndex >= storage.accounts.length) {
2329
+ storage.activeIndex = 0;
2330
+ }
2331
+ else if (storage.activeIndex > targetIndex) {
2332
+ storage.activeIndex -= 1;
2333
+ }
2334
+ if (storage.activeIndexByFamily) {
2335
+ for (const family of MODEL_FAMILIES) {
2336
+ const idx = storage.activeIndexByFamily[family];
2337
+ if (typeof idx === "number") {
2338
+ if (idx >= storage.accounts.length) {
2339
+ storage.activeIndexByFamily[family] = 0;
2340
+ }
2341
+ else if (idx > targetIndex) {
2342
+ storage.activeIndexByFamily[family] = idx - 1;
2343
+ }
3136
2344
  }
3137
2345
  }
3138
2346
  }
3139
2347
  }
3140
- }
3141
- try {
3142
- await saveAccounts(storage);
3143
- }
3144
- catch (saveError) {
3145
- logWarn("Failed to save account removal", { error: String(saveError) });
2348
+ try {
2349
+ await saveAccounts(storage);
2350
+ }
2351
+ catch (saveError) {
2352
+ logWarn("Failed to save account removal", {
2353
+ error: String(saveError),
2354
+ });
2355
+ if (ui.v2Enabled) {
2356
+ return [
2357
+ ...formatUiHeader(ui, "Remove account"),
2358
+ "",
2359
+ formatUiItem(ui, `Removed ${formatAccountLabel(account, targetIndex)} from memory`, "warning"),
2360
+ formatUiItem(ui, "Failed to persist. Change may be lost on restart.", "danger"),
2361
+ ].join("\n");
2362
+ }
2363
+ return `Removed ${formatAccountLabel(account, targetIndex)} from memory but failed to persist. Changes may be lost on restart.`;
2364
+ }
2365
+ if (cachedAccountManager) {
2366
+ await reloadRuntimeAccountManager(makeReloadAccountManagerDeps());
2367
+ }
2368
+ const remaining = storage.accounts.length;
3146
2369
  if (ui.v2Enabled) {
3147
2370
  return [
3148
2371
  ...formatUiHeader(ui, "Remove account"),
3149
2372
  "",
3150
- formatUiItem(ui, `Removed ${formatAccountLabel(account, targetIndex)} from memory`, "warning"),
3151
- formatUiItem(ui, "Failed to persist. Change may be lost on restart.", "danger"),
2373
+ formatUiItem(ui, `${getStatusMarker(ui, "ok")} Removed: ${label}`, "success"),
2374
+ remaining > 0
2375
+ ? formatUiKeyValue(ui, "Remaining accounts", String(remaining))
2376
+ : formatUiItem(ui, "No accounts remaining. Run: codex login", "warning"),
3152
2377
  ].join("\n");
3153
2378
  }
3154
- return `Removed ${formatAccountLabel(account, targetIndex)} from memory but failed to persist. Changes may be lost on restart.`;
3155
- }
3156
- if (cachedAccountManager) {
3157
- await reloadAccountManagerFromDisk();
3158
- }
3159
- const remaining = storage.accounts.length;
3160
- if (ui.v2Enabled) {
3161
2379
  return [
3162
- ...formatUiHeader(ui, "Remove account"),
2380
+ `Removed: ${label}`,
3163
2381
  "",
3164
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Removed: ${label}`, "success"),
3165
2382
  remaining > 0
3166
- ? formatUiKeyValue(ui, "Remaining accounts", String(remaining))
3167
- : formatUiItem(ui, "No accounts remaining. Run: codex login", "warning"),
2383
+ ? `Remaining accounts: ${remaining}`
2384
+ : "No accounts remaining. Run: codex login",
3168
2385
  ].join("\n");
3169
- }
3170
- return [
3171
- `Removed: ${label}`,
3172
- "",
3173
- remaining > 0
3174
- ? `Remaining accounts: ${remaining}`
3175
- : "No accounts remaining. Run: codex login",
3176
- ].join("\n");
3177
- },
3178
- }),
3179
- "codex-refresh": tool({
3180
- description: "Manually refresh OAuth tokens for all accounts to verify they're still valid.",
3181
- args: {},
3182
- async execute() {
3183
- const ui = resolveUiRuntime();
3184
- const storage = await loadAccounts();
3185
- if (!storage || storage.accounts.length === 0) {
3186
- if (ui.v2Enabled) {
3187
- return [
3188
- ...formatUiHeader(ui, "Refresh accounts"),
3189
- "",
3190
- formatUiItem(ui, "No accounts configured.", "warning"),
3191
- formatUiItem(ui, "Run: codex login", "accent"),
3192
- ].join("\n");
2386
+ },
2387
+ }),
2388
+ "codex-refresh": tool({
2389
+ description: "Manually refresh OAuth tokens for all accounts to verify they're still valid.",
2390
+ args: {},
2391
+ async execute() {
2392
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2393
+ const storage = await loadAccounts();
2394
+ if (!storage || storage.accounts.length === 0) {
2395
+ if (ui.v2Enabled) {
2396
+ return [
2397
+ ...formatUiHeader(ui, "Refresh accounts"),
2398
+ "",
2399
+ formatUiItem(ui, "No accounts configured.", "warning"),
2400
+ formatUiItem(ui, "Run: codex login", "accent"),
2401
+ ].join("\n");
2402
+ }
2403
+ return "No Codex accounts configured. Run: codex login";
3193
2404
  }
3194
- return "No Codex accounts configured. Run: codex login";
3195
- }
3196
- const results = ui.v2Enabled
3197
- ? []
3198
- : [`Refreshing ${storage.accounts.length} account(s):`, ""];
3199
- let refreshedCount = 0;
3200
- let failedCount = 0;
3201
- for (let i = 0; i < storage.accounts.length; i++) {
3202
- const account = storage.accounts[i];
3203
- if (!account)
3204
- continue;
3205
- const label = formatAccountLabel(account, i);
3206
- try {
3207
- const refreshResult = await queuedRefresh(account.refreshToken);
3208
- if (refreshResult.type === "success") {
3209
- account.refreshToken = refreshResult.refresh;
3210
- account.accessToken = refreshResult.access;
3211
- account.expiresAt = refreshResult.expires;
3212
- results.push(` ${getStatusMarker(ui, "ok")} ${label}: Refreshed`);
3213
- refreshedCount++;
2405
+ const results = ui.v2Enabled
2406
+ ? []
2407
+ : [`Refreshing ${storage.accounts.length} account(s):`, ""];
2408
+ let refreshedCount = 0;
2409
+ let failedCount = 0;
2410
+ for (let i = 0; i < storage.accounts.length; i++) {
2411
+ const account = storage.accounts[i];
2412
+ if (!account)
2413
+ continue;
2414
+ const label = formatAccountLabel(account, i);
2415
+ try {
2416
+ const refreshResult = await queuedRefresh(account.refreshToken);
2417
+ if (refreshResult.type === "success") {
2418
+ account.refreshToken = refreshResult.refresh;
2419
+ account.accessToken = refreshResult.access;
2420
+ account.expiresAt = refreshResult.expires;
2421
+ results.push(` ${getStatusMarker(ui, "ok")} ${label}: Refreshed`);
2422
+ refreshedCount++;
2423
+ }
2424
+ else {
2425
+ results.push(` ${getStatusMarker(ui, "error")} ${label}: Failed - ${refreshResult.message ?? refreshResult.reason}`);
2426
+ failedCount++;
2427
+ }
3214
2428
  }
3215
- else {
3216
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Failed - ${refreshResult.message ?? refreshResult.reason}`);
2429
+ catch (error) {
2430
+ const errorMsg = error instanceof Error ? error.message : String(error);
2431
+ results.push(` ${getStatusMarker(ui, "error")} ${label}: Error - ${errorMsg.slice(0, 120)}`);
3217
2432
  failedCount++;
3218
2433
  }
3219
2434
  }
3220
- catch (error) {
3221
- const errorMsg = error instanceof Error ? error.message : String(error);
3222
- results.push(` ${getStatusMarker(ui, "error")} ${label}: Error - ${errorMsg.slice(0, 120)}`);
3223
- failedCount++;
3224
- }
3225
- }
3226
- await saveAccounts(storage);
3227
- if (cachedAccountManager) {
3228
- await reloadAccountManagerFromDisk();
3229
- }
3230
- results.push("");
3231
- results.push(`Summary: ${refreshedCount} refreshed, ${failedCount} failed`);
3232
- if (ui.v2Enabled) {
3233
- return [
3234
- ...formatUiHeader(ui, "Refresh accounts"),
3235
- "",
3236
- ...results.map((line) => paintUiText(ui, line, "normal")),
3237
- ].join("\n");
3238
- }
3239
- return results.join("\n");
3240
- },
3241
- }),
3242
- "codex-export": tool({
3243
- description: "Export accounts to a JSON file for backup or migration to another machine.",
3244
- args: {
3245
- path: tool.schema.string().describe("File path to export to (e.g., ~/codex-backup.json)"),
3246
- force: tool.schema.boolean().optional().describe("Overwrite existing file (default: true)"),
3247
- },
3248
- async execute({ path: filePath, force }) {
3249
- const ui = resolveUiRuntime();
3250
- try {
3251
- await exportAccounts(filePath, force ?? true);
3252
- const storage = await loadAccounts();
3253
- const count = storage?.accounts.length ?? 0;
3254
- if (ui.v2Enabled) {
3255
- return [
3256
- ...formatUiHeader(ui, "Export accounts"),
3257
- "",
3258
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Exported ${count} account(s)`, "success"),
3259
- formatUiKeyValue(ui, "Path", filePath, "muted"),
3260
- ].join("\n");
2435
+ await saveAccounts(storage);
2436
+ if (cachedAccountManager) {
2437
+ await reloadRuntimeAccountManager(makeReloadAccountManagerDeps());
3261
2438
  }
3262
- return `Exported ${count} account(s) to: ${filePath}`;
3263
- }
3264
- catch (error) {
3265
- const msg = error instanceof Error ? error.message : String(error);
2439
+ results.push("");
2440
+ results.push(`Summary: ${refreshedCount} refreshed, ${failedCount} failed`);
3266
2441
  if (ui.v2Enabled) {
3267
2442
  return [
3268
- ...formatUiHeader(ui, "Export accounts"),
2443
+ ...formatUiHeader(ui, "Refresh accounts"),
3269
2444
  "",
3270
- formatUiItem(ui, `${getStatusMarker(ui, "error")} Export failed`, "danger"),
3271
- formatUiKeyValue(ui, "Error", msg, "danger"),
2445
+ ...results.map((line) => paintUiText(ui, line, "normal")),
3272
2446
  ].join("\n");
3273
2447
  }
3274
- return `Export failed: ${msg}`;
3275
- }
3276
- },
3277
- }),
3278
- "codex-import": tool({
3279
- description: "Import accounts from a JSON file, merging with existing accounts.",
3280
- args: {
3281
- path: tool.schema.string().describe("File path to import from (e.g., ~/codex-backup.json)"),
3282
- },
3283
- async execute({ path: filePath }) {
3284
- const ui = resolveUiRuntime();
3285
- try {
3286
- const result = await importAccounts(filePath);
3287
- invalidateAccountManagerCache();
3288
- const lines = [`Import complete.`, ``];
3289
- if (result.imported > 0) {
3290
- lines.push(`New accounts: ${result.imported}`);
2448
+ return results.join("\n");
2449
+ },
2450
+ }),
2451
+ "codex-export": tool({
2452
+ description: "Export accounts to a JSON file for backup or migration to another machine.",
2453
+ args: {
2454
+ path: tool.schema
2455
+ .string()
2456
+ .describe("File path to export to (e.g., ~/codex-backup.json)"),
2457
+ force: tool.schema
2458
+ .boolean()
2459
+ .optional()
2460
+ .describe("Overwrite existing file (default: true)"),
2461
+ },
2462
+ async execute({ path: filePath, force }) {
2463
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2464
+ try {
2465
+ await exportAccounts(filePath, force ?? true);
2466
+ const storage = await loadAccounts();
2467
+ const count = storage?.accounts.length ?? 0;
2468
+ if (ui.v2Enabled) {
2469
+ return [
2470
+ ...formatUiHeader(ui, "Export accounts"),
2471
+ "",
2472
+ formatUiItem(ui, `${getStatusMarker(ui, "ok")} Exported ${count} account(s)`, "success"),
2473
+ formatUiKeyValue(ui, "Path", filePath, "muted"),
2474
+ ].join("\n");
2475
+ }
2476
+ return `Exported ${count} account(s) to: ${filePath}`;
3291
2477
  }
3292
- if (result.skipped > 0) {
3293
- lines.push(`Duplicates skipped: ${result.skipped}`);
2478
+ catch (error) {
2479
+ const msg = error instanceof Error ? error.message : String(error);
2480
+ if (ui.v2Enabled) {
2481
+ return [
2482
+ ...formatUiHeader(ui, "Export accounts"),
2483
+ "",
2484
+ formatUiItem(ui, `${getStatusMarker(ui, "error")} Export failed`, "danger"),
2485
+ formatUiKeyValue(ui, "Error", msg, "danger"),
2486
+ ].join("\n");
2487
+ }
2488
+ return `Export failed: ${msg}`;
3294
2489
  }
3295
- lines.push(`Total accounts: ${result.total}`);
3296
- if (ui.v2Enabled) {
3297
- const styled = [
3298
- ...formatUiHeader(ui, "Import accounts"),
3299
- "",
3300
- formatUiItem(ui, `${getStatusMarker(ui, "ok")} Import complete`, "success"),
3301
- formatUiKeyValue(ui, "Path", filePath, "muted"),
3302
- formatUiKeyValue(ui, "New accounts", String(result.imported), result.imported > 0 ? "success" : "muted"),
3303
- formatUiKeyValue(ui, "Duplicates skipped", String(result.skipped), result.skipped > 0 ? "warning" : "muted"),
3304
- formatUiKeyValue(ui, "Total accounts", String(result.total), "accent"),
3305
- ];
3306
- return styled.join("\n");
2490
+ },
2491
+ }),
2492
+ "codex-import": tool({
2493
+ description: "Import accounts from a JSON file, merging with existing accounts.",
2494
+ args: {
2495
+ path: tool.schema
2496
+ .string()
2497
+ .describe("File path to import from (e.g., ~/codex-backup.json)"),
2498
+ },
2499
+ async execute({ path: filePath }) {
2500
+ const ui = applyUiRuntimeFromConfig(loadPluginConfig(), setUiRuntimeOptions);
2501
+ try {
2502
+ const result = await importAccounts(filePath);
2503
+ invalidateRuntimeAccountManagerCache(accountManagerCacheInvalidationDeps);
2504
+ const lines = [`Import complete.`, ``];
2505
+ if (result.imported > 0) {
2506
+ lines.push(`New accounts: ${result.imported}`);
2507
+ }
2508
+ if (result.skipped > 0) {
2509
+ lines.push(`Duplicates skipped: ${result.skipped}`);
2510
+ }
2511
+ lines.push(`Total accounts: ${result.total}`);
2512
+ if (ui.v2Enabled) {
2513
+ const styled = [
2514
+ ...formatUiHeader(ui, "Import accounts"),
2515
+ "",
2516
+ formatUiItem(ui, `${getStatusMarker(ui, "ok")} Import complete`, "success"),
2517
+ formatUiKeyValue(ui, "Path", filePath, "muted"),
2518
+ formatUiKeyValue(ui, "New accounts", String(result.imported), result.imported > 0 ? "success" : "muted"),
2519
+ formatUiKeyValue(ui, "Duplicates skipped", String(result.skipped), result.skipped > 0 ? "warning" : "muted"),
2520
+ formatUiKeyValue(ui, "Total accounts", String(result.total), "accent"),
2521
+ ];
2522
+ return styled.join("\n");
2523
+ }
2524
+ return lines.join("\n");
3307
2525
  }
3308
- return lines.join("\n");
3309
- }
3310
- catch (error) {
3311
- const msg = error instanceof Error ? error.message : String(error);
3312
- if (ui.v2Enabled) {
3313
- return [
3314
- ...formatUiHeader(ui, "Import accounts"),
3315
- "",
3316
- formatUiItem(ui, `${getStatusMarker(ui, "error")} Import failed`, "danger"),
3317
- formatUiKeyValue(ui, "Error", msg, "danger"),
3318
- ].join("\n");
2526
+ catch (error) {
2527
+ const msg = error instanceof Error ? error.message : String(error);
2528
+ if (ui.v2Enabled) {
2529
+ return [
2530
+ ...formatUiHeader(ui, "Import accounts"),
2531
+ "",
2532
+ formatUiItem(ui, `${getStatusMarker(ui, "error")} Import failed`, "danger"),
2533
+ formatUiKeyValue(ui, "Error", msg, "danger"),
2534
+ ].join("\n");
2535
+ }
2536
+ return `Import failed: ${msg}`;
3319
2537
  }
3320
- return `Import failed: ${msg}`;
3321
- }
3322
- },
3323
- }),
3324
- } : {}),
2538
+ },
2539
+ }),
2540
+ }
2541
+ : {}),
3325
2542
  },
3326
2543
  };
3327
2544
  };