@cortexkit/opencode-antigravity-auth 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/README.md +225 -569
  2. package/dist/cli.js +4017 -0
  3. package/dist/cli.js.map +7 -0
  4. package/dist/index.d.ts +1 -3
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +20742 -14586
  7. package/dist/index.js.map +4 -4
  8. package/dist/src/antigravity/oauth.d.ts +1 -1
  9. package/dist/src/antigravity/oauth.js +1 -1
  10. package/dist/src/cli.d.ts +21 -0
  11. package/dist/src/cli.d.ts.map +1 -0
  12. package/dist/src/cli.js +237 -0
  13. package/dist/src/cli.js.map +1 -0
  14. package/dist/src/constants.d.ts +1 -1
  15. package/dist/src/constants.js +1 -1
  16. package/dist/src/hooks/auto-update-checker/cache.d.ts.map +1 -1
  17. package/dist/src/hooks/auto-update-checker/cache.js +13 -13
  18. package/dist/src/hooks/auto-update-checker/cache.js.map +1 -1
  19. package/dist/src/hooks/auto-update-checker/checker.d.ts +1 -1
  20. package/dist/src/hooks/auto-update-checker/checker.d.ts.map +1 -1
  21. package/dist/src/hooks/auto-update-checker/checker.js +75 -51
  22. package/dist/src/hooks/auto-update-checker/checker.js.map +1 -1
  23. package/dist/src/hooks/auto-update-checker/constants.d.ts.map +1 -1
  24. package/dist/src/hooks/auto-update-checker/constants.js +12 -12
  25. package/dist/src/hooks/auto-update-checker/constants.js.map +1 -1
  26. package/dist/src/hooks/auto-update-checker/index.d.ts +6 -6
  27. package/dist/src/hooks/auto-update-checker/index.d.ts.map +1 -1
  28. package/dist/src/hooks/auto-update-checker/index.js +17 -17
  29. package/dist/src/hooks/auto-update-checker/index.js.map +1 -1
  30. package/dist/src/hooks/auto-update-checker/logging.js +2 -2
  31. package/dist/src/hooks/auto-update-checker/logging.js.map +1 -1
  32. package/dist/src/hooks/auto-update-checker/types.d.ts.map +1 -1
  33. package/dist/src/plugin/account-access.d.ts +99 -0
  34. package/dist/src/plugin/account-access.d.ts.map +1 -0
  35. package/dist/src/plugin/account-access.js +480 -0
  36. package/dist/src/plugin/account-access.js.map +1 -0
  37. package/dist/src/plugin/account-command-oauth.d.ts +34 -0
  38. package/dist/src/plugin/account-command-oauth.d.ts.map +1 -0
  39. package/dist/src/plugin/account-command-oauth.js +156 -0
  40. package/dist/src/plugin/account-command-oauth.js.map +1 -0
  41. package/dist/src/plugin/accounts.d.ts +7 -256
  42. package/dist/src/plugin/accounts.d.ts.map +1 -1
  43. package/dist/src/plugin/accounts.js +31 -1367
  44. package/dist/src/plugin/accounts.js.map +1 -1
  45. package/dist/src/plugin/agy-request-metadata.d.ts +1 -1
  46. package/dist/src/plugin/agy-request-metadata.js +1 -1
  47. package/dist/src/plugin/agy-transport.d.ts +1 -1
  48. package/dist/src/plugin/agy-transport.js +1 -1
  49. package/dist/src/plugin/auth-doctor.d.ts +6 -6
  50. package/dist/src/plugin/auth-doctor.d.ts.map +1 -1
  51. package/dist/src/plugin/auth-doctor.js +74 -66
  52. package/dist/src/plugin/auth-doctor.js.map +1 -1
  53. package/dist/src/plugin/auth-drift.d.ts +4 -4
  54. package/dist/src/plugin/auth-drift.d.ts.map +1 -1
  55. package/dist/src/plugin/auth-drift.js +15 -15
  56. package/dist/src/plugin/auth-drift.js.map +1 -1
  57. package/dist/src/plugin/auth-loader.d.ts +50 -0
  58. package/dist/src/plugin/auth-loader.d.ts.map +1 -0
  59. package/dist/src/plugin/auth-loader.js +225 -0
  60. package/dist/src/plugin/auth-loader.js.map +1 -0
  61. package/dist/src/plugin/auth.d.ts +1 -1
  62. package/dist/src/plugin/auth.js +1 -1
  63. package/dist/src/plugin/background-quota-refresh.d.ts +146 -0
  64. package/dist/src/plugin/background-quota-refresh.d.ts.map +1 -0
  65. package/dist/src/plugin/background-quota-refresh.js +347 -0
  66. package/dist/src/plugin/background-quota-refresh.js.map +1 -0
  67. package/dist/src/plugin/cache/index.d.ts +1 -1
  68. package/dist/src/plugin/cache/index.d.ts.map +1 -1
  69. package/dist/src/plugin/cache/index.js +1 -1
  70. package/dist/src/plugin/cache/index.js.map +1 -1
  71. package/dist/src/plugin/cache/signature-cache.d.ts +1 -1
  72. package/dist/src/plugin/cache/signature-cache.d.ts.map +1 -1
  73. package/dist/src/plugin/cache/signature-cache.js +23 -24
  74. package/dist/src/plugin/cache/signature-cache.js.map +1 -1
  75. package/dist/src/plugin/cache.d.ts +6 -5
  76. package/dist/src/plugin/cache.d.ts.map +1 -1
  77. package/dist/src/plugin/cache.js +21 -7
  78. package/dist/src/plugin/cache.js.map +1 -1
  79. package/dist/src/plugin/catalog.d.ts +23 -0
  80. package/dist/src/plugin/catalog.d.ts.map +1 -0
  81. package/dist/src/plugin/catalog.js +60 -0
  82. package/dist/src/plugin/catalog.js.map +1 -0
  83. package/dist/src/plugin/cli.d.ts +7 -7
  84. package/dist/src/plugin/cli.d.ts.map +1 -1
  85. package/dist/src/plugin/cli.js +62 -55
  86. package/dist/src/plugin/cli.js.map +1 -1
  87. package/dist/src/plugin/command-data.d.ts +329 -0
  88. package/dist/src/plugin/command-data.d.ts.map +1 -0
  89. package/dist/src/plugin/command-data.js +517 -0
  90. package/dist/src/plugin/command-data.js.map +1 -0
  91. package/dist/src/plugin/commands.d.ts +154 -0
  92. package/dist/src/plugin/commands.d.ts.map +1 -0
  93. package/dist/src/plugin/commands.js +746 -0
  94. package/dist/src/plugin/commands.js.map +1 -0
  95. package/dist/src/plugin/config/index.d.ts +2 -2
  96. package/dist/src/plugin/config/index.d.ts.map +1 -1
  97. package/dist/src/plugin/config/index.js +2 -2
  98. package/dist/src/plugin/config/index.js.map +1 -1
  99. package/dist/src/plugin/config/loader.d.ts +1 -1
  100. package/dist/src/plugin/config/loader.d.ts.map +1 -1
  101. package/dist/src/plugin/config/loader.js +18 -16
  102. package/dist/src/plugin/config/loader.js.map +1 -1
  103. package/dist/src/plugin/config/models.d.ts +2 -2
  104. package/dist/src/plugin/config/models.d.ts.map +1 -1
  105. package/dist/src/plugin/config/models.js +1 -1
  106. package/dist/src/plugin/config/models.js.map +1 -1
  107. package/dist/src/plugin/config/operator-settings-schema.d.ts +29 -0
  108. package/dist/src/plugin/config/operator-settings-schema.d.ts.map +1 -0
  109. package/dist/src/plugin/config/operator-settings-schema.js +28 -0
  110. package/dist/src/plugin/config/operator-settings-schema.js.map +1 -0
  111. package/dist/src/plugin/config/schema.d.ts +23 -2
  112. package/dist/src/plugin/config/schema.d.ts.map +1 -1
  113. package/dist/src/plugin/config/schema.js +109 -25
  114. package/dist/src/plugin/config/schema.js.map +1 -1
  115. package/dist/src/plugin/config/updater.d.ts.map +1 -1
  116. package/dist/src/plugin/config/updater.js +15 -15
  117. package/dist/src/plugin/config/updater.js.map +1 -1
  118. package/dist/src/plugin/config/writer.d.ts +34 -0
  119. package/dist/src/plugin/config/writer.d.ts.map +1 -0
  120. package/dist/src/plugin/config/writer.js +81 -0
  121. package/dist/src/plugin/config/writer.js.map +1 -0
  122. package/dist/src/plugin/core/streaming/index.d.ts +1 -1
  123. package/dist/src/plugin/core/streaming/index.d.ts.map +1 -1
  124. package/dist/src/plugin/core/streaming/index.js +1 -1
  125. package/dist/src/plugin/core/streaming/index.js.map +1 -1
  126. package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
  127. package/dist/src/plugin/core/streaming/transformer.js +85 -44
  128. package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
  129. package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
  130. package/dist/src/plugin/debug.d.ts +10 -3
  131. package/dist/src/plugin/debug.d.ts.map +1 -1
  132. package/dist/src/plugin/debug.js +110 -47
  133. package/dist/src/plugin/debug.js.map +1 -1
  134. package/dist/src/plugin/dependencies.d.ts +129 -0
  135. package/dist/src/plugin/dependencies.d.ts.map +1 -0
  136. package/dist/src/plugin/dependencies.js +107 -0
  137. package/dist/src/plugin/dependencies.js.map +1 -0
  138. package/dist/src/plugin/errors.d.ts +27 -0
  139. package/dist/src/plugin/errors.d.ts.map +1 -1
  140. package/dist/src/plugin/errors.js +29 -3
  141. package/dist/src/plugin/errors.js.map +1 -1
  142. package/dist/src/plugin/event-handler.d.ts +53 -0
  143. package/dist/src/plugin/event-handler.d.ts.map +1 -0
  144. package/dist/src/plugin/event-handler.js +101 -0
  145. package/dist/src/plugin/event-handler.js.map +1 -0
  146. package/dist/src/plugin/fetch/retry-state.d.ts +61 -0
  147. package/dist/src/plugin/fetch/retry-state.d.ts.map +1 -0
  148. package/dist/src/plugin/fetch/retry-state.js +248 -0
  149. package/dist/src/plugin/fetch/retry-state.js.map +1 -0
  150. package/dist/src/plugin/fetch/warmup.d.ts +30 -0
  151. package/dist/src/plugin/fetch/warmup.d.ts.map +1 -0
  152. package/dist/src/plugin/fetch/warmup.js +76 -0
  153. package/dist/src/plugin/fetch/warmup.js.map +1 -0
  154. package/dist/src/plugin/fetch-interceptor.d.ts +59 -0
  155. package/dist/src/plugin/fetch-interceptor.d.ts.map +1 -0
  156. package/dist/src/plugin/fetch-interceptor.js +1399 -0
  157. package/dist/src/plugin/fetch-interceptor.js.map +1 -0
  158. package/dist/src/plugin/fetch-routing.d.ts +26 -0
  159. package/dist/src/plugin/fetch-routing.d.ts.map +1 -0
  160. package/dist/src/plugin/fetch-routing.js +99 -0
  161. package/dist/src/plugin/fetch-routing.js.map +1 -0
  162. package/dist/src/plugin/fingerprint.d.ts +1 -1
  163. package/dist/src/plugin/fingerprint.js +1 -1
  164. package/dist/src/plugin/gemini-dump.d.ts +5 -5
  165. package/dist/src/plugin/gemini-dump.d.ts.map +1 -1
  166. package/dist/src/plugin/gemini-dump.js +109 -55
  167. package/dist/src/plugin/gemini-dump.js.map +1 -1
  168. package/dist/src/plugin/google-search-tool.d.ts +9 -0
  169. package/dist/src/plugin/google-search-tool.d.ts.map +1 -0
  170. package/dist/src/plugin/google-search-tool.js +56 -0
  171. package/dist/src/plugin/google-search-tool.js.map +1 -0
  172. package/dist/src/plugin/image-saver.d.ts.map +1 -1
  173. package/dist/src/plugin/image-saver.js +16 -16
  174. package/dist/src/plugin/image-saver.js.map +1 -1
  175. package/dist/src/plugin/index.d.ts +30 -0
  176. package/dist/src/plugin/index.d.ts.map +1 -0
  177. package/dist/src/plugin/index.js +411 -0
  178. package/dist/src/plugin/index.js.map +1 -0
  179. package/dist/src/plugin/killswitch.d.ts +94 -0
  180. package/dist/src/plugin/killswitch.d.ts.map +1 -0
  181. package/dist/src/plugin/killswitch.js +197 -0
  182. package/dist/src/plugin/killswitch.js.map +1 -0
  183. package/dist/src/plugin/lifecycle.d.ts +40 -0
  184. package/dist/src/plugin/lifecycle.d.ts.map +1 -0
  185. package/dist/src/plugin/lifecycle.js +73 -0
  186. package/dist/src/plugin/lifecycle.js.map +1 -0
  187. package/dist/src/plugin/logger.d.ts +10 -1
  188. package/dist/src/plugin/logger.d.ts.map +1 -1
  189. package/dist/src/plugin/logger.js +38 -12
  190. package/dist/src/plugin/logger.js.map +1 -1
  191. package/dist/src/plugin/logging-utils.d.ts +35 -1
  192. package/dist/src/plugin/logging-utils.d.ts.map +1 -1
  193. package/dist/src/plugin/logging-utils.js +98 -15
  194. package/dist/src/plugin/logging-utils.js.map +1 -1
  195. package/dist/src/plugin/model-registry.d.ts +1 -1
  196. package/dist/src/plugin/model-registry.js +1 -1
  197. package/dist/src/plugin/oauth-login.d.ts +22 -0
  198. package/dist/src/plugin/oauth-login.d.ts.map +1 -0
  199. package/dist/src/plugin/oauth-login.js +37 -0
  200. package/dist/src/plugin/oauth-login.js.map +1 -0
  201. package/dist/src/plugin/oauth-methods.d.ts +42 -0
  202. package/dist/src/plugin/oauth-methods.d.ts.map +1 -0
  203. package/dist/src/plugin/oauth-methods.js +1276 -0
  204. package/dist/src/plugin/oauth-methods.js.map +1 -0
  205. package/dist/src/plugin/operator-settings.d.ts +40 -0
  206. package/dist/src/plugin/operator-settings.d.ts.map +1 -0
  207. package/dist/src/plugin/operator-settings.js +148 -0
  208. package/dist/src/plugin/operator-settings.js.map +1 -0
  209. package/dist/src/plugin/persist-account-pool.d.ts +37 -0
  210. package/dist/src/plugin/persist-account-pool.d.ts.map +1 -0
  211. package/dist/src/plugin/persist-account-pool.js +142 -0
  212. package/dist/src/plugin/persist-account-pool.js.map +1 -0
  213. package/dist/src/plugin/project.d.ts +1 -1
  214. package/dist/src/plugin/project.js +1 -1
  215. package/dist/src/plugin/prompt-context.d.ts.map +1 -1
  216. package/dist/src/plugin/prompt-context.js +11 -11
  217. package/dist/src/plugin/prompt-context.js.map +1 -1
  218. package/dist/src/plugin/quota.d.ts +138 -41
  219. package/dist/src/plugin/quota.d.ts.map +1 -1
  220. package/dist/src/plugin/quota.js +417 -285
  221. package/dist/src/plugin/quota.js.map +1 -1
  222. package/dist/src/plugin/recovery/constants.d.ts.map +1 -1
  223. package/dist/src/plugin/recovery/constants.js +24 -15
  224. package/dist/src/plugin/recovery/constants.js.map +1 -1
  225. package/dist/src/plugin/recovery/index.d.ts +3 -3
  226. package/dist/src/plugin/recovery/index.d.ts.map +1 -1
  227. package/dist/src/plugin/recovery/index.js +3 -3
  228. package/dist/src/plugin/recovery/index.js.map +1 -1
  229. package/dist/src/plugin/recovery/storage.d.ts +1 -1
  230. package/dist/src/plugin/recovery/storage.d.ts.map +1 -1
  231. package/dist/src/plugin/recovery/storage.js +31 -39
  232. package/dist/src/plugin/recovery/storage.js.map +1 -1
  233. package/dist/src/plugin/recovery/types.d.ts +12 -12
  234. package/dist/src/plugin/recovery/types.d.ts.map +1 -1
  235. package/dist/src/plugin/recovery.d.ts +3 -3
  236. package/dist/src/plugin/recovery.d.ts.map +1 -1
  237. package/dist/src/plugin/recovery.js +62 -58
  238. package/dist/src/plugin/recovery.js.map +1 -1
  239. package/dist/src/plugin/refresh-queue.d.ts +5 -2
  240. package/dist/src/plugin/refresh-queue.d.ts.map +1 -1
  241. package/dist/src/plugin/refresh-queue.js +37 -15
  242. package/dist/src/plugin/refresh-queue.js.map +1 -1
  243. package/dist/src/plugin/request-helpers.d.ts +1 -1
  244. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  245. package/dist/src/plugin/request-helpers.js +451 -352
  246. package/dist/src/plugin/request-helpers.js.map +1 -1
  247. package/dist/src/plugin/request.d.ts +7 -7
  248. package/dist/src/plugin/request.d.ts.map +1 -1
  249. package/dist/src/plugin/request.js +622 -384
  250. package/dist/src/plugin/request.js.map +1 -1
  251. package/dist/src/plugin/rotation.d.ts +1 -168
  252. package/dist/src/plugin/rotation.d.ts.map +1 -1
  253. package/dist/src/plugin/rotation.js +1 -327
  254. package/dist/src/plugin/rotation.js.map +1 -1
  255. package/dist/src/plugin/search.d.ts.map +1 -1
  256. package/dist/src/plugin/search.js +39 -35
  257. package/dist/src/plugin/search.js.map +1 -1
  258. package/dist/src/plugin/server.d.ts +1 -1
  259. package/dist/src/plugin/server.d.ts.map +1 -1
  260. package/dist/src/plugin/server.js +42 -37
  261. package/dist/src/plugin/server.js.map +1 -1
  262. package/dist/src/plugin/session-context.d.ts +2 -1
  263. package/dist/src/plugin/session-context.d.ts.map +1 -1
  264. package/dist/src/plugin/session-context.js +11 -7
  265. package/dist/src/plugin/session-context.js.map +1 -1
  266. package/dist/src/plugin/storage.d.ts +59 -120
  267. package/dist/src/plugin/storage.d.ts.map +1 -1
  268. package/dist/src/plugin/storage.js +140 -485
  269. package/dist/src/plugin/storage.js.map +1 -1
  270. package/dist/src/plugin/stores/signature-store.d.ts.map +1 -1
  271. package/dist/src/plugin/stores/signature-store.js.map +1 -1
  272. package/dist/src/plugin/thinking-recovery.d.ts.map +1 -1
  273. package/dist/src/plugin/thinking-recovery.js +35 -33
  274. package/dist/src/plugin/thinking-recovery.js.map +1 -1
  275. package/dist/src/plugin/token.d.ts +2 -2
  276. package/dist/src/plugin/token.d.ts.map +1 -1
  277. package/dist/src/plugin/token.js +30 -21
  278. package/dist/src/plugin/token.js.map +1 -1
  279. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts +1 -1
  280. package/dist/src/plugin/transform/cross-model-sanitizer.js +1 -1
  281. package/dist/src/plugin/transform/index.d.ts +1 -1
  282. package/dist/src/plugin/transform/index.js +1 -1
  283. package/dist/src/plugin/transform/model-resolver.d.ts +1 -1
  284. package/dist/src/plugin/transform/model-resolver.js +1 -1
  285. package/dist/src/plugin/transform/types.d.ts +1 -1
  286. package/dist/src/plugin/transform/types.js +1 -1
  287. package/dist/src/plugin/types.d.ts +25 -69
  288. package/dist/src/plugin/types.d.ts.map +1 -1
  289. package/dist/src/plugin/ui/ansi.d.ts.map +1 -1
  290. package/dist/src/plugin/ui/ansi.js.map +1 -1
  291. package/dist/src/plugin/ui/auth-menu.d.ts +1 -1
  292. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
  293. package/dist/src/plugin/ui/auth-menu.js +84 -35
  294. package/dist/src/plugin/ui/auth-menu.js.map +1 -1
  295. package/dist/src/plugin/ui/confirm.d.ts.map +1 -1
  296. package/dist/src/plugin/ui/confirm.js.map +1 -1
  297. package/dist/src/plugin/ui/model-status.d.ts +3 -3
  298. package/dist/src/plugin/ui/model-status.d.ts.map +1 -1
  299. package/dist/src/plugin/ui/model-status.js +3 -3
  300. package/dist/src/plugin/ui/model-status.js.map +1 -1
  301. package/dist/src/plugin/ui/quota-status.d.ts +4 -4
  302. package/dist/src/plugin/ui/quota-status.d.ts.map +1 -1
  303. package/dist/src/plugin/ui/quota-status.js +49 -49
  304. package/dist/src/plugin/ui/quota-status.js.map +1 -1
  305. package/dist/src/plugin/ui/select.d.ts.map +1 -1
  306. package/dist/src/plugin/ui/select.js +22 -12
  307. package/dist/src/plugin/ui/select.js.map +1 -1
  308. package/dist/src/plugin/version.d.ts +1 -1
  309. package/dist/src/plugin/version.js +1 -1
  310. package/dist/src/rpc/notifications.d.ts +6 -0
  311. package/dist/src/rpc/notifications.d.ts.map +1 -0
  312. package/dist/src/rpc/notifications.js +46 -0
  313. package/dist/src/rpc/notifications.js.map +1 -0
  314. package/dist/src/rpc/port-file.d.ts +13 -0
  315. package/dist/src/rpc/port-file.d.ts.map +1 -0
  316. package/dist/src/rpc/port-file.js +109 -0
  317. package/dist/src/rpc/port-file.js.map +1 -0
  318. package/dist/src/rpc/protocol.d.ts +22 -0
  319. package/dist/src/rpc/protocol.d.ts.map +1 -0
  320. package/dist/src/rpc/protocol.js +1 -0
  321. package/dist/src/rpc/protocol.js.map +1 -0
  322. package/dist/src/rpc/rpc-client.d.ts +10 -0
  323. package/dist/src/rpc/rpc-client.d.ts.map +1 -0
  324. package/dist/src/rpc/rpc-client.js +58 -0
  325. package/dist/src/rpc/rpc-client.js.map +1 -0
  326. package/dist/src/rpc/rpc-dir.d.ts +4 -0
  327. package/dist/src/rpc/rpc-dir.d.ts.map +1 -0
  328. package/dist/src/rpc/rpc-dir.js +19 -0
  329. package/dist/src/rpc/rpc-dir.js.map +1 -0
  330. package/dist/src/rpc/rpc-server.d.ts +13 -0
  331. package/dist/src/rpc/rpc-server.d.ts.map +1 -0
  332. package/dist/src/rpc/rpc-server.js +231 -0
  333. package/dist/src/rpc/rpc-server.js.map +1 -0
  334. package/dist/src/sidebar-state.d.ts +357 -0
  335. package/dist/src/sidebar-state.d.ts.map +1 -0
  336. package/dist/src/sidebar-state.js +680 -0
  337. package/dist/src/sidebar-state.js.map +1 -0
  338. package/dist/src/tui/command-dialogs.d.ts +61 -0
  339. package/dist/src/tui/command-dialogs.d.ts.map +1 -0
  340. package/dist/src/tui/command-dialogs.js +595 -0
  341. package/dist/src/tui/command-dialogs.js.map +1 -0
  342. package/dist/src/tui/file-logger.d.ts +41 -0
  343. package/dist/src/tui/file-logger.d.ts.map +1 -0
  344. package/dist/src/tui/file-logger.js +114 -0
  345. package/dist/src/tui/file-logger.js.map +1 -0
  346. package/dist/src/tui-preferences.d.ts +40 -0
  347. package/dist/src/tui-preferences.d.ts.map +1 -0
  348. package/dist/src/tui-preferences.js +328 -0
  349. package/dist/src/tui-preferences.js.map +1 -0
  350. package/dist/src/tui.d.ts +68 -0
  351. package/dist/src/tui.d.ts.map +1 -0
  352. package/dist/src/tui.js +697 -0
  353. package/dist/src/tui.js.map +1 -0
  354. package/package.json +46 -22
  355. package/src/plugin/command-data.ts +931 -0
  356. package/src/rpc/port-file.ts +143 -0
  357. package/src/rpc/protocol.ts +31 -0
  358. package/src/rpc/rpc-client.ts +102 -0
  359. package/src/rpc/rpc-dir.ts +24 -0
  360. package/src/sidebar-state.ts +991 -0
  361. package/src/tui/command-dialogs.tsx +832 -0
  362. package/src/tui/entry.mjs +74 -0
  363. package/src/tui/file-logger.ts +145 -0
  364. package/src/tui-compiled/plugin/command-data.ts +931 -0
  365. package/src/tui-compiled/rpc/port-file.ts +143 -0
  366. package/src/tui-compiled/rpc/protocol.ts +31 -0
  367. package/src/tui-compiled/rpc/rpc-client.ts +102 -0
  368. package/src/tui-compiled/rpc/rpc-dir.ts +24 -0
  369. package/src/tui-compiled/sidebar-state.ts +991 -0
  370. package/src/tui-compiled/tui/command-dialogs.tsx +779 -0
  371. package/src/tui-compiled/tui/file-logger.ts +145 -0
  372. package/src/tui-compiled/tui-preferences.ts +416 -0
  373. package/src/tui-compiled/tui.tsx +1193 -0
  374. package/src/tui-preferences.ts +416 -0
  375. package/src/tui.tsx +1171 -0
  376. package/dist/src/plugin.d.ts +0 -48
  377. package/dist/src/plugin.d.ts.map +0 -1
  378. package/dist/src/plugin.js +0 -3311
  379. package/dist/src/plugin.js.map +0 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli.ts", "../../core/src/account-storage.ts", "../../core/src/atomic-write.ts", "../../core/src/file-lock.ts", "../../core/src/logger.ts", "../../core/src/auth.ts", "../../core/src/fingerprint.ts", "../../core/src/model-registry.ts", "../../core/src/account-manager.ts", "../../core/src/agy-request-metadata.ts", "../../core/src/agy-transport.ts", "../../core/src/antigravity/oauth.ts", "../../core/src/constants.ts", "../../core/src/fetch-timeout.ts", "../../core/src/project.ts", "../../core/src/quota-manager.ts", "../../core/src/transform/model-resolver.ts", "../src/plugin/oauth-login.ts", "../src/plugin/storage.ts", "../src/plugin/debug.ts", "../src/plugin/logging-utils.ts", "../src/plugin/logger.ts", "../src/plugin/persist-account-pool.ts", "../../../node_modules/.bun/xdg-basedir@5.1.0/node_modules/xdg-basedir/index.js", "../src/sidebar-state.ts", "../src/plugin/cache.ts", "../src/plugin/token.ts", "../src/plugin/quota.ts", "../src/plugin/server.ts"],
4
+ "sourcesContent": ["import { execFile } from 'node:child_process'\nimport { createInterface } from 'node:readline/promises'\nimport { promisify } from 'node:util'\n\nimport type {\n AccountMetadataV3,\n AccountQuotaResult,\n AccountStorageV4,\n} from '@cortexkit/antigravity-auth-core'\nimport { authorizeAntigravity, exchangeAntigravity } from './antigravity/oauth'\nimport {\n type AntigravityTokenExchangeSuccess,\n type OAuthLoginRequest,\n performOAuthLogin,\n} from './plugin/oauth-login'\nimport { persistAccountPool } from './plugin/persist-account-pool'\nimport { checkAccountsQuotaStandalone } from './plugin/quota'\nimport { startOAuthListener } from './plugin/server'\nimport { loadAccounts } from './plugin/storage'\n\ninterface WritableOutput {\n write(value: string): unknown\n}\n\nexport interface CliDependencies {\n stdout: WritableOutput\n stderr: WritableOutput\n prompt(message: string): Promise<string>\n openBrowser(url: string): Promise<void>\n isHeadless?(): boolean\n performLogin(\n request: OAuthLoginRequest,\n openBrowser: (url: string) => Promise<void>,\n ): Promise<AntigravityTokenExchangeSuccess>\n loadAccounts(): Promise<AccountStorageV4 | null>\n getQuota(\n accounts: AccountMetadataV3[],\n options: { refresh: boolean },\n ): Promise<AccountQuotaResult[]>\n}\n\nconst HELP = `Usage: antigravity-auth <command> [options]\n\nCommands:\n login [--project <id>] [--no-browser]\n list [--json]\n quota [--json] [--refresh]\n\nOptions:\n --help Show help\n`\n\ntype ParsedCommand =\n | { command: 'help' }\n | { command: 'login'; projectId?: string; noBrowser: boolean }\n | { command: 'list'; json: boolean }\n | { command: 'quota'; json: boolean; refresh: boolean }\n\ntype ParseResult =\n | { ok: true; value: ParsedCommand }\n | { ok: false; error: string }\n\nfunction parseArgs(argv: string[]): ParseResult {\n const [command, ...args] = argv\n if (command === '--help') {\n return args.length === 0\n ? { ok: true, value: { command: 'help' } }\n : { ok: false, error: `Unknown argument: ${args[0]}` }\n }\n if (!command) return { ok: false, error: 'Missing command' }\n\n if (command === 'login') {\n let projectId: string | undefined\n let noBrowser = false\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index]\n if (arg === '--no-browser') {\n noBrowser = true\n continue\n }\n if (arg === '--project') {\n const value = args[index + 1]\n if (!value || value.startsWith('--')) {\n return { ok: false, error: 'Missing value for --project' }\n }\n projectId = value\n index += 1\n continue\n }\n return { ok: false, error: `Unknown option for login: ${arg}` }\n }\n return { ok: true, value: { command, projectId, noBrowser } }\n }\n\n if (command === 'list') {\n let json = false\n for (const arg of args) {\n if (arg === '--json') json = true\n else return { ok: false, error: `Unknown option for list: ${arg}` }\n }\n return { ok: true, value: { command, json } }\n }\n\n if (command === 'quota') {\n let json = false\n let refresh = false\n for (const arg of args) {\n if (arg === '--json') json = true\n else if (arg === '--refresh') refresh = true\n else return { ok: false, error: `Unknown option for quota: ${arg}` }\n }\n return { ok: true, value: { command, json, refresh } }\n }\n\n return { ok: false, error: `Unknown command: ${command}` }\n}\n\nfunction accountStatus(account: AccountMetadataV3): string {\n if (account.enabled === false) return 'disabled'\n if (account.accountIneligible) return 'ineligible'\n if (account.verificationRequired) return 'verification-required'\n return 'active'\n}\n\nfunction accountSummary(storage: AccountStorageV4 | null) {\n return {\n accounts: (storage?.accounts ?? []).map((account, index) => ({\n index: index + 1,\n email: account.email ?? `Account ${index + 1}`,\n status: accountStatus(account),\n })),\n }\n}\n\nfunction formatTable(headers: string[], rows: string[][]): string {\n const widths = headers.map((header, column) =>\n Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)),\n )\n const formatRow = (row: string[]) =>\n row\n .map((value, column) =>\n column === row.length - 1\n ? value\n : value.padEnd((widths[column] ?? value.length) + 2),\n )\n .join('')\n return `${[formatRow(headers), ...rows.map(formatRow)].join('\\n')}\\n`\n}\n\nfunction quotaSummary(results: AccountQuotaResult[]) {\n return {\n accounts: results.map((result) => ({\n index: result.index + 1,\n email: result.email ?? `Account ${result.index + 1}`,\n status: result.status,\n ...(result.error ? { error: result.error } : {}),\n groups: Object.entries(result.quota?.groups ?? {}).map(\n ([name, group]) => ({\n name,\n ...(typeof group.remainingFraction === 'number'\n ? { remainingPercent: group.remainingFraction * 100 }\n : {}),\n ...(group.resetTime ? { resetTime: group.resetTime } : {}),\n }),\n ),\n })),\n }\n}\n\nfunction formatQuotaTable(results: AccountQuotaResult[]): string {\n const rows: string[][] = []\n for (const account of quotaSummary(results).accounts) {\n if (account.groups.length === 0) {\n rows.push([account.email, account.status, '-', '-', account.error ?? '-'])\n continue\n }\n for (const group of account.groups) {\n rows.push([\n account.email,\n account.status,\n group.name,\n group.remainingPercent === undefined\n ? '-'\n : `${group.remainingPercent}%`,\n group.resetTime ?? '-',\n ])\n }\n }\n return formatTable(['ACCOUNT', 'STATUS', 'GROUP', 'REMAINING', 'RESET'], rows)\n}\n\nexport async function runCli(\n argv: string[],\n deps: CliDependencies,\n): Promise<number> {\n const parsed = parseArgs(argv)\n if (!parsed.ok) {\n deps.stderr.write(`${parsed.error}\\n`)\n return 2\n }\n\n if (parsed.value.command === 'help') {\n deps.stdout.write(HELP)\n return 0\n }\n\n try {\n if (parsed.value.command === 'login') {\n const result = await deps.performLogin(\n {\n projectId: parsed.value.projectId,\n noBrowser: parsed.value.noBrowser,\n isHeadless: deps.isHeadless?.() ?? false,\n refreshAccountIndex: undefined,\n accounts: [],\n startFresh: true,\n },\n deps.openBrowser,\n )\n deps.stdout.write(\n `Authenticated ${result.email ?? 'Antigravity account'}\\n`,\n )\n return 0\n }\n\n const storage = await deps.loadAccounts()\n if (parsed.value.command === 'list') {\n const summary = accountSummary(storage)\n deps.stdout.write(\n parsed.value.json\n ? `${JSON.stringify(summary)}\\n`\n : formatTable(\n ['INDEX', 'EMAIL', 'STATUS'],\n summary.accounts.map((account) => [\n String(account.index),\n account.email,\n account.status,\n ]),\n ),\n )\n return 0\n }\n\n const results = await deps.getQuota(storage?.accounts ?? [], {\n refresh: parsed.value.refresh,\n })\n deps.stdout.write(\n parsed.value.json\n ? `${JSON.stringify(quotaSummary(results))}\\n`\n : formatQuotaTable(results),\n )\n return 0\n } catch (error) {\n deps.stderr.write(\n `${error instanceof Error ? error.message : String(error)}\\n`,\n )\n return 1\n }\n}\n\nconst execFileAsync = promisify(execFile)\n\nasync function openBrowserDefault(url: string): Promise<void> {\n if (process.platform === 'win32') {\n await execFileAsync('cmd', ['/c', 'start', '', url])\n return\n }\n await execFileAsync(process.platform === 'darwin' ? 'open' : 'xdg-open', [\n url,\n ])\n}\n\nexport function createDefaultCliDependencies(): CliDependencies {\n return {\n stdout: process.stdout,\n stderr: process.stderr,\n prompt: async (message) => {\n const readline = createInterface({\n input: process.stdin,\n output: process.stdout,\n })\n try {\n return (await readline.question(message)).trim()\n } finally {\n readline.close()\n }\n },\n openBrowser: openBrowserDefault,\n isHeadless: () =>\n Boolean(\n process.env.SSH_CONNECTION ||\n process.env.SSH_CLIENT ||\n process.env.SSH_TTY ||\n process.env.OPENCODE_HEADLESS,\n ),\n performLogin: async (request, openBrowser) =>\n performOAuthLogin(request, {\n authorize: authorizeAntigravity,\n exchange: exchangeAntigravity,\n startListener: startOAuthListener,\n openBrowser,\n upsert: async (result) => {\n await persistAccountPool(\n [result],\n request.startFresh && request.accounts.length === 0,\n )\n },\n }),\n loadAccounts,\n getQuota: (accounts, options) =>\n checkAccountsQuotaStandalone(accounts, options),\n }\n}\n\nif (import.meta.main) {\n process.exitCode = await runCli(\n process.argv.slice(2),\n createDefaultCliDependencies(),\n )\n}\n\nexport { performOAuthLogin }\n", "/**\n * Lock-held account storage engine.\n *\n * Owns the on-disk schema for the multi-account pool (v4) plus every\n * migration from older versions, the load/merge/save primitives, and a\n * `mutateAccountStorage` entry point that holds a fenced file lock for\n * the duration of read-modify-write. Concurrent writes retry up to a\n * bounded schedule (`100, 200, 400, 800, 1000ms` with factor 2 / max 1000)\n * before surfacing a typed `AccountStorageLockContentionError`.\n *\n * Fail-closed semantics: when the accounts file exists but cannot be\n * read as a valid v4 (parse error, schema mismatch, unknown future\n * version, I/O error), `mutateAccountStorage` and `loadAccountStorage`\n * throw a typed `AccountStorageUnreadableError` instead of treating\n * the bad state as \"empty pool\" and overwriting the file on the next\n * write. A best-effort `.corrupt-<ISO-timestamp>` backup is created\n * before throwing so a future bug can never permanently destroy user\n * data. The backup itself never throws.\n *\n * This module is harness-agnostic: it does not own a path (the harness\n * adapter passes one in via `loadAccountStorage(path)`). The harness is\n * responsible for picking the right on-disk path and ensuring the parent\n * directory exists.\n */\n\nimport { chmod, copyFile, mkdir, readFile, unlink } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type {\n AccountMetadataV3,\n AccountStorageV3,\n AccountStorageV4,\n AnyAccountStorage,\n RateLimitStateV2,\n RateLimitStateV3,\n} from './account-types.ts'\nimport { writeJsonAtomic } from './atomic-write.ts'\nimport { acquireFencedFileLock, type FencedFileLock } from './file-lock.ts'\nimport { createLogger } from './logger.ts'\n\nconst log = createLogger('account-storage')\n\n/**\n * Thrown when `mutateAccountStorage` exhausts its initial attempt plus\n * five retries against a lock that is still held. Surfaces a typed\n * signal harnesses can distinguish from transient I/O errors.\n */\nexport class AccountStorageLockContentionError extends Error {\n readonly details: { path: string; attempts: number }\n\n constructor(\n message: string,\n details: AccountStorageLockContentionError['details'],\n ) {\n super(message)\n this.name = 'AccountStorageLockContentionError'\n this.details = details\n }\n}\n\n/**\n * Reason the on-disk accounts file could not be parsed as a usable v4.\n * Harnesses distinguish between `malformed-json` (the file is broken\n * JSON \u2014 possibly truncated), `invalid-shape` (JSON parsed but the\n * shape does not match the storage schema), `unsupported-version` (a\n * version newer than v4 that this build cannot migrate), and `io-error`\n * (a real I/O failure other than ENOENT \u2014 typically EACCES).\n */\nexport type AccountStorageUnreadableReason =\n | 'malformed-json'\n | 'invalid-shape'\n | 'unsupported-version'\n | 'io-error'\n\n/**\n * Thrown when the on-disk accounts file exists but cannot be read as\n * a valid v4. Distinguishes ENOENT (first-run UX: missing file is\n * fine) from \"the file is there but we can't trust it\" (fail closed).\n *\n * `backupPath` is set when a `.corrupt-<ISO-timestamp>` sidecar was\n * successfully written; consumers should mention it in any user-facing\n * recovery message so the user knows their data is preserved.\n */\nexport class AccountStorageUnreadableError extends Error {\n readonly details: {\n path: string\n reason: AccountStorageUnreadableReason\n detail: string\n backupPath: string | null\n }\n\n constructor(\n message: string,\n details: AccountStorageUnreadableError['details'],\n ) {\n super(message)\n this.name = 'AccountStorageUnreadableError'\n this.details = details\n }\n}\n\n/**\n * Discriminated result of an internal read attempt. Used by the\n * mutation path so it can distinguish first-run (missing file is fine)\n * from \"file is corrupt, refuse to write\" without overloading the\n * public `null` contract that legitimate callers still expect.\n */\ntype StorageReadOutcome =\n | { state: 'missing' }\n | { state: 'ok'; storage: AccountStorageV4 }\n | {\n state: 'unreadable'\n reason: AccountStorageUnreadableReason\n detail: string\n }\n\nexport interface AccountStorageOptions {\n /**\n * Sleep override for deterministic retry timing in tests. Defaults to\n * a real `setTimeout`-based sleep.\n */\n sleep?: (ms: number) => Promise<void>\n /**\n * Backup override for deterministic backup paths in tests. Defaults\n * to `path + '.corrupt-<ISO-timestamp>'` with the time captured at\n * call time. Returning `null` skips the backup.\n */\n buildBackupPath?: (path: string, now: Date) => string | null\n /**\n * Clock override for deterministic timestamps in tests. Defaults to\n * `() => new Date()`.\n */\n now?: () => Date\n}\n\n/**\n * Configuration for the lock-acquisition retry schedule. Mirrors the\n * legacy wait-and-retry schedule (5 retries, 100ms min, 1000ms max,\n * factor 2) so an immediate contention throw is a regression.\n */\nconst RETRY_DELAYS_MS = [100, 200, 400, 800, 1000] as const\n\nconst DEFAULT_SLEEP = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms))\n\nconst DEFAULT_BUILD_BACKUP_PATH = (path: string, now: Date): string =>\n `${path}.corrupt-${now.toISOString().replace(/[:.]/g, '-')}`\n\nasync function ensureSecurePermissions(path: string): Promise<void> {\n try {\n await chmod(path, 0o600)\n } catch {\n // best-effort; Windows + non-POSIX FS ignore this.\n }\n}\n\nasync function _ensureFileExists(path: string): Promise<void> {\n try {\n await readFile(path)\n } catch {\n await mkdir(dirname(path), { recursive: true })\n await writeJsonAtomic(path, { version: 4, accounts: [], activeIndex: 0 })\n }\n}\n\n/**\n * Copy a corrupt file to a `.corrupt-<ISO-timestamp>` sidecar. The\n * backup itself never throws \u2014 if it fails (e.g. disk full, permission\n * denied on the sidecar path), the original unreadable error is what\n * the caller sees. Returns the backup path on success, `null` on\n * failure.\n */\nasync function backupCorruptFile(\n sourcePath: string,\n buildBackupPath: (path: string, now: Date) => string | null,\n now: Date,\n): Promise<string | null> {\n const backupPath = buildBackupPath(sourcePath, now)\n if (!backupPath) return null\n try {\n await copyFile(sourcePath, backupPath)\n await ensureSecurePermissions(backupPath)\n return backupPath\n } catch (backupError) {\n log.warn('Failed to back up corrupt account storage file', {\n sourcePath,\n backupPath,\n error: String(backupError),\n })\n return null\n }\n}\n\n/**\n * Deduplicate accounts that share an email, keeping the entry with the\n * newest `lastUsed` then `addedAt`. Order of the kept entries in the\n * output array is determined by the position of the *newest* matching\n * account in the input array (not necessarily the original positions).\n */\nexport function deduplicateAccountsByEmail<\n T extends { email?: string; lastUsed?: number; addedAt?: number },\n>(accounts: T[]): T[] {\n const emailToNewestIndex = new Map<string, number>()\n const indicesToKeep = new Set<number>()\n\n for (let i = 0; i < accounts.length; i++) {\n const acc = accounts[i]\n if (!acc) continue\n\n if (!acc.email) {\n indicesToKeep.add(i)\n continue\n }\n\n const existingIndex = emailToNewestIndex.get(acc.email)\n if (existingIndex === undefined) {\n emailToNewestIndex.set(acc.email, i)\n continue\n }\n\n const existing = accounts[existingIndex]\n if (!existing) {\n emailToNewestIndex.set(acc.email, i)\n continue\n }\n\n const currLastUsed = acc.lastUsed || 0\n const existLastUsed = existing.lastUsed || 0\n const currAddedAt = acc.addedAt || 0\n const existAddedAt = existing.addedAt || 0\n\n const isNewer =\n currLastUsed > existLastUsed ||\n (currLastUsed === existLastUsed && currAddedAt > existAddedAt)\n\n if (isNewer) {\n emailToNewestIndex.set(acc.email, i)\n }\n }\n\n for (const idx of emailToNewestIndex.values()) {\n indicesToKeep.add(idx)\n }\n\n const result: T[] = []\n for (let i = 0; i < accounts.length; i++) {\n if (indicesToKeep.has(i)) {\n const acc = accounts[i]\n if (acc) {\n result.push(acc)\n }\n }\n }\n\n return result\n}\n\nfunction migrateV1ToV2(\n v1: Extract<AnyAccountStorage, { version: 1 }>,\n): Extract<AnyAccountStorage, { version: 2 }> {\n return {\n version: 2,\n accounts: v1.accounts.map((acc) => {\n const rateLimitResetTimes: RateLimitStateV2 = {}\n if (\n acc.isRateLimited &&\n acc.rateLimitResetTime &&\n acc.rateLimitResetTime > Date.now()\n ) {\n rateLimitResetTimes.claude = acc.rateLimitResetTime\n rateLimitResetTimes.gemini = acc.rateLimitResetTime\n }\n return {\n email: acc.email,\n refreshToken: acc.refreshToken,\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n addedAt: acc.addedAt,\n lastUsed: acc.lastUsed,\n lastSwitchReason: acc.lastSwitchReason,\n rateLimitResetTimes:\n Object.keys(rateLimitResetTimes).length > 0\n ? rateLimitResetTimes\n : undefined,\n }\n }),\n activeIndex: v1.activeIndex,\n }\n}\n\nexport function migrateV2ToV3(\n v2: Extract<AnyAccountStorage, { version: 2 }>,\n): AccountStorageV3 {\n return {\n version: 3,\n accounts: v2.accounts.map((acc) => {\n const rateLimitResetTimes: RateLimitStateV3 = {}\n if (\n acc.rateLimitResetTimes?.claude &&\n acc.rateLimitResetTimes.claude > Date.now()\n ) {\n rateLimitResetTimes.claude = acc.rateLimitResetTimes.claude\n }\n if (\n acc.rateLimitResetTimes?.gemini &&\n acc.rateLimitResetTimes.gemini > Date.now()\n ) {\n rateLimitResetTimes['gemini-antigravity'] =\n acc.rateLimitResetTimes.gemini\n }\n return {\n email: acc.email,\n refreshToken: acc.refreshToken,\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n addedAt: acc.addedAt,\n lastUsed: acc.lastUsed,\n lastSwitchReason: acc.lastSwitchReason,\n rateLimitResetTimes:\n Object.keys(rateLimitResetTimes).length > 0\n ? rateLimitResetTimes\n : undefined,\n }\n }),\n activeIndex: v2.activeIndex,\n }\n}\n\nfunction migrateV3ToV4(v3: AccountStorageV3): AccountStorageV4 {\n return {\n version: 4,\n accounts: v3.accounts.map((acc) => ({\n ...acc,\n fingerprint: undefined,\n fingerprintHistory: undefined,\n })),\n activeIndex: v3.activeIndex,\n activeIndexByFamily: v3.activeIndexByFamily,\n }\n}\n\n/**\n * Merge two v4 account pools keyed by `refreshToken`. Preserves\n * `projectId`/`managedProjectId` from either side when the incoming\n * payload omits them. Eligibility state survives via a per-field\n * `eligibilityStateUpdatedAt` comparison so a stale concurrent writer\n * cannot regress an explicit ineligible decision.\n */\nexport function mergeAccountStorage(\n existing: AccountStorageV4,\n incoming: AccountStorageV4,\n): AccountStorageV4 {\n const accountMap = new Map<string, AccountMetadataV3>()\n\n for (const acc of existing.accounts) {\n if (acc.refreshToken) {\n accountMap.set(acc.refreshToken, acc)\n }\n }\n\n for (const acc of incoming.accounts) {\n if (!acc.refreshToken) continue\n const existingAcc = accountMap.get(acc.refreshToken)\n if (existingAcc) {\n const eligibilitySource =\n (acc.eligibilityStateUpdatedAt ?? 0) >=\n (existingAcc.eligibilityStateUpdatedAt ?? 0)\n ? acc\n : existingAcc\n const merged: AccountMetadataV3 = {\n ...existingAcc,\n ...acc,\n projectId: acc.projectId ?? existingAcc.projectId,\n managedProjectId: acc.managedProjectId ?? existingAcc.managedProjectId,\n rateLimitResetTimes: {\n ...existingAcc.rateLimitResetTimes,\n ...acc.rateLimitResetTimes,\n },\n lastUsed: Math.max(existingAcc.lastUsed || 0, acc.lastUsed || 0),\n accountIneligible: eligibilitySource.accountIneligible,\n accountIneligibleAt: eligibilitySource.accountIneligibleAt,\n accountIneligibleReason: eligibilitySource.accountIneligibleReason,\n eligibilityStateUpdatedAt: eligibilitySource.eligibilityStateUpdatedAt,\n }\n if (merged.accountIneligible) {\n merged.enabled = false\n }\n accountMap.set(acc.refreshToken, merged)\n } else {\n accountMap.set(acc.refreshToken, acc)\n }\n }\n\n return {\n version: 4,\n accounts: Array.from(accountMap.values()),\n activeIndex: incoming.activeIndex,\n activeIndexByFamily: incoming.activeIndexByFamily,\n }\n}\n\n/**\n * Read the on-disk file and reduce it to a `StorageReadOutcome`. Used\n * by `mutateAccountStorage` (which needs to distinguish \"missing \u2192\n * first-run empty pool\" from \"exists but unreadable \u2192 refuse to write\")\n * and by `loadAccountStorage` (which maps unreadable to a thrown error).\n */\nasync function readAndNormalizeV4(path: string): Promise<StorageReadOutcome> {\n let raw: string\n try {\n raw = await readFile(path, 'utf-8')\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (code === 'ENOENT') {\n return { state: 'missing' }\n }\n return {\n state: 'unreadable',\n reason: 'io-error',\n detail: `${code ?? 'UNKNOWN'}: ${(error as Error).message ?? String(error)}`,\n }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (parseError) {\n return {\n state: 'unreadable',\n reason: 'malformed-json',\n detail: (parseError as Error).message ?? String(parseError),\n }\n }\n\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return {\n state: 'unreadable',\n reason: 'invalid-shape',\n detail: 'top-level value is not an object',\n }\n }\n\n const candidate = parsed as { accounts?: unknown; version?: unknown }\n if (!Array.isArray(candidate.accounts)) {\n return {\n state: 'unreadable',\n reason: 'invalid-shape',\n detail: '`accounts` is missing or not an array',\n }\n }\n\n const parsedVersion = candidate.version\n if (\n parsedVersion !== 1 &&\n parsedVersion !== 2 &&\n parsedVersion !== 3 &&\n parsedVersion !== 4\n ) {\n return {\n state: 'unreadable',\n reason: 'unsupported-version',\n detail: `unsupported version: ${String(parsedVersion)}`,\n }\n }\n\n let storage: AccountStorageV4 | null = null\n switch (parsedVersion) {\n case 1:\n storage = migrateV3ToV4(\n migrateV2ToV3(\n migrateV1ToV2(parsed as Extract<AnyAccountStorage, { version: 1 }>),\n ),\n )\n break\n case 2:\n storage = migrateV3ToV4(\n migrateV2ToV3(parsed as Extract<AnyAccountStorage, { version: 2 }>),\n )\n break\n case 3:\n storage = migrateV3ToV4(parsed as AccountStorageV3)\n break\n case 4:\n storage = parsed as AccountStorageV4\n break\n }\n\n if (!storage) {\n return {\n state: 'unreadable',\n reason: 'unsupported-version',\n detail: `unhandled version after switch: ${String(parsedVersion)}`,\n }\n }\n\n // Strict per-record validation. A v4 record must be a non-null object\n // with a string `refreshToken`, finite numeric `addedAt` and `lastUsed`.\n // Legacy versions (v1/v2/v3) reach this point only after migration,\n // which already threw on a non-object entry and produced a populated\n // refreshToken by reading it directly \u2014 so a record that survived\n // migration is fine. We do NOT validate legacy versions BEFORE\n // migration because the migration itself enforces the required fields\n // by reading them off the input shape.\n for (let i = 0; i < storage.accounts.length; i++) {\n const detail = validateV4AccountRecord(storage.accounts[i], i)\n if (detail !== null) {\n return {\n state: 'unreadable',\n reason: 'invalid-shape',\n detail,\n }\n }\n }\n\n const deduplicatedAccounts = deduplicateAccountsByEmail(storage.accounts)\n\n let activeIndex =\n typeof storage.activeIndex === 'number' &&\n Number.isFinite(storage.activeIndex)\n ? storage.activeIndex\n : 0\n if (deduplicatedAccounts.length > 0) {\n activeIndex = Math.min(activeIndex, deduplicatedAccounts.length - 1)\n activeIndex = Math.max(activeIndex, 0)\n } else {\n activeIndex = 0\n }\n\n return {\n state: 'ok',\n storage: {\n version: 4,\n accounts: deduplicatedAccounts,\n activeIndex,\n activeIndexByFamily: storage.activeIndexByFamily,\n },\n }\n}\n\n/**\n * Return a human-readable detail string when `record` is not a valid\n * v4 account record, `null` when it is. Used by `readAndNormalizeV4`\n * to surface per-record shape problems as `invalid-shape`\n * `AccountStorageUnreadableError` instances instead of silently\n * filtering them out \u2014 the latter used to drop a malformed account\n * without any user-visible signal and risked losing data.\n */\nfunction validateV4AccountRecord(\n record: unknown,\n index: number,\n): string | null {\n if (record === null || typeof record !== 'object' || Array.isArray(record)) {\n return `accounts[${index}] is not an object`\n }\n const acc = record as Record<string, unknown>\n if (typeof acc.refreshToken !== 'string' || acc.refreshToken.length === 0) {\n return `accounts[${index}].refreshToken is missing or not a non-empty string`\n }\n if (typeof acc.addedAt !== 'number' || !Number.isFinite(acc.addedAt)) {\n return `accounts[${index}].addedAt is missing or not a finite number`\n }\n if (typeof acc.lastUsed !== 'number' || !Number.isFinite(acc.lastUsed)) {\n return `accounts[${index}].lastUsed is missing or not a finite number`\n }\n return null\n}\n\n/**\n * Load the account pool from `path`, migrating older versions in-place\n * to v4 and persisting the migrated copy.\n *\n * Returns `null` when the file does not exist (first-run UX).\n *\n * Throws `AccountStorageUnreadableError` when the file exists but\n * cannot be read as a valid v4 \u2014 callers must NOT treat this as an\n * empty pool, or they will silently destroy the user's data on the\n * next write.\n */\nexport async function loadAccountStorage(\n path: string,\n): Promise<AccountStorageV4 | null> {\n const buildBackupPath = DEFAULT_BUILD_BACKUP_PATH\n const now = () => new Date()\n await ensureSecurePermissions(path)\n\n const outcome = await readAndNormalizeV4(path)\n if (outcome.state === 'missing') {\n return null\n }\n if (outcome.state === 'unreadable') {\n const backupPath = await backupCorruptFile(path, buildBackupPath, now())\n throw new AccountStorageUnreadableError(\n `Account storage at ${path} is unreadable (${outcome.reason}: ${outcome.detail}).` +\n (backupPath\n ? ` A backup was written to ${backupPath}. The plugin will refuse to write until the file is removed or repaired.`\n : ' A backup could not be written; the file has been left in place. The plugin will refuse to write until the file is removed or repaired.'),\n {\n path,\n reason: outcome.reason,\n detail: outcome.detail,\n backupPath,\n },\n )\n }\n\n // Persist the migrated copy for v1/v2/v3 so subsequent loads are\n // a single pass. Migration never changes semantics; a write failure\n // is logged but not fatal \u2014 the in-memory result is still correct.\n if (outcome.storage.version === 4) {\n const onDiskVersion = await readVersionOnly(path)\n if (onDiskVersion !== null && onDiskVersion !== 4) {\n try {\n await saveAccountStorage(path, outcome.storage)\n log.info('Migration to v4 complete')\n } catch (saveError) {\n log.warn('Failed to persist migrated storage', {\n error: String(saveError),\n })\n }\n }\n }\n\n return outcome.storage\n}\n\nasync function readVersionOnly(path: string): Promise<number | null> {\n try {\n const raw = await readFile(path, 'utf-8')\n const parsed = JSON.parse(raw) as { version?: unknown }\n return typeof parsed.version === 'number' ? parsed.version : null\n } catch {\n return null\n }\n}\n\n/**\n * Acquire the lock for `path`, retrying on contention up to the legacy\n * wait-and-retry schedule (`100, 200, 400, 800, 1000ms`) so an\n * immediate contention throw is a regression. Throws a typed\n * `AccountStorageLockContentionError` after the initial attempt plus\n * five retries \u2014 but only when the failure mode is \"another writer\n * holds the lock\" (`null` from `acquireFencedFileLock`). Real I/O\n * errors (permission denied, missing path, disk failure) are rethrown\n * immediately so callers can distinguish them from contention.\n */\nasync function acquireWithRetry(\n path: string,\n sleep: (ms: number) => Promise<void>,\n): Promise<FencedFileLock> {\n for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {\n // acquireFencedFileLock throws on real I/O failure (permission,\n // missing dir, disk error); it only returns `null` for a live,\n // contended lock. Rethrow immediately so callers don't mistake a\n // missing config dir for \"someone else is writing\".\n const lock = await acquireFencedFileLock({\n path,\n name: 'accounts',\n ttlMs: 10_000,\n renew: true,\n })\n if (lock) {\n return lock\n }\n\n if (attempt < RETRY_DELAYS_MS.length) {\n const delay = RETRY_DELAYS_MS[attempt]\n if (delay !== undefined) {\n await sleep(delay)\n }\n }\n }\n\n throw new AccountStorageLockContentionError(\n `account storage lock contention at ${path} after ${RETRY_DELAYS_MS.length + 1} attempts`,\n {\n path,\n attempts: RETRY_DELAYS_MS.length + 1,\n },\n )\n}\n\n/**\n * Run `mutate` against the freshest persisted pool while holding the\n * file lock. The mutator may return a partial v4 (or `undefined` to\n * keep the input unchanged) and is guaranteed to see the post-migration\n * v4 shape.\n *\n * Throws `AccountStorageLockContentionError` after exhausting the\n * retry schedule against a lock that is still held by another writer.\n *\n * Throws `AccountStorageUnreadableError` when the file exists but\n * cannot be read as a valid v4. In that case the file is first copied\n * to a `.corrupt-<ISO-timestamp>` sidecar (best-effort) and the write\n * is aborted \u2014 a user with a corrupt-but-recoverable accounts file (or\n * one written by a newer plugin version) who adds one account MUST NOT\n * have their entire pool silently destroyed.\n */\nexport async function mutateAccountStorage(\n path: string,\n mutate: (\n current: AccountStorageV4,\n ) => AccountStorageV4 | undefined | Promise<AccountStorageV4 | undefined>,\n options: AccountStorageOptions = {},\n): Promise<AccountStorageV4> {\n const sleep = options.sleep ?? DEFAULT_SLEEP\n const buildBackupPath = options.buildBackupPath ?? DEFAULT_BUILD_BACKUP_PATH\n const now = options.now ?? (() => new Date())\n const lock = await acquireWithRetry(path, sleep)\n\n try {\n const outcome = await readAndNormalizeV4(path)\n\n if (outcome.state === 'unreadable') {\n const backupPath = await backupCorruptFile(path, buildBackupPath, now())\n throw new AccountStorageUnreadableError(\n `Refusing to write: account storage at ${path} is unreadable (${outcome.reason}: ${outcome.detail}).` +\n (backupPath\n ? ` A backup of the existing file was written to ${backupPath} and the on-disk file has been left untouched. Repair or remove the existing file before retrying.`\n : ' A backup could not be written; the on-disk file has been left untouched. Repair or remove the existing file before retrying.'),\n {\n path,\n reason: outcome.reason,\n detail: outcome.detail,\n backupPath,\n },\n )\n }\n\n const existing: AccountStorageV4 =\n outcome.state === 'ok'\n ? outcome.storage\n : { version: 4, accounts: [], activeIndex: 0 }\n\n // The mutator may be sync or async; awaiting its result lets long\n // mutators hold the lock for the duration of their work (the OS-level\n // create-lock + renewal keeps us exclusive that whole time).\n const next = await mutate(existing)\n const finalStorage: AccountStorageV4 = next ?? existing\n\n await lock.assertOwned()\n await writeJsonAtomic(path, finalStorage)\n\n return finalStorage\n } finally {\n try {\n await lock.release()\n } catch (releaseError) {\n log.warn('Failed to release account storage lock', {\n error: String(releaseError),\n })\n }\n }\n}\n\n/**\n * Merge `incoming` into the persisted pool under the lock. Returns the\n * merged v4 result that was written to disk.\n */\nexport async function saveAccountStorage(\n path: string,\n incoming: AccountStorageV4,\n): Promise<AccountStorageV4> {\n return mutateAccountStorage(path, (current) =>\n mergeAccountStorage(current, incoming),\n )\n}\n\n/**\n * Write `incoming` to disk unconditionally (no merge). Required for\n * destructive operations like delete where the next-state must replace\n * \u2014 never be merged with \u2014 what is on disk.\n */\nexport async function saveAccountStorageReplace(\n path: string,\n incoming: AccountStorageV4,\n): Promise<AccountStorageV4> {\n return mutateAccountStorage(path, () => incoming)\n}\n\n/**\n * Unlink the persisted pool while holding the lock so a concurrent\n * debounced save cannot resurrect the pool mid-clear. Missing files are\n * treated as a successful no-op (matches the legacy semantics).\n */\nexport async function clearAccountStorage(path: string): Promise<void> {\n const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms))\n const lock = await acquireWithRetry(path, sleep)\n\n try {\n await unlink(path)\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (code !== 'ENOENT') {\n log.error('Failed to clear account storage', { error: String(error) })\n throw error\n }\n } finally {\n try {\n await lock.release()\n } catch (releaseError) {\n log.warn('Failed to release account storage lock', {\n error: String(releaseError),\n })\n }\n }\n}\n\n/**\n * Bundle the harness-facing storage primitives. Used by `AccountManager`\n * so tests can inject a fake store without going through the filesystem.\n */\nexport interface AccountStorageStore {\n load: (path: string) => Promise<AccountStorageV4 | null>\n saveMerged: (\n path: string,\n next: AccountStorageV4,\n ) => Promise<AccountStorageV4>\n mutate: (\n path: string,\n fn: (\n current: AccountStorageV4,\n ) => AccountStorageV4 | undefined | Promise<AccountStorageV4 | undefined>,\n options?: AccountStorageOptions,\n ) => Promise<AccountStorageV4>\n clear: (path: string) => Promise<void>\n}\n\nexport const defaultAccountStorageStore: AccountStorageStore = {\n load: loadAccountStorage,\n saveMerged: saveAccountStorage,\n mutate: mutateAccountStorage,\n clear: clearAccountStorage,\n}\n", "/**\n * Atomic JSON file writer.\n *\n * Stages the payload at `${path}.${randomUUID()}.tmp` (same directory as\n * the target so the rename is atomic on POSIX, and uses the same NTFS\n * volume on Windows) and renames onto `path`. The `0o600` mode is enforced\n * on the staged file; on POSIX the rename replaces the target's inode so\n * the new file inherits the staged mode bits. Windows ignores POSIX mode\n * bits and relies on the current user's inherited ACL \u2014 we do not attempt\n * to harden ACLs from Node.\n *\n * Failures are deliberately NOT masked by a copy fallback: a blind\n * copy-then-unlink after a failing replace can paper over partial writes\n * and let concurrent writers silently corrupt state. The caller decides\n * whether to retry, surface, or back off.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { mkdir, rename, rm, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\n/**\n * Atomically serialize `value` as pretty-printed JSON and rename it onto\n * `path`. Throws if any step fails; the staged temp file is removed before\n * rethrow so no `${path}.<uuid>.tmp` is left behind.\n */\nexport async function writeJsonAtomic(\n path: string,\n value: unknown,\n): Promise<void> {\n const serialized = `${JSON.stringify(value, null, 2)}\\n`\n const tempPath = `${path}.${randomUUID()}.tmp`\n\n await mkdir(dirname(path), { recursive: true })\n\n let renamed = false\n try {\n await writeFile(tempPath, serialized, {\n encoding: 'utf8',\n mode: 0o600,\n })\n await rename(tempPath, path)\n renamed = true\n } finally {\n if (!renamed) {\n // Cleanup the staged temp whether `writeFile` threw after a partial\n // write or `rename` failed. `force: true` makes the call a no-op\n // when the file never landed on disk.\n await rm(tempPath, { force: true }).catch(() => {})\n }\n }\n}\n", "/**\n * Renewable fenced file lock.\n *\n * The lock file at `${path}.${name}.lock` holds the JSON\n * `{ ownerId, expiresAt }`. Acquiring it uses an exclusive `wx` write so\n * concurrent processes race deterministically \u2014 only one wins.\n *\n * A contended, live lock (ownerId present, expiresAt > now) returns\n * `null` immediately; no eviction is attempted and no destructive op\n * touches the file. A contended, stale lock (no owner, expired, or\n * malformed-but-old) opens an eviction protocol that uses an exclusive\n * \"evicting\" marker directory at `${lockPath}.evicting/owner.json` as a\n * fence: the file holds the in-progress evicter's ownerId. The marker\n * is verified before every destructive seam (re-read, unlink,\n * re-acquire) so a contender whose marker has been hijacked will see\n * ownership has changed and back off without touching the winner's lock.\n *\n * The renewal timer is `setInterval(...).unref()`-ed so it does not\n * keep the Node/Bun runtime alive. Default renewal interval is\n * `max(1000, floor(ttlMs / 3))`.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport {\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n unlink,\n writeFile,\n} from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\nconst RENEW_MIN_INTERVAL_MS = 1_000\nconst MARKER_CLAIM_MAX_ATTEMPTS = 8\nconst MARKER_CLAIM_BACKOFF_MS = 25\n\nexport type FileLockStep =\n | 'stale-marker-stat'\n | 'stale-marker-claimed'\n | 'stale-lock-confirmed'\n | 'eviction-marker-acquired'\n | 'renew-read'\n | 'renew-committed'\n\nexport interface FencedFileLockOptions {\n path: string\n name: string\n ttlMs: number\n /**\n * Defaults to `true`. When false, no renewal timer is scheduled.\n */\n renew?: boolean\n /**\n * Override the renewal cadence in milliseconds.\n * Defaults to `max(RENEW_MIN_INTERVAL_MS, floor(ttlMs / 3))`.\n */\n renewIntervalMs?: number\n /**\n * Clock injection. Defaults to `Date.now`. Tests use this to\n * simulate elapsed time without waiting for real wall-clock ticks.\n */\n now?: () => number\n /**\n * Hook invoked at well-defined milestones inside the eviction\n * protocol and the renewal tick. Used by tests to inject\n * interleavings and simulate racing contenders:\n *\n * - `renew-read` \u2014 after the renewal tick reads the lock payload,\n * before the ownership checks and the temp-write/rename commit.\n * - `renew-committed` \u2014 after the renewal rename lands, before the\n * verify-after-commit re-read.\n */\n onStep?: (step: FileLockStep) => Promise<void> | void\n}\n\nexport interface FencedFileLock {\n ownerId: string\n assertOwned(): Promise<void>\n release(): Promise<void>\n /**\n * Resolves when the renewal loop detects the lock has been taken over\n * by another owner (or otherwise lost before `release()`). Resolves\n * immediately if the lock is already lost.\n */\n whenLost(): Promise<void>\n hasLost(): boolean\n}\n\n/**\n * Thrown by `assertOwned` when the lock file no longer carries our\n * ownerId or has expired.\n */\nexport class FileLockOwnershipError extends Error {\n readonly details: {\n path: string\n expectedOwner: string\n observedOwner?: string\n observedExpiresAt?: number\n }\n\n constructor(message: string, details: FileLockOwnershipError['details']) {\n super(message)\n this.name = 'FileLockOwnershipError'\n this.details = details\n }\n}\n\ninterface LockPayload {\n ownerId: string\n expiresAt: number\n}\n\ninterface MarkerPayload {\n ownerId: string\n /** Owning process ID \u2014 used to detect a dead/in-progress evicter. */\n pid: number\n /** Wall-clock time (ms) the marker was created. Used for the TTL floor. */\n createdAt: number\n}\n\nconst MARKER_TTL_MS = 30_000\n\nasync function readLockPayload(path: string): Promise<LockPayload | null> {\n try {\n const text = await readFile(path, 'utf8')\n const parsed: unknown = JSON.parse(text)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as Record<string, unknown>).ownerId === 'string' &&\n typeof (parsed as Record<string, unknown>).expiresAt === 'number'\n ) {\n return parsed as LockPayload\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function readMarkerPayload(path: string): Promise<MarkerPayload | null> {\n try {\n const text = await readFile(path, 'utf8')\n const parsed: unknown = JSON.parse(text)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as Record<string, unknown>).ownerId === 'string' &&\n typeof (parsed as Record<string, unknown>).pid === 'number' &&\n typeof (parsed as Record<string, unknown>).createdAt === 'number'\n ) {\n return parsed as MarkerPayload\n }\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Best-effort liveness check for a PID. Returns `true` when the process\n * is alive or when the platform lacks a reliable probe (Windows), so\n * the marker is only reclaimed when we have positive evidence of death.\n */\nfunction isProcessAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ESRCH') return false\n // EPERM (process exists but we cannot signal it) still counts as alive.\n return true\n }\n}\n\nasync function rmEvictingDir(evictingPath: string): Promise<void> {\n await rm(evictingPath, { force: true }).catch(() => {})\n await rm(dirname(evictingPath), { recursive: true, force: true }).catch(\n () => {},\n )\n}\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nexport async function acquireFencedFileLock(\n options: FencedFileLockOptions,\n): Promise<FencedFileLock | null> {\n const lockPath = `${options.path}.${options.name}.lock`\n const evictingDir = `${lockPath}.evicting`\n const evictingPath = join(evictingDir, 'owner.json')\n\n const ownerId = randomUUID()\n const now = options.now ?? Date.now\n\n const tryAcquire = async (\n expiresAt: number,\n ): Promise<FencedFileLock | null> => {\n const payload = JSON.stringify({ ownerId, expiresAt })\n try {\n await mkdir(dirname(lockPath), { recursive: true })\n await writeFile(lockPath, payload, {\n flag: 'wx',\n encoding: 'utf8',\n mode: 0o600,\n })\n return buildLock(lockPath, evictingPath, ownerId, options, now)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n return null\n }\n throw err\n }\n }\n\n // Outer loop: each iteration represents one full attempt to acquire\n // (fresh OR after eviction). We may need to retry if the contender\n // creates a new lock between our unlink and our re-acquire.\n for (;;) {\n const firstTry = await tryAcquire(now() + options.ttlMs)\n if (firstTry) return firstTry\n\n const observed = await readLockPayload(lockPath)\n const lockStats = await stat(lockPath).catch(() => null)\n if (!lockStats) continue // lock vanished between attempts\n\n const currentTime = now()\n const looksLive =\n observed !== null &&\n typeof observed.ownerId === 'string' &&\n observed.expiresAt > currentTime\n if (looksLive) {\n return null\n }\n\n // malformed fail-closed: only proceed when mtime proves staleness.\n const looksValid = observed !== null\n if (!looksValid) {\n const ageMs = currentTime - lockStats.mtimeMs\n if (ageMs < options.ttlMs) return null\n }\n\n // Past this point we have decided the lock is stale and we will\n // attempt eviction.\n await options.onStep?.('stale-marker-stat')\n\n // Claim the exclusive evicting marker (bounded retries so a stuck\n // contender cannot trap us in an infinite loop). Before each retry,\n // inspect the existing marker: if its PID is dead or the marker is\n // older than MARKER_TTL_MS, it is abandoned and we reclaim it.\n let markerClaimed = false\n for (let attempt = 0; attempt < MARKER_CLAIM_MAX_ATTEMPTS; attempt++) {\n try {\n await mkdir(evictingDir, { recursive: true, mode: 0o700 })\n await writeFile(\n evictingPath,\n JSON.stringify({\n ownerId,\n pid: process.pid,\n createdAt: now(),\n }),\n {\n flag: 'wx',\n encoding: 'utf8',\n mode: 0o600,\n },\n )\n markerClaimed = true\n break\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err\n // Existing marker \u2014 check whether it is reclaimed-able.\n const existing = await readMarkerPayload(evictingPath)\n if (existing) {\n const ageMs = now() - existing.createdAt\n const deadPid = !isProcessAlive(existing.pid)\n if (deadPid || ageMs > MARKER_TTL_MS) {\n // Stale marker \u2014 recycle the directory and retry the claim.\n await rmEvictingDir(evictingPath)\n continue\n }\n }\n await sleep(MARKER_CLAIM_BACKOFF_MS)\n }\n }\n if (!markerClaimed) {\n return null\n }\n\n await options.onStep?.('stale-marker-claimed')\n\n // First re-check boundary: confirm the marker is still ours AND\n // the lock still looks stale (its real owner may have refreshed).\n let markerPayload = await readMarkerPayload(evictingPath)\n if (markerPayload?.ownerId !== ownerId) continue\n\n const observedAgain = await readLockPayload(lockPath)\n const refreshed =\n observedAgain !== null &&\n observedAgain.ownerId !== ownerId &&\n observedAgain.expiresAt > now()\n if (refreshed) {\n await rmEvictingDir(evictingPath)\n return null\n }\n\n await options.onStep?.('stale-lock-confirmed')\n\n // Second re-check boundary: marker must still be ours before\n // unlinking. If we lost ownership between the previous check and\n // this one, the winner is now responsible for the lock file.\n markerPayload = await readMarkerPayload(evictingPath)\n if (markerPayload?.ownerId !== ownerId) continue\n\n try {\n await unlink(lockPath)\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code !== 'ENOENT') {\n await rmEvictingDir(evictingPath)\n continue\n }\n }\n\n await options.onStep?.('eviction-marker-acquired')\n\n // Re-acquire while still holding the marker as the new lock's\n // \"we evicted this\" witness. If another contender grabs between our\n // unlink and our re-acquire, drop the marker and loop back.\n const reentry = await tryAcquire(now() + options.ttlMs)\n if (reentry) {\n await rmEvictingDir(evictingPath)\n return reentry\n }\n await rmEvictingDir(evictingPath)\n }\n}\n\nfunction buildLock(\n lockPath: string,\n evictingPath: string,\n ownerId: string,\n options: FencedFileLockOptions,\n now: () => number,\n): FencedFileLock {\n let renewTimer: ReturnType<typeof setInterval> | null = null\n let inFlightRenew: Promise<void> | null = null\n let released = false\n let lost = false\n let lostResolve: (() => void) | null = null\n let lostPromise: Promise<void> | null = new Promise<void>((resolve) => {\n lostResolve = resolve\n })\n\n const markLost = (): void => {\n if (lost) return\n lost = true\n if (renewTimer !== null) {\n clearInterval(renewTimer)\n renewTimer = null\n }\n const resolve = lostResolve\n lostResolve = null\n resolve?.()\n }\n\n const setupRenew = (): void => {\n if (options.renew === false) return\n const interval =\n options.renewIntervalMs ??\n Math.max(RENEW_MIN_INTERVAL_MS, Math.floor(options.ttlMs / 3))\n\n const renew = async (): Promise<void> => {\n if (released || lost) return\n try {\n const observed = await readLockPayload(lockPath)\n if (released || lost) return\n await options.onStep?.('renew-read')\n if (released || lost) return\n // If the lock file is gone or carries a different ownerId, the\n // lock has been taken over (e.g. owner B evicted and replaced\n // the lock while this renewal was paused). Mark it lost so the\n // next iteration stops renewing.\n if (observed === null) {\n markLost()\n return\n }\n if (observed.ownerId !== ownerId) {\n markLost()\n return\n }\n if (observed.expiresAt > now()) {\n if (released || lost) return\n // Rename cannot compare-and-swap ownership. Re-read after the\n // rename and mark this lock lost if another owner replaced it;\n // writers call assertOwned() before mutating shared state, so a\n // hijack in this seam is detected and stops future renewals.\n const tempPath = `${lockPath}.${ownerId}.tmp`\n let shouldCommit = false\n try {\n await writeFile(\n tempPath,\n JSON.stringify({\n ownerId,\n expiresAt: now() + options.ttlMs,\n }),\n { encoding: 'utf8', mode: 0o600 },\n )\n if (released || lost) {\n await unlink(tempPath).catch(() => {})\n return\n }\n const currentObserved = await readLockPayload(lockPath)\n if (released || lost) {\n await unlink(tempPath).catch(() => {})\n return\n }\n if (!currentObserved || currentObserved.ownerId !== ownerId) {\n // Another owner slipped in \u2014 leave their content intact,\n // mark lost, drop our temp draft.\n markLost()\n await unlink(tempPath).catch(() => {})\n return\n }\n shouldCommit = true\n await rename(tempPath, lockPath)\n await options.onStep?.('renew-committed')\n if (released || lost) return\n // Verify-after-commit: a replacement owner may have arrived\n // between the re-read above and the rename \u2014 our rename then\n // just overwrote their fresh lock \u2014 or may land a microsecond\n // after our rename. Re-read and, if the file no longer carries\n // our ownerId, mark lost and stop renewing so the double-owner\n // window collapses to this seam; the fresh owner's next\n // renewal/acquire attempt re-acquires.\n const committed = await readLockPayload(lockPath)\n if (released || lost) return\n if (!committed || committed.ownerId !== ownerId) {\n markLost()\n return\n }\n } catch {\n // Lost the race against a rename/evict \u2014 mark lost so the\n // next iteration stops renewing, and clean up any temp draft.\n markLost()\n if (!shouldCommit) {\n await unlink(tempPath).catch(() => {})\n }\n }\n }\n } catch {\n // best-effort renewal; nothing to do on failure\n }\n }\n\n // The timer callback is small: it only schedules a renew if one is not\n // already in flight and the lock has not been released. The renew\n // Promise itself runs to completion; its in-flight state is tracked\n // so release() can await it before unlinking.\n const tick = (): void => {\n if (released) return\n if (inFlightRenew) return\n inFlightRenew = renew().finally(() => {\n inFlightRenew = null\n })\n }\n\n renewTimer = setInterval(tick, interval)\n if (\n renewTimer &&\n typeof (renewTimer as { unref?: () => void }).unref === 'function'\n ) {\n ;(renewTimer as { unref: () => void }).unref()\n }\n }\n\n const release = async (): Promise<void> => {\n if (released) return\n released = true\n if (renewTimer !== null) {\n clearInterval(renewTimer)\n renewTimer = null\n }\n const pendingRenew = inFlightRenew\n inFlightRenew = null\n if (pendingRenew) {\n try {\n await pendingRenew\n } catch {\n // renewal swallows its own errors; awaiting is just a fence\n }\n }\n\n try {\n const observed = await readLockPayload(lockPath)\n if (!observed || observed.ownerId !== ownerId) {\n // Not ours anymore \u2014 refuse to delete.\n await rmEvictingDir(evictingPath).catch(() => {})\n return\n }\n try {\n await unlink(lockPath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err\n }\n await rmEvictingDir(evictingPath).catch(() => {})\n } finally {\n markLost()\n renewTimer = null\n inFlightRenew = null\n lostResolve = null\n lostPromise = null\n }\n }\n\n const assertOwned = async (): Promise<void> => {\n const observed = await readLockPayload(lockPath)\n if (!observed || observed.ownerId !== ownerId) {\n throw new FileLockOwnershipError(\n `file lock ${lockPath} is not owned by ${ownerId}`,\n {\n path: lockPath,\n expectedOwner: ownerId,\n observedOwner: observed?.ownerId,\n observedExpiresAt: observed?.expiresAt,\n },\n )\n }\n if (observed.expiresAt <= now()) {\n throw new FileLockOwnershipError(`file lock ${lockPath} has expired`, {\n path: lockPath,\n expectedOwner: ownerId,\n observedOwner: observed.ownerId,\n observedExpiresAt: observed.expiresAt,\n })\n }\n }\n\n setupRenew()\n return {\n ownerId,\n assertOwned,\n release,\n whenLost: () => lostPromise ?? Promise.resolve(),\n hasLost: () => lost,\n }\n}\n", "/**\n * Harness-agnostic structured logger for the Antigravity core.\n *\n * Core code never talks to a specific harness UI. Instead it emits log\n * records to a pluggable sink. Harnesses (OpenCode, pi) register their own\n * sink via `setLogSink()` to route logs into their TUI/log panel. When no\n * sink is registered, an env-gated console fallback is used.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error'\n\nexport interface Logger {\n debug(message: string, extra?: Record<string, unknown>): void\n info(message: string, extra?: Record<string, unknown>): void\n warn(message: string, extra?: Record<string, unknown>): void\n error(message: string, extra?: Record<string, unknown>): void\n}\n\nexport interface LogRecord {\n service: string\n level: LogLevel\n message: string\n extra?: Record<string, unknown>\n}\n\nexport type LogSink = (record: LogRecord) => void\n\nconst ENV_CONSOLE_LOG = 'ANTIGRAVITY_CORE_CONSOLE_LOG'\n\nlet _sink: LogSink | null = null\n\n/**\n * Register the harness-specific log sink. Pass `null` to clear it.\n */\nexport function setLogSink(sink: LogSink | null): void {\n _sink = sink\n}\n\nfunction isTruthyFlag(flag?: string): boolean {\n return flag === '1' || flag?.toLowerCase() === 'true'\n}\n\nfunction isConsoleLogEnabled(): boolean {\n return isTruthyFlag(process.env[ENV_CONSOLE_LOG])\n}\n\nfunction writeConsoleLog(level: LogLevel, ...args: unknown[]): void {\n switch (level) {\n case 'debug':\n console.debug(...args)\n break\n case 'info':\n console.info(...args)\n break\n case 'warn':\n console.warn(...args)\n break\n case 'error':\n console.error(...args)\n break\n }\n}\n\n/**\n * Create a logger for a specific module. Records are forwarded to the\n * registered sink, with an env-gated console fallback.\n */\nexport function createLogger(module: string): Logger {\n const service = `antigravity.${module}`\n\n const log = (\n level: LogLevel,\n message: string,\n extra?: Record<string, unknown>,\n ): void => {\n if (_sink) {\n try {\n _sink({ service, level, message, extra })\n } catch {\n // Never let logging failures break core logic.\n }\n }\n\n if (isConsoleLogEnabled()) {\n const prefix = `[${service}]`\n const args = extra ? [prefix, message, extra] : [prefix, message]\n writeConsoleLog(level, ...args)\n }\n }\n\n return {\n debug: (message, extra) => log('debug', message, extra),\n info: (message, extra) => log('info', message, extra),\n warn: (message, extra) => log('warn', message, extra),\n error: (message, extra) => log('error', message, extra),\n }\n}\n", "import type { OAuthAuthDetails, RefreshParts } from './auth-types.ts'\n\nconst ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1000\n\nexport function isOAuthAuth(auth: unknown): auth is OAuthAuthDetails {\n return (\n typeof auth === 'object' &&\n auth !== null &&\n 'type' in auth &&\n auth.type === 'oauth'\n )\n}\n\n/**\n * Splits a packed refresh string into its constituent refresh token and project IDs.\n */\nexport function parseRefreshParts(refresh: string): RefreshParts {\n const [refreshToken = '', projectId = '', managedProjectId = ''] = (\n refresh ?? ''\n ).split('|')\n return {\n refreshToken,\n projectId: projectId || undefined,\n managedProjectId: managedProjectId || undefined,\n }\n}\n\n/**\n * Serializes refresh token parts into the stored string format.\n */\nexport function formatRefreshParts(parts: RefreshParts): string {\n const projectSegment = parts.projectId ?? ''\n const base = `${parts.refreshToken}|${projectSegment}`\n return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base\n}\n\n/**\n * Determines whether an access token is expired or missing, with buffer for clock skew.\n */\nexport function accessTokenExpired(auth: OAuthAuthDetails): boolean {\n if (!auth.access || typeof auth.expires !== 'number') {\n return true\n }\n return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS\n}\n\n/**\n * Calculates absolute expiry timestamp based on a duration.\n * @param requestTimeMs The local time when the request was initiated\n * @param expiresInSeconds The duration returned by the server\n */\nexport function calculateTokenExpiry(\n requestTimeMs: number,\n expiresInSeconds: unknown,\n): number {\n const seconds = typeof expiresInSeconds === 'number' ? expiresInSeconds : 3600\n // Safety check for bad data - if it's not a positive number, treat as immediately expired\n if (Number.isNaN(seconds) || seconds <= 0) {\n return requestTimeMs\n }\n return requestTimeMs + seconds * 1000\n}\n", "/**\n * Device Fingerprint Generator for Rate Limit Mitigation\n *\n * Uses the agy CLI 1.1.6 content-request identity captured with mitmproxy:\n * an Antigravity CLI User-Agent with explicit client, OS, architecture, and auth metadata.\n * The stored deviceId/sessionToken fields are\n * retained for account history, but content requests only send User-Agent.\n */\n\nimport * as crypto from 'node:crypto'\n\nexport const AGY_CLI_VERSION = '1.1.6'\nconst ANTIGRAVITY_API_CLIENT = 'antigravity-cli'\n\nexport interface ClientMetadata {\n ideType: string\n platform: string\n pluginType: string\n}\n\nexport interface Fingerprint {\n deviceId: string\n sessionToken: string\n userAgent: string\n apiClient: string\n clientMetadata: ClientMetadata\n createdAt: number\n}\n/**\n * Fingerprint version for history tracking.\n * Stores a snapshot of a fingerprint with metadata about when/why it was saved.\n */\nexport interface FingerprintVersion {\n fingerprint: Fingerprint\n timestamp: number\n reason: 'initial' | 'regenerated' | 'restored'\n}\n\n/** Maximum number of fingerprint versions to keep per account */\nexport const MAX_FINGERPRINT_HISTORY = 5\n\nexport interface FingerprintHeaders {\n 'User-Agent': string\n}\n\nfunction normalizeHarnessPlatform(platform = process.platform): string {\n return platform === 'win32' ? 'windows' : platform || 'unknown'\n}\n\nfunction normalizeHarnessArch(arch = process.arch): string {\n switch (arch) {\n case 'x64':\n return 'amd64'\n case 'ia32':\n return '386'\n default:\n return arch || 'unknown'\n }\n}\n\nexport function buildAntigravityHarnessPlatformArch(\n platform = process.platform,\n arch = process.arch,\n): string {\n return `${normalizeHarnessPlatform(platform)}/${normalizeHarnessArch(arch)}`\n}\n\nexport function buildAntigravityHarnessUserAgent(\n version = AGY_CLI_VERSION,\n platform = process.platform,\n arch = process.arch,\n authMethod = 'consumer',\n): string {\n const osType = normalizeHarnessPlatform(platform)\n const normalizedArch = normalizeHarnessArch(arch)\n return `antigravity/cli/${version} (aidev_client; os_type=${osType}; arch=${normalizedArch}; auth_method=${authMethod})`\n}\n\nexport function buildAntigravityHarnessLoadCodeAssistUserAgent(\n version = AGY_CLI_VERSION,\n): string {\n return buildAntigravityHarnessUserAgent(version)\n}\n\nfunction platformToMetadataPlatform(\n platform: string = process.platform,\n): 'WINDOWS' | 'MACOS' {\n return platform === 'win32' ? 'WINDOWS' : 'MACOS'\n}\n\nexport function buildAntigravityLoadCodeAssistMetadata(): Record<\n string,\n string\n> {\n return { ideType: 'ANTIGRAVITY' }\n}\n\nexport function buildAntigravityHarnessBootstrapHeaders(\n accessToken: string,\n): Record<string, string> {\n return {\n 'User-Agent': buildAntigravityHarnessLoadCodeAssistUserAgent(),\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'gzip',\n }\n}\n\nfunction generateDeviceId(): string {\n return crypto.randomUUID()\n}\n\nfunction generateSessionToken(): string {\n return crypto.randomBytes(16).toString('hex')\n}\n\n/**\n * Generate the per-account content-request fingerprint.\n * The outward HTTP identity is stable; deviceId/sessionToken remain unique for history.\n */\nexport function generateFingerprint(): Fingerprint {\n return {\n deviceId: generateDeviceId(),\n sessionToken: generateSessionToken(),\n userAgent: buildAntigravityHarnessUserAgent(),\n apiClient: ANTIGRAVITY_API_CLIENT,\n clientMetadata: {\n ideType: 'ANTIGRAVITY',\n platform: platformToMetadataPlatform(),\n pluginType: 'GEMINI',\n },\n createdAt: Date.now(),\n }\n}\n\n/**\n * Collect the current content-request fingerprint.\n */\nexport function collectCurrentFingerprint(): Fingerprint {\n return generateFingerprint()\n}\n\n/**\n * Update a saved fingerprint's User-Agent to the current Antigravity\n * agy CLI identity. This migrates older randomized fingerprints and the\n * pre-1.1.3 platform/arch-only User-Agent to the captured metadata form.\n * Returns true if the User-Agent was changed.\n */\nexport function updateFingerprintVersion(fingerprint: Fingerprint): boolean {\n const userAgent = buildAntigravityHarnessUserAgent()\n if (fingerprint.userAgent === userAgent) {\n return false\n }\n\n fingerprint.userAgent = userAgent\n return true\n}\n\n/**\n * Build HTTP headers from a fingerprint object.\n * These headers are used to identify the \"device\" making API requests.\n */\nexport function buildFingerprintHeaders(\n fingerprint: Fingerprint | null,\n): Partial<FingerprintHeaders> {\n if (!fingerprint) {\n return {}\n }\n\n return {\n 'User-Agent': fingerprint.userAgent,\n }\n}\n\n/**\n * Session-level fingerprint instance.\n * Generated once at module load, persists for the lifetime of the process.\n */\nlet sessionFingerprint: Fingerprint | null = null\n\n/**\n * Get or create the session fingerprint.\n * Returns the same fingerprint for all calls within a session.\n */\nexport function getSessionFingerprint(): Fingerprint {\n if (!sessionFingerprint) {\n sessionFingerprint = generateFingerprint()\n }\n return sessionFingerprint\n}\n\n/**\n * Regenerate the session fingerprint.\n * Call this to get a fresh identity (e.g., after rate limiting).\n */\nexport function regenerateSessionFingerprint(): Fingerprint {\n sessionFingerprint = generateFingerprint()\n return sessionFingerprint\n}\n\n/**\n * Clear the cached session fingerprint so the next `getSessionFingerprint`\n * call generates a fresh one. Test-only escape hatch \u2014 production code should\n * use `regenerateSessionFingerprint` to also return the new value.\n */\nexport function clearSessionFingerprint(): void {\n sessionFingerprint = null\n}\n", "import type { ProviderModel } from './model-types.ts'\nimport type { ThinkingTier } from './transform/types.ts'\n\nexport type ModelThinkingLevel = 'minimal' | 'low' | 'medium' | 'high'\n\nexport interface ModelThinkingConfig {\n thinkingBudget: number\n}\n\nexport interface ModelVariant {\n thinkingLevel?: ModelThinkingLevel\n thinkingConfig?: ModelThinkingConfig\n disabled?: boolean\n}\n\nexport interface ModelLimit {\n context: number\n output: number\n}\n\nexport type ModelModality = 'text' | 'image' | 'pdf'\nexport type ModelQuotaGroup = 'gemini' | 'non-gemini'\n\nexport interface ModelModalities {\n input: ModelModality[]\n output: ModelModality[]\n}\n\nexport interface OpencodeModelDefinition extends ProviderModel {\n id: string\n name: string\n release_date: string\n attachment: boolean\n reasoning: boolean\n temperature: boolean\n tool_call: boolean\n limit: ModelLimit\n modalities: ModelModalities\n cost: {\n input: number\n output: number\n }\n options: Record<string, unknown>\n variants?: Record<string, ModelVariant>\n}\n\nexport type OpencodeModelDefinitions = Record<string, OpencodeModelDefinition>\n\ninterface OpencodeModelDefinitionInput {\n name: string\n reasoning: boolean\n limit: ModelLimit\n modalities: ModelModalities\n variants?: Record<string, ModelVariant>\n}\n\ninterface AntigravityTieredRouteMetadata {\n defaultModel: string\n byTier: Partial<Record<ThinkingTier, string>>\n}\n\ninterface GeminiFlashRouteMetadata {\n antigravity: AntigravityTieredRouteMetadata\n geminiCliFallbackModel: string\n}\n\nconst DEFAULT_MODALITIES: ModelModalities = {\n input: ['text', 'image', 'pdf'],\n output: ['text'],\n}\n\nconst MODEL_RELEASE_DATE = ''\nconst DEFAULT_COST = { input: 0, output: 0 }\nconst DEFAULT_OPTIONS: Record<string, unknown> = {}\n\nfunction defineModel(\n id: string,\n model: OpencodeModelDefinitionInput,\n): OpencodeModelDefinition {\n return {\n id,\n release_date: MODEL_RELEASE_DATE,\n attachment: model.modalities.input.some((modality) => modality !== 'text'),\n temperature: true,\n tool_call: true,\n cost: { ...DEFAULT_COST },\n options: { ...DEFAULT_OPTIONS },\n ...model,\n }\n}\n\nconst ALL_MODEL_DEFINITIONS: OpencodeModelDefinitions = {\n 'antigravity-gemini-3.1-pro': defineModel('antigravity-gemini-3.1-pro', {\n name: 'Gemini 3.1 Pro (Antigravity)',\n reasoning: true,\n limit: { context: 1048576, output: 65535 },\n modalities: DEFAULT_MODALITIES,\n variants: {\n low: { thinkingLevel: 'low' },\n high: { thinkingLevel: 'high' },\n },\n }),\n 'antigravity-gemini-3.6-flash': defineModel('antigravity-gemini-3.6-flash', {\n name: 'Gemini 3.6 Flash (Antigravity)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n variants: {\n low: { thinkingLevel: 'low' },\n high: { thinkingLevel: 'high' },\n },\n }),\n 'antigravity-gemini-3.5-flash': defineModel('antigravity-gemini-3.5-flash', {\n name: 'Gemini 3.5 Flash (Antigravity)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n variants: {\n low: { thinkingLevel: 'low' },\n high: { thinkingLevel: 'high' },\n },\n }),\n 'antigravity-claude-sonnet-4-6-thinking': defineModel(\n 'antigravity-claude-sonnet-4-6-thinking',\n {\n name: 'Claude Sonnet 4.6 Thinking (Antigravity)',\n reasoning: true,\n limit: { context: 250000, output: 64000 },\n modalities: DEFAULT_MODALITIES,\n variants: {\n low: { disabled: true },\n high: { disabled: true },\n },\n },\n ),\n 'antigravity-claude-opus-4-6-thinking': defineModel(\n 'antigravity-claude-opus-4-6-thinking',\n {\n name: 'Claude Opus 4.6 Thinking (Antigravity)',\n reasoning: true,\n limit: { context: 250000, output: 64000 },\n modalities: DEFAULT_MODALITIES,\n variants: {\n low: { disabled: true },\n high: { disabled: true },\n },\n },\n ),\n 'antigravity-gemini-3.1-flash-image': defineModel(\n 'antigravity-gemini-3.1-flash-image',\n {\n name: 'Gemini 3.1 Flash Image (Antigravity)',\n reasoning: false,\n limit: { context: 66000, output: 33000 },\n modalities: {\n input: ['text', 'image'],\n output: ['text', 'image'],\n },\n },\n ),\n 'antigravity-gpt-oss-120b-medium': defineModel(\n 'antigravity-gpt-oss-120b-medium',\n {\n name: 'GPT-OSS 120B Medium (Antigravity)',\n reasoning: true,\n limit: { context: 131072, output: 32768 },\n modalities: DEFAULT_MODALITIES,\n },\n ),\n 'gemini-2.5-flash': defineModel('gemini-2.5-flash', {\n name: 'Gemini 2.5 Flash (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n }),\n 'gemini-2.5-pro': defineModel('gemini-2.5-pro', {\n name: 'Gemini 2.5 Pro (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n }),\n 'gemini-3-flash-preview': defineModel('gemini-3-flash-preview', {\n name: 'Gemini 3 Flash Preview (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n }),\n 'gemini-3.1-pro-preview': defineModel('gemini-3.1-pro-preview', {\n name: 'Gemini 3.1 Pro Preview (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65535 },\n modalities: DEFAULT_MODALITIES,\n }),\n 'gemini-3.5-flash-preview': defineModel('gemini-3.5-flash-preview', {\n name: 'Gemini 3.5 Flash Preview (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65536 },\n modalities: DEFAULT_MODALITIES,\n }),\n 'gemini-3.1-flash-image': defineModel('gemini-3.1-flash-image', {\n name: 'Gemini 3.1 Flash Image (Gemini CLI)',\n reasoning: false,\n limit: { context: 66000, output: 33000 },\n modalities: {\n input: ['text', 'image'],\n output: ['text', 'image'],\n },\n }),\n 'gemini-3.1-flash-image-preview': defineModel(\n 'gemini-3.1-flash-image-preview',\n {\n name: 'Gemini 3.1 Flash Image Preview (Gemini CLI)',\n reasoning: false,\n limit: { context: 66000, output: 33000 },\n modalities: {\n input: ['text', 'image'],\n output: ['text', 'image'],\n },\n },\n ),\n 'gemini-3.1-pro-preview-customtools': defineModel(\n 'gemini-3.1-pro-preview-customtools',\n {\n name: 'Gemini 3.1 Pro Preview Custom Tools (Gemini CLI)',\n reasoning: true,\n limit: { context: 1048576, output: 65535 },\n modalities: DEFAULT_MODALITIES,\n },\n ),\n}\n\nconst RESOLVER_ALIASES: Record<string, string> = {\n 'gemini-3.1-pro-low': 'gemini-3.1-pro',\n 'gemini-3.1-pro-high': 'gemini-3.1-pro',\n 'gemini-3-flash-low': 'gemini-3-flash',\n 'gemini-3-flash-medium': 'gemini-3-flash',\n 'gemini-3-flash-high': 'gemini-3-flash',\n 'gemini-3.5-flash-low': 'gemini-3.5-flash',\n 'gemini-3.5-flash-medium': 'gemini-3.5-flash',\n 'gemini-3.5-flash-high': 'gemini-3.5-flash',\n 'gemini-3.6-flash-low': 'gemini-3.6-flash',\n 'gemini-3.6-flash-medium': 'gemini-3.6-flash',\n 'gemini-3.6-flash-high': 'gemini-3.6-flash',\n 'gemini-claude-opus-4-6-thinking-low': 'claude-opus-4-6-thinking',\n 'gemini-claude-opus-4-6-thinking-medium': 'claude-opus-4-6-thinking',\n 'gemini-claude-opus-4-6-thinking-high': 'claude-opus-4-6-thinking',\n 'gemini-claude-sonnet-4-6-thinking-low': 'claude-sonnet-4-6',\n 'gemini-claude-sonnet-4-6-thinking-medium': 'claude-sonnet-4-6',\n 'gemini-claude-sonnet-4-6-thinking-high': 'claude-sonnet-4-6',\n 'gemini-claude-sonnet-4-6': 'claude-sonnet-4-6',\n 'claude-sonnet-4-6-thinking': 'claude-sonnet-4-6',\n 'claude-sonnet-4-6-thinking-low': 'claude-sonnet-4-6',\n 'claude-sonnet-4-6-thinking-medium': 'claude-sonnet-4-6',\n 'claude-sonnet-4-6-thinking-high': 'claude-sonnet-4-6',\n 'gpt-oss-120b': 'gpt-oss-120b-medium',\n}\n\nconst GEMINI_35_FLASH_ROUTES: GeminiFlashRouteMetadata = {\n antigravity: {\n defaultModel: 'gemini-3-flash-agent',\n byTier: {\n low: 'gemini-3.5-flash-extra-low',\n medium: 'gemini-3.5-flash-low',\n high: 'gemini-3-flash-agent',\n },\n },\n geminiCliFallbackModel: 'gemini-3-flash-preview',\n}\n\nconst GEMINI_36_FLASH_ROUTES: AntigravityTieredRouteMetadata = {\n defaultModel: 'gemini-3.6-flash-medium',\n byTier: {\n low: 'gemini-3.6-flash-low',\n medium: 'gemini-3.6-flash-medium',\n high: 'gemini-3.6-flash-high',\n },\n}\n\nconst QUOTA_GROUP_BY_MODEL_ID: Record<string, ModelQuotaGroup> = {\n 'claude-opus-4-6-thinking': 'non-gemini',\n 'claude-opus-4-6': 'non-gemini',\n 'claude-sonnet-4-6-thinking': 'non-gemini',\n 'claude-sonnet-4-6': 'non-gemini',\n 'gemini-pro-agent': 'gemini',\n 'gemini-3.1-pro': 'gemini',\n 'gemini-3.1-pro-low': 'gemini',\n 'gemini-3.1-pro-high': 'gemini',\n 'gemini-3-flash': 'gemini',\n 'gemini-3-flash-agent': 'gemini',\n 'gemini-3.5-flash-low': 'gemini',\n 'gemini-3.5-flash-extra-low': 'gemini',\n 'gemini-3.6-flash-low': 'gemini',\n 'gemini-3.6-flash-medium': 'gemini',\n 'gemini-3.6-flash-high': 'gemini',\n 'gemini-3.6-flash-tiered': 'gemini',\n 'gemini-3.1-flash-image': 'gemini',\n 'gpt-oss-120b': 'non-gemini',\n 'gpt-oss-120b-medium': 'non-gemini',\n}\n\nconst ANTIGRAVITY_OPENCODE_MODEL_IDS = [\n 'antigravity-gemini-3.6-flash',\n 'antigravity-gemini-3.5-flash',\n 'antigravity-gemini-3.1-pro',\n 'antigravity-claude-sonnet-4-6-thinking',\n 'antigravity-claude-opus-4-6-thinking',\n 'antigravity-gemini-3.1-flash-image',\n 'antigravity-gpt-oss-120b-medium',\n] as const\n\nfunction pickModelDefinitions(\n ids: readonly string[],\n): OpencodeModelDefinitions {\n return Object.fromEntries(ids.map((id) => [id, ALL_MODEL_DEFINITIONS[id]!]))\n}\n\nexport const OPENCODE_MODEL_DEFINITIONS = pickModelDefinitions(\n ANTIGRAVITY_OPENCODE_MODEL_IDS,\n)\n\nexport function getPublicModelDefinitions(): OpencodeModelDefinitions {\n return OPENCODE_MODEL_DEFINITIONS\n}\n\nexport function getAntigravityOpencodeModelIds(): string[] {\n return [...ANTIGRAVITY_OPENCODE_MODEL_IDS]\n}\n\nexport function getResolverAliasMap(): Record<string, string> {\n return RESOLVER_ALIASES\n}\n\nexport function getGemini35FlashAntigravityModel(tier?: ThinkingTier): string {\n if (!tier) {\n return GEMINI_35_FLASH_ROUTES.antigravity.defaultModel\n }\n return (\n GEMINI_35_FLASH_ROUTES.antigravity.byTier[tier] ??\n GEMINI_35_FLASH_ROUTES.antigravity.defaultModel\n )\n}\n\nexport function getGemini35FlashGeminiCliFallbackModel(): string {\n return GEMINI_35_FLASH_ROUTES.geminiCliFallbackModel\n}\n\nexport function getGemini36FlashAntigravityModel(tier?: ThinkingTier): string {\n if (!tier) {\n return GEMINI_36_FLASH_ROUTES.defaultModel\n }\n return (\n GEMINI_36_FLASH_ROUTES.byTier[tier] ?? GEMINI_36_FLASH_ROUTES.defaultModel\n )\n}\n\nexport function getQuotaGroupForModel(\n modelId: string,\n): ModelQuotaGroup | undefined {\n const normalized = modelId.toLowerCase()\n return (\n QUOTA_GROUP_BY_MODEL_ID[normalized] ??\n // Check Claude / GPT-OSS substrings BEFORE the `gemini` substring so\n // a `gemini-claude-*` alias (which is a Claude route exposed under\n // a `gemini-` namespace) attributes to the non-gemini pool rather\n // than the gemini pool. Substring matching is required because the\n // alias IDs start with `gemini-` but contain `claude`.\n (normalized.includes('claude') || normalized.includes('gpt-oss')\n ? 'non-gemini'\n : normalized.startsWith('gemini') || normalized.startsWith('tab_')\n ? 'gemini'\n : undefined)\n )\n}\n", "import { createHash } from 'node:crypto'\nimport type { AccountStorageStore } from './account-storage.ts'\nimport { AccountStorageLockContentionError } from './account-storage.ts'\nimport type {\n AccountMetadataV3,\n AccountSelectionStrategy,\n AccountStorageV4,\n CooldownReason,\n HeaderStyle,\n AccountModelFamily as ModelFamily,\n RateLimitStateV3,\n} from './account-types.ts'\nimport { formatRefreshParts, parseRefreshParts } from './auth.ts'\nimport type { OAuthAuthDetails, RefreshParts } from './auth-types.ts'\nimport {\n type Fingerprint,\n type FingerprintVersion,\n generateFingerprint,\n MAX_FINGERPRINT_HISTORY,\n updateFingerprintVersion,\n} from './fingerprint.ts'\nimport { getQuotaGroupForModel } from './model-registry.ts'\nimport {\n normalizeLegacyCachedQuota,\n type QuotaGroup,\n type QuotaGroupSummary,\n} from './quota-types.ts'\nimport {\n type AccountWithMetrics,\n getHealthTracker,\n getTokenTracker,\n selectHybridAccount,\n} from './rotation.ts'\n\nexport type {\n AccountSelectionStrategy,\n CooldownReason,\n HeaderStyle,\n ModelFamily,\n}\n\nfunction isStorageLockContention(error: unknown): boolean {\n if (error instanceof AccountStorageLockContentionError) return true\n const message = String(error)\n return (\n message.includes('Lock file is already being held') ||\n message.includes('ELOCKED')\n )\n}\n\nexport interface AccountManagerOptions {\n store: AccountStorageStore\n storagePath?: string\n now?: () => number\n random?: () => number\n pid?: number\n onDiagnostic?: (message: string, fields?: Record<string, unknown>) => void\n}\n\nexport type { RateLimitReason } from './rotation.ts'\nexport {\n calculateBackoffMs,\n computeSoftQuotaCacheTtlMs,\n parseRateLimitReason,\n} from './rotation.ts'\n\nimport type { RateLimitReason } from './rotation.ts'\nimport { calculateBackoffMs } from './rotation.ts'\n\nexport type BaseQuotaKey = 'claude' | 'gemini-antigravity' | 'gemini-cli'\nexport type QuotaKey = BaseQuotaKey | `${BaseQuotaKey}:${string}`\n\nexport interface ManagedAccount {\n index: number\n email?: string\n label?: string\n addedAt: number\n lastUsed: number\n parts: RefreshParts\n /** Authoritative project ID from the persisted account record. Survives\n * bare-refresh-token rotations where `parts.projectId` may be lost. */\n projectId?: string\n /** Authoritative managed project ID from the persisted account record.\n * Survives bare-refresh-token rotations where `parts.managedProjectId`\n * may be lost. */\n managedProjectId?: string\n access?: string\n expires?: number\n enabled: boolean\n rateLimitResetTimes: RateLimitStateV3\n lastSwitchReason?: 'rate-limit' | 'initial' | 'rotation'\n coolingDownUntil?: number\n cooldownReason?: CooldownReason\n touchedForQuota: Record<string, number>\n consecutiveFailures?: number\n /** Timestamp of last failure for TTL-based reset of consecutiveFailures */\n lastFailureTime?: number\n /** Per-account device fingerprint for rate limit mitigation */\n fingerprint?: import('./fingerprint').Fingerprint\n /** History of previous fingerprints for this account */\n fingerprintHistory?: FingerprintVersion[]\n /** Cached quota data from last checkAccountsQuota() call */\n cachedQuota?: Partial<Record<QuotaGroup, QuotaGroupSummary>>\n /** Opaque identity of the refresh token that produced `cachedQuota`. */\n cachedQuotaAccountId?: string\n cachedQuotaUpdatedAt?: number\n /**\n * Captured plan tier ID from the most recent `loadCodeAssist` response.\n * Raw upstream string (e.g. `\"free-tier\"`) \u2014 never normalised.\n */\n capturedTierId?: string\n /** Raw paid-tier ID from the most recent `loadCodeAssist` response. */\n capturedPaidTierId?: string\n /** Epoch ms when `capturedTierId` was last recorded. */\n capturedTierAt?: number\n /** Schema version of the most recent tier capture, even when paid tier is absent. */\n capturedTierSchemaVersion?: number\n verificationRequired?: boolean\n verificationRequiredAt?: number\n verificationRequiredReason?: string\n verificationUrl?: string\n accountIneligible?: boolean\n accountIneligibleAt?: number\n accountIneligibleReason?: string\n eligibilityStateUpdatedAt?: number\n /** Daily request counts per model family */\n dailyRequestCounts?: {\n date: string\n claude: number\n gemini: number\n }\n}\n\nfunction clampNonNegativeInt(value: unknown, fallback: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return fallback\n }\n return value < 0 ? 0 : Math.floor(value)\n}\n\n/**\n * Opaque identity for a refresh token.\n *\n * Antigravity refresh tokens are stable (they do not rotate), so hashing\n * the token produces a durable, prunable identity to detect stale cached\n * quota after an account-index shift.\n */\nfunction quotaAccountIdentity(refreshToken: string): string {\n return createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)\n}\n\nfunction getQuotaKey(\n family: ModelFamily,\n headerStyle: HeaderStyle,\n model?: string | null,\n): QuotaKey {\n if (family === 'claude') {\n return 'claude'\n }\n const base =\n headerStyle === 'gemini-cli' ? 'gemini-cli' : 'gemini-antigravity'\n if (model) {\n return `${base}:${model}`\n }\n return base\n}\n\nfunction isRateLimitedForQuotaKey(\n account: ManagedAccount,\n key: QuotaKey,\n now: () => number,\n): boolean {\n const resetTime = account.rateLimitResetTimes[key]\n return resetTime !== undefined && now() < resetTime\n}\n\nfunction isRateLimitedForFamily(\n account: ManagedAccount,\n family: ModelFamily,\n now: () => number,\n model?: string | null,\n): boolean {\n if (family === 'claude') {\n return isRateLimitedForQuotaKey(account, 'claude', now)\n }\n\n const antigravityIsLimited = isRateLimitedForHeaderStyle(\n account,\n family,\n 'antigravity',\n now,\n model,\n )\n const cliIsLimited = isRateLimitedForHeaderStyle(\n account,\n family,\n 'gemini-cli',\n now,\n model,\n )\n\n return antigravityIsLimited && cliIsLimited\n}\n\nfunction isRateLimitedForHeaderStyle(\n account: ManagedAccount,\n family: ModelFamily,\n headerStyle: HeaderStyle,\n now: () => number,\n model?: string | null,\n): boolean {\n clearExpiredRateLimits(account, now)\n\n if (family === 'claude') {\n return isRateLimitedForQuotaKey(account, 'claude', now)\n }\n\n // Check model-specific quota first if provided\n if (model) {\n const modelKey = getQuotaKey(family, headerStyle, model)\n if (isRateLimitedForQuotaKey(account, modelKey, now)) {\n return true\n }\n }\n\n // Then check base family quota\n const baseKey = getQuotaKey(family, headerStyle)\n return isRateLimitedForQuotaKey(account, baseKey, now)\n}\n\nfunction clearExpiredRateLimits(\n account: ManagedAccount,\n clock: () => number,\n): void {\n const now = clock()\n const keys = Object.keys(account.rateLimitResetTimes) as QuotaKey[]\n for (const key of keys) {\n const resetTime = account.rateLimitResetTimes[key]\n if (resetTime !== undefined && now >= resetTime) {\n delete account.rateLimitResetTimes[key]\n }\n }\n}\n\n/**\n * Resolve the quota group for soft quota checks.\n *\n * When a model string is available we use the model-registry lookup first,\n * then fall back to substring matching. When model is null/undefined we\n * fall back based on family:\n * - Claude \u2192 \"non-gemini\" quota group\n * - Gemini \u2192 \"gemini\" quota group\n *\n * @param family - The model family (\"claude\" | \"gemini\")\n * @param model - Optional model string for precise resolution\n * @returns The QuotaGroup to use for soft quota checks\n */\nexport function resolveQuotaGroup(\n family: ModelFamily,\n model?: string | null,\n): QuotaGroup {\n if (model) {\n const registryGroup = getQuotaGroupForModel(model)\n if (registryGroup) return registryGroup\n const lower = model.toLowerCase()\n // Check Claude / GPT-OSS substrings BEFORE the `gemini` substring so\n // a `gemini-claude-*` alias (Claude route exposed under a `gemini-`\n // namespace) attributes to the non-gemini pool. The model-registry\n // check above already handles registered aliases; this substring\n // fallback mirrors the same precedence rule for unregistered models.\n if (lower.includes('claude') || lower.includes('gpt-oss')) {\n return 'non-gemini'\n }\n if (lower.includes('gemini')) return 'gemini'\n }\n return family === 'claude' ? 'non-gemini' : 'gemini'\n}\n\nfunction isOverSoftQuotaThreshold(\n account: ManagedAccount,\n family: ModelFamily,\n thresholdPercent: number,\n cacheTtlMs: number,\n now: () => number,\n model?: string | null,\n): boolean {\n if (thresholdPercent >= 100) return false\n if (!account.cachedQuota) return false\n\n if (account.cachedQuotaUpdatedAt == null) return false\n const age = now() - account.cachedQuotaUpdatedAt\n if (age > cacheTtlMs) return false\n\n const quotaGroup = resolveQuotaGroup(family, model)\n\n const groupData = account.cachedQuota[quotaGroup]\n if (groupData?.remainingFraction == null) return false\n\n const remainingFraction = Math.max(\n 0,\n Math.min(1, groupData.remainingFraction),\n )\n const usedPercent = (1 - remainingFraction) * 100\n const isOverThreshold = usedPercent >= thresholdPercent\n\n return isOverThreshold\n}\n\nexport interface AccountSessionIdentity {\n id: string\n parentId?: string | null\n}\n\ninterface AccountSessionState {\n parentId: string | null\n currentAccountIndexByFamily: Record<ModelFamily, number>\n cursorByFamily: Record<ModelFamily, number>\n offsetAppliedByFamily: Record<ModelFamily, boolean>\n usedAccounts: Set<number>\n lastAccessedAt: number\n}\n\nconst ACCOUNT_SESSION_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst MAX_ACCOUNT_SESSION_STATES = 256\n\n/**\n * In-memory multi-account manager with sticky account selection.\n *\n * Uses the same account until it hits a rate limit (429), then switches.\n * Rate limits are tracked per-model-family (claude/gemini) so an account\n * rate-limited for Claude can still be used for Gemini.\n *\n * Source of truth for the pool is `antigravity-accounts.json`.\n */\nexport class AccountManager {\n private accounts: ManagedAccount[] = []\n private cursorByFamily: Record<ModelFamily, number> = { claude: 0, gemini: 0 }\n private currentAccountIndexByFamily: Record<ModelFamily, number> = {\n claude: -1,\n gemini: -1,\n }\n private sessionOffsetApplied: Record<ModelFamily, boolean> = {\n claude: false,\n gemini: false,\n }\n private lastToastAccountIndex = -1\n private lastToastTime = 0\n\n private savePending = false\n private saveTimeout: ReturnType<typeof setTimeout> | null = null\n private saveInFlight: Promise<void> | null = null\n private disposed = false\n private savePromiseResolvers: Array<{\n resolve: () => void\n reject: (err: unknown) => void\n }> = []\n\n private sessionStartTime: number\n private sessionRequestCounts: Map<\n string,\n { claude: number; gemini: number }\n > = new Map()\n private sessionUsedAccounts: Set<number> = new Set()\n private requestSessionStates = new Map<string, AccountSessionState>()\n\n private readonly store: AccountStorageStore\n private readonly storagePath: string\n private readonly onDiagnostic: AccountManagerOptions['onDiagnostic']\n private readonly now: () => number\n private readonly random: () => number\n private readonly pid: number\n\n constructor(\n authFallback: OAuthAuthDetails | undefined,\n stored: AccountStorageV4 | null | undefined,\n options: AccountManagerOptions,\n ) {\n this.store = options.store\n this.storagePath = options.storagePath ?? ''\n this.onDiagnostic = options.onDiagnostic\n this.now = options.now ?? (() => Date.now())\n this.random = options.random ?? (() => Math.random())\n this.pid = options.pid ?? process.pid\n this.sessionStartTime = this.now()\n const authParts = authFallback\n ? parseRefreshParts(authFallback.refresh)\n : null\n\n if (stored && stored.accounts.length === 0) {\n this.accounts = []\n this.cursorByFamily = { claude: 0, gemini: 0 }\n return\n }\n\n if (stored && stored.accounts.length > 0) {\n const baseNow = this.now()\n this.accounts = stored.accounts\n .map((acc, index): ManagedAccount | null => {\n if (!acc.refreshToken || typeof acc.refreshToken !== 'string') {\n return null\n }\n const matchesFallback = !!(\n authFallback &&\n authParts?.refreshToken &&\n acc.refreshToken === authParts.refreshToken\n )\n\n return {\n index,\n email: acc.email,\n label: acc.label,\n addedAt: clampNonNegativeInt(acc.addedAt, baseNow),\n lastUsed: clampNonNegativeInt(acc.lastUsed, 0),\n parts: {\n refreshToken: acc.refreshToken,\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n },\n // Authoritative record-level fields that survive bare-refresh-token\n // rotations where `parts.*` may be overwritten with undefined.\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n access: matchesFallback ? authFallback?.access : undefined,\n expires: matchesFallback ? authFallback?.expires : undefined,\n enabled: acc.enabled !== false,\n rateLimitResetTimes: acc.rateLimitResetTimes ?? {},\n lastSwitchReason: acc.lastSwitchReason,\n coolingDownUntil: acc.coolingDownUntil,\n cooldownReason: acc.cooldownReason,\n touchedForQuota: {},\n fingerprint: acc.fingerprint ?? generateFingerprint(),\n fingerprintHistory: acc.fingerprintHistory ?? [],\n cachedQuota: normalizeLegacyCachedQuota(acc.cachedQuota),\n // Restore the opaque identity stamp alongside the quota so the\n // post-load projection can detect a stale snapshot captured\n // for a different account after an index shift.\n cachedQuotaAccountId: acc.cachedQuotaAccountId,\n cachedQuotaUpdatedAt: acc.cachedQuotaUpdatedAt,\n capturedTierId: acc.capturedTierId,\n capturedPaidTierId: acc.capturedPaidTierId,\n capturedTierAt: acc.capturedTierAt,\n capturedTierSchemaVersion: acc.capturedTierSchemaVersion,\n dailyRequestCounts: acc.dailyRequestCounts,\n verificationRequired: acc.verificationRequired,\n verificationRequiredAt: acc.verificationRequiredAt,\n verificationRequiredReason: acc.verificationRequiredReason,\n verificationUrl: acc.verificationUrl,\n accountIneligible: acc.accountIneligible,\n accountIneligibleAt: acc.accountIneligibleAt,\n accountIneligibleReason: acc.accountIneligibleReason,\n eligibilityStateUpdatedAt: acc.eligibilityStateUpdatedAt,\n }\n })\n .filter((a): a is ManagedAccount => a !== null)\n\n // Update fingerprint versions to match the current runtime version.\n // Saved fingerprints may carry an older version string; this ensures\n // they always reflect the latest fetched (or fallback) version.\n let fingerprintVersionChanged = false\n for (const acc of this.accounts) {\n if (acc.fingerprint && updateFingerprintVersion(acc.fingerprint)) {\n fingerprintVersionChanged = true\n }\n }\n\n const legacyCursor = clampNonNegativeInt(stored.activeIndex, 0)\n if (this.accounts.length > 0) {\n const defaultIndex = legacyCursor % this.accounts.length\n this.currentAccountIndexByFamily.claude =\n clampNonNegativeInt(\n stored.activeIndexByFamily?.claude,\n defaultIndex,\n ) % this.accounts.length\n this.currentAccountIndexByFamily.gemini =\n clampNonNegativeInt(\n stored.activeIndexByFamily?.gemini,\n defaultIndex,\n ) % this.accounts.length\n this.cursorByFamily.claude = this.currentAccountIndexByFamily.claude\n this.cursorByFamily.gemini = this.currentAccountIndexByFamily.gemini\n }\n\n // Persist updated fingerprint versions to disk\n if (fingerprintVersionChanged) {\n this.requestSaveToDisk()\n }\n\n // If current auth isn't in the loaded accounts, add it to the pool\n if (authFallback && authParts?.refreshToken) {\n const hasMatching = this.accounts.some(\n (acc) => acc.parts.refreshToken === authParts.refreshToken,\n )\n if (!hasMatching) {\n const now = this.now()\n const newAccount: ManagedAccount = {\n index: this.accounts.length,\n email: undefined,\n addedAt: now,\n lastUsed: 0,\n parts: authParts,\n access: authFallback.access,\n expires: authFallback.expires,\n enabled: true,\n rateLimitResetTimes: {},\n touchedForQuota: {},\n fingerprint: generateFingerprint(),\n fingerprintHistory: [],\n }\n this.accounts.push(newAccount)\n }\n }\n\n return\n }\n\n if (authFallback) {\n const parts = parseRefreshParts(authFallback.refresh)\n if (parts.refreshToken) {\n const now = this.now()\n this.accounts = [\n {\n index: 0,\n email: undefined,\n addedAt: now,\n lastUsed: 0,\n parts,\n access: authFallback.access,\n expires: authFallback.expires,\n enabled: true,\n rateLimitResetTimes: {},\n touchedForQuota: {},\n },\n ]\n this.cursorByFamily = { claude: 0, gemini: 0 }\n this.currentAccountIndexByFamily.claude = 0\n this.currentAccountIndexByFamily.gemini = 0\n }\n }\n }\n\n getAccountCount(): number {\n return this.getEnabledAccounts().length\n }\n\n getTotalAccountCount(): number {\n return this.accounts.length\n }\n\n getEnabledAccounts(): ManagedAccount[] {\n return this.accounts.filter((account) => account.enabled !== false)\n }\n\n private getEffectiveSoftQuotaThreshold(thresholdPercent: number): number {\n // Soft-quota protection only has a purpose when another enabled account\n // exists to rotate to. Never block the sole usable account.\n return this.getEnabledAccounts().length > 1 ? thresholdPercent : 100\n }\n\n getAccountsSnapshot(): ManagedAccount[] {\n return this.accounts.map((a) => ({\n ...a,\n parts: { ...a.parts },\n rateLimitResetTimes: { ...a.rateLimitResetTimes },\n }))\n }\n\n private getRequestSessionState(\n identity: AccountSessionIdentity,\n ): AccountSessionState {\n const now = this.now()\n this.pruneRequestSessionStates(now, identity.id)\n\n const existing = this.requestSessionStates.get(identity.id)\n if (existing) {\n existing.lastAccessedAt = now\n if (identity.parentId) {\n existing.parentId = identity.parentId\n }\n return existing\n }\n\n const state: AccountSessionState = {\n parentId: identity.parentId ?? null,\n currentAccountIndexByFamily: { claude: -1, gemini: -1 },\n cursorByFamily: { ...this.cursorByFamily },\n offsetAppliedByFamily: { claude: false, gemini: false },\n usedAccounts: new Set<number>(),\n lastAccessedAt: now,\n }\n this.requestSessionStates.set(identity.id, state)\n return state\n }\n\n private pruneRequestSessionStates(now: number, preservedId: string): void {\n const expiry = now - ACCOUNT_SESSION_STATE_TTL_MS\n for (const [id, state] of this.requestSessionStates) {\n if (id !== preservedId && state.lastAccessedAt < expiry) {\n this.requestSessionStates.delete(id)\n }\n }\n\n if (\n this.requestSessionStates.size < MAX_ACCOUNT_SESSION_STATES ||\n this.requestSessionStates.has(preservedId)\n ) {\n return\n }\n\n let oldestId: string | null = null\n let oldestAccess = Number.POSITIVE_INFINITY\n for (const [id, state] of this.requestSessionStates) {\n if (id !== preservedId && state.lastAccessedAt < oldestAccess) {\n oldestId = id\n oldestAccess = state.lastAccessedAt\n }\n }\n if (oldestId) {\n this.requestSessionStates.delete(oldestId)\n }\n }\n\n private getActiveIndex(\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): number {\n return identity\n ? this.getRequestSessionState(identity).currentAccountIndexByFamily[\n family\n ]\n : this.currentAccountIndexByFamily[family]\n }\n\n private setActiveIndex(\n family: ModelFamily,\n index: number,\n identity?: AccountSessionIdentity,\n ): void {\n if (!identity) {\n this.currentAccountIndexByFamily[family] = index\n return\n }\n\n const state = this.getRequestSessionState(identity)\n state.currentAccountIndexByFamily[family] = index\n if (!state.parentId) {\n // Preserve a useful persisted starting point without coupling active root sessions.\n this.currentAccountIndexByFamily[family] = index\n }\n }\n\n private getCursor(\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): number {\n return identity\n ? this.getRequestSessionState(identity).cursorByFamily[family]\n : this.cursorByFamily[family]\n }\n\n private advanceCursor(\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): void {\n const nextGlobalCursor = this.cursorByFamily[family] + 1\n this.cursorByFamily[family] = nextGlobalCursor\n if (identity) {\n this.getRequestSessionState(identity).cursorByFamily[family] += 1\n }\n }\n\n private getUsedAccounts(identity?: AccountSessionIdentity): Set<number> {\n return identity\n ? this.getRequestSessionState(identity).usedAccounts\n : this.sessionUsedAccounts\n }\n\n private preferAccountOutsideParent(\n accounts: ManagedAccount[],\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): ManagedAccount[] {\n if (!identity) {\n return accounts\n }\n const parentId = this.getRequestSessionState(identity).parentId\n if (!parentId) {\n return accounts\n }\n const parentState = this.requestSessionStates.get(parentId)\n const parentIndex = parentState?.currentAccountIndexByFamily[family] ?? -1\n if (parentIndex < 0) {\n return accounts\n }\n const isolated = accounts.filter((account) => account.index !== parentIndex)\n return isolated.length > 0 ? isolated : accounts\n }\n\n deleteSessionState(sessionId: string): void {\n this.requestSessionStates.delete(sessionId)\n }\n\n getCurrentAccountForFamily(\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): ManagedAccount | null {\n const currentIndex = this.getActiveIndex(family, identity)\n if (currentIndex >= 0 && currentIndex < this.accounts.length) {\n const account = this.accounts[currentIndex] ?? null\n // Only return account if it's enabled - disabled accounts should not be selected\n if (account && account.enabled !== false) {\n return account\n }\n }\n return null\n }\n\n /**\n * Numeric active indexes for each model family. Exposed so callers\n * that persist `activeIndexByFamily` (e.g. command-data's remove\n * path) can capture the live cursor per family without going\n * through the account-lookup layer.\n */\n getActiveIndexByFamily(\n identity?: AccountSessionIdentity,\n ): Record<ModelFamily, number> {\n return {\n claude: this.getActiveIndex('claude', identity),\n gemini: this.getActiveIndex('gemini', identity),\n }\n }\n\n markSwitched(\n account: ManagedAccount,\n reason: 'rate-limit' | 'initial' | 'rotation',\n family: ModelFamily,\n identity?: AccountSessionIdentity,\n ): void {\n account.lastSwitchReason = reason\n this.setActiveIndex(family, account.index, identity)\n }\n\n /**\n * Check if we should show an account switch toast.\n * Debounces repeated toasts for the same account.\n */\n shouldShowAccountToast(accountIndex: number, debounceMs = 30000): boolean {\n const now = this.now()\n if (accountIndex !== this.lastToastAccountIndex) {\n return true\n }\n return now - this.lastToastTime >= debounceMs\n }\n\n markToastShown(accountIndex: number): void {\n this.lastToastAccountIndex = accountIndex\n this.lastToastTime = this.now()\n }\n\n getCurrentOrNextForFamily(\n family: ModelFamily,\n model?: string | null,\n strategy: AccountSelectionStrategy = 'sticky',\n headerStyle: HeaderStyle = 'antigravity',\n pidOffsetEnabled: boolean = false,\n softQuotaThresholdPercent: number = 100,\n softQuotaCacheTtlMs: number = 10 * 60 * 1000,\n identity?: AccountSessionIdentity,\n /**\n * Account indexes the caller has ruled out (e.g. the operator\n * killswitch pre-filter). Every selection path \u2014 pinned session,\n * round-robin, hybrid, and sticky fallback \u2014 skips these indexes\n * so a killed current account falls through to the next eligible\n * account instead of collapsing the request into the rate-limit\n * wait path.\n */\n excludeIndexes?: Set<number>,\n ): ManagedAccount | null {\n const quotaKey = getQuotaKey(family, headerStyle, model)\n const effectiveSoftQuotaThreshold = this.getEffectiveSoftQuotaThreshold(\n softQuotaThresholdPercent,\n )\n\n // OpenCode may run many root and child sessions concurrently in one plugin\n // process. Pin each exact session until its account becomes unavailable.\n if (identity) {\n const pinned = this.getCurrentAccountForFamily(family, identity)\n if (pinned) {\n clearExpiredRateLimits(pinned, this.now)\n const unavailable =\n (excludeIndexes?.has(pinned.index) ?? false) ||\n isRateLimitedForHeaderStyle(\n pinned,\n family,\n headerStyle,\n this.now,\n model,\n ) ||\n isOverSoftQuotaThreshold(\n pinned,\n family,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n this.now,\n model,\n ) ||\n this.isAccountCoolingDown(pinned)\n if (!unavailable) {\n this.markTouchedForQuota(pinned, quotaKey)\n return pinned\n }\n }\n }\n\n if (strategy === 'round-robin') {\n const next = this.getNextForFamily(\n family,\n model,\n headerStyle,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n identity,\n excludeIndexes,\n )\n if (next) {\n this.markTouchedForQuota(next, quotaKey)\n this.setActiveIndex(family, next.index, identity)\n }\n return next\n }\n\n if (strategy === 'hybrid') {\n const healthTracker = getHealthTracker()\n const tokenTracker = getTokenTracker()\n\n const eligibleAccounts = this.preferAccountOutsideParent(\n this.accounts.filter(\n (acc) => acc.enabled !== false && !excludeIndexes?.has(acc.index),\n ),\n family,\n identity,\n )\n const accountsWithMetrics: AccountWithMetrics[] = eligibleAccounts.map(\n (acc) => {\n clearExpiredRateLimits(acc, this.now)\n return {\n index: acc.index,\n lastUsed: acc.lastUsed,\n healthScore: healthTracker.getScore(acc.index),\n isRateLimited:\n isRateLimitedForHeaderStyle(\n acc,\n family,\n headerStyle,\n this.now,\n model,\n ) ||\n isOverSoftQuotaThreshold(\n acc,\n family,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n this.now,\n model,\n ),\n isCoolingDown: this.isAccountCoolingDown(acc),\n }\n },\n )\n\n // Get current account index for stickiness\n const currentIndex = this.getActiveIndex(family, identity)\n\n const selectedIndex = selectHybridAccount(\n accountsWithMetrics,\n tokenTracker,\n currentIndex,\n 50,\n this.now,\n )\n if (selectedIndex !== null) {\n const selected = this.accounts[selectedIndex]\n if (selected) {\n selected.lastUsed = this.now()\n this.markTouchedForQuota(selected, quotaKey)\n this.setActiveIndex(family, selected.index, identity)\n return selected\n }\n }\n }\n\n // Fallback: sticky selection (used when hybrid finds no candidates)\n // PID-based offset for multi-session distribution (opt-in)\n // Different sessions (PIDs) will prefer different starting accounts\n const offsetApplied = identity\n ? this.getRequestSessionState(identity).offsetAppliedByFamily\n : this.sessionOffsetApplied\n if (\n pidOffsetEnabled &&\n !offsetApplied[family] &&\n this.accounts.length > 1\n ) {\n const pidOffset = this.pid % this.accounts.length\n const activeIndex = this.getActiveIndex(family, identity)\n const baseIndex =\n activeIndex >= 0 ? activeIndex : this.getCursor(family, identity)\n const newIndex = (baseIndex + pidOffset) % this.accounts.length\n\n this.onDiagnostic?.('Applying PID account offset', {\n pid: this.pid,\n offset: pidOffset,\n family,\n fromIndex: baseIndex,\n toIndex: newIndex,\n })\n\n this.setActiveIndex(family, newIndex, identity)\n offsetApplied[family] = true\n }\n\n const current = this.getCurrentAccountForFamily(family, identity)\n if (current && !excludeIndexes?.has(current.index)) {\n clearExpiredRateLimits(current, this.now)\n const isLimitedForRequestedStyle = isRateLimitedForHeaderStyle(\n current,\n family,\n headerStyle,\n this.now,\n model,\n )\n const isOverThreshold = isOverSoftQuotaThreshold(\n current,\n family,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n this.now,\n model,\n )\n if (\n !isLimitedForRequestedStyle &&\n !isOverThreshold &&\n !this.isAccountCoolingDown(current)\n ) {\n this.markTouchedForQuota(current, quotaKey)\n return current\n }\n }\n\n const next = this.getNextForFamily(\n family,\n model,\n headerStyle,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n identity,\n excludeIndexes,\n )\n if (next) {\n this.markTouchedForQuota(next, quotaKey)\n this.setActiveIndex(family, next.index, identity)\n }\n return next\n }\n\n getNextForFamily(\n family: ModelFamily,\n model?: string | null,\n headerStyle: HeaderStyle = 'antigravity',\n softQuotaThresholdPercent: number = 100,\n softQuotaCacheTtlMs: number = 10 * 60 * 1000,\n identity?: AccountSessionIdentity,\n /** Indexes ruled out by the caller (e.g. killswitch pre-filter). */\n excludeIndexes?: Set<number>,\n ): ManagedAccount | null {\n const effectiveSoftQuotaThreshold = this.getEffectiveSoftQuotaThreshold(\n softQuotaThresholdPercent,\n )\n const allAvailable = this.accounts.filter((account) => {\n clearExpiredRateLimits(account, this.now)\n return (\n account.enabled !== false &&\n !excludeIndexes?.has(account.index) &&\n !isRateLimitedForHeaderStyle(\n account,\n family,\n headerStyle,\n this.now,\n model,\n ) &&\n !isOverSoftQuotaThreshold(\n account,\n family,\n effectiveSoftQuotaThreshold,\n softQuotaCacheTtlMs,\n this.now,\n model,\n ) &&\n !this.isAccountCoolingDown(account)\n )\n })\n const available = this.preferAccountOutsideParent(\n allAvailable,\n family,\n identity,\n )\n\n if (available.length === 0) {\n return null\n }\n\n const usedAccounts = this.getUsedAccounts(identity)\n const sessionUsed = available.filter((account) =>\n usedAccounts.has(account.index),\n )\n const candidates = sessionUsed.length > 0 ? sessionUsed : available\n\n const cursor = this.getCursor(family, identity)\n const account = candidates[cursor % candidates.length]\n if (!account) {\n return null\n }\n\n this.advanceCursor(family, identity)\n return account\n }\n markRateLimited(\n account: ManagedAccount,\n retryAfterMs: number,\n family: ModelFamily,\n headerStyle: HeaderStyle = 'antigravity',\n model?: string | null,\n ): void {\n const key = getQuotaKey(family, headerStyle, model)\n account.rateLimitResetTimes[key] = this.now() + retryAfterMs\n }\n\n /**\n * Mark an account as used after a successful API request.\n * This updates the lastUsed timestamp for freshness calculations.\n * Should be called AFTER request completion, not during account selection.\n */\n markAccountUsed(accountIndex: number): void {\n const account = this.accounts.find((a) => a.index === accountIndex)\n if (account) {\n account.lastUsed = this.now()\n }\n }\n\n recordSessionUsage(\n accountIndex: number,\n identity?: AccountSessionIdentity,\n ): void {\n this.getUsedAccounts(identity).add(accountIndex)\n }\n\n wasUsedInSession(\n accountIndex: number,\n identity?: AccountSessionIdentity,\n ): boolean {\n return this.getUsedAccounts(identity).has(accountIndex)\n }\n\n shouldProactivelyRotate(\n family: ModelFamily,\n model: string | null | undefined,\n thresholdPercent: number,\n cacheTtlMs: number,\n identity?: AccountSessionIdentity,\n ): boolean {\n if (thresholdPercent <= 0) return false\n\n const current = this.getCurrentAccountForFamily(family, identity)\n if (!current?.cachedQuota || current.cachedQuotaUpdatedAt == null)\n return false\n\n const age = this.now() - current.cachedQuotaUpdatedAt\n if (age > cacheTtlMs) return false\n\n const quotaGroup = resolveQuotaGroup(family, model)\n const groupData = current.cachedQuota[quotaGroup]\n if (groupData?.remainingFraction == null) return false\n\n const remainingPercent = Math.max(\n 0,\n Math.min(100, groupData.remainingFraction * 100),\n )\n return remainingPercent < thresholdPercent\n }\n\n proactivelyRotateForFamily(\n family: ModelFamily,\n model: string | null | undefined,\n headerStyle: HeaderStyle,\n softQuotaThresholdPercent: number,\n softQuotaCacheTtlMs: number,\n identity?: AccountSessionIdentity,\n ): ManagedAccount | null {\n const currentIndex = this.getActiveIndex(family, identity)\n\n const candidates = this.preferAccountOutsideParent(\n this.accounts.filter((acc) => {\n if (acc.enabled === false) return false\n if (acc.index === currentIndex) return false\n clearExpiredRateLimits(acc, this.now)\n if (\n isRateLimitedForHeaderStyle(acc, family, headerStyle, this.now, model)\n )\n return false\n if (\n isOverSoftQuotaThreshold(\n acc,\n family,\n softQuotaThresholdPercent,\n softQuotaCacheTtlMs,\n this.now,\n model,\n )\n )\n return false\n if (this.isAccountCoolingDown(acc)) return false\n return true\n }),\n family,\n identity,\n )\n\n if (candidates.length === 0) return null\n\n const usedAccounts = this.getUsedAccounts(identity)\n const warmCandidates = candidates.filter((account) =>\n usedAccounts.has(account.index),\n )\n const pool = warmCandidates.length > 0 ? warmCandidates : candidates\n\n const quotaGroup = resolveQuotaGroup(family, model)\n pool.sort((a, b) => {\n const aRemaining = a.cachedQuota?.[quotaGroup]?.remainingFraction ?? 0\n const bRemaining = b.cachedQuota?.[quotaGroup]?.remainingFraction ?? 0\n return bRemaining - aRemaining\n })\n\n const selected = pool[0]\n if (!selected) return null\n\n const quotaKey = getQuotaKey(family, headerStyle, model)\n this.markTouchedForQuota(selected, quotaKey)\n this.setActiveIndex(family, selected.index, identity)\n\n return selected\n }\n\n markRateLimitedWithReason(\n account: ManagedAccount,\n family: ModelFamily,\n headerStyle: HeaderStyle,\n model: string | null | undefined,\n reason: RateLimitReason,\n retryAfterMs?: number | null,\n failureTtlMs: number = 3600_000, // Default 1 hour TTL\n ): number {\n const now = this.now()\n\n // TTL-based reset: if last failure was more than failureTtlMs ago, reset count\n if (\n account.lastFailureTime !== undefined &&\n now - account.lastFailureTime > failureTtlMs\n ) {\n account.consecutiveFailures = 0\n }\n\n const failures = (account.consecutiveFailures ?? 0) + 1\n account.consecutiveFailures = failures\n account.lastFailureTime = now\n\n const backoffMs = calculateBackoffMs(\n reason,\n failures - 1,\n retryAfterMs,\n this.random,\n )\n const key = getQuotaKey(family, headerStyle, model)\n account.rateLimitResetTimes[key] = now + backoffMs\n\n return backoffMs\n }\n\n markRequestSuccess(account: ManagedAccount): void {\n if (account.consecutiveFailures) {\n account.consecutiveFailures = 0\n }\n }\n\n clearAllRateLimitsForFamily(\n family: ModelFamily,\n model?: string | null,\n ): void {\n for (const account of this.accounts) {\n if (family === 'claude') {\n delete account.rateLimitResetTimes.claude\n } else {\n const antigravityKey = getQuotaKey(family, 'antigravity', model)\n const cliKey = getQuotaKey(family, 'gemini-cli', model)\n delete account.rateLimitResetTimes[antigravityKey]\n delete account.rateLimitResetTimes[cliKey]\n }\n account.consecutiveFailures = 0\n }\n }\n\n shouldTryOptimisticReset(\n family: ModelFamily,\n model?: string | null,\n ): boolean {\n const minWaitMs = this.getMinWaitTimeForFamily(family, model)\n return minWaitMs > 0 && minWaitMs <= 2_000\n }\n\n markAccountCoolingDown(\n account: ManagedAccount,\n cooldownMs: number,\n reason: CooldownReason,\n ): void {\n account.coolingDownUntil = this.now() + cooldownMs\n account.cooldownReason = reason\n }\n\n isAccountCoolingDown(account: ManagedAccount): boolean {\n if (account.coolingDownUntil === undefined) {\n return false\n }\n if (this.now() >= account.coolingDownUntil) {\n this.clearAccountCooldown(account)\n return false\n }\n return true\n }\n\n clearAccountCooldown(account: ManagedAccount): void {\n delete account.coolingDownUntil\n delete account.cooldownReason\n }\n\n getAccountCooldownReason(\n account: ManagedAccount,\n ): CooldownReason | undefined {\n return this.isAccountCoolingDown(account)\n ? account.cooldownReason\n : undefined\n }\n\n markTouchedForQuota(account: ManagedAccount, quotaKey: string): void {\n account.touchedForQuota[quotaKey] = this.now()\n }\n\n isFreshForQuota(account: ManagedAccount, quotaKey: string): boolean {\n const touchedAt = account.touchedForQuota[quotaKey]\n if (!touchedAt) return true\n\n const resetTime = account.rateLimitResetTimes[quotaKey as QuotaKey]\n if (resetTime && touchedAt < resetTime) return true\n\n return false\n }\n\n getFreshAccountsForQuota(\n quotaKey: string,\n family: ModelFamily,\n model?: string | null,\n ): ManagedAccount[] {\n return this.accounts.filter((acc) => {\n clearExpiredRateLimits(acc, this.now)\n return (\n acc.enabled !== false &&\n this.isFreshForQuota(acc, quotaKey) &&\n !isRateLimitedForFamily(acc, family, this.now, model) &&\n !this.isAccountCoolingDown(acc)\n )\n })\n }\n\n isRateLimitedForHeaderStyle(\n account: ManagedAccount,\n family: ModelFamily,\n headerStyle: HeaderStyle,\n model?: string | null,\n ): boolean {\n return isRateLimitedForHeaderStyle(\n account,\n family,\n headerStyle,\n this.now,\n model,\n )\n }\n\n getAvailableHeaderStyle(\n account: ManagedAccount,\n family: ModelFamily,\n model?: string | null,\n ): HeaderStyle | null {\n clearExpiredRateLimits(account, this.now)\n if (family === 'claude') {\n return isRateLimitedForHeaderStyle(\n account,\n family,\n 'antigravity',\n this.now,\n )\n ? null\n : 'antigravity'\n }\n if (\n !isRateLimitedForHeaderStyle(\n account,\n family,\n 'antigravity',\n this.now,\n model,\n )\n ) {\n return 'antigravity'\n }\n if (\n !isRateLimitedForHeaderStyle(\n account,\n family,\n 'gemini-cli',\n this.now,\n model,\n )\n ) {\n return 'gemini-cli'\n }\n return null\n }\n\n /**\n * Check if any OTHER account has antigravity quota available for the given family/model.\n *\n * Used to determine whether to switch accounts vs fall back to gemini-cli:\n * - If true: Switch to another account (preserve antigravity priority)\n * - If false: All accounts exhausted antigravity, safe to fall back to gemini-cli\n *\n * @param currentAccountIndex - Index of the current account (will be excluded from check)\n * @param family - Model family (\"gemini\" or \"claude\")\n * @param model - Optional model name for model-specific rate limits\n * @returns true if any other enabled, non-cooling-down account has antigravity available\n */\n hasOtherAccountWithAntigravityAvailable(\n currentAccountIndex: number,\n family: ModelFamily,\n model?: string | null,\n ): boolean {\n // Claude has no gemini-cli fallback - always return false\n // (This method is only relevant for Gemini's dual quota pools)\n if (family === 'claude') {\n return false\n }\n\n return this.accounts.some((acc) => {\n // Skip current account\n if (acc.index === currentAccountIndex) {\n return false\n }\n // Skip disabled accounts\n if (acc.enabled === false) {\n return false\n }\n // Skip cooling down accounts\n if (this.isAccountCoolingDown(acc)) {\n return false\n }\n // Clear expired rate limits before checking\n clearExpiredRateLimits(acc, this.now)\n // Check if antigravity is available for this account\n return !isRateLimitedForHeaderStyle(\n acc,\n family,\n 'antigravity',\n this.now,\n model,\n )\n })\n }\n\n setAccountEnabled(accountIndex: number, enabled: boolean): boolean {\n const account = this.accounts[accountIndex]\n if (!account) {\n return false\n }\n if (enabled && account.accountIneligible) {\n return false\n }\n account.enabled = enabled\n\n if (!enabled) {\n for (const family of Object.keys(\n this.currentAccountIndexByFamily,\n ) as ModelFamily[]) {\n if (this.currentAccountIndexByFamily[family] === accountIndex) {\n const next = this.accounts.find(\n (a, i) => i !== accountIndex && a.enabled !== false,\n )\n this.currentAccountIndexByFamily[family] = next?.index ?? -1\n }\n }\n }\n\n this.requestSaveToDisk()\n return true\n }\n\n markAccountVerificationRequired(\n accountIndex: number,\n reason?: string,\n verifyUrl?: string,\n ): boolean {\n const account = this.accounts[accountIndex]\n if (!account) {\n return false\n }\n\n const timestamp = this.now()\n account.verificationRequired = true\n account.verificationRequiredAt = timestamp\n account.verificationRequiredReason = reason?.trim() || undefined\n if (\n account.accountIneligible === true ||\n account.accountIneligibleAt !== undefined ||\n account.accountIneligibleReason !== undefined\n ) {\n account.accountIneligible = false\n account.accountIneligibleAt = undefined\n account.accountIneligibleReason = undefined\n account.eligibilityStateUpdatedAt = timestamp\n }\n\n const normalizedVerifyUrl = verifyUrl?.trim()\n if (normalizedVerifyUrl) {\n account.verificationUrl = normalizedVerifyUrl\n }\n\n if (account.enabled !== false) {\n this.setAccountEnabled(accountIndex, false)\n } else {\n this.requestSaveToDisk()\n }\n\n return true\n }\n\n markAccountIneligible(accountIndex: number, reason?: string): boolean {\n const account = this.accounts[accountIndex]\n if (!account) {\n return false\n }\n\n const timestamp = this.now()\n account.accountIneligible = true\n account.accountIneligibleAt = timestamp\n account.accountIneligibleReason =\n reason?.trim() || 'Google marked this account as ineligible.'\n account.eligibilityStateUpdatedAt = timestamp\n account.verificationRequired = false\n account.verificationRequiredAt = undefined\n account.verificationRequiredReason = undefined\n account.verificationUrl = undefined\n\n if (account.enabled !== false) {\n this.setAccountEnabled(accountIndex, false)\n } else {\n this.requestSaveToDisk()\n }\n return true\n }\n\n clearAccountAccessBlocks(\n accountIndex: number,\n enableAccount = false,\n ): boolean {\n const account = this.accounts[accountIndex]\n if (!account) {\n return false\n }\n\n const wasVerificationRequired = account.verificationRequired === true\n const wasIneligible = account.accountIneligible === true\n const hadMetadata =\n wasVerificationRequired ||\n wasIneligible ||\n account.verificationRequiredAt !== undefined ||\n account.verificationRequiredReason !== undefined ||\n account.verificationUrl !== undefined ||\n account.accountIneligibleAt !== undefined ||\n account.accountIneligibleReason !== undefined ||\n account.eligibilityStateUpdatedAt !== undefined\n\n account.verificationRequired = false\n account.verificationRequiredAt = undefined\n account.verificationRequiredReason = undefined\n account.verificationUrl = undefined\n account.accountIneligible = false\n account.accountIneligibleAt = undefined\n account.accountIneligibleReason = undefined\n if (wasIneligible || account.eligibilityStateUpdatedAt !== undefined) {\n account.eligibilityStateUpdatedAt = this.now()\n }\n\n if (\n enableAccount &&\n (wasVerificationRequired || wasIneligible) &&\n account.enabled === false\n ) {\n this.setAccountEnabled(accountIndex, true)\n } else if (hadMetadata) {\n this.requestSaveToDisk()\n }\n return true\n }\n\n removeAccountByIndex(accountIndex: number): boolean {\n if (accountIndex < 0 || accountIndex >= this.accounts.length) {\n return false\n }\n const account = this.accounts[accountIndex]\n if (!account) {\n return false\n }\n return this.removeAccount(account)\n }\n\n removeAccount(account: ManagedAccount): boolean {\n const idx = this.accounts.indexOf(account)\n if (idx < 0) {\n return false\n }\n\n this.accounts.splice(idx, 1)\n this.accounts.forEach((acc, index) => {\n acc.index = index\n })\n\n if (this.accounts.length === 0) {\n this.cursorByFamily = { claude: 0, gemini: 0 }\n this.currentAccountIndexByFamily.claude = -1\n this.currentAccountIndexByFamily.gemini = -1\n this.requestSessionStates.clear()\n return true\n }\n\n for (const family of ['claude', 'gemini'] as ModelFamily[]) {\n if (this.cursorByFamily[family] > idx) {\n this.cursorByFamily[family] -= 1\n }\n this.cursorByFamily[family] =\n this.cursorByFamily[family] % this.accounts.length\n\n if (this.currentAccountIndexByFamily[family] > idx) {\n this.currentAccountIndexByFamily[family] -= 1\n }\n if (this.currentAccountIndexByFamily[family] >= this.accounts.length) {\n this.currentAccountIndexByFamily[family] = -1\n }\n\n for (const state of this.requestSessionStates.values()) {\n const currentIndex = state.currentAccountIndexByFamily[family]\n if (currentIndex === idx) {\n state.currentAccountIndexByFamily[family] = -1\n } else if (currentIndex > idx) {\n state.currentAccountIndexByFamily[family] -= 1\n }\n if (state.cursorByFamily[family] > idx) {\n state.cursorByFamily[family] -= 1\n }\n state.cursorByFamily[family] %= this.accounts.length\n }\n }\n\n for (const state of this.requestSessionStates.values()) {\n state.usedAccounts = new Set(\n [...state.usedAccounts]\n .filter((accountIndex) => accountIndex !== idx)\n .map((accountIndex) =>\n accountIndex > idx ? accountIndex - 1 : accountIndex,\n ),\n )\n }\n\n return true\n }\n\n updateFromAuth(account: ManagedAccount, auth: OAuthAuthDetails): void {\n const parts = parseRefreshParts(auth.refresh)\n // Preserve existing projectId/managedProjectId if not in the new parts\n account.parts = {\n ...parts,\n projectId: parts.projectId ?? account.parts.projectId,\n managedProjectId:\n parts.managedProjectId ?? account.parts.managedProjectId,\n }\n // Keep the record-level fields in sync with the authoritative source.\n account.projectId = parts.projectId ?? account.projectId\n account.managedProjectId =\n parts.managedProjectId ?? account.managedProjectId\n account.access = auth.access\n account.expires = auth.expires\n }\n\n toAuthDetails(account: ManagedAccount): OAuthAuthDetails {\n return {\n type: 'oauth',\n refresh: formatRefreshParts(account.parts),\n access: account.access,\n expires: account.expires,\n }\n }\n\n getMinWaitTimeForFamily(\n family: ModelFamily,\n model?: string | null,\n headerStyle?: HeaderStyle,\n strict?: boolean,\n ): number {\n const available = this.accounts.filter((a) => {\n clearExpiredRateLimits(a, this.now)\n return (\n a.enabled !== false &&\n (strict && headerStyle\n ? !isRateLimitedForHeaderStyle(\n a,\n family,\n headerStyle,\n this.now,\n model,\n )\n : !isRateLimitedForFamily(a, family, this.now, model))\n )\n })\n if (available.length > 0) {\n return 0\n }\n\n const waitTimes: number[] = []\n for (const a of this.accounts) {\n if (family === 'claude') {\n const t = a.rateLimitResetTimes.claude\n if (t !== undefined) waitTimes.push(Math.max(0, t - this.now()))\n } else if (strict && headerStyle) {\n const key = getQuotaKey(family, headerStyle, model)\n const t = a.rateLimitResetTimes[key]\n if (t !== undefined) waitTimes.push(Math.max(0, t - this.now()))\n } else {\n // For Gemini, account becomes available when EITHER pool expires for this model/family\n const antigravityKey = getQuotaKey(family, 'antigravity', model)\n const cliKey = getQuotaKey(family, 'gemini-cli', model)\n\n const t1 = a.rateLimitResetTimes[antigravityKey]\n const t2 = a.rateLimitResetTimes[cliKey]\n\n const accountWait = Math.min(\n t1 !== undefined ? Math.max(0, t1 - this.now()) : Infinity,\n t2 !== undefined ? Math.max(0, t2 - this.now()) : Infinity,\n )\n if (accountWait !== Infinity) waitTimes.push(accountWait)\n }\n }\n\n return waitTimes.length > 0 ? Math.min(...waitTimes) : 0\n }\n\n getAccounts(): ManagedAccount[] {\n return [...this.accounts]\n }\n\n private buildStorageSnapshot(): AccountStorageV4 {\n const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude)\n const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini)\n\n return {\n version: 4,\n accounts: this.accounts.map((a) => ({\n email: a.email,\n label: a.label,\n refreshToken: a.parts.refreshToken,\n projectId: a.parts.projectId ?? a.projectId,\n managedProjectId: a.parts.managedProjectId ?? a.managedProjectId,\n addedAt: a.addedAt,\n lastUsed: a.lastUsed,\n enabled: a.enabled,\n rateLimitResetTimes:\n Object.keys(a.rateLimitResetTimes).length > 0\n ? a.rateLimitResetTimes\n : undefined,\n fingerprint: a.fingerprint,\n fingerprintHistory: a.fingerprintHistory?.length\n ? a.fingerprintHistory\n : undefined,\n cachedQuota:\n a.cachedQuota && Object.keys(a.cachedQuota).length > 0\n ? a.cachedQuota\n : undefined,\n // Persist the opaque identity stamp alongside the quota so a later\n // loadFromDisk + projection can detect a stale snapshot captured\n // for a different account after an index shift.\n cachedQuotaAccountId: a.cachedQuotaAccountId,\n cachedQuotaUpdatedAt: a.cachedQuotaUpdatedAt,\n capturedTierId: a.capturedTierId,\n capturedPaidTierId: a.capturedPaidTierId,\n capturedTierAt: a.capturedTierAt,\n capturedTierSchemaVersion: a.capturedTierSchemaVersion,\n dailyRequestCounts: a.dailyRequestCounts,\n verificationRequired: a.verificationRequired,\n verificationRequiredAt: a.verificationRequiredAt,\n verificationRequiredReason: a.verificationRequiredReason,\n verificationUrl: a.verificationUrl,\n accountIneligible: a.accountIneligible,\n accountIneligibleAt: a.accountIneligibleAt,\n accountIneligibleReason: a.accountIneligibleReason,\n eligibilityStateUpdatedAt: a.eligibilityStateUpdatedAt,\n })),\n activeIndex: claudeIndex,\n activeIndexByFamily: {\n claude: claudeIndex,\n gemini: geminiIndex,\n },\n }\n }\n\n async saveToDisk(): Promise<void> {\n await this.store.saveMerged(this.storagePath, this.buildStorageSnapshot())\n }\n\n /**\n * Persist via full-file replace (no merge). Required after destructive\n * operations (account removal) so a deleted account is not resurrected by\n * mergeAccountStorage re-reading it from disk.\n */\n async saveToDiskReplace(): Promise<void> {\n const snapshot = this.buildStorageSnapshot()\n await this.store.mutate(this.storagePath, () => snapshot)\n }\n\n requestSaveToDisk(): void {\n if (this.disposed || this.savePending) {\n return\n }\n this.savePending = true\n this.saveTimeout = setTimeout(() => {\n this.saveInFlight = this.executeSave().finally(() => {\n this.saveInFlight = null\n })\n }, 1000)\n }\n\n async flushSaveToDisk(): Promise<void> {\n if (!this.savePending) {\n await this.saveInFlight\n return\n }\n return new Promise<void>((resolve, reject) => {\n this.savePromiseResolvers.push({ resolve, reject })\n })\n }\n async dispose(): Promise<void> {\n if (this.disposed) return\n this.disposed = true\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout)\n this.saveTimeout = null\n }\n if (this.savePending) {\n await this.executeSave()\n }\n await this.saveInFlight\n }\n\n private async executeSave(): Promise<void> {\n this.savePending = false\n this.saveTimeout = null\n\n const resolvers = this.savePromiseResolvers\n this.savePromiseResolvers = []\n\n try {\n await this.saveToDisk()\n for (const { resolve } of resolvers) {\n resolve()\n }\n } catch (error) {\n if (isStorageLockContention(error)) {\n this.onDiagnostic?.(\n 'Skipped account-state persist due to storage lock contention',\n {\n error: String(error),\n },\n )\n for (const { resolve } of resolvers) {\n resolve()\n }\n return\n }\n\n this.onDiagnostic?.('Failed to persist account state', {\n error: String(error),\n })\n for (const { reject } of resolvers) {\n reject(error)\n }\n }\n }\n // ========== Fingerprint Management ==========\n\n /**\n * Regenerate fingerprint for an account, saving the old one to history.\n * @param accountIndex - Index of the account to regenerate fingerprint for\n * @returns The new fingerprint, or null if account not found\n */\n regenerateAccountFingerprint(accountIndex: number): Fingerprint | null {\n const account = this.accounts[accountIndex]\n if (!account) return null\n\n // Save current fingerprint to history if it exists\n if (account.fingerprint) {\n const historyEntry: FingerprintVersion = {\n fingerprint: account.fingerprint,\n timestamp: this.now(),\n reason: 'regenerated',\n }\n\n if (!account.fingerprintHistory) {\n account.fingerprintHistory = []\n }\n\n // Add to beginning of history (most recent first)\n account.fingerprintHistory.unshift(historyEntry)\n\n // Trim to max history size\n if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {\n account.fingerprintHistory = account.fingerprintHistory.slice(\n 0,\n MAX_FINGERPRINT_HISTORY,\n )\n }\n }\n\n // Generate and assign new fingerprint\n account.fingerprint = generateFingerprint()\n this.requestSaveToDisk()\n\n return account.fingerprint\n }\n\n /**\n * Restore a fingerprint from history for an account.\n * @param accountIndex - Index of the account\n * @param historyIndex - Index in the fingerprint history to restore from (0 = most recent)\n * @returns The restored fingerprint, or null if account/history not found\n */\n restoreAccountFingerprint(\n accountIndex: number,\n historyIndex: number,\n ): Fingerprint | null {\n const account = this.accounts[accountIndex]\n if (!account) return null\n\n const history = account.fingerprintHistory\n if (!history || historyIndex < 0 || historyIndex >= history.length) {\n return null\n }\n\n // Capture the fingerprint to restore BEFORE modifying history\n const fingerprintToRestore = history[historyIndex]!.fingerprint\n\n // Save current fingerprint to history before restoring (if it exists)\n if (account.fingerprint) {\n const historyEntry: FingerprintVersion = {\n fingerprint: account.fingerprint,\n timestamp: this.now(),\n reason: 'restored',\n }\n\n account.fingerprintHistory!.unshift(historyEntry)\n\n // Trim to max history size\n if (account.fingerprintHistory!.length > MAX_FINGERPRINT_HISTORY) {\n account.fingerprintHistory = account.fingerprintHistory!.slice(\n 0,\n MAX_FINGERPRINT_HISTORY,\n )\n }\n }\n\n // Restore the fingerprint\n account.fingerprint = { ...fingerprintToRestore, createdAt: this.now() }\n\n this.requestSaveToDisk()\n\n return account.fingerprint\n }\n\n /**\n * Get fingerprint history for an account.\n * @param accountIndex - Index of the account\n * @returns Array of fingerprint versions, or empty array if not found\n */\n getAccountFingerprintHistory(accountIndex: number): FingerprintVersion[] {\n const account = this.accounts[accountIndex]\n if (!account?.fingerprintHistory) {\n return []\n }\n return [...account.fingerprintHistory]\n }\n\n updateQuotaCache(\n accountIndex: number,\n quotaGroups: Partial<Record<QuotaGroup, QuotaGroupSummary>>,\n expectedRefreshToken?: string,\n ): void {\n const account = this.accounts[accountIndex]\n if (\n !account ||\n (account.parts.refreshToken !== expectedRefreshToken &&\n expectedRefreshToken !== undefined)\n )\n return\n account.cachedQuota = quotaGroups\n // Stamp the cached quota with an opaque identity derived from the refresh\n // token so a later projection can detect a stale snapshot captured for\n // a different account after an index shift.\n account.cachedQuotaAccountId = quotaAccountIdentity(\n account.parts.refreshToken,\n )\n account.cachedQuotaUpdatedAt = this.now()\n }\n\n /**\n * Apply a subset of fields from a quota-fetch `updatedAccount` result onto\n * the live in-memory record for the given index. Only patches fields that\n * are present and non-empty in `patch` to avoid overwriting valid state\n * with stale or missing values.\n *\n * Identity guard: if `expectedRefreshToken` is provided and the account at\n * `accountIndex` no longer carries that token (concurrent reorder/replace),\n * the patch is silently dropped.\n *\n * `managedProjectId` is intentionally absent from the patch type: the only\n * caller (`BackgroundQuotaRefresh`) routes through `PollerAccountView` which\n * exposes only `capturedTierId`/`capturedTierAt`; project-context updates\n * happen via `ensureProjectContext`, not through this method.\n */\n applyUpdatedAccount(\n accountIndex: number,\n patch: Partial<\n Pick<\n AccountMetadataV3,\n | 'capturedTierId'\n | 'capturedPaidTierId'\n | 'capturedTierAt'\n | 'capturedTierSchemaVersion'\n >\n >,\n expectedRefreshToken?: string,\n ): void {\n const account = this.accounts[accountIndex]\n if (\n !account ||\n (expectedRefreshToken !== undefined &&\n account.parts.refreshToken !== expectedRefreshToken)\n )\n return\n if (patch.capturedTierId !== undefined) {\n account.capturedTierId = patch.capturedTierId\n }\n if (patch.capturedPaidTierId !== undefined) {\n account.capturedPaidTierId = patch.capturedPaidTierId\n }\n if (patch.capturedTierAt !== undefined) {\n account.capturedTierAt = patch.capturedTierAt\n }\n if (patch.capturedTierSchemaVersion !== undefined) {\n account.capturedTierSchemaVersion = patch.capturedTierSchemaVersion\n }\n }\n\n /**\n * Record a successful API request for an account.\n * Tracks per model family with daily reset.\n */\n recordRequest(accountIndex: number, family: ModelFamily): void {\n const account = this.accounts[accountIndex]\n if (!account) return\n\n const today = new Date(this.now()).toISOString().slice(0, 10)\n\n if (\n !account.dailyRequestCounts ||\n account.dailyRequestCounts.date !== today\n ) {\n account.dailyRequestCounts = { date: today, claude: 0, gemini: 0 }\n }\n\n account.dailyRequestCounts[family]++\n account.lastUsed = this.now()\n\n // Also track for session\n this.recordSessionRequest(accountIndex, family)\n }\n\n /**\n * Get request counts for an account for today.\n */\n getDailyRequestCounts(\n accountIndex: number,\n ): { date: string; claude: number; gemini: number } | null {\n const account = this.accounts[accountIndex]\n if (!account?.dailyRequestCounts) return null\n\n const today = new Date(this.now()).toISOString().slice(0, 10)\n if (account.dailyRequestCounts.date !== today) return null\n\n return { ...account.dailyRequestCounts }\n }\n\n /**\n * Get total daily request counts across all accounts for a model family.\n */\n getTotalDailyRequests(family: ModelFamily): number {\n const today = new Date(this.now()).toISOString().slice(0, 10)\n let total = 0\n for (const account of this.accounts) {\n if (account.dailyRequestCounts?.date === today) {\n total += account.dailyRequestCounts[family]\n }\n }\n return total\n }\n\n /**\n * Get a summary of daily request distribution across accounts.\n * Returns accounts sorted by request count (descending).\n */\n getDailyRequestSummary(\n family: ModelFamily,\n ): Array<{ index: number; email?: string; count: number }> {\n const today = new Date(this.now()).toISOString().slice(0, 10)\n const result: Array<{ index: number; email?: string; count: number }> = []\n\n for (const account of this.accounts) {\n const count =\n account.dailyRequestCounts?.date === today\n ? account.dailyRequestCounts[family]\n : 0\n if (count > 0) {\n result.push({ index: account.index, email: account.email, count })\n }\n }\n\n return result.sort((a, b) => b.count - a.count)\n }\n\n /**\n * Record a request for the current session (in-memory only).\n */\n recordSessionRequest(accountIndex: number, family: ModelFamily): void {\n const key = String(accountIndex)\n const current = this.sessionRequestCounts.get(key) ?? {\n claude: 0,\n gemini: 0,\n }\n current[family]++\n this.sessionRequestCounts.set(key, current)\n }\n\n /**\n * Get a summary of the current session's request usage.\n */\n getSessionSummary(): {\n durationMinutes: number\n totalClaude: number\n totalGemini: number\n requestsPerHour: number\n accountsUsed: number\n perAccount: Array<{\n index: number\n email?: string\n claude: number\n gemini: number\n }>\n } {\n const durationMs = this.now() - this.sessionStartTime\n const durationMinutes = Math.round(durationMs / 60000)\n const durationHours = durationMs / 3600000\n\n let totalClaude = 0\n let totalGemini = 0\n const perAccount: Array<{\n index: number\n email?: string\n claude: number\n gemini: number\n }> = []\n\n for (const [key, counts] of this.sessionRequestCounts) {\n const idx = Number(key)\n const account = this.accounts[idx]\n totalClaude += counts.claude\n totalGemini += counts.gemini\n if (counts.claude > 0 || counts.gemini > 0) {\n perAccount.push({\n index: idx,\n email: account?.email,\n claude: counts.claude,\n gemini: counts.gemini,\n })\n }\n }\n\n const totalRequests = totalClaude + totalGemini\n const requestsPerHour =\n durationHours > 0 ? Math.round(totalRequests / durationHours) : 0\n\n return {\n durationMinutes,\n totalClaude,\n totalGemini,\n requestsPerHour,\n accountsUsed: perAccount.length,\n perAccount: perAccount.sort(\n (a, b) => b.claude + b.gemini - (a.claude + a.gemini),\n ),\n }\n }\n\n isAccountOverSoftQuota(\n account: ManagedAccount,\n family: ModelFamily,\n thresholdPercent: number,\n cacheTtlMs: number,\n model?: string | null,\n ): boolean {\n return isOverSoftQuotaThreshold(\n account,\n family,\n this.getEffectiveSoftQuotaThreshold(thresholdPercent),\n cacheTtlMs,\n this.now,\n model,\n )\n }\n\n getAccountsForQuotaCheck(): AccountMetadataV3[] {\n return this.accounts.map((a) => ({\n email: a.email,\n refreshToken: a.parts.refreshToken,\n projectId: a.parts.projectId ?? a.projectId,\n managedProjectId: a.parts.managedProjectId ?? a.managedProjectId,\n addedAt: a.addedAt,\n lastUsed: a.lastUsed,\n enabled: a.enabled,\n }))\n }\n\n getOldestQuotaCacheAge(): number | null {\n let oldest: number | null = null\n for (const acc of this.accounts) {\n if (acc.enabled === false) continue\n if (acc.cachedQuotaUpdatedAt == null) return null\n const age = this.now() - acc.cachedQuotaUpdatedAt\n if (oldest === null || age > oldest) oldest = age\n }\n return oldest\n }\n\n areAllAccountsOverSoftQuota(\n family: ModelFamily,\n thresholdPercent: number,\n cacheTtlMs: number,\n model?: string | null,\n ): boolean {\n if (thresholdPercent >= 100) return false\n const enabled = this.accounts.filter((a) => a.enabled !== false)\n if (enabled.length <= 1) return false\n return enabled.every((a) =>\n isOverSoftQuotaThreshold(\n a,\n family,\n thresholdPercent,\n cacheTtlMs,\n this.now,\n model,\n ),\n )\n }\n\n /**\n * Get minimum wait time until any account's soft quota resets.\n * Returns 0 if any account is available (not over threshold).\n * Returns the minimum resetTime across all over-threshold accounts.\n * Returns null if no resetTime data is available.\n */\n getMinWaitTimeForSoftQuota(\n family: ModelFamily,\n thresholdPercent: number,\n cacheTtlMs: number,\n model?: string | null,\n ): number | null {\n if (thresholdPercent >= 100) return 0\n\n const enabled = this.accounts.filter((a) => a.enabled !== false)\n if (enabled.length === 0) return null\n if (enabled.length === 1) return 0\n\n // If any account is available (not over threshold), no wait needed\n const available = enabled.filter(\n (a) =>\n !isOverSoftQuotaThreshold(\n a,\n family,\n thresholdPercent,\n cacheTtlMs,\n this.now,\n model,\n ),\n )\n if (available.length > 0) return 0\n\n // All accounts are over threshold - find earliest reset time\n // For gemini family, we MUST have the model to distinguish pro vs flash quotas.\n // Fail-open (return null = no wait info) if model is missing to avoid blocking on wrong quota.\n if (!model && family !== 'claude') return null\n const quotaGroup = resolveQuotaGroup(family, model)\n const now = this.now()\n const waitTimes: number[] = []\n\n for (const acc of enabled) {\n const groupData = acc.cachedQuota?.[quotaGroup]\n if (groupData?.resetTime) {\n const resetTimestamp = Date.parse(groupData.resetTime)\n if (Number.isFinite(resetTimestamp)) {\n waitTimes.push(Math.max(0, resetTimestamp - now))\n }\n }\n }\n\n if (waitTimes.length === 0) return null\n const minWait = Math.min(...waitTimes)\n // Treat 0 as stale cache (resetTime in the past) \u2192 fail-open to avoid spin loop\n return minWait === 0 ? null : minWait\n }\n}\n", "import { Buffer } from 'node:buffer'\nimport { randomUUID } from 'node:crypto'\n\nconst FNV1A_64_OFFSET_BASIS = 0xcbf29ce484222325n\nconst FNV1A_64_PRIME = 0x100000001b3n\nconst DEFAULT_SESSION_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst DEFAULT_MAX_SESSION_STATES = 256\n\nconst AGY_REQUEST_FIELD_ORDER = [\n 'contents',\n 'systemInstruction',\n 'tools',\n 'toolConfig',\n 'labels',\n 'generationConfig',\n 'sessionId',\n] as const\n\nconst AGY_MODEL_ENUM_BY_WIRE_MODEL: Readonly<Record<string, string>> = {\n 'gemini-3.5-flash-extra-low': 'MODEL_PLACEHOLDER_M187',\n 'gemini-3.5-flash-low': 'MODEL_PLACEHOLDER_M20',\n 'gemini-3-flash-agent': 'MODEL_PLACEHOLDER_M84',\n 'gemini-3.6-flash-low': 'MODEL_PLACEHOLDER_M73',\n 'gemini-3.6-flash-medium': 'MODEL_PLACEHOLDER_M72',\n 'gemini-3.6-flash-high': 'MODEL_PLACEHOLDER_M71',\n 'gemini-3.1-pro-low': 'MODEL_PLACEHOLDER_M36',\n 'gemini-pro-agent': 'MODEL_PLACEHOLDER_M16',\n 'claude-sonnet-4-6': 'MODEL_PLACEHOLDER_M35',\n 'claude-opus-4-6-thinking': 'MODEL_PLACEHOLDER_M26',\n 'gemini-3.1-flash-image': 'MODEL_PLACEHOLDER_M21',\n 'gpt-oss-120b-medium': 'MODEL_OPENAI_GPT_OSS_120B_MEDIUM',\n}\n\nexport interface AgyRequestSessionContext {\n conversationId: string\n trajectoryId: string\n numericSessionId: string\n usedClaude?: boolean\n usedNonGeminiModel?: boolean\n lastExecutionId?: string\n}\n\ninterface StoredAgyRequestSession {\n context: AgyRequestSessionContext\n lastAccessedAt: number\n lastRequestTimestamp: number\n}\n\nexport interface AgyRequestSessionStoreOptions {\n ttlMs?: number\n maxEntries?: number\n now?: () => number\n}\n\nexport interface AgyRequestScope {\n session: AgyRequestSessionContext\n timestamp: number\n}\n\nexport interface AgyRequestLabels {\n last_execution_id?: string\n last_step_index: string\n model_enum?: string\n trajectory_id: string\n used_claude: 'true' | 'false'\n used_claude_conservative: 'true' | 'false'\n used_non_gemini_model: 'true' | 'false'\n}\n\nexport interface AgyAgentRequestMetadata {\n requestId: string\n sessionId: string\n labels: AgyRequestLabels\n lastStepIndex: number\n}\n\nexport interface AgyAgentRequestMetadataOptions {\n stepCountMode?: 'parts' | 'contents' | 'cli'\n}\n\nexport function fnv1a64Signed(input: string): string {\n let hash = FNV1A_64_OFFSET_BASIS\n for (const byte of Buffer.from(input, 'utf8')) {\n hash ^= BigInt(byte)\n hash = BigInt.asUintN(64, hash * FNV1A_64_PRIME)\n }\n return BigInt.asIntN(64, hash).toString()\n}\n\nexport function createAgyRequestSessionContext(\n workspaceUri: string,\n ids: { conversationId?: string; trajectoryId?: string } = {},\n): AgyRequestSessionContext {\n return {\n conversationId: ids.conversationId ?? randomUUID(),\n trajectoryId: ids.trajectoryId ?? randomUUID(),\n numericSessionId: fnv1a64Signed(workspaceUri),\n }\n}\n\nexport class AgyRequestSessionStore {\n private readonly entries = new Map<string, StoredAgyRequestSession>()\n private readonly workspaceUri: string\n private readonly ttlMs: number\n private readonly maxEntries: number\n private readonly now: () => number\n\n constructor(\n workspaceUri: string,\n options: AgyRequestSessionStoreOptions = {},\n ) {\n this.workspaceUri = workspaceUri\n this.ttlMs = options.ttlMs ?? DEFAULT_SESSION_STATE_TTL_MS\n this.maxEntries = options.maxEntries ?? DEFAULT_MAX_SESSION_STATES\n this.now = options.now ?? Date.now\n }\n\n getOrCreate(key: string): AgyRequestSessionContext {\n const timestamp = this.now()\n this.prune(timestamp, key)\n\n const existing = this.entries.get(key)\n if (existing) {\n existing.lastAccessedAt = timestamp\n return existing.context\n }\n\n const context = createAgyRequestSessionContext(this.workspaceUri)\n this.entries.set(key, {\n context,\n lastAccessedAt: timestamp,\n lastRequestTimestamp: 0,\n })\n return context\n }\n\n beginRequest(key: string): AgyRequestScope {\n const session = this.getOrCreate(key)\n const stored = this.entries.get(key)!\n const timestamp = Math.max(\n stored.lastAccessedAt,\n stored.lastRequestTimestamp + 1,\n )\n stored.lastRequestTimestamp = timestamp\n return { session, timestamp }\n }\n\n completeExecution(key: string): void {\n const stored = this.entries.get(key)\n if (stored) {\n stored.context.lastExecutionId = randomUUID()\n }\n }\n\n has(key: string): boolean {\n return this.entries.has(key)\n }\n\n delete(key: string): void {\n this.entries.delete(key)\n }\n\n clear(): void {\n this.entries.clear()\n }\n\n get size(): number {\n return this.entries.size\n }\n\n private prune(timestamp: number, preservedKey: string): void {\n const expiry = timestamp - this.ttlMs\n for (const [key, value] of this.entries) {\n if (key !== preservedKey && value.lastAccessedAt < expiry) {\n this.entries.delete(key)\n }\n }\n\n while (\n this.entries.size >= this.maxEntries &&\n !this.entries.has(preservedKey)\n ) {\n let oldestKey: string | null = null\n let oldestAccess = Number.POSITIVE_INFINITY\n for (const [key, value] of this.entries) {\n if (key !== preservedKey && value.lastAccessedAt < oldestAccess) {\n oldestKey = key\n oldestAccess = value.lastAccessedAt\n }\n }\n if (!oldestKey) {\n break\n }\n this.entries.delete(oldestKey)\n }\n }\n}\n\nexport function getAgyModelEnum(model: string): string | undefined {\n return AGY_MODEL_ENUM_BY_WIRE_MODEL[model.toLowerCase()]\n}\n\nexport function orderAgyRequestPayloadInPlace(\n payload: Record<string, unknown>,\n): void {\n const ordered: Record<string, unknown> = {}\n const remaining = new Set(Object.keys(payload))\n\n for (const key of AGY_REQUEST_FIELD_ORDER) {\n if (key in payload) {\n ordered[key] = payload[key]\n remaining.delete(key)\n }\n }\n for (const key of remaining) {\n ordered[key] = payload[key]\n }\n\n for (const key of Object.keys(payload)) {\n delete payload[key]\n }\n Object.assign(payload, ordered)\n}\n\nexport function countAgyRequestSteps(\n payload: Record<string, unknown>,\n mode: 'parts' | 'contents' | 'cli' = 'parts',\n): number {\n const contents = payload.contents\n if (!Array.isArray(contents)) return 1\n if (mode === 'contents') return Math.max(1, contents.length)\n\n let partCount = 0\n let functionResponseCount = 0\n for (const content of contents) {\n if (!content || typeof content !== 'object' || Array.isArray(content))\n continue\n const parts = (content as Record<string, unknown>).parts\n if (!Array.isArray(parts)) continue\n partCount += parts.length\n if (mode === 'cli') {\n functionResponseCount += parts.filter((part) => {\n if (!part || typeof part !== 'object' || Array.isArray(part))\n return false\n return 'functionResponse' in part\n }).length\n }\n }\n\n if (mode === 'cli') {\n return Math.max(1, contents.length + functionResponseCount)\n }\n return Math.max(1, partCount)\n}\n\nexport function buildAgyAgentRequestMetadata(\n session: AgyRequestSessionContext,\n payload: Record<string, unknown>,\n model: string,\n timestamp = Date.now(),\n options: AgyAgentRequestMetadataOptions = {},\n): AgyAgentRequestMetadata {\n const lastStepIndex =\n countAgyRequestSteps(payload, options.stepCountMode) +\n (session.lastExecutionId ? 1 : 0)\n const isClaude = model.toLowerCase().startsWith('claude-')\n const isNonGemini = isClaude || model.toLowerCase().startsWith('gpt-')\n session.usedClaude = session.usedClaude === true || isClaude\n session.usedNonGeminiModel =\n session.usedNonGeminiModel === true || isNonGemini\n const modelEnum = getAgyModelEnum(model)\n const labels: AgyRequestLabels = {\n ...(session.lastExecutionId\n ? { last_execution_id: session.lastExecutionId }\n : {}),\n last_step_index: String(lastStepIndex),\n ...(modelEnum ? { model_enum: modelEnum } : {}),\n trajectory_id: session.trajectoryId,\n used_claude: session.usedClaude ? 'true' : 'false',\n used_claude_conservative: session.usedClaude ? 'true' : 'false',\n used_non_gemini_model: session.usedNonGeminiModel ? 'true' : 'false',\n }\n\n return {\n requestId: `agent/${session.conversationId}/${timestamp}/${session.trajectoryId}/${lastStepIndex + 1}`,\n sessionId: session.numericSessionId,\n labels,\n lastStepIndex,\n }\n}\n", "import { Buffer } from 'node:buffer'\nimport * as net from 'node:net'\nimport { PassThrough, Readable, Transform } from 'node:stream'\nimport * as tls from 'node:tls'\nimport { createGunzip } from 'node:zlib'\n\nimport { buildAntigravityHarnessUserAgent } from './fingerprint.ts'\n\nconst DEFAULT_HTTPS_PORT = 443\nconst DEFAULT_PROXY_PORT = 8080\n\nexport const DEFAULT_AGY_RESPONSE_HEADER_TIMEOUT_MS = 180_000\n// Max time the response body may stall (no bytes received) before the socket is\n// destroyed. Streaming responses emit data far more frequently than this; a\n// truly hung body would otherwise hold the socket open indefinitely.\nexport const DEFAULT_AGY_IDLE_TIMEOUT_MS = 180_000\n\nexport type AgyTransportOptions = {\n /**\n * Maximum time to wait for the request to connect and receive HTTP response headers.\n * The response body/stream is not bounded by this timeout.\n */\n timeoutMs?: number\n /**\n * Maximum time the response body may stall (no bytes) before the socket is\n * destroyed. Resets on every received chunk. Defaults to\n * DEFAULT_AGY_IDLE_TIMEOUT_MS.\n */\n idleTimeoutMs?: number\n signal?: AbortSignal | null\n onDebug?: (message: string) => void\n}\n\ntype HeaderPair = readonly [string, string]\n\ntype ParsedResponseHead = {\n status: number\n statusText: string\n headers: Headers\n chunked: boolean\n gzip: boolean\n contentLength?: number\n}\n\nfunction headersToRecord(headers?: HeadersInit): Record<string, string> {\n const result: Record<string, string> = {}\n if (!headers) return result\n\n const normalized = new Headers(headers)\n normalized.forEach((value, key) => {\n result[key.toLowerCase()] = value\n })\n return result\n}\n\nfunction getHeader(\n headers: Record<string, string>,\n name: string,\n): string | undefined {\n return headers[name.toLowerCase()]\n}\n\nfunction bodyToBuffer(body: BodyInit | null | undefined): Buffer {\n if (body == null) return Buffer.alloc(0)\n if (typeof body === 'string') return Buffer.from(body)\n if (body instanceof Uint8Array) return Buffer.from(body)\n if (body instanceof ArrayBuffer) return Buffer.from(body)\n throw new Error('agy transport only supports string/byte request bodies')\n}\n\nfunction shouldUseChunkedBody(url: URL): boolean {\n return url.pathname.includes(':streamGenerateContent')\n}\n\nexport function buildAgyCliHeaderPairs(\n url: string,\n init: RequestInit = {},\n): HeaderPair[] {\n const parsedUrl = new URL(url)\n const headers = headersToRecord(init.headers)\n const body = bodyToBuffer(init.body)\n const host = parsedUrl.port\n ? `${parsedUrl.hostname}:${parsedUrl.port}`\n : parsedUrl.hostname\n const userAgent =\n getHeader(headers, 'User-Agent') ?? buildAntigravityHarnessUserAgent()\n const authorization = getHeader(headers, 'Authorization')\n const contentType = getHeader(headers, 'Content-Type') ?? 'application/json'\n const acceptEncoding = getHeader(headers, 'Accept-Encoding') ?? 'gzip'\n const chunked = shouldUseChunkedBody(parsedUrl)\n\n const pairs: HeaderPair[] = [\n ['Host', host],\n ['User-Agent', userAgent],\n ]\n\n if (chunked) {\n pairs.push(['Transfer-Encoding', 'chunked'])\n } else {\n pairs.push(['Content-Length', String(body.byteLength)])\n }\n\n if (authorization) {\n pairs.push(['Authorization', authorization])\n }\n pairs.push(['Content-Type', contentType])\n pairs.push(['Accept-Encoding', acceptEncoding])\n\n return pairs\n}\n\nfunction noProxyIncludes(hostname: string): boolean {\n const raw = process.env.NO_PROXY || process.env.no_proxy || ''\n if (!raw) return false\n const host = hostname.toLowerCase()\n return raw\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .some((entry) => {\n if (!entry) return false\n if (entry === '*') return true\n if (entry.startsWith('.')) return host.endsWith(entry)\n return host === entry || host.endsWith(`.${entry}`)\n })\n}\n\nfunction getHttpsProxy(url: URL): URL | undefined {\n if (url.protocol !== 'https:' || noProxyIncludes(url.hostname))\n return undefined\n const rawProxy =\n process.env.HTTPS_PROXY ||\n process.env.https_proxy ||\n process.env.ALL_PROXY ||\n process.env.all_proxy\n if (!rawProxy) return undefined\n try {\n return new URL(rawProxy)\n } catch {\n return undefined\n }\n}\n\nfunction waitForHead(\n socket: net.Socket,\n timeoutMs: number,\n onTimeout: () => void,\n): Promise<{ head: string; leftover: Buffer }> {\n return new Promise((resolve, reject) => {\n let buffer = Buffer.alloc(0)\n const timeout = setTimeout(() => {\n onTimeout()\n cleanup(() =>\n reject(\n new Error(\n `Antigravity request timed out waiting for response headers after ${timeoutMs}ms`,\n ),\n ),\n )\n }, timeoutMs)\n\n const cleanup = (finish: () => void) => {\n socket.off('data', onData)\n socket.off('error', onError)\n clearTimeout(timeout)\n finish()\n }\n\n const onError = (error: Error) => cleanup(() => reject(error))\n const onData = (chunk: Buffer) => {\n buffer = Buffer.concat([buffer, chunk])\n const marker = buffer.indexOf('\\r\\n\\r\\n')\n if (marker === -1) return\n const head = buffer.subarray(0, marker).toString('latin1')\n const leftover = buffer.subarray(marker + 4)\n cleanup(() => resolve({ head, leftover }))\n }\n\n socket.on('data', onData)\n socket.once('error', onError)\n })\n}\n\nasync function connectViaProxy(\n proxyUrl: URL,\n targetUrl: URL,\n timeoutMs: number,\n onDebug?: (message: string) => void,\n): Promise<tls.TLSSocket> {\n const proxySocket = net.connect({\n host: proxyUrl.hostname,\n port: Number(proxyUrl.port || DEFAULT_PROXY_PORT),\n })\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n onDebug?.(`agy transport proxy connect timeout after ${timeoutMs}ms`)\n proxySocket.destroy()\n reject(\n new Error(\n `Antigravity request timed out connecting to HTTPS proxy after ${timeoutMs}ms`,\n ),\n )\n }, timeoutMs)\n const cleanup = () => clearTimeout(timeout)\n proxySocket.once('connect', () => {\n cleanup()\n resolve()\n })\n proxySocket.once('error', (error) => {\n cleanup()\n reject(error)\n })\n })\n\n const targetHost = targetUrl.hostname\n const targetPort = Number(targetUrl.port || DEFAULT_HTTPS_PORT)\n const auth = proxyUrl.username\n ? `Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64')}\\r\\n`\n : ''\n\n proxySocket.write(\n `CONNECT ${targetHost}:${targetPort} HTTP/1.1\\r\\n` +\n `Host: ${targetHost}:${targetPort}\\r\\n` +\n auth +\n '\\r\\n',\n )\n\n const { head, leftover } = await waitForHead(proxySocket, timeoutMs, () => {\n onDebug?.(\n `agy transport proxy CONNECT response timeout after ${timeoutMs}ms`,\n )\n proxySocket.destroy()\n })\n if (!/^HTTP\\/1\\.[01] 2\\d\\d\\b/.test(head)) {\n proxySocket.destroy()\n throw new Error(\n `Proxy CONNECT failed: ${head.split('\\r\\n')[0] ?? 'unknown'}`,\n )\n }\n if (leftover.length > 0) {\n proxySocket.unshift(leftover)\n }\n\n return await new Promise<tls.TLSSocket>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: proxySocket,\n servername: targetHost,\n })\n const timeout = setTimeout(() => {\n onDebug?.(\n `agy transport proxy TLS handshake timeout after ${timeoutMs}ms`,\n )\n tlsSocket.destroy()\n reject(\n new Error(\n `Antigravity request timed out during proxy TLS handshake after ${timeoutMs}ms`,\n ),\n )\n }, timeoutMs)\n const cleanup = () => clearTimeout(timeout)\n tlsSocket.once('secureConnect', () => {\n cleanup()\n resolve(tlsSocket)\n })\n tlsSocket.once('error', (error: Error) => {\n cleanup()\n reject(error)\n })\n })\n}\n\nasync function connectDirect(\n targetUrl: URL,\n timeoutMs: number,\n onDebug?: (message: string) => void,\n): Promise<tls.TLSSocket> {\n return await new Promise<tls.TLSSocket>((resolve, reject) => {\n const socket = tls.connect({\n host: targetUrl.hostname,\n port: Number(targetUrl.port || DEFAULT_HTTPS_PORT),\n servername: targetUrl.hostname,\n })\n const timeout = setTimeout(() => {\n onDebug?.(`agy transport TLS connect timeout after ${timeoutMs}ms`)\n socket.destroy()\n reject(\n new Error(\n `Antigravity request timed out connecting after ${timeoutMs}ms`,\n ),\n )\n }, timeoutMs)\n const cleanup = () => clearTimeout(timeout)\n socket.once('secureConnect', () => {\n cleanup()\n resolve(socket)\n })\n socket.once('error', (error: Error) => {\n cleanup()\n reject(error)\n })\n })\n}\n\nasync function connectTls(\n targetUrl: URL,\n timeoutMs: number,\n onDebug?: (message: string) => void,\n): Promise<tls.TLSSocket> {\n const proxyUrl = getHttpsProxy(targetUrl)\n return proxyUrl\n ? await connectViaProxy(proxyUrl, targetUrl, timeoutMs, onDebug)\n : await connectDirect(targetUrl, timeoutMs, onDebug)\n}\n\nfunction serializeRequest(url: URL, init: RequestInit, body: Buffer): Buffer {\n const method = init.method ?? 'POST'\n const path = `${url.pathname}${url.search}`\n const headerLines = buildAgyCliHeaderPairs(url.toString(), init)\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\r\\n')\n const head = Buffer.from(\n `${method} ${path} HTTP/1.1\\r\\n${headerLines}\\r\\n\\r\\n`,\n )\n\n if (body.byteLength === 0) {\n return head\n }\n\n if (!shouldUseChunkedBody(url)) {\n return Buffer.concat([head, body])\n }\n\n return Buffer.concat([\n head,\n Buffer.from(`${body.byteLength.toString(16)}\\r\\n`),\n body,\n Buffer.from('\\r\\n0\\r\\n\\r\\n'),\n ])\n}\n\nfunction parseResponseHead(head: string): ParsedResponseHead {\n const lines = head.split('\\r\\n')\n const statusLine = lines.shift() ?? ''\n const match = /^HTTP\\/1\\.[01]\\s+(\\d{3})\\s*(.*)$/.exec(statusLine)\n if (!match) {\n throw new Error(`Invalid HTTP response: ${statusLine}`)\n }\n\n const headers = new Headers()\n let chunked = false\n let gzip = false\n let contentLength: number | undefined\n for (const line of lines) {\n const index = line.indexOf(':')\n if (index <= 0) continue\n const key = line.slice(0, index)\n const value = line.slice(index + 1).trim()\n const lowerKey = key.toLowerCase()\n const lowerValue = value.toLowerCase()\n if (lowerKey === 'transfer-encoding' && lowerValue.includes('chunked')) {\n chunked = true\n continue\n }\n if (lowerKey === 'content-encoding' && lowerValue.includes('gzip')) {\n gzip = true\n continue\n }\n if (lowerKey === 'content-length') {\n const parsed = Number.parseInt(value, 10)\n if (Number.isFinite(parsed) && parsed >= 0) {\n contentLength = parsed\n }\n // Drop content-length from the surfaced headers when gzip is set, since\n // the decoded body length differs from the wire length.\n if (gzip) continue\n }\n headers.append(key, value)\n }\n\n return {\n status: Number(match[1]),\n statusText: match[2] ?? '',\n headers,\n chunked,\n gzip,\n contentLength,\n }\n}\n\nexport class ContentLengthStream extends Transform {\n private remaining: number\n\n constructor(contentLength: number) {\n super()\n this.remaining = contentLength\n }\n\n override _transform(\n chunk: Buffer,\n _encoding: BufferEncoding,\n callback: (error?: Error | null) => void,\n ): void {\n if (this.remaining <= 0) {\n callback()\n return\n }\n if (chunk.length <= this.remaining) {\n this.remaining -= chunk.length\n this.push(chunk)\n } else {\n // Emit only up to the declared length; discard any trailing bytes that\n // belong to the next keep-alive response.\n this.push(chunk.subarray(0, this.remaining))\n this.remaining = 0\n }\n if (this.remaining <= 0) {\n this.push(null)\n }\n callback()\n }\n}\n\nclass ChunkedDecodeStream extends Transform {\n private buffer = Buffer.alloc(0)\n\n override _transform(\n chunk: Buffer,\n _encoding: BufferEncoding,\n callback: (error?: Error | null) => void,\n ): void {\n this.buffer = Buffer.concat([this.buffer, chunk])\n try {\n this.flushAvailableChunks()\n callback()\n } catch (error) {\n callback(error instanceof Error ? error : new Error(String(error)))\n }\n }\n\n override _flush(callback: (error?: Error | null) => void): void {\n try {\n this.flushAvailableChunks()\n callback()\n } catch (error) {\n callback(error instanceof Error ? error : new Error(String(error)))\n }\n }\n\n private flushAvailableChunks(): void {\n while (true) {\n const lineEnd = this.buffer.indexOf('\\r\\n')\n if (lineEnd === -1) return\n const sizeLine = this.buffer.subarray(0, lineEnd).toString('latin1')\n const sizeText = sizeLine.split(';', 1)[0]?.trim() ?? ''\n const size = Number.parseInt(sizeText, 16)\n if (!Number.isFinite(size)) {\n throw new Error(`Invalid chunk size: ${sizeLine}`)\n }\n const chunkStart = lineEnd + 2\n const chunkEnd = chunkStart + size\n const nextOffset = chunkEnd + 2\n if (this.buffer.length < nextOffset) return\n if (size === 0) {\n this.buffer = Buffer.alloc(0)\n this.push(null)\n return\n }\n this.push(this.buffer.subarray(chunkStart, chunkEnd))\n this.buffer = this.buffer.subarray(nextOffset)\n }\n }\n}\n\nfunction buildResponseStream(\n socket: tls.TLSSocket,\n leftover: Buffer,\n head: ParsedResponseHead,\n signal?: AbortSignal | null,\n idleTimeoutMs?: number,\n onDebug?: (message: string) => void,\n): ReadableStream<Uint8Array> {\n const source = new PassThrough()\n if (leftover.length > 0) {\n source.write(leftover)\n }\n socket.pipe(source)\n\n let responseBody: Readable = source\n if (head.chunked) {\n responseBody = responseBody.pipe(new ChunkedDecodeStream())\n } else if (typeof head.contentLength === 'number') {\n // Non-chunked with a known length: emit exactly contentLength bytes then\n // end, rather than reading until socket EOF (which over-reads on keep-alive\n // connections and never ends if the server keeps the socket open).\n responseBody = responseBody.pipe(\n new ContentLengthStream(head.contentLength),\n )\n }\n if (head.gzip) {\n responseBody = responseBody.pipe(createGunzip())\n }\n\n // Idle-read watchdog: if no body bytes arrive within idleTimeoutMs, destroy\n // the socket so a hung/stalled response can't hold the connection forever.\n let idleTimer: ReturnType<typeof setTimeout> | undefined\n const clearIdle = () => {\n if (idleTimer) {\n clearTimeout(idleTimer)\n idleTimer = undefined\n }\n }\n const armIdle = () => {\n if (!idleTimeoutMs || idleTimeoutMs <= 0) return\n clearIdle()\n idleTimer = setTimeout(() => {\n onDebug?.(\n `agy transport idle timeout after ${idleTimeoutMs}ms with no body data`,\n )\n socket.destroy(\n new Error(\n `Antigravity response stalled: no data for ${idleTimeoutMs}ms`,\n ),\n )\n }, idleTimeoutMs)\n }\n socket.on('data', armIdle)\n armIdle()\n\n const abort = () =>\n socket.destroy(new DOMException('The operation was aborted', 'AbortError'))\n const cleanup = () => {\n clearIdle()\n socket.off('data', armIdle)\n signal?.removeEventListener('abort', abort)\n }\n if (signal?.aborted) {\n abort()\n } else {\n signal?.addEventListener('abort', abort, { once: true })\n }\n\n responseBody.once('end', () => {\n cleanup()\n socket.destroy()\n })\n responseBody.once('error', () => {\n cleanup()\n socket.destroy()\n })\n responseBody.once('close', cleanup)\n return Readable.toWeb(responseBody) as unknown as ReadableStream<Uint8Array>\n}\n\nexport async function fetchWithAgyCliTransport(\n url: string,\n init: RequestInit = {},\n options: AgyTransportOptions = {},\n): Promise<Response> {\n const parsedUrl = new URL(url)\n if (parsedUrl.protocol !== 'https:') {\n throw new Error(`agy transport only supports https URLs: ${url}`)\n }\n\n if (options.signal?.aborted) {\n throw new DOMException('The operation was aborted', 'AbortError')\n }\n\n const body = bodyToBuffer(init.body)\n const requestBytes = serializeRequest(parsedUrl, init, body)\n const timeoutMs = options.timeoutMs ?? DEFAULT_AGY_RESPONSE_HEADER_TIMEOUT_MS\n const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_AGY_IDLE_TIMEOUT_MS\n options.onDebug?.(\n `agy transport connecting to ${parsedUrl.hostname} with header timeout ${timeoutMs}ms`,\n )\n // Race the connect against abort so a cancel during TLS/proxy connect is\n // honored immediately instead of waiting out the connect timeout.\n const socket = await connectTlsWithAbort(\n parsedUrl,\n timeoutMs,\n options.signal,\n options.onDebug,\n )\n\n const abort = () => {\n socket.destroy(new DOMException('The operation was aborted', 'AbortError'))\n }\n\n try {\n options.signal?.addEventListener('abort', abort, { once: true })\n socket.write(requestBytes)\n options.onDebug?.(\n `agy transport request dispatched (${requestBytes.byteLength} bytes)`,\n )\n\n const { head, leftover } = await waitForHead(socket, timeoutMs, () => {\n options.onDebug?.(\n `agy transport response header timeout after ${timeoutMs}ms`,\n )\n socket.destroy()\n })\n const parsedHead = parseResponseHead(head)\n options.onDebug?.(\n `agy transport response headers received: ${parsedHead.status} ${parsedHead.statusText}`,\n )\n const bodyStream = buildResponseStream(\n socket,\n leftover,\n parsedHead,\n options.signal,\n idleTimeoutMs,\n options.onDebug,\n )\n return new Response(bodyStream, {\n status: parsedHead.status,\n statusText: parsedHead.statusText,\n headers: parsedHead.headers,\n })\n } catch (error) {\n socket.destroy()\n throw error\n } finally {\n options.signal?.removeEventListener('abort', abort)\n }\n}\n\nasync function connectTlsWithAbort(\n targetUrl: URL,\n timeoutMs: number,\n signal: AbortSignal | null | undefined,\n onDebug?: (message: string) => void,\n): Promise<tls.TLSSocket> {\n if (!signal) {\n return connectTls(targetUrl, timeoutMs, onDebug)\n }\n const connectPromise = connectTls(targetUrl, timeoutMs, onDebug)\n let onAbort: (() => void) | undefined\n const abortPromise = new Promise<never>((_, reject) => {\n onAbort = () =>\n reject(new DOMException('The operation was aborted', 'AbortError'))\n signal.addEventListener('abort', onAbort, { once: true })\n })\n try {\n return await Promise.race([connectPromise, abortPromise])\n } catch (error) {\n // If abort won the race, make sure the in-flight socket is torn down once it resolves.\n void connectPromise.then((socket) => socket.destroy()).catch(() => {})\n throw error\n } finally {\n if (onAbort) signal.removeEventListener('abort', onAbort)\n }\n}\n", "import { generatePKCE } from '@openauthjs/openauth/pkce'\nimport { fetchWithAgyCliTransport } from '../agy-transport.ts'\nimport { calculateTokenExpiry } from '../auth.ts'\nimport {\n ANTIGRAVITY_CLIENT_ID,\n ANTIGRAVITY_CLIENT_SECRET,\n ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ANTIGRAVITY_LOAD_ENDPOINTS,\n ANTIGRAVITY_REDIRECT_URI,\n ANTIGRAVITY_SCOPES,\n GEMINI_CLI_HEADERS,\n} from '../constants.ts'\nimport { fetchWithActiveTimeout } from '../fetch-timeout.ts'\nimport {\n buildAntigravityHarnessBootstrapHeaders,\n buildAntigravityLoadCodeAssistMetadata,\n} from '../fingerprint.ts'\nimport { createLogger } from '../logger.ts'\n\nconst log = createLogger('oauth')\n\ninterface PkcePair {\n challenge: string\n verifier: string\n}\n\ninterface AntigravityAuthState {\n verifier: string\n projectId: string\n}\n\n/**\n * Result returned to the caller after constructing an OAuth authorization URL.\n */\nexport interface AntigravityAuthorization {\n url: string\n verifier: string\n projectId: string\n}\n\ninterface AntigravityTokenExchangeSuccess {\n type: 'success'\n refresh: string\n access: string\n expires: number\n email?: string\n label?: string\n projectId: string\n}\n\ninterface AntigravityTokenExchangeFailure {\n type: 'failed'\n error: string\n}\n\nexport type AntigravityTokenExchangeResult =\n | AntigravityTokenExchangeSuccess\n | AntigravityTokenExchangeFailure\n\nexport interface AntigravityRefreshResult {\n access: string\n refresh: string\n expires: number\n}\n\n/**\n * Refresh an Antigravity OAuth access token using a bare refresh token.\n * Harness-agnostic: performs only the token POST and returns the new\n * credentials. Persistence/caching is the caller's responsibility.\n */\nexport async function refreshAntigravityToken(\n refreshToken: string,\n): Promise<AntigravityRefreshResult> {\n const startTime = Date.now()\n const response = await fetchWithActiveTimeout(\n 'https://oauth2.googleapis.com/token',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: ANTIGRAVITY_CLIENT_ID,\n client_secret: ANTIGRAVITY_CLIENT_SECRET,\n }),\n },\n )\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => '')\n throw new Error(\n `Antigravity token refresh failed (${response.status} ${response.statusText})${errorText ? ` - ${errorText}` : ''}`,\n )\n }\n\n const payload = (await response.json()) as {\n access_token: string\n expires_in: number\n refresh_token?: string\n }\n\n return {\n access: payload.access_token,\n refresh: payload.refresh_token ?? refreshToken,\n expires: calculateTokenExpiry(startTime, payload.expires_in),\n }\n}\n\ninterface AntigravityTokenResponse {\n access_token: string\n expires_in: number\n refresh_token: string\n}\n\ninterface AntigravityUserInfo {\n email?: string\n name?: string\n}\n\n/**\n * Encode an object into a URL-safe base64 string.\n */\nfunction encodeState(payload: AntigravityAuthState): string {\n return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url')\n}\n\n/**\n * Decode an OAuth state parameter back into its structured representation.\n */\nfunction decodeState(state: string): AntigravityAuthState {\n const normalized = state.replace(/-/g, '+').replace(/_/g, '/')\n const padded = normalized.padEnd(\n normalized.length + ((4 - (normalized.length % 4)) % 4),\n '=',\n )\n const json = Buffer.from(padded, 'base64').toString('utf8')\n const parsed = JSON.parse(json)\n if (typeof parsed.verifier !== 'string') {\n throw new Error('Missing PKCE verifier in state')\n }\n return {\n verifier: parsed.verifier,\n projectId: typeof parsed.projectId === 'string' ? parsed.projectId : '',\n }\n}\n\n/**\n * Build the Antigravity OAuth authorization URL including PKCE and optional project metadata.\n */\nexport async function authorizeAntigravity(\n projectId = '',\n): Promise<AntigravityAuthorization> {\n const pkce = (await generatePKCE()) as PkcePair\n\n const url = new URL('https://accounts.google.com/o/oauth2/v2/auth')\n url.searchParams.set('client_id', ANTIGRAVITY_CLIENT_ID)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('redirect_uri', ANTIGRAVITY_REDIRECT_URI)\n url.searchParams.set('scope', ANTIGRAVITY_SCOPES.join(' '))\n url.searchParams.set('code_challenge', pkce.challenge)\n url.searchParams.set('code_challenge_method', 'S256')\n url.searchParams.set(\n 'state',\n encodeState({ verifier: pkce.verifier, projectId: projectId || '' }),\n )\n url.searchParams.set('access_type', 'offline')\n url.searchParams.set('prompt', 'consent')\n\n return {\n url: url.toString(),\n verifier: pkce.verifier,\n projectId: projectId || '',\n }\n}\n\nasync function fetchProjectID(accessToken: string): Promise<string> {\n const errors: string[] = []\n const loadHeaders = buildAntigravityHarnessBootstrapHeaders(accessToken)\n\n const loadEndpoints = Array.from(\n new Set<string>([\n ...ANTIGRAVITY_LOAD_ENDPOINTS,\n ...ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ]),\n )\n\n for (const baseEndpoint of loadEndpoints) {\n try {\n const url = `${baseEndpoint}/v1internal:loadCodeAssist`\n const response = await fetchWithAgyCliTransport(\n url,\n {\n method: 'POST',\n headers: loadHeaders,\n body: JSON.stringify({\n metadata: buildAntigravityLoadCodeAssistMetadata(),\n }),\n },\n { timeoutMs: 10000 },\n )\n\n if (!response.ok) {\n const message = await response.text().catch(() => '')\n errors.push(\n `loadCodeAssist ${response.status} at ${baseEndpoint}${\n message ? `: ${message}` : ''\n }`,\n )\n continue\n }\n\n const data = await response.json()\n if (\n typeof data.cloudaicompanionProject === 'string' &&\n data.cloudaicompanionProject\n ) {\n return data.cloudaicompanionProject\n }\n if (\n data.cloudaicompanionProject &&\n typeof data.cloudaicompanionProject.id === 'string' &&\n data.cloudaicompanionProject.id\n ) {\n return data.cloudaicompanionProject.id\n }\n\n errors.push(`loadCodeAssist missing project id at ${baseEndpoint}`)\n } catch (e) {\n errors.push(\n `loadCodeAssist error at ${baseEndpoint}: ${\n e instanceof Error ? e.message : String(e)\n }`,\n )\n }\n }\n\n if (errors.length) {\n log.warn('Failed to resolve Antigravity project via loadCodeAssist', {\n errors: errors.join('; '),\n })\n }\n return ''\n}\n\n/**\n * Exchange an authorization code for Antigravity CLI access and refresh tokens.\n */\nexport async function exchangeAntigravity(\n code: string,\n state: string,\n): Promise<AntigravityTokenExchangeResult> {\n try {\n const { verifier, projectId } = decodeState(state)\n\n const startTime = Date.now()\n const tokenResponse = await fetchWithActiveTimeout(\n 'https://oauth2.googleapis.com/token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',\n Accept: '*/*',\n 'Accept-Encoding': 'gzip, deflate, br',\n 'User-Agent': GEMINI_CLI_HEADERS['User-Agent'],\n },\n body: new URLSearchParams({\n client_id: ANTIGRAVITY_CLIENT_ID,\n client_secret: ANTIGRAVITY_CLIENT_SECRET,\n code,\n grant_type: 'authorization_code',\n redirect_uri: ANTIGRAVITY_REDIRECT_URI,\n code_verifier: verifier,\n }),\n },\n )\n\n if (!tokenResponse.ok) {\n const errorText = await tokenResponse.text()\n return { type: 'failed', error: errorText }\n }\n\n const tokenPayload =\n (await tokenResponse.json()) as AntigravityTokenResponse\n\n const userInfoResponse = await fetchWithActiveTimeout(\n 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',\n {\n headers: {\n Authorization: `Bearer ${tokenPayload.access_token}`,\n 'User-Agent': GEMINI_CLI_HEADERS['User-Agent'],\n },\n },\n )\n\n const userInfo = userInfoResponse.ok\n ? ((await userInfoResponse.json()) as AntigravityUserInfo)\n : {}\n\n const refreshToken = tokenPayload.refresh_token\n if (!refreshToken) {\n return { type: 'failed', error: 'Missing refresh token in response' }\n }\n\n let effectiveProjectId = projectId\n if (!effectiveProjectId) {\n effectiveProjectId = await fetchProjectID(tokenPayload.access_token)\n }\n\n const storedRefresh = `${refreshToken}|${effectiveProjectId || ''}`\n\n return {\n type: 'success',\n refresh: storedRefresh,\n access: tokenPayload.access_token,\n expires: calculateTokenExpiry(startTime, tokenPayload.expires_in),\n email: userInfo.email,\n label: userInfo.name?.trim() || undefined,\n projectId: effectiveProjectId || '',\n }\n } catch (error) {\n return {\n type: 'failed',\n error: error instanceof Error ? error.message : 'Unknown error',\n }\n }\n}\n", "/**\n * Constants used for Antigravity OAuth flows and Cloud Code Assist API integration.\n */\n\nimport { buildAntigravityHarnessUserAgent } from './fingerprint.ts'\n\nexport const ANTIGRAVITY_CLIENT_ID =\n '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'\n\n/**\n * Client secret issued for the Antigravity OAuth application.\n */\nexport const ANTIGRAVITY_CLIENT_SECRET = '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'\n\n/**\n * Scopes required for Antigravity integrations.\n */\nexport const ANTIGRAVITY_SCOPES: readonly string[] = [\n 'https://www.googleapis.com/auth/cloud-platform',\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'https://www.googleapis.com/auth/cclog',\n 'https://www.googleapis.com/auth/experimentsandconfigs',\n]\n\n/**\n * OAuth redirect URI used by the local CLI callback server.\n */\nexport const ANTIGRAVITY_REDIRECT_URI = 'http://localhost:51121/oauth-callback'\n\n/**\n * Root endpoints for the Antigravity API (in fallback order).\n * Live agy CLI 1.1.6 traffic uses daily-cloudcode-pa.googleapis.com.\n */\nexport const ANTIGRAVITY_ENDPOINT_DAILY =\n 'https://daily-cloudcode-pa.googleapis.com'\nexport const ANTIGRAVITY_ENDPOINT_AUTOPUSH =\n 'https://autopush-cloudcode-pa.sandbox.googleapis.com'\nexport const ANTIGRAVITY_ENDPOINT_PROD = 'https://cloudcode-pa.googleapis.com'\n\n/**\n * Endpoint fallback order (daily \u2192 prod).\n * Autopush removed to reduce unnecessary fallback API calls \u2014 it rarely works when daily fails.\n * Shared across request handling and project discovery to mirror CLIProxy behavior.\n */\nexport const ANTIGRAVITY_ENDPOINT_FALLBACKS = [\n ANTIGRAVITY_ENDPOINT_DAILY,\n ANTIGRAVITY_ENDPOINT_PROD,\n] as const\n/**\n * Preferred endpoint order for project discovery.\n * agy CLI probes daily-cloudcode-pa.googleapis.com first.\n */\nexport const ANTIGRAVITY_LOAD_ENDPOINTS = [\n ANTIGRAVITY_ENDPOINT_DAILY,\n ANTIGRAVITY_ENDPOINT_PROD,\n] as const\n/**\n * Primary endpoint to use (captured agy CLI daily endpoint).\n */\nexport const ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_ENDPOINT_DAILY\n\n/**\n * Gemini CLI endpoint (production).\n * Used for models without :antigravity suffix.\n * Same as opencode-gemini-auth's GEMINI_CODE_ASSIST_ENDPOINT.\n */\nexport const GEMINI_CLI_ENDPOINT = ANTIGRAVITY_ENDPOINT_PROD\n\n/**\n * Hardcoded project id used when Antigravity does not return one (e.g., business/workspace accounts).\n */\nexport const ANTIGRAVITY_DEFAULT_PROJECT_ID = 'rising-fact-p41fc'\n\nexport const ANTIGRAVITY_VERSION_FALLBACK = '1.18.3'\nlet antigravityVersion = ANTIGRAVITY_VERSION_FALLBACK\nlet versionLocked = false\n\nexport function getAntigravityVersion(): string {\n return antigravityVersion\n}\n\n/**\n * Set the runtime Antigravity version. Can only be called once (at startup).\n * Subsequent calls are silently ignored to prevent accidental mutation.\n */\nexport function setAntigravityVersion(version: string): void {\n if (versionLocked) return\n antigravityVersion = version\n versionLocked = true\n}\n\n/**\n * Test-only: reset the version lock so `setAntigravityVersion` can run again.\n * Bun has no Vitest-style module reset hook, and module state lives in this\n * singleton \u2014 tests that exercise the lock need a way to start clean per\n * scenario.\n */\nexport function __resetAntigravityVersionForTesting(): void {\n antigravityVersion = ANTIGRAVITY_VERSION_FALLBACK\n versionLocked = false\n}\n\n/** @deprecated Use getAntigravityVersion() for runtime access. */\nexport const ANTIGRAVITY_VERSION = ANTIGRAVITY_VERSION_FALLBACK\n\nexport function getAntigravityHeaders(): HeaderSet & {\n 'Client-Metadata': string\n} {\n return {\n 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${getAntigravityVersion()} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,\n 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',\n 'Client-Metadata': `{\"ideType\":\"ANTIGRAVITY\",\"platform\":\"${process.platform === 'win32' ? 'WINDOWS' : 'MACOS'}\",\"pluginType\":\"GEMINI\"}`,\n }\n}\n\n/** @deprecated Use getAntigravityHeaders() for runtime access. */\nexport const ANTIGRAVITY_HEADERS = {\n 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${ANTIGRAVITY_VERSION} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,\n 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',\n 'Client-Metadata': `{\"ideType\":\"ANTIGRAVITY\",\"platform\":\"${process.platform === 'win32' ? 'WINDOWS' : 'MACOS'}\",\"pluginType\":\"GEMINI\"}`,\n} as const\n\nexport const GEMINI_CLI_VERSION = '1.0.0'\n\n/**\n * Default model used in Gemini CLI User-Agent when no model is specified.\n */\nexport const GEMINI_CLI_DEFAULT_MODEL = 'gemini-2.5-pro'\n\n/**\n * Build Gemini CLI User-Agent string matching the official google-gemini/gemini-cli format.\n * Format: `GeminiCLI/{version}/{model} ({platform}; {arch})`\n *\n * @see https://github.com/google-gemini/gemini-cli\n */\nexport function buildGeminiCliUserAgent(model?: string): string {\n const effectiveModel = model || GEMINI_CLI_DEFAULT_MODEL\n const platform = process.platform || 'darwin'\n const arch = process.arch || 'arm64'\n return `GeminiCLI/${GEMINI_CLI_VERSION}/${effectiveModel} (${platform}; ${arch})`\n}\n\n/** @deprecated Use buildGeminiCliUserAgent() for runtime access. */\nexport const GEMINI_CLI_HEADERS = {\n 'User-Agent': 'google-api-nodejs-client/9.15.1',\n 'X-Goog-Api-Client': 'gl-node/22.17.0',\n 'Client-Metadata':\n 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI',\n} as const\nexport type HeaderSet = {\n 'User-Agent': string\n 'X-Goog-Api-Client'?: string\n 'Client-Metadata'?: string\n}\n\nexport function getRandomizedHeaders(\n style: HeaderStyle,\n model?: string,\n): HeaderSet {\n if (style === 'gemini-cli') {\n return {\n 'User-Agent': buildGeminiCliUserAgent(model),\n 'X-Goog-Api-Client': GEMINI_CLI_HEADERS['X-Goog-Api-Client'],\n 'Client-Metadata': GEMINI_CLI_HEADERS['Client-Metadata'],\n }\n }\n return {\n 'User-Agent': buildAntigravityHarnessUserAgent(),\n }\n}\n\nexport type HeaderStyle = 'antigravity' | 'gemini-cli'\n\n/**\n * Provider identifier shared between the plugin loader and credential store.\n */\nexport const ANTIGRAVITY_PROVIDER_ID = 'google'\n\n// ============================================================================\n// TOOL HALLUCINATION PREVENTION (Ported from LLM-API-Key-Proxy)\n// ============================================================================\n\n/**\n * System instruction for Claude tool usage hardening.\n * Prevents hallucinated parameters by explicitly stating the rules.\n *\n * This is injected when tools are present to reduce cases where Claude\n * uses parameter names from its training data instead of the actual schema.\n */\nexport const CLAUDE_TOOL_SYSTEM_INSTRUCTION = `CRITICAL TOOL USAGE INSTRUCTIONS:\nYou are operating in a custom environment where tool definitions differ from your training data.\nYou MUST follow these rules strictly:\n\n1. DO NOT use your internal training data to guess tool parameters\n2. ONLY use the exact parameter structure defined in the tool schema\n3. Parameter names in schemas are EXACT - do not substitute with similar names from your training\n4. Array parameters have specific item types - check the schema's 'items' field for the exact structure\n5. When you see \"STRICT PARAMETERS\" in a tool description, those type definitions override any assumptions\n6. Tool use in agentic workflows is REQUIRED - you must call tools with the exact parameters specified\n\nIf you are unsure about a tool's parameters, YOU MUST read the schema definition carefully.`\n\n/**\n * Template for parameter signature injection into tool descriptions.\n * {params} will be replaced with the actual parameter list.\n */\nexport const CLAUDE_DESCRIPTION_PROMPT = '\\n\\n\u26A0\uFE0F STRICT PARAMETERS: {params}.'\n\nexport const EMPTY_SCHEMA_PLACEHOLDER_NAME = '_placeholder'\nexport const EMPTY_SCHEMA_PLACEHOLDER_DESCRIPTION =\n 'Placeholder. Always pass true.'\n\n/**\n * Sentinel value to bypass thought signature validation.\n *\n * When a thinking block has an invalid or missing signature (e.g., cache miss,\n * session mismatch, plugin restart), this sentinel can be injected to skip\n * validation instead of failing with \"Invalid signature in thinking block\".\n *\n * This is an officially supported Google API feature, used by:\n * - gemini-cli: https://github.com/google-gemini/gemini-cli\n * - Google .NET SDK: PredictionServiceChatClient.cs\n *\n * @see https://ai.google.dev/gemini-api/docs/thought-signatures\n */\nexport const SKIP_THOUGHT_SIGNATURE = 'skip_thought_signature_validator'\n\n// ============================================================================\n// GOOGLE SEARCH TOOL CONSTANTS\n// ============================================================================\n\n/**\n * Model used for Google Search grounding requests.\n * Uses gemini-2.5-flash for fast, cost-effective search operations. (3-flash is always at capacity and doesn't support souce citation).\n */\nexport const SEARCH_MODEL = 'gemini-2.5-flash'\n\n/**\n * Thinking budget for deep search (more thorough analysis).\n */\nexport const SEARCH_THINKING_BUDGET_DEEP = 16384\n\n/**\n * Thinking budget for fast search (quick results).\n */\nexport const SEARCH_THINKING_BUDGET_FAST = 4096\n\n/**\n * Timeout for search requests in milliseconds (60 seconds).\n */\nexport const SEARCH_TIMEOUT_MS = 60000\n\n/**\n * System instruction for the Google Search tool.\n */\nexport const SEARCH_SYSTEM_INSTRUCTION = `You are an expert web search assistant with access to Google Search and URL analysis tools.\n\nYour capabilities:\n- Use google_search to find real-time information from the web\n- Use url_context to fetch and analyze content from specific URLs when provided\n\nGuidelines:\n- Always provide accurate, well-sourced information\n- Cite your sources when presenting facts\n- If analyzing URLs, extract the most relevant information\n- Be concise but comprehensive in your responses\n- If information is uncertain or conflicting, acknowledge it\n- Focus on answering the user's question directly`\n", "/**\n * Stream-safe active-fetch timeout helper.\n *\n * Bounds the time spent waiting for HTTP response headers while leaving any\n * returned response body fully readable until the caller (or its own signal)\n * chooses to abort it. Naively forwarding `AbortSignal.timeout(...)` into\n * `fetch()` aborts the body at the deadline even when headers arrived\n * promptly \u2014 the helper only forwards the timeout into the request signal\n * until the underlying fetch resolves, then drops the timeout listener so\n * the body remains consumable.\n */\n\nexport const ACTIVE_FETCH_TIMEOUT_MS = 15_000\n\nexport type ActiveFetchOptions = {\n timeoutMs?: number\n fetchImpl?: typeof fetch\n}\n\n/**\n * Issue an HTTP fetch with a bounded active-fetch (header) wait.\n *\n * The `init.signal` is composed into the request abort signal so caller\n * cancellation still propagates, but the 15s active timeout is only enforced\n * until `fetchImpl` resolves \u2014 once headers arrive the timeout listener is\n * removed and the returned Response body can be streamed past the deadline.\n */\nexport async function fetchWithActiveTimeout(\n input: RequestInfo | URL,\n init: RequestInit = {},\n options: ActiveFetchOptions = {},\n): Promise<Response> {\n const fetchImpl = options.fetchImpl ?? globalThis.fetch\n const timeoutMs = options.timeoutMs ?? ACTIVE_FETCH_TIMEOUT_MS\n\n const timeoutSignal = AbortSignal.timeout(timeoutMs)\n const activeController = new AbortController()\n const onTimeoutAbort = () => activeController.abort(timeoutSignal.reason)\n if (timeoutSignal.aborted) {\n onTimeoutAbort()\n } else {\n timeoutSignal.addEventListener('abort', onTimeoutAbort, { once: true })\n }\n\n const composedSignal = init.signal\n ? AbortSignal.any([activeController.signal, init.signal])\n : activeController.signal\n\n try {\n return await fetchImpl(input, { ...init, signal: composedSignal })\n } finally {\n timeoutSignal.removeEventListener('abort', onTimeoutAbort)\n }\n}\n", "import { fetchWithAgyCliTransport } from './agy-transport.ts'\nimport { formatRefreshParts, parseRefreshParts } from './auth.ts'\nimport type { OAuthAuthDetails, ProjectContextResult } from './auth-types.ts'\nimport {\n ANTIGRAVITY_DEFAULT_PROJECT_ID,\n ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ANTIGRAVITY_ENDPOINT_PROD,\n ANTIGRAVITY_LOAD_ENDPOINTS,\n} from './constants.ts'\nimport {\n buildAntigravityHarnessBootstrapHeaders,\n buildAntigravityLoadCodeAssistMetadata,\n} from './fingerprint.ts'\nimport { createLogger } from './logger.ts'\n\nconst log = createLogger('project')\n\n/** TTL for project context cache entries (30 minutes). */\nconst PROJECT_CONTEXT_CACHE_TTL_MS = 30 * 60 * 1000\n\ninterface CachedProjectContext {\n result: ProjectContextResult\n cachedAt: number\n}\n\nconst projectContextResultCache = new Map<string, CachedProjectContext>()\nconst projectContextPendingCache = new Map<\n string,\n Promise<ProjectContextResult>\n>()\nconst provisionFailedKeys = new Set<string>()\ninterface AntigravityUserTier {\n id?: string\n isDefault?: boolean\n userDefinedCloudaicompanionProject?: boolean\n}\n\ninterface LoadCodeAssistPayload {\n cloudaicompanionProject?: string | { id?: string }\n currentTier?: {\n id?: string\n }\n paidTier?:\n | string\n | {\n id?: string\n }\n allowedTiers?: AntigravityUserTier[]\n}\n\ninterface OnboardUserPayload {\n done?: boolean\n response?: {\n cloudaicompanionProject?: {\n id?: string\n }\n }\n}\n\nfunction buildBootstrapRequestBody(\n extra: Record<string, unknown> = {},\n): Record<string, unknown> {\n return {\n ...extra,\n metadata: buildAntigravityLoadCodeAssistMetadata(),\n }\n}\n\n/**\n * Selects the default tier ID from the allowed tiers list.\n */\nfunction getDefaultTierId(\n allowedTiers?: AntigravityUserTier[],\n): string | undefined {\n if (!allowedTiers || allowedTiers.length === 0) {\n return undefined\n }\n for (const tier of allowedTiers) {\n if (tier?.isDefault) {\n return tier.id\n }\n }\n return allowedTiers[0]?.id\n}\n\n/**\n * Promise-based delay utility.\n */\nfunction wait(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n}\n\n/**\n * Extracts the cloudaicompanion project id from loadCodeAssist responses.\n */\nfunction extractManagedProjectId(\n payload: LoadCodeAssistPayload | null,\n): string | undefined {\n if (!payload) {\n return undefined\n }\n if (typeof payload.cloudaicompanionProject === 'string') {\n return payload.cloudaicompanionProject\n }\n if (\n payload.cloudaicompanionProject &&\n typeof payload.cloudaicompanionProject.id === 'string'\n ) {\n return payload.cloudaicompanionProject.id\n }\n return undefined\n}\n\n/**\n * Generates a stable cache key from the OAuth refresh token itself. Packed\n * project fields may change after discovery, but they still identify the same\n * credential and must hit the same project-context cache entry.\n */\nfunction getCacheKeyFromRefresh(\n refresh: string | undefined,\n): string | undefined {\n const packedRefresh = refresh?.trim()\n if (!packedRefresh) return undefined\n return parseRefreshParts(packedRefresh).refreshToken.trim() || packedRefresh\n}\n\nfunction getCacheKey(auth: OAuthAuthDetails): string | undefined {\n return getCacheKeyFromRefresh(auth.refresh)\n}\n\n/**\n * Clears cached project context results and pending promises, globally or for a refresh key.\n */\nexport function invalidateProjectContextCache(refresh?: string): void {\n if (!refresh) {\n projectContextPendingCache.clear()\n projectContextResultCache.clear()\n provisionFailedKeys.clear()\n return\n }\n const cacheKey = getCacheKeyFromRefresh(refresh)\n if (!cacheKey) return\n projectContextPendingCache.delete(cacheKey)\n projectContextResultCache.delete(cacheKey)\n provisionFailedKeys.delete(cacheKey)\n}\n\nexport function clearProvisionFailedKeys(): void {\n provisionFailedKeys.clear()\n}\n\n/**\n * Loads managed project information for the given access token and optional project.\n */\nexport async function loadManagedProject(\n accessToken: string,\n _projectId?: string,\n): Promise<LoadCodeAssistPayload | null> {\n const requestBody = buildBootstrapRequestBody()\n const loadHeaders = buildAntigravityHarnessBootstrapHeaders(accessToken)\n\n const loadEndpoints = Array.from(\n new Set<string>([\n ...ANTIGRAVITY_LOAD_ENDPOINTS,\n ...ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ]),\n )\n\n for (const baseEndpoint of loadEndpoints) {\n try {\n const response = await fetchWithAgyCliTransport(\n `${baseEndpoint}/v1internal:loadCodeAssist`,\n {\n method: 'POST',\n headers: loadHeaders,\n body: JSON.stringify(requestBody),\n },\n )\n\n if (!response.ok) {\n continue\n }\n\n return (await response.json()) as LoadCodeAssistPayload\n } catch (error) {\n log.debug('Failed to load managed project', {\n endpoint: baseEndpoint,\n error: String(error),\n })\n }\n }\n\n return null\n}\n\n/**\n * Onboards a managed project for the user, optionally retrying until completion.\n */\nexport async function onboardManagedProject(\n accessToken: string,\n tierId: string,\n projectId?: string,\n attempts = 10,\n delayMs = 5000,\n): Promise<string | undefined> {\n const requestBody: Record<string, unknown> = { tierId }\n const onboardEndpoints = Array.from(\n new Set<string>([\n ANTIGRAVITY_ENDPOINT_PROD,\n ...ANTIGRAVITY_LOAD_ENDPOINTS,\n ...ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ]),\n )\n\n for (const baseEndpoint of onboardEndpoints) {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n try {\n const response = await fetchWithAgyCliTransport(\n `${baseEndpoint}/v1internal:onboardUser`,\n {\n method: 'POST',\n headers: buildAntigravityHarnessBootstrapHeaders(accessToken),\n body: JSON.stringify(requestBody),\n },\n )\n\n if (!response.ok) {\n log.debug('Onboard request failed', {\n endpoint: baseEndpoint,\n status: response.status,\n statusText: response.statusText,\n })\n break\n }\n\n const payload = (await response.json()) as OnboardUserPayload\n const managedProjectId = payload.response?.cloudaicompanionProject?.id\n if (payload.done && managedProjectId) {\n return managedProjectId\n }\n if (payload.done && projectId) {\n return projectId\n }\n } catch (error) {\n log.debug('Failed to onboard managed project', {\n endpoint: baseEndpoint,\n error: String(error),\n })\n break\n }\n\n await wait(delayMs)\n }\n }\n\n return undefined\n}\n\n/**\n * Resolves an effective project ID for the current auth state, caching results per refresh token.\n */\nexport async function ensureProjectContext(\n auth: OAuthAuthDetails,\n): Promise<ProjectContextResult> {\n const accessToken = auth.access\n if (!accessToken) {\n return { auth, effectiveProjectId: '' }\n }\n\n const cacheKey = getCacheKey(auth)\n if (cacheKey) {\n const cached = projectContextResultCache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < PROJECT_CONTEXT_CACHE_TTL_MS) {\n return cached.result\n }\n if (cached) {\n // Expired \u2014 evict stale entry\n projectContextResultCache.delete(cacheKey)\n }\n const pending = projectContextPendingCache.get(cacheKey)\n if (pending) {\n return pending\n }\n }\n\n const resolveContext = async (): Promise<ProjectContextResult> => {\n const parts = parseRefreshParts(auth.refresh)\n if (parts.managedProjectId) {\n return { auth, effectiveProjectId: parts.managedProjectId }\n }\n\n const fallbackProjectId = ANTIGRAVITY_DEFAULT_PROJECT_ID\n\n if (cacheKey && provisionFailedKeys.has(cacheKey)) {\n const effectiveProjectId = parts.projectId || fallbackProjectId\n return { auth, effectiveProjectId }\n }\n\n const persistManagedProject = async (\n managedProjectId: string,\n capturedTier?: ProjectContextResult['capturedTier'],\n ): Promise<ProjectContextResult> => {\n const updatedAuth: OAuthAuthDetails = {\n ...auth,\n refresh: formatRefreshParts({\n refreshToken: parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId,\n }),\n }\n\n return {\n auth: updatedAuth,\n effectiveProjectId: managedProjectId,\n capturedTier,\n }\n }\n\n // Try to resolve a managed project from Antigravity if possible.\n const loadPayload = await loadManagedProject(\n accessToken,\n parts.projectId ?? fallbackProjectId,\n )\n // Capture tier from the loadCodeAssist payload. The raw id is stored\n // as-is; absent payload or missing tier leaves capturedTier undefined.\n const capturedTierId = loadPayload?.currentTier?.id\n const paidTierId =\n typeof loadPayload?.paidTier === 'string'\n ? loadPayload.paidTier\n : loadPayload?.paidTier?.id\n const tierFromPayload: ProjectContextResult['capturedTier'] = capturedTierId\n ? {\n id: capturedTierId,\n ...(paidTierId ? { paidId: paidTierId } : {}),\n capturedAt: Date.now(),\n }\n : undefined\n const resolvedManagedProjectId = extractManagedProjectId(loadPayload)\n\n if (resolvedManagedProjectId) {\n return persistManagedProject(resolvedManagedProjectId, tierFromPayload)\n }\n\n // No managed project found - try to auto-provision one via onboarding.\n // This handles accounts that were added before managed project provisioning was required.\n const tierId = getDefaultTierId(loadPayload?.allowedTiers) ?? 'free-tier'\n log.debug('Auto-provisioning managed project', {\n tierId,\n projectId: parts.projectId,\n })\n\n const provisionedProjectId = await onboardManagedProject(\n accessToken,\n tierId,\n parts.projectId,\n )\n\n if (provisionedProjectId) {\n log.debug('Successfully provisioned managed project', {\n provisionedProjectId,\n })\n return persistManagedProject(provisionedProjectId, tierFromPayload)\n }\n\n log.warn(\n 'Failed to provision managed project - account may not work correctly',\n {\n hasProjectId: !!parts.projectId,\n },\n )\n\n if (cacheKey) {\n provisionFailedKeys.add(cacheKey)\n }\n\n if (parts.projectId) {\n return {\n auth,\n effectiveProjectId: parts.projectId,\n capturedTier: tierFromPayload,\n }\n }\n\n // No project id present in auth; fall back to the hardcoded id for requests.\n return {\n auth,\n effectiveProjectId: fallbackProjectId,\n capturedTier: tierFromPayload,\n }\n }\n\n if (!cacheKey) {\n return resolveContext()\n }\n\n const promise = resolveContext()\n .then((result) => {\n const nextKey = getCacheKey(result.auth) ?? cacheKey\n projectContextPendingCache.delete(cacheKey)\n projectContextResultCache.set(nextKey, { result, cachedAt: Date.now() })\n if (nextKey !== cacheKey) {\n projectContextResultCache.delete(cacheKey)\n }\n return result\n })\n .catch((error) => {\n projectContextPendingCache.delete(cacheKey)\n throw error\n })\n\n projectContextPendingCache.set(cacheKey, promise)\n return promise\n}\n", "/**\n * Harness-agnostic attributed quota manager.\n *\n * Owns the per-account quota cache, in-flight de-duplication, and exponential\n * backoff that backgrounds proactive quota refreshes. The manager has no host\n * dependencies \u2014 harnesses supply a `fetchAccountQuota` callback that returns\n * the already-attributed `AccountQuotaResult` (with `index`, `email`,\n * `updatedAccount`, etc.) so core stays harness-agnostic.\n *\n * Manual quota dialogs must force a refresh even when background refresh is\n * backed off; proactive refreshes must dedupe by stable account identity and\n * respect backoff. Account identity is supplied via `keyOf` so reorders/\n * removals of the underlying array do not invalidate cache entries.\n */\n\nimport { createHash } from 'node:crypto'\nimport type { AccountMetadataV3 } from './account-types.ts'\nimport { fetchWithActiveTimeout } from './fetch-timeout.ts'\nimport { buildAntigravityHarnessUserAgent } from './fingerprint.ts'\nimport { createLogger } from './logger.ts'\nimport { getQuotaGroupForModel } from './model-registry.ts'\nimport type {\n AccountQuotaResult,\n GeminiCliQuotaSummary,\n PerModelQuotaEntry,\n QuotaGroup,\n QuotaGroupSummary,\n QuotaSummary,\n QuotaWindowEntry,\n} from './quota-types.ts'\n\nconst log = createLogger('quota-manager')\n\nexport const QUOTA_MANAGER_DEFAULT_BASE_BACKOFF_MS = 30_000\nexport const QUOTA_MANAGER_DEFAULT_MAX_BACKOFF_MS = 10 * 60 * 1000\nexport const QUOTA_MANAGER_DEFAULT_TIMEOUT_MS = 10_000\n\n/**\n * Signature for the harness-supplied quota fetch callback.\n *\n * `index`/`email`/`updatedAccount` come from the harness \u2014 the manager\n * preserves the harness's attribution and only injects status/error info\n * for disabled accounts and failed fetches.\n */\nexport type FetchAccountQuota = (\n account: AccountMetadataV3,\n signal: AbortSignal,\n) => Promise<AccountQuotaResult>\n\nexport interface QuotaManagerOptions {\n fetchAccountQuota: FetchAccountQuota\n /**\n * Stable identity for an account. Used as cache key + backoff key. Should\n * NOT be the array index \u2014 reorders and removals would otherwise corrupt\n * cached state. The harness can hash the refresh token or compose email +\n * a refresh-token fallback.\n */\n keyOf: (account: AccountMetadataV3) => string\n now?: () => number\n baseBackoffMs?: number\n maxBackoffMs?: number\n /** Per-fetch active timeout for the harness callback. */\n fetchTimeoutMs?: number\n}\n\nexport interface RefreshAccountOptions {\n /** Position of the account in the harness-visible array. Preserved on result. */\n index: number\n /** Bypass backoff (used by manual quota dialogs). */\n force?: boolean\n}\n\nexport interface RefreshAccountsOptions {\n indexFor: (account: AccountMetadataV3) => number\n force?: boolean\n}\n\nexport interface QuotaManager {\n refreshAccount(\n account: AccountMetadataV3,\n options: RefreshAccountOptions,\n ): Promise<AccountQuotaResult>\n refreshAccounts(\n accounts: AccountMetadataV3[],\n options: RefreshAccountsOptions,\n ): Promise<AccountQuotaResult[]>\n getCached(account: AccountMetadataV3): AccountQuotaResult | undefined\n getBackoffUntil(account: AccountMetadataV3): number\n /** Stable hash for an account, used for log labels. */\n hashedLogLabel(prefix: string, account: AccountMetadataV3 | string): string\n /**\n * Await any in-flight refresh, then cancel and reject subsequent\n * refreshes. Returns a promise so a lifecycle producer can fence its\n * fire-and-forget sidebar writes: awaiting `dispose()` guarantees no\n * refresh is still mid-flight (and therefore cannot enqueue a write)\n * once it resolves.\n */\n dispose(): Promise<void>\n /** Pure helpers exposed for tests and adapter wiring. */\n classifyQuotaGroup: typeof classifyQuotaGroup\n aggregateQuota: typeof aggregateQuota\n aggregateGeminiCliQuota: typeof aggregateGeminiCliQuota\n}\n\ninterface AccountState {\n consecutiveFailures: number\n backoffUntil: number\n inflight?: Promise<AccountQuotaResult>\n controller?: AbortController\n cached?: AccountQuotaResult\n}\n\n/**\n * Default keyOf \u2014 prefers email, falls back to refresh-token hash so the\n * same identity is keyed even when emails are missing.\n */\nexport function defaultKeyOf(account: AccountMetadataV3): string {\n if (account.email) return `e:${account.email.toLowerCase()}`\n const token = account.refreshToken || ''\n return `t:${createHash('sha256').update(token).digest('hex').slice(0, 16)}`\n}\n\nexport function createQuotaManager(options: QuotaManagerOptions): QuotaManager {\n const now = options.now ?? (() => Date.now())\n const baseBackoffMs =\n options.baseBackoffMs ?? QUOTA_MANAGER_DEFAULT_BASE_BACKOFF_MS\n const maxBackoffMs =\n options.maxBackoffMs ?? QUOTA_MANAGER_DEFAULT_MAX_BACKOFF_MS\n const fetchTimeoutMs =\n options.fetchTimeoutMs ?? QUOTA_MANAGER_DEFAULT_TIMEOUT_MS\n\n const state = new Map<string, AccountState>()\n let disposed = false\n\n const keyOf = (account: AccountMetadataV3): string => options.keyOf(account)\n\n const recordSuccess = (key: string): void => {\n const entry = state.get(key)\n if (!entry) return\n entry.consecutiveFailures = 0\n entry.backoffUntil = 0\n }\n\n const recordFailure = (\n key: string,\n ): { backoffMs: number; backoffUntil: number } => {\n const current = now()\n const entry = state.get(key) ?? {\n consecutiveFailures: 0,\n backoffUntil: 0,\n }\n const failures = entry.consecutiveFailures + 1\n const backoffMs = Math.min(\n maxBackoffMs,\n baseBackoffMs * 2 ** (failures - 1),\n )\n entry.consecutiveFailures = failures\n entry.backoffUntil = current + backoffMs\n state.set(key, entry)\n return { backoffMs, backoffUntil: entry.backoffUntil }\n }\n\n const cacheResult = (key: string, result: AccountQuotaResult): void => {\n const entry = state.get(key) ?? {\n consecutiveFailures: 0,\n backoffUntil: 0,\n }\n entry.cached = result\n state.set(key, entry)\n }\n\n const buildDisabledResult = (\n account: AccountMetadataV3,\n index: number,\n ): AccountQuotaResult => ({\n index,\n email: account.email,\n status: 'disabled',\n disabled: true,\n quota: undefined,\n geminiCliQuota: undefined,\n updatedAccount: undefined,\n })\n\n const buildSkippedResult = (\n account: AccountMetadataV3,\n index: number,\n backoffUntil: number,\n ): AccountQuotaResult => {\n const cached = state.get(keyOf(account))?.cached\n if (cached) {\n return { ...cached, index }\n }\n return {\n index,\n email: account.email,\n status: 'error',\n error: `quota refresh skipped (backoff until ${new Date(backoffUntil).toISOString()})`,\n }\n }\n\n const refreshAccount = async (\n account: AccountMetadataV3,\n refreshOptions: RefreshAccountOptions,\n ): Promise<AccountQuotaResult> => {\n if (disposed) {\n return {\n index: refreshOptions.index,\n email: account.email,\n status: 'error',\n error: 'quota manager disposed',\n }\n }\n\n const { index, force = false } = refreshOptions\n const key = keyOf(account)\n\n if (account.enabled === false) {\n const result = buildDisabledResult(account, index)\n cacheResult(key, result)\n return result\n }\n\n const current = now()\n const entry = state.get(key)\n if (!force && entry && entry.backoffUntil > current) {\n return buildSkippedResult(account, index, entry.backoffUntil)\n }\n\n const inflight = entry?.inflight\n if (inflight) {\n // Reuse the cached result of the in-flight fetch, but preserve this\n // caller's requested index for attribution.\n const cached = await inflight\n return { ...cached, index }\n }\n\n const controller = new AbortController()\n const stored = state.get(key) ?? {\n consecutiveFailures: 0,\n backoffUntil: 0,\n }\n stored.controller = controller\n state.set(key, stored)\n\n const promise = (async (): Promise<AccountQuotaResult> => {\n try {\n const signal = AbortSignal.timeout(fetchTimeoutMs)\n const composite = controller.signal.aborted\n ? controller.signal\n : AbortSignal.any([controller.signal, signal])\n\n const result = await options.fetchAccountQuota(account, composite)\n if (controller.signal.aborted) {\n throw new Error('quota manager disposed mid-fetch')\n }\n const attributed: AccountQuotaResult = { ...result, index }\n cacheResult(key, attributed)\n\n // Adapter contract: `fetchAccountQuota` resolves with an\n // attributed result. Failures should arrive as `{ status: 'error' }`\n // rather than throwing \u2014 treat them like thrown failures so backoff\n // actually protects the account from re-hammering. `disabled` and\n // `ok` both count as success (no fetch happened, or it succeeded).\n if (result.status === 'error') {\n const { backoffMs } = recordFailure(key)\n log.debug('quota-refresh-failed', {\n key: hashKey(key),\n backoffMs,\n error: result.error ?? 'attributed error result',\n })\n return attributed\n }\n\n recordSuccess(key)\n return attributed\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (disposed || controller.signal.aborted) {\n const failureResult: AccountQuotaResult = {\n index,\n email: account.email,\n status: 'error',\n error: message,\n }\n cacheResult(key, failureResult)\n return failureResult\n }\n const { backoffMs } = recordFailure(key)\n log.debug('quota-refresh-failed', {\n key: hashKey(key),\n backoffMs,\n error: message,\n })\n const failureResult: AccountQuotaResult = {\n index,\n email: account.email,\n status: 'error',\n error: message,\n }\n cacheResult(key, failureResult)\n return failureResult\n } finally {\n const finished = state.get(key)\n if (finished) {\n finished.inflight = undefined\n finished.controller = undefined\n state.set(key, finished)\n }\n }\n })()\n\n stored.inflight = promise\n state.set(key, stored)\n\n return promise\n }\n\n const refreshAccounts = async (\n accounts: AccountMetadataV3[],\n refreshOptions: RefreshAccountsOptions,\n ): Promise<AccountQuotaResult[]> => {\n const results: AccountQuotaResult[] = []\n for (const account of accounts) {\n const index = refreshOptions.indexFor(account)\n const result = await refreshAccount(account, {\n index,\n force: refreshOptions.force,\n })\n results.push(result)\n }\n return results\n }\n\n const getCached = (\n account: AccountMetadataV3,\n ): AccountQuotaResult | undefined => {\n const entry = state.get(keyOf(account))\n return entry?.cached\n }\n\n const getBackoffUntil = (account: AccountMetadataV3): number => {\n return state.get(keyOf(account))?.backoffUntil ?? 0\n }\n\n const hashedLogLabel = (\n prefix: string,\n account: AccountMetadataV3 | string,\n ): string => {\n const identity = typeof account === 'string' ? account : keyOf(account)\n return `${prefix} ${hashKey(identity)}`\n }\n\n const dispose = async (): Promise<void> => {\n if (disposed) return\n disposed = true\n // Snapshot the in-flight refreshes before aborting \u2014 the refresh\n // `finally` clears `entry.inflight` as each promise settles, so we\n // must capture the promises first.\n const pending = Array.from(\n state.values(),\n (entry) => entry.inflight,\n ).filter(\n (promise): promise is Promise<AccountQuotaResult> => promise != null,\n )\n // Abort so a genuinely-stuck fetch unwinds promptly (the refresh\n // catches the abort and resolves with an error result rather than\n // hanging dispose), then AWAIT the settled promises. Awaiting after\n // the abort still fences the producer: the opencode quota wrapper's\n // fire-and-forget sidebar write runs in the continuation after the\n // core refresh resolves, so by the time dispose() resolves that\n // write has already been enqueued onto the sidebar chain and the\n // subsequent lifecycle drain will flush it.\n for (const [, entry] of state) {\n entry.controller?.abort()\n }\n if (pending.length > 0) {\n // Each refresh swallows its own errors and resolves; awaiting is a\n // fence, never a rejection surface.\n await Promise.allSettled(pending)\n }\n for (const [, entry] of state) {\n entry.inflight = undefined\n }\n }\n\n return {\n refreshAccount,\n refreshAccounts,\n getCached,\n getBackoffUntil,\n hashedLogLabel,\n dispose,\n classifyQuotaGroup,\n aggregateQuota,\n aggregateGeminiCliQuota,\n }\n}\n\nfunction hashKey(key: string): string {\n return createHash('sha256').update(key).digest('hex').slice(0, 8)\n}\n\n// ============================================================================\n// Pure helpers \u2014 also exported for adapter use.\n// ============================================================================\n\n/**\n * Classify a model into its quota group.\n */\nexport function classifyQuotaGroup(\n modelName: string,\n displayName?: string,\n): QuotaGroup | null {\n const registryGroup = getQuotaGroupForModel(modelName)\n if (registryGroup) {\n return registryGroup\n }\n\n const combined = `${modelName} ${displayName ?? ''}`.toLowerCase()\n // Check Claude / GPT-OSS substrings BEFORE the `gemini` substring so a\n // `gemini-claude-*` alias (Claude route exposed under a `gemini-`\n // namespace) attributes to the non-gemini pool rather than the gemini\n // pool. `tab_*` autocomplete IDs are already classified by\n // `getQuotaGroupForModel` above (the registry/prefix branches), so\n // this fallback only runs for genuinely-unrecognised model strings.\n if (combined.includes('claude') || combined.includes('gpt-oss')) {\n return 'non-gemini'\n }\n if (combined.includes('gemini')) {\n return 'gemini'\n }\n return null\n}\n\nfunction normalizeRemainingFraction(value: unknown): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return 0\n }\n if (value < 0) return 0\n if (value > 1) return 1\n return value\n}\n\nfunction parseResetTime(resetTime?: string): number | null {\n if (!resetTime) return null\n const timestamp = Date.parse(resetTime)\n if (!Number.isFinite(timestamp)) {\n return null\n }\n return timestamp\n}\n\nexport interface FetchAvailableModelEntry {\n quotaInfo?: {\n remainingFraction?: number\n resetTime?: string\n }\n displayName?: string\n modelName?: string\n}\n\n/**\n * Aggregate per-model quota entries into group summaries + per-model list.\n *\n * Pure helper \u2014 exposed for harness adapters that want to reuse the same\n * aggregation logic without re-implementing it.\n */\nexport function aggregateQuota(\n models?: Record<string, FetchAvailableModelEntry>,\n): QuotaSummary {\n const groups: Partial<Record<QuotaGroup, QuotaGroupSummary>> = {}\n const perModel: PerModelQuotaEntry[] = []\n if (!models) {\n return { groups, perModel, modelCount: 0 }\n }\n\n let totalCount = 0\n for (const [modelName, entry] of Object.entries(models)) {\n const group = classifyQuotaGroup(\n modelName,\n entry.displayName ?? entry.modelName,\n )\n const quotaInfo = entry.quotaInfo\n const remainingFraction = quotaInfo\n ? normalizeRemainingFraction(quotaInfo.remainingFraction)\n : undefined\n const resetTime = quotaInfo?.resetTime\n const resetTimestamp = parseResetTime(resetTime)\n\n totalCount += 1\n\n perModel.push({\n modelId: modelName,\n displayName: entry.displayName ?? entry.modelName,\n group,\n remainingFraction: remainingFraction ?? 0,\n resetTime,\n })\n\n if (!group) {\n continue\n }\n\n const existing = groups[group]\n const nextCount = (existing?.modelCount ?? 0) + 1\n const nextRemaining =\n remainingFraction === undefined\n ? existing?.remainingFraction\n : existing?.remainingFraction === undefined\n ? remainingFraction\n : Math.min(existing.remainingFraction, remainingFraction)\n\n let nextResetTime = existing?.resetTime\n if (resetTimestamp !== null) {\n if (!existing?.resetTime) {\n nextResetTime = resetTime\n } else {\n const existingTimestamp = parseResetTime(existing.resetTime)\n if (existingTimestamp === null || resetTimestamp < existingTimestamp) {\n nextResetTime = resetTime\n }\n }\n }\n\n groups[group] = {\n remainingFraction: nextRemaining,\n resetTime: nextResetTime,\n modelCount: nextCount,\n }\n }\n\n perModel.sort((a, b) => a.modelId.localeCompare(b.modelId))\n\n return { groups, perModel, modelCount: totalCount }\n}\n\nexport interface RetrieveUserQuotaBucket {\n remainingAmount?: string\n remainingFraction?: number\n resetTime?: string\n tokenType?: string\n modelId?: string\n}\n\nexport interface RetrieveUserQuotaResponse {\n buckets?: RetrieveUserQuotaBucket[]\n}\n\nexport interface FetchAvailableModelsResponse {\n models?: Record<string, FetchAvailableModelEntry>\n}\n\n// ============================================================================\n// retrieveUserQuotaSummary \u2014 windowed quota source (2 pools \u00D7 variable windows)\n// ============================================================================\n\nexport interface RetrieveUserQuotaSummaryBucket {\n bucketId: string\n displayName: string\n window: 'weekly' | '5h'\n resetTime: string\n remainingFraction: number\n description?: string\n}\n\nexport interface RetrieveUserQuotaSummaryGroup {\n displayName: string\n description?: string\n buckets: RetrieveUserQuotaSummaryBucket[]\n}\n\nexport interface RetrieveUserQuotaSummaryResponse {\n groups: RetrieveUserQuotaSummaryGroup[]\n description?: string\n}\n\n/**\n * Derive the most-constrained window from a set of window entries.\n * Returns the entry with the smallest `remainingFraction` \u2014 this is the\n * binding constraint for the pool. `resetTime` comes from the same window.\n * Returns `undefined` when there are no windows.\n */\nfunction mostConstrainedWindow(\n windows: QuotaWindowEntry[],\n): { remainingFraction: number; resetTime: string } | undefined {\n if (windows.length === 0) return undefined\n let best = windows[0]!\n for (let i = 1; i < windows.length; i++) {\n if (windows[i]!.remainingFraction < best.remainingFraction) {\n best = windows[i]!\n }\n }\n return {\n remainingFraction: best.remainingFraction,\n resetTime: best.resetTime,\n }\n}\n\n/**\n * Map a retrieveUserQuotaSummary bucketId prefix to our internal pool.\n *\n * bucketId prefixes:\n * `gemini-*` \u2192 gemini\n * `3p-*` \u2192 non-gemini\n */\nfunction poolForBucketId(bucketId: string): QuotaGroup | null {\n if (bucketId.startsWith('gemini-')) return 'gemini'\n if (bucketId.startsWith('3p-')) return 'non-gemini'\n return null\n}\n\n/**\n * Count models listed in a group's description. The description is\n * typically a comma-separated list of model names (\"Claude Sonnet 4.6,\n * Gemini 3.1 Pro, Flash\") often prefixed with a label like\n * \"Models within this group:\". Strip the prefix before splitting so\n * the label itself is not counted as a model. Returns 0 for shapes\n * that don't match a comma-separated list.\n */\nfunction parseDescriptionModelCount(description: string): number {\n const prefixMatch = description.match(/^[^:]+:\\s*/)\n const payload = prefixMatch\n ? description.slice(prefixMatch[0].length)\n : description\n if (!payload) return 0\n const entries = payload\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n return entries.length\n}\n\n/**\n * Map a quota window kind to its duration in milliseconds for\n * deterministic sort order. Unknown windows get MAX_SAFE_INTEGER so\n * they sort last without random reordering across runs.\n */\nfunction windowDurationMs(window: 'weekly' | '5h' | string): number {\n switch (window) {\n case '5h':\n return 5 * 60 * 60 * 1000\n case 'weekly':\n return 7 * 24 * 60 * 60 * 1000\n default:\n return Number.MAX_SAFE_INTEGER\n }\n}\n\n/**\n * Aggregate a retrieveUserQuotaSummary response into a QuotaSummary.\n *\n * Each RUQS group maps to a pool via bucketId prefix. Within a pool,\n * windows are stored shortest-first (5h before weekly, etc.). The pool's\n * `remainingFraction`/`resetTime` derive from the most-constrained window.\n */\nexport function aggregateQuotaSummary(\n response: RetrieveUserQuotaSummaryResponse,\n): QuotaSummary {\n const groups: Partial<Record<QuotaGroup, QuotaGroupSummary>> = {}\n let totalCount = 0\n\n for (const group of response.groups) {\n const windows: QuotaWindowEntry[] = []\n for (const bucket of group.buckets) {\n const pool = poolForBucketId(bucket.bucketId)\n if (!pool) continue\n windows.push({\n window: bucket.window,\n remainingFraction: normalizeRemainingFraction(bucket.remainingFraction),\n resetTime: bucket.resetTime,\n })\n }\n if (windows.length === 0) continue\n\n // Order: shortest window first so the binding 5h window leads visually.\n windows.sort(\n (a, b) => windowDurationMs(a.window) - windowDurationMs(b.window),\n )\n\n const constrained = mostConstrainedWindow(windows)\n // Pick the first RECOGNIZED bucket for pool derivation. Older\n // servers may prepend a junk bucket (system noise, a non-standard\n // prefix like `claude-3p-`) whose bucketId doesn't match any of\n // our pool prefixes; the legacy poolForBucketId derivation used\n // group.buckets[0] unconditionally, which silently dropped the\n // whole group when an unknown prefix led the array.\n const recognizedBucket = group.buckets.find((bucket) =>\n poolForBucketId(bucket.bucketId),\n )\n if (!recognizedBucket) continue\n const pool = poolForBucketId(recognizedBucket.bucketId)\n if (!pool || !constrained) continue\n\n // The description is a list of model names comma-separated, often\n // prefixed with a label like \"Models within this group:\". Strip\n // the prefix before splitting so it doesn't count as a model.\n const modelCount = group.description\n ? parseDescriptionModelCount(group.description)\n : 0\n\n groups[pool] = {\n remainingFraction: constrained.remainingFraction,\n resetTime: constrained.resetTime,\n modelCount,\n windows,\n }\n totalCount += modelCount\n }\n\n return { groups, modelCount: totalCount }\n}\n\nexport interface FetchQuotaSummaryOptions {\n accessToken: string\n /** Managed project ID. Falls back to regular projectId on 403. */\n managedProjectId?: string\n /** Regular project ID \u2014 used as fallback when managedProjectId is missing or returns 403. */\n projectId?: string\n endpoints: readonly string[]\n timeoutMs?: number\n userAgent?: string\n fetchVia?: (\n url: string,\n options: RequestInit,\n extra: { timeoutMs: number; signal?: AbortSignal | null },\n ) => Promise<Response>\n}\n\nexport interface FetchQuotaSummaryResult {\n summary: RetrieveUserQuotaSummaryResponse\n /** True when the result came from the legacy fallback path. */\n fellBackToLegacy?: boolean\n}\n\n/**\n * Fetch the windowed quota summary via `retrieveUserQuotaSummary`.\n *\n * Uses the same transport/UA/timeout conventions as `fetchAvailableModels`.\n * On a 429 or 5xx against one endpoint, falls through to the next entry\n * in `options.endpoints` (matching the legacy fetchers' failover\n * convention). On 403 with the managedProjectId, retries with the\n * regular projectId. If that also 403s, falls back to\n * `fetchAvailableModels` so quota never goes dark. On missing\n * managedProjectId, tries projectId first.\n */\nexport async function fetchQuotaSummary(\n options: FetchQuotaSummaryOptions,\n): Promise<FetchQuotaSummaryResult> {\n const timeoutMs = options.timeoutMs ?? QUOTA_MANAGER_DEFAULT_TIMEOUT_MS\n const userAgent = options.userAgent ?? buildAntigravityHarnessUserAgent()\n const transport = options.fetchVia ?? defaultTransport\n const errors: string[] = []\n\n if (options.endpoints.length === 0) {\n throw new Error('No endpoints configured for fetchQuotaSummary')\n }\n\n // Discriminated result so the fallback is only entered on a 403, not on\n // transient errors (429, 5xx, network). A transient failure on the managed\n // project ID must not fall through to the regular projectId \u2014 that could\n // return a DIFFERENT project's quota data instead of signalling a retry.\n type TryBodyResult =\n | { ok: true; summary: RetrieveUserQuotaSummaryResponse }\n | { ok: false; reason: '403' | 'transient' }\n\n const tryBody = async (\n endpoint: string,\n projectId: string,\n ): Promise<TryBodyResult> => {\n const body = { project: projectId }\n try {\n const response = await transport(\n `${endpoint}/v1internal:retrieveUserQuotaSummary`,\n {\n method: 'POST',\n headers: {\n 'User-Agent': userAgent,\n Authorization: `Bearer ${options.accessToken}`,\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'gzip',\n },\n body: JSON.stringify(body),\n },\n { timeoutMs },\n )\n\n if (response.ok) {\n return {\n ok: true,\n summary: (await response.json()) as RetrieveUserQuotaSummaryResponse,\n }\n }\n\n const status = response.status\n if (status === 403) {\n errors.push(\n `retrieveUserQuotaSummary 403 at ${endpoint} (project=${projectId.slice(0, 12)}\u2026)`,\n )\n return { ok: false, reason: '403' }\n }\n\n if (status === 429 || status >= 500) {\n const message = await response.text().catch(() => '')\n errors.push(\n `retrieveUserQuotaSummary ${status} at ${endpoint}${message ? `: ${message.trim().slice(0, 200)}` : ''}`,\n )\n // Endpoint failover: the caller may have multiple endpoints;\n // continue to the next one with the same project ID.\n return { ok: false, reason: 'transient' }\n }\n\n const message = await response.text().catch(() => '')\n errors.push(\n `retrieveUserQuotaSummary ${status} at ${endpoint}${message ? `: ${message.trim().slice(0, 200)}` : ''}`,\n )\n return { ok: false, reason: 'transient' }\n } catch (error) {\n errors.push(\n `retrieveUserQuotaSummary network error at ${endpoint}: ${error instanceof Error ? error.message : String(error)}`,\n )\n return { ok: false, reason: 'transient' }\n }\n }\n\n // Try managedProjectId first, then projectId, then legacy fallback.\n // For each project, iterate the endpoint list so a 500/429 on the\n // primary endpoint falls through to the next entry.\n const primary = options.managedProjectId ?? options.projectId\n let primaryGot403 = false\n if (primary) {\n for (const endpoint of options.endpoints) {\n const result = await tryBody(endpoint, primary)\n if (result.ok) return { summary: result.summary }\n if (result.reason === '403') primaryGot403 = true\n }\n }\n\n // Retry with the regular projectId ONLY when the managed-project attempt\n // got a 403 (i.e. the managed project does not own the user). A transient\n // failure (429, 5xx, network) must NOT fall through here \u2014 the regular\n // projectId could return a different project's quota data.\n const fallbackId =\n primaryGot403 &&\n options.managedProjectId &&\n options.projectId &&\n options.managedProjectId !== options.projectId\n ? options.projectId\n : undefined\n if (fallbackId) {\n for (const endpoint of options.endpoints) {\n const result = await tryBody(endpoint, fallbackId)\n if (result.ok) return { summary: result.summary }\n }\n }\n\n // Give up \u2014 the caller should fall back to fetchAvailableModels.\n throw new Error(\n errors.join('; ') || 'fetchQuotaSummary failed: no project ID available',\n )\n}\n\n/**\n * Aggregate Gemini CLI quota buckets into a summary.\n */\nexport function aggregateGeminiCliQuota(\n response: RetrieveUserQuotaResponse,\n): GeminiCliQuotaSummary {\n const models: GeminiCliQuotaSummary['models'] = []\n\n if (!response.buckets || response.buckets.length === 0) {\n return { models }\n }\n\n for (const bucket of response.buckets) {\n if (!bucket.modelId) {\n continue\n }\n\n const modelId = bucket.modelId\n const isRelevantModel =\n modelId.startsWith('gemini-3-') ||\n modelId.startsWith('gemini-3.') ||\n modelId.startsWith('gemini-2.5-')\n if (!isRelevantModel) {\n continue\n }\n\n models.push({\n modelId: bucket.modelId,\n remainingFraction: normalizeRemainingFraction(bucket.remainingFraction),\n resetTime: bucket.resetTime,\n })\n }\n\n models.sort((a, b) => a.modelId.localeCompare(b.modelId))\n\n return { models }\n}\n\n// ============================================================================\n// Network helpers \u2014 used by the OpenCode adapter.\n// Re-exported so harnesses can call the same endpoint probes with their own\n// retry policy via `fetchWithActiveTimeout`.\n// ============================================================================\n\nexport interface FetchAvailableModelsOptions {\n accessToken: string\n projectId: string\n endpoints: readonly string[]\n timeoutMs?: number\n userAgent?: string\n /**\n * Override the transport used for the probe. Defaults to\n * `fetchWithActiveTimeout` so callers get the same stream-safe active-fetch\n * timeout that the rest of the core uses.\n */\n fetchVia?: (\n url: string,\n options: RequestInit,\n extra: { timeoutMs: number; signal?: AbortSignal | null },\n ) => Promise<Response>\n}\n\nexport async function fetchAvailableModels(\n options: FetchAvailableModelsOptions,\n): Promise<FetchAvailableModelsResponse> {\n const timeoutMs = options.timeoutMs ?? QUOTA_MANAGER_DEFAULT_TIMEOUT_MS\n const userAgent = options.userAgent ?? buildAntigravityHarnessUserAgent()\n const errors: string[] = []\n\n const transport = options.fetchVia ?? defaultTransport\n\n for (const endpoint of options.endpoints) {\n const body = options.projectId ? { project: options.projectId } : {}\n try {\n const response = await transport(\n `${endpoint}/v1internal:fetchAvailableModels`,\n {\n method: 'POST',\n headers: {\n 'User-Agent': userAgent,\n Authorization: `Bearer ${options.accessToken}`,\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'gzip',\n },\n body: JSON.stringify(body),\n },\n { timeoutMs },\n )\n\n if (response.ok) {\n return (await response.json()) as FetchAvailableModelsResponse\n }\n\n const status = response.status\n\n if (status === 403 && options.projectId) {\n try {\n const retryResponse = await transport(\n `${endpoint}/v1internal:fetchAvailableModels`,\n {\n method: 'POST',\n headers: {\n 'User-Agent': userAgent,\n Authorization: `Bearer ${options.accessToken}`,\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'gzip',\n },\n body: JSON.stringify({}),\n },\n { timeoutMs },\n )\n if (retryResponse.ok) {\n return (await retryResponse.json()) as FetchAvailableModelsResponse\n }\n } catch {\n // Fall through to next endpoint\n }\n }\n\n if (status === 429 || status >= 500) {\n const message = await response.text().catch(() => '')\n const snippet = message.trim().slice(0, 200)\n errors.push(\n `fetchAvailableModels ${status} at ${endpoint}${snippet ? `: ${snippet}` : ''}`,\n )\n continue\n }\n\n const message = await response.text().catch(() => '')\n const snippet = message.trim().slice(0, 200)\n errors.push(\n `fetchAvailableModels ${status} at ${endpoint}${snippet ? `: ${snippet}` : ''}`,\n )\n break\n } catch (error) {\n errors.push(\n `fetchAvailableModels network error at ${endpoint}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n throw new Error(errors.join('; ') || 'fetchAvailableModels failed')\n}\n\nexport interface FetchGeminiCliQuotaOptions {\n accessToken: string\n projectId: string\n endpoints: readonly string[]\n timeoutMs?: number\n userAgent?: string\n /**\n * Optional transport override. Production callers omit this; the e2e\n * harness and tests inject a stub. Mirrors `fetchQuotaSummary`'s seam.\n */\n fetchVia?: (\n url: string,\n options: RequestInit,\n extra: { timeoutMs: number },\n ) => Promise<Response>\n}\n\nexport async function fetchGeminiCliQuota(\n options: FetchGeminiCliQuotaOptions,\n): Promise<RetrieveUserQuotaResponse> {\n const timeoutMs = options.timeoutMs ?? QUOTA_MANAGER_DEFAULT_TIMEOUT_MS\n const userAgent = options.userAgent ?? buildAntigravityHarnessUserAgent()\n const transport = options.fetchVia ?? defaultTransport\n const errors: string[] = []\n\n for (const endpoint of options.endpoints) {\n const body = options.projectId ? { project: options.projectId } : {}\n try {\n const response = await transport(\n `${endpoint}/v1internal:retrieveUserQuota`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${options.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n },\n { timeoutMs },\n )\n\n if (response.ok) {\n return (await response.json()) as RetrieveUserQuotaResponse\n }\n\n const status = response.status\n if (status === 429 || status >= 500) {\n errors.push(`fetchGeminiCliQuota ${status} at ${endpoint}`)\n continue\n }\n // Non-retryable server response (e.g. 403) \u2014 treat as no CLI quota.\n return { buckets: [] }\n } catch (error) {\n // Transport-level failure (network abort, DNS, timeout). Collect the\n // error and try the next endpoint; if all endpoints fail, throw so\n // the caller can distinguish a transient network failure from an\n // authoritative 'no CLI quota configured' response.\n errors.push(error instanceof Error ? error.message : String(error))\n }\n }\n\n // All endpoints produced transport-level errors \u2014 propagate so the\n // outer caller (quota.ts .catch) can surface the real failure reason\n // rather than the generic 'No Gemini CLI quota available' message.\n if (errors.length > 0) {\n throw new Error(errors.join('; ') || 'fetchGeminiCliQuota failed')\n }\n\n return { buckets: [] }\n}\n\nasync function defaultTransport(\n url: string,\n init: RequestInit,\n options: { timeoutMs: number },\n): Promise<Response> {\n return fetchWithActiveTimeout(url, init, { timeoutMs: options.timeoutMs })\n}\n", "/**\n * Model Resolution with Thinking Tier Support\n *\n * Resolves model names with tier suffixes (e.g., gemini-3-pro-high, claude-opus-4-6-thinking-low)\n * to their actual API model names and corresponding thinking configurations.\n */\n\nimport {\n getGemini35FlashAntigravityModel,\n getGemini35FlashGeminiCliFallbackModel,\n getGemini36FlashAntigravityModel,\n getResolverAliasMap,\n} from '../model-registry.ts'\nimport type {\n GoogleSearchConfig,\n ResolvedModel,\n ThinkingTier,\n} from './types.ts'\n\nexport interface ModelResolverOptions {\n cli_first?: boolean\n}\n\n/**\n * Thinking tier budgets by model family.\n * Claude and Gemini 2.5 Pro use numeric budgets.\n */\nexport const THINKING_TIER_BUDGETS = {\n claude: { low: 8192, medium: 16384, high: 32768 },\n 'gemini-2.5-pro': { low: 8192, medium: 16384, high: 32768 },\n 'gemini-2.5-flash': { low: 6144, medium: 12288, high: 24576 },\n default: { low: 4096, medium: 8192, high: 16384 },\n} as const\n\n/**\n * Gemini 3 uses thinkingLevel strings instead of numeric budgets.\n * Flash supports: minimal, low, medium, high\n * Pro supports: low, high (no minimal/medium)\n */\nexport const GEMINI_3_THINKING_LEVELS = [\n 'minimal',\n 'low',\n 'medium',\n 'high',\n] as const\n\n/**\n * Model aliases - maps user-friendly names to API model names.\n *\n * Format:\n * - Gemini 3.x Pro variants: gemini-3.1-pro-{low,high}\n * - Claude thinking variants: claude-{model}-thinking-{low,medium,high}\n * - Claude non-thinking: claude-{model} (no -thinking suffix)\n */\nexport const MODEL_ALIASES: Record<string, string> = getResolverAliasMap()\nconst TIER_REGEX = /-(minimal|low|medium|high)$/\nconst QUOTA_PREFIX_REGEX = /^antigravity-/i\nconst GEMINI_3_PRO_REGEX = /^gemini-3(?:\\.\\d+)?-pro/i\nconst GEMINI_3_FLASH_REGEX = /^gemini-3(?:\\.\\d+)?-flash/i\n\n// ANTIGRAVITY_ONLY_MODELS removed - all models now default to antigravity\n\n/**\n * Image generation models - always route to Antigravity.\n * These models don't support thinking and require imageConfig.\n */\nconst IMAGE_GENERATION_MODELS = /image|imagen/i\n\n// Legacy LEGACY_ANTIGRAVITY_GEMINI3 regex removed - all Gemini models now default to antigravity\n\n/**\n * Models that support thinking tier suffixes.\n * Only these models should have -low/-medium/-high stripped as thinking tiers.\n * GPT models like gpt-oss-120b-medium should NOT have -medium stripped.\n */\nfunction supportsThinkingTiers(model: string): boolean {\n const lower = model.toLowerCase()\n return (\n lower.includes('gemini-3') ||\n lower.includes('gemini-2.5') ||\n (lower.includes('claude') && lower.includes('thinking'))\n )\n}\n\n/**\n * Extracts thinking tier from model name suffix.\n * Only extracts tier for models that support thinking tiers.\n */\nfunction extractThinkingTierFromModel(model: string): ThinkingTier | undefined {\n // Only extract tier for models that support thinking tiers\n if (!supportsThinkingTiers(model)) {\n return undefined\n }\n const tierMatch = model.match(TIER_REGEX)\n return tierMatch?.[1] as ThinkingTier | undefined\n}\n\n/**\n * Determines the budget family for a model.\n */\nfunction getBudgetFamily(model: string): keyof typeof THINKING_TIER_BUDGETS {\n if (model.includes('claude')) {\n return 'claude'\n }\n if (model.includes('gemini-2.5-pro')) {\n return 'gemini-2.5-pro'\n }\n if (model.includes('gemini-2.5-flash')) {\n return 'gemini-2.5-flash'\n }\n return 'default'\n}\n\n/**\n * Checks if a model is a thinking-capable model.\n */\nfunction isThinkingCapableModel(model: string): boolean {\n const lower = model.toLowerCase()\n return (\n lower.includes('thinking') ||\n lower.includes('gemini-3') ||\n lower.includes('gemini-2.5')\n )\n}\n\nfunction isGemini3ProModel(model: string): boolean {\n return GEMINI_3_PRO_REGEX.test(model)\n}\n\nfunction isGemini3FlashModel(model: string): boolean {\n return GEMINI_3_FLASH_REGEX.test(model)\n}\n\nfunction isGemini35FlashModel(model: string): boolean {\n return /^gemini-3\\.5-flash/i.test(model)\n}\n\nfunction resolveGemini35FlashAntigravityModel(tier?: ThinkingTier): string {\n return getGemini35FlashAntigravityModel(tier)\n}\n\nfunction getAgyGeminiFlashThinkingBudget(tier?: ThinkingTier): number {\n switch (tier) {\n case 'low':\n return 1000\n case 'high':\n return 10000\n default:\n return 4000\n }\n}\n\nfunction getAgyGemini31ProModel(tier?: ThinkingTier): string {\n return tier === 'high' ? 'gemini-pro-agent' : 'gemini-3.1-pro-low'\n}\n\nfunction getAgyGemini31ProThinkingBudget(tier?: ThinkingTier): number {\n return tier === 'high' ? 10001 : 1001\n}\n\n/**\n * Resolves a model name with optional tier suffix and quota prefix to its actual API model name\n * and corresponding thinking configuration.\n *\n * Quota routing:\n * - Default to Antigravity quota unless cli_first is enabled for Gemini models\n * - Fallback to Gemini CLI happens at account rotation level when Antigravity is exhausted\n * - \"antigravity-\" prefix marks explicit quota (no fallback allowed)\n * - Claude and image models always use Antigravity\n *\n * Examples:\n * - \"gemini-2.5-flash\" \u2192 { quotaPreference: \"antigravity\" }\n * - \"antigravity-gemini-3.1-pro-high\" \u2192 { quotaPreference: \"antigravity\", explicitQuota: true } * - \"claude-opus-4-6-thinking-medium\" \u2192 { quotaPreference: \"antigravity\" }\n *\n * @param requestedModel - The model name from the request\n * @param options - Optional configuration including cli_first preference\n * @returns Resolved model with thinking configuration\n */\nexport function resolveModelWithTier(\n requestedModel: string,\n options: ModelResolverOptions = {},\n): ResolvedModel {\n const isAntigravity = QUOTA_PREFIX_REGEX.test(requestedModel)\n const modelWithoutQuota = requestedModel.replace(QUOTA_PREFIX_REGEX, '')\n\n const tier = extractThinkingTierFromModel(modelWithoutQuota)\n const baseName = tier\n ? modelWithoutQuota.replace(TIER_REGEX, '')\n : modelWithoutQuota\n\n const isImageModel = IMAGE_GENERATION_MODELS.test(modelWithoutQuota)\n const isClaudeModel = modelWithoutQuota.toLowerCase().includes('claude')\n\n // All models default to Antigravity quota unless cli_first is enabled\n // Fallback to gemini-cli happens at the account rotation level when Antigravity is exhausted\n const preferGeminiCli =\n options.cli_first === true &&\n !isAntigravity &&\n !isImageModel &&\n !isClaudeModel\n const quotaPreference = preferGeminiCli\n ? ('gemini-cli' as const)\n : ('antigravity' as const)\n const explicitQuota = isAntigravity || isImageModel\n\n const isGemini3 = modelWithoutQuota.toLowerCase().startsWith('gemini-3')\n const skipAlias = isAntigravity && isGemini3\n\n // For older Antigravity Gemini 3 models without explicit tier, append the\n // tier to the model id. Gemini 3.5 and 3.6 Flash use live-catalog route maps\n // with numeric thinking budgets instead.\n const isGemini3Pro = isGemini3ProModel(modelWithoutQuota)\n const isGemini3Flash = isGemini3FlashModel(modelWithoutQuota)\n const isGemini31Pro = /^gemini-3\\.1-pro/i.test(baseName)\n const isGemini35Flash = /^gemini-3\\.5-flash/i.test(baseName)\n const isGemini36Flash = /^gemini-3\\.6-flash/i.test(baseName)\n const isGptOss120b = /^gpt-oss-120b(?:-medium)?$/i.test(baseName)\n\n if (isGemini31Pro && quotaPreference === 'antigravity') {\n return {\n actualModel: getAgyGemini31ProModel(tier),\n thinkingBudget: getAgyGemini31ProThinkingBudget(tier),\n tier,\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n if (isGemini35Flash && quotaPreference === 'antigravity') {\n return {\n actualModel: resolveGemini35FlashAntigravityModel(tier ?? 'medium'),\n thinkingBudget: getAgyGeminiFlashThinkingBudget(tier),\n tier: tier ?? 'medium',\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n if (isGemini36Flash && quotaPreference === 'antigravity') {\n return {\n actualModel: getGemini36FlashAntigravityModel(tier ?? 'medium'),\n thinkingBudget: getAgyGeminiFlashThinkingBudget(tier),\n tier: tier ?? 'medium',\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n if (isGptOss120b && quotaPreference === 'antigravity') {\n return {\n actualModel: 'gpt-oss-120b-medium',\n thinkingBudget: 8192,\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n let antigravityModel = modelWithoutQuota\n if (skipAlias) {\n if ((isGemini3Pro || isGemini3Flash) && !tier && !isImageModel) {\n const defaultTier = isGemini3Pro ? 'low' : 'medium'\n antigravityModel = `${modelWithoutQuota}-${defaultTier}`\n }\n // When tier is present, modelWithoutQuota already contains the tier suffix\n // (e.g., \"gemini-3.5-flash-high\") \u2014 no modification needed\n }\n const actualModel = skipAlias\n ? antigravityModel\n : MODEL_ALIASES[modelWithoutQuota] || MODEL_ALIASES[baseName] || baseName\n\n const resolvedModel = actualModel\n\n const isThinking = isThinkingCapableModel(resolvedModel)\n\n // Image generation models don't support thinking - return early without thinking config\n if (isImageModel) {\n return {\n actualModel: resolvedModel,\n isThinkingModel: false,\n isImageModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n // Check if this is a Gemini 3 model (works for both aliased and skipAlias paths)\n const isEffectiveGemini3 = resolvedModel.toLowerCase().includes('gemini-3')\n const lowerModelWithoutQuota = modelWithoutQuota.toLowerCase()\n const isClaudeThinking =\n (resolvedModel.toLowerCase().includes('claude') &&\n resolvedModel.toLowerCase().includes('thinking')) ||\n (lowerModelWithoutQuota.includes('claude') &&\n lowerModelWithoutQuota.includes('thinking')) ||\n lowerModelWithoutQuota === 'gemini-claude-sonnet-4-6'\n\n if (!tier) {\n // Gemini 3 models without explicit tier get a default thinkingLevel\n if (isEffectiveGemini3) {\n return {\n actualModel: resolvedModel,\n thinkingLevel: isGemini35Flash ? 'medium' : 'low',\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n // agy CLI sends a compact 1024-token budget for Claude thinking models.\n if (isClaudeThinking) {\n return {\n actualModel: resolvedModel,\n thinkingBudget: 1024,\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n return {\n actualModel: resolvedModel,\n isThinkingModel: isThinking,\n quotaPreference,\n explicitQuota,\n }\n }\n\n // Gemini 3 models with tier always get thinkingLevel set\n if (isEffectiveGemini3) {\n return {\n actualModel: resolvedModel,\n thinkingLevel: tier,\n tier,\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n if (isClaudeThinking) {\n return {\n actualModel: resolvedModel,\n thinkingBudget: 1024,\n tier,\n isThinkingModel: true,\n quotaPreference,\n explicitQuota,\n }\n }\n\n const budgetFamily = getBudgetFamily(resolvedModel)\n const budgets = THINKING_TIER_BUDGETS[budgetFamily]\n const thinkingBudget = budgets[tier]\n\n return {\n actualModel: resolvedModel,\n thinkingBudget,\n tier,\n isThinkingModel: isThinking,\n quotaPreference,\n explicitQuota,\n }\n}\n\n/**\n * Gets the model family for routing decisions.\n */\nexport function getModelFamily(\n model: string,\n): 'claude' | 'gemini-flash' | 'gemini-pro' {\n const lower = model.toLowerCase()\n if (lower.includes('claude')) {\n return 'claude'\n }\n if (lower.includes('flash')) {\n return 'gemini-flash'\n }\n return 'gemini-pro'\n}\n\n/**\n * Variant config from OpenCode's providerOptions.\n */\nexport interface VariantConfig {\n thinkingBudget?: number\n googleSearch?: GoogleSearchConfig\n}\n\n/**\n * Maps a thinking budget to Gemini 3 thinking level.\n * \u22648192 \u2192 low, \u226416384 \u2192 medium, >16384 \u2192 high\n */\nfunction budgetToGemini3Level(budget: number): 'low' | 'medium' | 'high' {\n if (budget <= 8192) return 'low'\n if (budget <= 16384) return 'medium'\n return 'high'\n}\n\n/**\n * Resolves model name for a specific headerStyle (quota fallback support).\n * Transforms model names when switching between gemini-cli and antigravity quotas.\n *\n * Issue #103: When quota fallback occurs, model names need to be transformed:\n * - gemini-3-flash-preview (gemini-cli) \u2192 gemini-3-flash (antigravity)\n * - gemini-3-flash (antigravity) \u2192 gemini-3-flash-preview (gemini-cli) */\nexport function resolveModelForHeaderStyle(\n requestedModel: string,\n headerStyle: 'antigravity' | 'gemini-cli',\n): ResolvedModel {\n const lower = requestedModel.toLowerCase()\n const isGemini3 = lower.includes('gemini-3')\n\n if (!isGemini3) {\n return resolveModelWithTier(requestedModel)\n }\n\n if (headerStyle === 'antigravity') {\n let transformedModel = requestedModel\n .replace(/-preview-customtools$/i, '')\n .replace(/-preview$/i, '')\n .replace(/^antigravity-/i, '')\n\n const isGemini3Pro = isGemini3ProModel(transformedModel)\n const isGemini3Flash = isGemini3FlashModel(transformedModel)\n const hasTierSuffix = /-(minimal|low|medium|high)$/i.test(transformedModel)\n const isImageModel = IMAGE_GENERATION_MODELS.test(transformedModel)\n const isGemini35Flash = isGemini35FlashModel(\n transformedModel.replace(TIER_REGEX, ''),\n )\n\n // Don't add tier suffix to image models - they don't support thinking\n if (\n (isGemini3Pro || isGemini3Flash) &&\n !isGemini35Flash &&\n !hasTierSuffix &&\n !isImageModel\n ) {\n const defaultTier = isGemini3Pro ? 'low' : 'medium'\n transformedModel = `${transformedModel}-${defaultTier}`\n }\n const prefixedModel = `antigravity-${transformedModel}`\n return resolveModelWithTier(prefixedModel)\n }\n\n if (headerStyle === 'gemini-cli') {\n const requestedTier = extractThinkingTierFromModel(\n requestedModel.replace(/^antigravity-/i, ''),\n )\n let transformedModel = requestedModel\n .replace(/^antigravity-/i, '')\n .replace(/-(minimal|low|medium|high)$/i, '')\n\n const hasPreviewSuffix = /-preview($|-)/i.test(transformedModel)\n // Gemini Code Assist still exposes Gemini 3.5 Flash through the\n // gemini-3-flash-preview bucket; retrieveUserQuota does not list a\n // gemini-3.5-flash bucket for the gemini-cli header path.\n const isGemini35Flash = isGemini35FlashModel(transformedModel)\n if (isGemini35Flash) {\n transformedModel = getGemini35FlashGeminiCliFallbackModel()\n } else if (!hasPreviewSuffix) {\n transformedModel = `${transformedModel}-preview`\n }\n\n const resolved = resolveModelWithTier(transformedModel, { cli_first: true })\n return {\n ...resolved,\n thinkingLevel: requestedTier ?? resolved.thinkingLevel,\n tier: requestedTier ?? resolved.tier,\n quotaPreference: 'gemini-cli',\n }\n }\n\n return resolveModelWithTier(requestedModel)\n}\n\n/**\n * Resolves model with variant config from providerOptions.\n * Variant config takes priority over tier suffix in model name.\n */\nexport function resolveModelWithVariant(\n requestedModel: string,\n variantConfig?: VariantConfig,\n): ResolvedModel {\n const base = resolveModelWithTier(requestedModel)\n\n if (!variantConfig) {\n return base\n }\n\n // Apply Google Search config if present\n if (variantConfig.googleSearch) {\n base.googleSearch = variantConfig.googleSearch\n base.configSource = 'variant'\n }\n\n if (!variantConfig.thinkingBudget) {\n return base\n }\n\n const budget = variantConfig.thinkingBudget\n const isGemini3 = base.actualModel.toLowerCase().includes('gemini-3')\n\n if (isGemini3) {\n const level = budgetToGemini3Level(budget)\n const requestedBase = requestedModel\n .replace(/^antigravity-/i, '')\n .replace(TIER_REGEX, '')\n const isGemini35FlashAlias =\n isGemini35FlashModel(requestedBase) ||\n base.actualModel === 'gemini-3-flash-agent' ||\n base.actualModel === 'gemini-3.5-flash-low'\n const isAntigravityGemini3WithTier =\n base.quotaPreference === 'antigravity' &&\n (isGemini3ProModel(base.actualModel) ||\n isGemini3FlashModel(base.actualModel))\n\n let actualModel = base.actualModel\n if (isGemini35FlashAlias) {\n actualModel = resolveGemini35FlashAntigravityModel(level)\n } else if (isAntigravityGemini3WithTier) {\n const baseModel = base.actualModel.replace(/-(low|medium|high)$/, '')\n actualModel = `${baseModel}-${level}`\n }\n return {\n ...base,\n actualModel,\n thinkingLevel: level,\n thinkingBudget: undefined,\n configSource: 'variant',\n }\n }\n\n return {\n ...base,\n thinkingBudget: budget,\n configSource: 'variant',\n }\n}\n", "import type {\n AntigravityTokenExchangeResult,\n authorizeAntigravity,\n exchangeAntigravity,\n} from '@cortexkit/antigravity-auth-core'\n\nimport type { startOAuthListener } from './server'\n\nexport type AntigravityTokenExchangeSuccess = Extract<\n AntigravityTokenExchangeResult,\n { type: 'success' }\n>\n\nexport interface OAuthLoginRequest {\n projectId?: string\n noBrowser: boolean\n isHeadless: boolean\n refreshAccountIndex?: number\n accounts: AntigravityTokenExchangeSuccess[]\n startFresh: boolean\n}\n\nexport interface OAuthLoginDependencies {\n authorize: typeof authorizeAntigravity\n exchange: typeof exchangeAntigravity\n startListener: typeof startOAuthListener\n openBrowser(url: string): Promise<void>\n upsert(result: AntigravityTokenExchangeSuccess): Promise<void>\n}\n\nfunction expectedState(authorizationUrl: string): string {\n try {\n return new URL(authorizationUrl).searchParams.get('state') ?? ''\n } catch {\n return ''\n }\n}\n\nfunction callbackParams(\n callbackUrl: URL,\n expected: string,\n): { code: string; state: string } {\n const code = callbackUrl.searchParams.get('code')\n const state = callbackUrl.searchParams.get('state')\n if (!code || !state) throw new Error('Missing code or state in callback URL')\n if (expected && state !== expected) throw new Error('OAuth state mismatch')\n return { code, state }\n}\n\nexport async function performOAuthLogin(\n request: OAuthLoginRequest,\n deps: OAuthLoginDependencies,\n): Promise<AntigravityTokenExchangeSuccess> {\n const listener = await deps.startListener()\n try {\n const authorization = await deps.authorize(request.projectId ?? '')\n if (!request.noBrowser && !request.isHeadless) {\n await deps.openBrowser(authorization.url)\n }\n\n const params = callbackParams(\n await listener.waitForCallback(),\n expectedState(authorization.url),\n )\n const result = await deps.exchange(params.code, params.state)\n if (result.type === 'failed') throw new Error(result.error)\n\n await deps.upsert(result)\n request.accounts.push(result)\n return result\n } finally {\n await listener.close().catch(() => {})\n }\n}\n", "/**\n * Host-path adapter for account storage.\n *\n * Resolves the on-disk path for the OpenCode config directory, handles\n * the legacy Windows migration, and keeps the .gitignore in sync. All\n * data operations are delegated to `@cortexkit/antigravity-auth-core`'s\n * lock-held account-storage engine.\n *\n * The split keeps this module harness-specific (it knows about\n * `OPENCODE_CONFIG_DIR`, `%APPDATA%`, and OpenCode-specific gitignore\n * entries) while the schema, migrations, and lock semantics live in\n * core.\n */\n\nimport {\n appendFileSync,\n copyFileSync,\n existsSync,\n promises as fs,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nimport type {\n AccountMetadataV2,\n AccountMetadataV3,\n AccountModelFamily,\n AccountStorageUnreadableReason,\n AccountStorageV2,\n AccountStorageV4,\n AnyAccountStorage,\n CooldownReason,\n HeaderStyle,\n RateLimitStateV2,\n RateLimitStateV3,\n} from '@cortexkit/antigravity-auth-core'\nimport {\n AccountStorageUnreadableError,\n clearAccountStorage as coreClearAccountStorage,\n deduplicateAccountsByEmail as coreDeduplicateAccountsByEmail,\n loadAccountStorage as coreLoadAccountStorage,\n mergeAccountStorage as coreMergeAccountStorage,\n migrateV2ToV3 as coreMigrateV2ToV3,\n mutateAccountStorage as coreMutateAccountStorage,\n saveAccountStorage as coreSaveAccountStorage,\n saveAccountStorageReplace as coreSaveAccountStorageReplace,\n} from '@cortexkit/antigravity-auth-core'\nimport { createLogger } from './logger'\n\nconst log = createLogger('storage')\n\n// ============================================================================\n// Re-export types for backward compatibility.\n// Harnesses (and existing call sites in plugin.ts / accounts.ts) import\n// the metadata + storage shapes from `./storage`; keep the surface stable.\n// ============================================================================\n\n/**\n * @deprecated use `AccountModelFamily` from `@cortexkit/antigravity-auth-core`.\n * Retained under the old name so existing call sites continue to compile.\n */\nexport type ModelFamily = AccountModelFamily\n\nexport type {\n AccountMetadataV2,\n AccountMetadataV3,\n AccountStorageUnreadableReason,\n AccountStorageV2,\n AccountStorageV4,\n AnyAccountStorage,\n CooldownReason,\n HeaderStyle,\n RateLimitStateV2,\n RateLimitStateV3,\n}\n\n/**\n * Re-export the typed unreadable-storage error so consumers can\n * `instanceof`-check without pulling core into their own dependency\n * graph. When the on-disk accounts file exists but cannot be parsed\n * as a valid v4 (corrupt JSON, schema mismatch, unknown version, or\n * an I/O error other than ENOENT), every read/write here throws this\n * \u2014 never silently overwrites the user's data.\n */\nexport { AccountStorageUnreadableError }\n\n/**\n * Backward-compat re-exports for harnesses still importing\n * `deduplicateAccountsByEmail` / `mergeAccountStorage` / `migrateV2ToV3`\n * from `./storage`. The definitions live in core; the adapter exposes\n * them so legacy test files compile without modification.\n */\nexport const deduplicateAccountsByEmail = coreDeduplicateAccountsByEmail\nexport const mergeAccountStorage = coreMergeAccountStorage\nexport const migrateV2ToV3 = coreMigrateV2ToV3\nexport const mutateAccountStorage = coreMutateAccountStorage\n\n/**\n * Files/directories that should be gitignored in the config directory.\n * These contain sensitive data or machine-specific state.\n */\n// NOTE: deliberately no \".gitignore\" self-ignore entry \u2014 users who track their\n// config dir as a git repo (with .gitignore committed) get endless working-tree\n// drift from re-appending it, and for a tracked file the entry is a no-op anyway.\nexport const GITIGNORE_ENTRIES = [\n 'antigravity-accounts.json',\n 'antigravity-accounts.json.*.tmp',\n 'antigravity-signature-cache.json',\n 'antigravity-logs/',\n]\n\n/**\n * Ensures a .gitignore file exists in the config directory with entries\n * for sensitive files. Creates the file if missing, or appends missing\n * entries if it already exists.\n */\nexport async function ensureGitignore(configDir: string): Promise<void> {\n const gitignorePath = join(configDir, '.gitignore')\n\n try {\n let content: string\n let existingLines: string[] = []\n\n try {\n content = await fs.readFile(gitignorePath, 'utf-8')\n existingLines = content.split('\\n').map((line) => line.trim())\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n return\n }\n content = ''\n }\n\n const missingEntries = GITIGNORE_ENTRIES.filter(\n (entry) => !existingLines.includes(entry),\n )\n\n if (missingEntries.length === 0) {\n return\n }\n\n if (content === '') {\n await fs.writeFile(\n gitignorePath,\n `${missingEntries.join('\\n')}\\n`,\n 'utf-8',\n )\n log.info('Created .gitignore in config directory')\n } else {\n const suffix = content.endsWith('\\n') ? '' : '\\n'\n await fs.appendFile(\n gitignorePath,\n `${suffix + missingEntries.join('\\n')}\\n`,\n 'utf-8',\n )\n log.info('Updated .gitignore with missing entries', {\n added: missingEntries,\n })\n }\n } catch (error) {\n log.warn('Failed to update .gitignore with account storage entries', {\n error: String(error),\n })\n }\n}\n\n/**\n * Synchronous version of ensureGitignore for use in sync code paths.\n */\nexport function ensureGitignoreSync(configDir: string): void {\n const gitignorePath = join(configDir, '.gitignore')\n\n try {\n let content: string\n let existingLines: string[] = []\n\n if (existsSync(gitignorePath)) {\n content = readFileSync(gitignorePath, 'utf-8')\n existingLines = content.split('\\n').map((line) => line.trim())\n } else {\n content = ''\n }\n\n const missingEntries = GITIGNORE_ENTRIES.filter(\n (entry) => !existingLines.includes(entry),\n )\n\n if (missingEntries.length === 0) {\n return\n }\n\n if (content === '') {\n writeFileSync(gitignorePath, `${missingEntries.join('\\n')}\\n`, 'utf-8')\n log.info('Created .gitignore in config directory')\n } else {\n const suffix = content.endsWith('\\n') ? '' : '\\n'\n appendFileSync(\n gitignorePath,\n `${suffix + missingEntries.join('\\n')}\\n`,\n 'utf-8',\n )\n log.info('Updated .gitignore with missing entries', {\n added: missingEntries,\n })\n }\n } catch (error) {\n log.warn('Failed to update .gitignore with account storage entries', {\n error: String(error),\n })\n }\n}\n\n/**\n * Gets the legacy Windows config directory (%APPDATA%\\opencode).\n * Used for migration from older plugin versions.\n */\nfunction getLegacyWindowsConfigDir(): string {\n return join(\n process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'),\n 'opencode',\n )\n}\n\n/**\n * Gets the config directory path, with the following precedence:\n * 1. OPENCODE_CONFIG_DIR env var (if set)\n * 2. ~/.config/opencode (all platforms, including Windows)\n *\n * On Windows, also checks for legacy %APPDATA%\\opencode path for migration.\n */\nfunction getConfigDir(): string {\n if (process.env.OPENCODE_CONFIG_DIR) {\n return process.env.OPENCODE_CONFIG_DIR\n }\n\n const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')\n return join(xdgConfig, 'opencode')\n}\n\n/**\n * Migrates config from legacy Windows location to the new path.\n * Moves the file if legacy exists and new doesn't.\n * Returns true if migration was performed.\n */\nfunction migrateLegacyWindowsConfig(): boolean {\n if (process.platform !== 'win32') {\n return false\n }\n\n const newPath = join(getConfigDir(), 'antigravity-accounts.json')\n const legacyPath = join(\n getLegacyWindowsConfigDir(),\n 'antigravity-accounts.json',\n )\n\n if (!existsSync(legacyPath) || existsSync(newPath)) {\n return false\n }\n\n try {\n const newConfigDir = getConfigDir()\n\n mkdirSync(newConfigDir, { recursive: true })\n\n try {\n renameSync(legacyPath, newPath)\n log.info('Migrated Windows config via rename', {\n from: legacyPath,\n to: newPath,\n })\n } catch {\n copyFileSync(legacyPath, newPath)\n unlinkSync(legacyPath)\n log.info('Migrated Windows config via copy+delete', {\n from: legacyPath,\n to: newPath,\n })\n }\n\n return true\n } catch (error) {\n log.warn('Failed to migrate legacy Windows config, will use legacy path', {\n legacyPath,\n newPath,\n error: String(error),\n })\n return false\n }\n}\n\n/**\n * Gets the storage path, migrating from legacy Windows location if needed.\n * On Windows, attempts to move legacy config to new path for alignment.\n */\nfunction getStoragePathWithMigration(): string {\n const newPath = join(getConfigDir(), 'antigravity-accounts.json')\n\n if (process.platform === 'win32') {\n migrateLegacyWindowsConfig()\n\n if (!existsSync(newPath)) {\n const legacyPath = join(\n getLegacyWindowsConfigDir(),\n 'antigravity-accounts.json',\n )\n if (existsSync(legacyPath)) {\n log.info('Using legacy Windows config path (migration failed)', {\n legacyPath,\n newPath,\n })\n return legacyPath\n }\n }\n }\n\n return newPath\n}\n\nexport function getStoragePath(): string {\n return getStoragePathWithMigration()\n}\n\n/**\n * Gets the config directory path. Exported for use by other modules.\n */\nexport { getConfigDir }\n\n// ============================================================================\n// Host path delegation. Each of these resolves the on-disk path via the\n// adapter above and hands it to the core lock-held engine. Callers that\n// want to run their own mutator (e.g. persist-account-pool) should\n// import from `@cortexkit/antigravity-auth-core` directly and pass\n// `getStoragePath()` as the path argument.\n// ============================================================================\n\nexport async function loadAccounts(): Promise<AccountStorageV4 | null> {\n const path = getStoragePath()\n await ensureGitignore(dirname(path))\n return coreLoadAccountStorage(path)\n}\n\n/**\n * Merge `storage` into the persisted pool. Use this for non-destructive\n * writes (quota cache, eligibility, last-used) so concurrent writers\n * do not silently drop each other's data.\n */\nexport async function saveAccounts(storage: AccountStorageV4): Promise<void> {\n const path = getStoragePath()\n const configDir = dirname(path)\n await fs.mkdir(configDir, { recursive: true })\n await ensureGitignore(configDir)\n await coreSaveAccountStorage(path, storage)\n}\n\n/**\n * Save accounts storage by replacing the entire file (no merge).\n * Required for destructive operations like delete where the next-state\n * must replace \u2014 never be merged with \u2014 what is on disk.\n */\nexport async function saveAccountsReplace(\n storage: AccountStorageV4,\n): Promise<void> {\n const path = getStoragePath()\n const configDir = dirname(path)\n await fs.mkdir(configDir, { recursive: true })\n await ensureGitignore(configDir)\n await coreSaveAccountStorageReplace(path, storage)\n}\n\nexport async function clearAccounts(): Promise<void> {\n const path = getStoragePath()\n try {\n await coreClearAccountStorage(path)\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (code !== 'ENOENT') {\n log.error('Failed to clear account storage', { error: String(error) })\n }\n }\n}\n\n/**\n * Locate a stored account by its refresh token under the lock-held\n * mutator and apply `mutate(account)`. Returns the (possibly mutated)\n * account, or `undefined` when the token no longer matches any stored\n * account. Concurrent writers that add/remove accounts will not\n * disturb the lookup \u2014 the read happens while the lock is held.\n *\n * `mutate` may mutate `account` in place and return `true` to commit\n * the change; returning `false` is treated as \"no change\" and skips\n * the write.\n */\nexport async function mutateAccountByRefreshToken(\n refreshToken: string,\n mutate: (account: AccountMetadataV3) => boolean,\n): Promise<AccountMetadataV3 | undefined> {\n const path = getStoragePath()\n const configDir = dirname(path)\n await fs.mkdir(configDir, { recursive: true })\n await ensureGitignore(configDir)\n\n let result: AccountMetadataV3 | undefined\n await coreMutateAccountStorage(path, (current) => {\n const idx = current.accounts.findIndex(\n (acc) => acc.refreshToken === refreshToken,\n )\n if (idx === -1) return current\n const target = current.accounts[idx]\n if (!target) return current\n const changed = mutate(target)\n if (!changed) return current\n result = target\n current.accounts[idx] = target\n return current\n })\n return result\n}\n", "import {\n createWriteStream,\n mkdirSync,\n readdirSync,\n statSync,\n unlinkSync,\n} from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { env } from 'node:process'\nimport type { AntigravityConfig } from './config'\nimport {\n deriveDebugPolicy,\n formatAccountContextLabel,\n formatAccountLabel,\n formatBodyPreviewForLog,\n formatErrorForLog,\n isTruthyFlag,\n redactBodyForLog,\n redactSensitive,\n redactSensitiveFields,\n truncateTextForLog,\n} from './logging-utils'\nimport { ensureGitignoreSync } from './storage'\n\nconst MAX_BODY_PREVIEW_CHARS = 12000\nconst MAX_BODY_LOG_CHARS = 50000\n\nexport const DEBUG_MESSAGE_PREFIX = '[opencode-antigravity-auth debug]'\n\n// =============================================================================\n// Debug State (lazily initialized with config)\n// =============================================================================\n\ninterface DebugState {\n debugEnabled: boolean\n debugTuiEnabled: boolean\n logFilePath: string | undefined\n logWriter: (line: string) => void\n /**\n * Owning stream for the underlying file (when debug is enabled). The\n * previous stream is closed when initializeDebug is called again so\n * tests that re-initialize the debug state don't leak file\n * descriptors; the OS-level \"file descriptor\" alone does not keep\n * the process alive, but the writeStream's underlying socket pair\n * does, and a long-lived test suite can starve.\n */\n logStream: import('node:fs').WriteStream | null\n}\n\nlet debugState: DebugState | null = null\n\nfunction closeDebugStream(): void {\n const current = debugState?.logStream\n if (!current) return\n try {\n current.end()\n } catch {\n // best-effort; the stream is already detached\n }\n}\n\n/**\n * Close the debug log stream and resolve once all buffered writes\n * have been flushed to disk. Tests use this to wait deterministically\n * for log lines before reading the log file; the internal reinitialize\n * path calls {@link closeDebugStream} directly (fire-and-forget).\n */\nexport function closeDebugLog(): Promise<void> {\n const current = debugState?.logStream\n if (!current) return Promise.resolve()\n // Detach the reference so reinitialization won't double-close.\n if (debugState) {\n debugState.logStream = null\n }\n return new Promise<void>((resolve) => {\n current.once('close', () => resolve())\n current.once('error', () => resolve())\n try {\n current.end()\n } catch {\n resolve()\n }\n })\n}\n\n/**\n * Get the OS-specific config directory.\n */\nfunction getConfigDir(): string {\n const platform = process.platform\n if (platform === 'win32') {\n return join(\n env.APPDATA || join(homedir(), 'AppData', 'Roaming'),\n 'opencode',\n )\n }\n const xdgConfig = env.XDG_CONFIG_HOME || join(homedir(), '.config')\n return join(xdgConfig, 'opencode')\n}\n\n/**\n * Returns the logs directory, creating it if needed.\n */\nfunction getLogsDir(customLogDir?: string): string {\n const logsDir = customLogDir || join(getConfigDir(), 'antigravity-logs')\n\n try {\n // Debug logs can contain prompt/response bodies \u2014 keep them user-only.\n mkdirSync(logsDir, { recursive: true, mode: 0o700 })\n } catch {\n // Directory may already exist or we don't have permission\n }\n\n return logsDir\n}\n\n/**\n * Builds a timestamped log file path.\n */\nfunction createLogFilePath(customLogDir?: string): string {\n const logsDir = getLogsDir(customLogDir)\n cleanupOldLogs(logsDir, 25)\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-')\n return join(logsDir, `antigravity-debug-${timestamp}.log`)\n}\n\n/**\n * Cleans up old log files, keeping only the most recent maxFiles.\n */\nfunction cleanupOldLogs(logsDir: string, maxFiles: number): void {\n try {\n const files = readdirSync(logsDir)\n .filter(\n (file) =>\n file.startsWith('antigravity-debug-') && file.endsWith('.log'),\n )\n .map((file) => join(logsDir, file))\n\n if (files.length <= maxFiles) {\n return\n }\n\n const sortedFiles = files\n .map((file) => ({\n file,\n mtime: statSync(file).mtimeMs,\n }))\n .sort((a, b) => b.mtime - a.mtime)\n\n for (let i = maxFiles; i < sortedFiles.length; i++) {\n try {\n unlinkSync(sortedFiles[i]!.file)\n } catch {\n // Ignore deletion errors\n }\n }\n } catch {\n // Ignore directory read errors\n }\n}\n\n/**\n * Creates a log writer function that writes to a file.\n */\nfunction createLogWriter(filePath?: string): {\n writer: (line: string) => void\n stream: import('node:fs').WriteStream | null\n} {\n if (!filePath) {\n return { writer: () => {}, stream: null }\n }\n\n try {\n const stream = createWriteStream(filePath, { flags: 'a', mode: 0o600 })\n stream.on('error', () => {})\n return {\n stream,\n writer: (line: string) => {\n const timestamp = new Date().toISOString()\n const formatted = `[${timestamp}] ${line}`\n stream.write(`${formatted}\\n`)\n },\n }\n } catch {\n return { writer: () => {}, stream: null }\n }\n}\n\n/**\n * Initialize or reinitialize debug state with the given config.\n * Call this once at plugin startup after loading config.\n */\nexport function initializeDebug(config: AntigravityConfig): void {\n // Close any previously opened log stream so re-initialized state\n // does not leak file descriptors (each open writeStream keeps the\n // process alive through its duplex pair).\n closeDebugStream()\n\n // Config takes precedence, but env var can force enable for debugging\n const envDebugFlag = env.OPENCODE_ANTIGRAVITY_DEBUG ?? ''\n const { debugEnabled } = deriveDebugPolicy({\n configDebug: config.debug,\n configDebugTui: config.debug_tui,\n envDebugFlag,\n envDebugTuiFlag: env.OPENCODE_ANTIGRAVITY_DEBUG_TUI,\n })\n const debugTuiEnabled =\n config.debug_tui || isTruthyFlag(env.OPENCODE_ANTIGRAVITY_DEBUG_TUI)\n const logFilePath = debugEnabled\n ? createLogFilePath(config.log_dir)\n : undefined\n const { writer: logWriter, stream: logStream } = createLogWriter(logFilePath)\n\n if (debugEnabled) {\n ensureGitignoreSync(getConfigDir())\n }\n\n debugState = {\n debugEnabled,\n debugTuiEnabled,\n logFilePath,\n logWriter,\n logStream,\n }\n}\n\n/**\n * Get the current debug state, initializing with defaults if needed.\n * This allows the module to work even before initializeDebug is called.\n */\nfunction getDebugState(): DebugState {\n if (!debugState) {\n // Fallback to env-based initialization for backward compatibility\n const { debugEnabled } = deriveDebugPolicy({\n configDebug: false,\n configDebugTui: false,\n envDebugFlag: env.OPENCODE_ANTIGRAVITY_DEBUG,\n envDebugTuiFlag: env.OPENCODE_ANTIGRAVITY_DEBUG_TUI,\n })\n const debugTuiEnabled = isTruthyFlag(env.OPENCODE_ANTIGRAVITY_DEBUG_TUI)\n const logFilePath = debugEnabled ? createLogFilePath() : undefined\n const { writer: logWriter, stream: logStream } =\n createLogWriter(logFilePath)\n\n debugState = {\n debugEnabled,\n debugTuiEnabled,\n logFilePath,\n logWriter,\n logStream,\n }\n }\n return debugState!\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\nexport function isDebugEnabled(): boolean {\n return getDebugState().debugEnabled\n}\n\nexport function isDebugTuiEnabled(): boolean {\n return getDebugState().debugTuiEnabled\n}\n\nexport function getLogFilePath(): string | undefined {\n return getDebugState().logFilePath\n}\n\nexport interface AntigravityDebugContext {\n id: string\n streaming: boolean\n startedAt: number\n}\n\ninterface AntigravityDebugRequestMeta {\n originalUrl: string\n resolvedUrl: string\n method?: string\n headers?: HeadersInit\n body?: BodyInit | null\n streaming: boolean\n projectId?: string\n}\n\ninterface AntigravityDebugResponseMeta {\n body?: string\n note?: string\n error?: unknown\n headersOverride?: HeadersInit\n}\n\nlet requestCounter = 0\n\n/**\n * Begins a debug trace for an Antigravity request.\n */\nexport function startAntigravityDebugRequest(\n meta: AntigravityDebugRequestMeta,\n): AntigravityDebugContext | null {\n const state = getDebugState()\n if (!state.debugEnabled) {\n return null\n }\n\n const id = `ANTIGRAVITY-${++requestCounter}`\n const method = meta.method ?? 'GET'\n logDebug(\n `[Antigravity Debug ${id}] pid=${process.pid} ${method} ${meta.resolvedUrl}`,\n )\n if (meta.originalUrl && meta.originalUrl !== meta.resolvedUrl) {\n logDebug(`[Antigravity Debug ${id}] Original URL: ${meta.originalUrl}`)\n }\n if (meta.projectId) {\n logDebug(\n `[Antigravity Debug ${id}] Project: ${redactSensitive(meta.projectId)}`,\n )\n }\n logDebug(\n `[Antigravity Debug ${id}] Streaming: ${meta.streaming ? 'yes' : 'no'}`,\n )\n logDebug(\n `[Antigravity Debug ${id}] Headers: ${JSON.stringify(maskHeaders(meta.headers))}`,\n )\n // The request body embeds raw project IDs \u2014 redact credential-shaped\n // fields before the verbatim preview hits the log.\n const bodyPreview = formatBodyPreviewForLog(\n redactBodyForLog(meta.body),\n MAX_BODY_PREVIEW_CHARS,\n )\n if (bodyPreview) {\n logDebug(`[Antigravity Debug ${id}] Body Preview: ${bodyPreview}`)\n }\n\n return { id, streaming: meta.streaming, startedAt: Date.now() }\n}\n\n/**\n * Logs response details for a previously started debug trace.\n */\nexport function logAntigravityDebugResponse(\n context: AntigravityDebugContext | null | undefined,\n response: Response,\n meta: AntigravityDebugResponseMeta = {},\n): void {\n const state = getDebugState()\n if (!state.debugEnabled || !context) {\n return\n }\n\n const durationMs = Date.now() - context.startedAt\n logDebug(\n `[Antigravity Debug ${context.id}] Response ${response.status} ${response.statusText} (${durationMs}ms)`,\n )\n logDebug(\n `[Antigravity Debug ${context.id}] Response Headers: ${JSON.stringify(\n maskHeaders(meta.headersOverride ?? response.headers),\n )}`,\n )\n\n if (meta.note) {\n logDebug(`[Antigravity Debug ${context.id}] Note: ${meta.note}`)\n }\n\n if (meta.error) {\n logDebug(\n `[Antigravity Debug ${context.id}] Error: ${formatErrorForLog(meta.error)}`,\n )\n }\n\n if (meta.body) {\n logDebug(\n `[Antigravity Debug ${context.id}] Response Body Preview: ${truncateTextForLog(meta.body, MAX_BODY_PREVIEW_CHARS)}`,\n )\n }\n}\n\n/**\n * Obscures sensitive headers and returns a plain object for logging.\n */\nfunction maskHeaders(headers?: HeadersInit | Headers): Record<string, string> {\n if (!headers) {\n return {}\n }\n\n const result: Record<string, string> = {}\n const SENSITIVE_HEADERS = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'cookie',\n 'set-cookie',\n ])\n const parsed = headers instanceof Headers ? headers : new Headers(headers)\n parsed.forEach((value, key) => {\n if (SENSITIVE_HEADERS.has(key.toLowerCase())) {\n result[key] = '[redacted]'\n } else if (key.toLowerCase() === 'user-agent') {\n // Fingerprint User-Agent strings are stable identifiers that\n // could be replayed. Mask the body but keep the header shape\n // so the log still tells operators a UA was sent.\n result[key] = redactSensitive(value)\n } else {\n result[key] = value\n }\n })\n return redactSensitiveFields(result) as Record<string, string>\n}\n\n/**\n * Writes a single debug line using the configured writer.\n */\nfunction logDebug(line: string): void {\n getDebugState().logWriter(line)\n}\n\nfunction runWithDebugEnabled(action: () => void): void {\n if (!getDebugState().debugEnabled) return\n action()\n}\n\nexport interface AccountDebugInfo {\n index: number\n email?: string\n family: string\n totalAccounts: number\n rateLimitState?: { claude?: number; gemini?: number }\n}\n\nexport function logAccountContext(label: string, info: AccountDebugInfo): void {\n runWithDebugEnabled(() => {\n const accountLabel = formatAccountContextLabel(info.email, info.index)\n\n const indexLabel =\n info.index >= 0\n ? `${info.index + 1}/${info.totalAccounts}`\n : `-/${info.totalAccounts}`\n\n let rateLimitInfo = ''\n if (info.rateLimitState && Object.keys(info.rateLimitState).length > 0) {\n const now = Date.now()\n const activeRateLimits: Record<string, string> = {}\n for (const [key, resetTime] of Object.entries(info.rateLimitState)) {\n if (typeof resetTime === 'number' && resetTime > now) {\n const remainingSec = Math.ceil((resetTime - now) / 1000)\n activeRateLimits[key] = `${remainingSec}s`\n }\n }\n if (Object.keys(activeRateLimits).length > 0) {\n rateLimitInfo = ` rateLimits=${JSON.stringify(activeRateLimits)}`\n }\n }\n\n logDebug(\n `[Account] ${label}: ${accountLabel} (${indexLabel}) family=${info.family}${rateLimitInfo}`,\n )\n })\n}\n\nexport function logRateLimitEvent(\n accountIndex: number,\n email: string | undefined,\n family: string,\n status: number,\n retryAfterMs: number,\n bodyInfo: {\n message?: string\n quotaResetTime?: string\n retryDelayMs?: number | null\n reason?: string\n },\n): void {\n runWithDebugEnabled(() => {\n const accountLabel = formatAccountLabel(email, accountIndex)\n logDebug(\n `[RateLimit] ${status} on ${accountLabel} family=${family} retryAfterMs=${retryAfterMs}`,\n )\n if (bodyInfo.message) {\n logDebug(`[RateLimit] message: ${bodyInfo.message}`)\n }\n if (bodyInfo.quotaResetTime) {\n logDebug(`[RateLimit] quotaResetTime: ${bodyInfo.quotaResetTime}`)\n }\n if (bodyInfo.retryDelayMs !== undefined && bodyInfo.retryDelayMs !== null) {\n logDebug(`[RateLimit] body retryDelayMs: ${bodyInfo.retryDelayMs}`)\n }\n if (bodyInfo.reason) {\n logDebug(`[RateLimit] reason: ${bodyInfo.reason}`)\n }\n })\n}\n\nexport function logRateLimitSnapshot(\n family: string,\n accounts: Array<{\n index: number\n email?: string\n rateLimitResetTimes?: { claude?: number; gemini?: number }\n }>,\n): void {\n runWithDebugEnabled(() => {\n const now = Date.now()\n const entries = accounts.map((account) => {\n const label = formatAccountLabel(account.email, account.index)\n const reset = account.rateLimitResetTimes?.[family as 'claude' | 'gemini']\n if (typeof reset !== 'number') {\n return `${label}=ready`\n }\n const remaining = Math.max(0, reset - now)\n const seconds = Math.ceil(remaining / 1000)\n return `${label}=wait ${seconds}s`\n })\n logDebug(`[RateLimit] snapshot family=${family} ${entries.join(' | ')}`)\n })\n}\n\nexport async function logResponseBody(\n context: AntigravityDebugContext | null | undefined,\n response: Response,\n status: number,\n): Promise<string | undefined> {\n const state = getDebugState()\n if (!state.debugEnabled || !context) return undefined\n\n try {\n const text = await response.clone().text()\n const preview = truncateTextForLog(text, MAX_BODY_LOG_CHARS)\n logDebug(\n `[Antigravity Debug ${context.id}] Response Body (${status}): ${preview}`,\n )\n return text\n } catch (e) {\n logDebug(\n `[Antigravity Debug ${context.id}] Failed to read response body: ${formatErrorForLog(e)}`,\n )\n return undefined\n }\n}\n\nexport function logModelFamily(\n url: string,\n extractedModel: string | null,\n family: string,\n): void {\n runWithDebugEnabled(() => {\n logDebug(\n `[ModelFamily] url=${url} model=${extractedModel ?? 'unknown'} family=${family}`,\n )\n })\n}\n\nexport function debugLogToFile(message: string): void {\n runWithDebugEnabled(() => {\n logDebug(message)\n })\n}\n\n/**\n * Logs a toast message to the debug file.\n * This helps correlate what the user saw with debug events.\n */\nexport function logToast(\n message: string,\n variant: 'info' | 'warning' | 'success' | 'error',\n): void {\n runWithDebugEnabled(() => {\n const variantLabel = variant.toUpperCase()\n logDebug(`[Toast/${variantLabel}] ${message}`)\n })\n}\n\n/**\n * Logs retry attempt information.\n * @param maxAttempts - Use -1 for unlimited retries\n */\nexport function logRetryAttempt(\n attempt: number,\n maxAttempts: number,\n reason: string,\n delayMs?: number,\n): void {\n runWithDebugEnabled(() => {\n const delayInfo = delayMs !== undefined ? ` delay=${delayMs}ms` : ''\n const maxInfo = maxAttempts < 0 ? '\u221E' : maxAttempts.toString()\n logDebug(\n `[Retry] Attempt ${attempt}/${maxInfo} reason=${reason}${delayInfo}`,\n )\n })\n}\n\n/**\n * Logs cache hit/miss information from response usage metadata.\n */\nexport function logCacheStats(\n model: string,\n cacheReadTokens: number,\n cacheWriteTokens: number,\n totalInputTokens: number,\n): void {\n runWithDebugEnabled(() => {\n const cacheHitRate =\n totalInputTokens > 0\n ? Math.round((cacheReadTokens / totalInputTokens) * 100)\n : 0\n const status =\n cacheReadTokens > 0 ? 'HIT' : cacheWriteTokens > 0 ? 'WRITE' : 'MISS'\n logDebug(\n `[Cache] ${status} model=${model} read=${cacheReadTokens} write=${cacheWriteTokens} total=${totalInputTokens} hitRate=${cacheHitRate}%`,\n )\n })\n}\n\n/**\n * Logs quota status for an account.\n */\nexport function logQuotaStatus(\n accountEmail: string | undefined,\n accountIndex: number,\n quotaPercent: number,\n family?: string,\n): void {\n runWithDebugEnabled(() => {\n const accountLabel = formatAccountLabel(accountEmail, accountIndex)\n const familyInfo = family ? ` family=${family}` : ''\n const status =\n quotaPercent <= 0 ? 'EXHAUSTED' : quotaPercent < 20 ? 'LOW' : 'OK'\n logDebug(\n `[Quota] ${accountLabel} remaining=${quotaPercent.toFixed(1)}% status=${status}${familyInfo}`,\n )\n })\n}\n\n/**\n * Logs background quota fetch events.\n */\nexport function logQuotaFetch(\n event: 'start' | 'complete' | 'error',\n accountCount?: number,\n details?: string,\n): void {\n runWithDebugEnabled(() => {\n const countInfo =\n accountCount !== undefined ? ` accounts=${accountCount}` : ''\n const detailsInfo = details ? ` ${details}` : ''\n logDebug(`[QuotaFetch] ${event.toUpperCase()}${countInfo}${detailsInfo}`)\n })\n}\n\n/**\n * Logs which model is being used for a request.\n */\nexport function logModelUsed(\n requestedModel: string,\n actualModel: string,\n accountEmail?: string,\n): void {\n runWithDebugEnabled(() => {\n const accountInfo = accountEmail ? ` account=${accountEmail}` : ''\n if (requestedModel !== actualModel) {\n logDebug(\n `[Model] requested=${requestedModel} actual=${actualModel}${accountInfo}`,\n )\n } else {\n logDebug(`[Model] ${actualModel}${accountInfo}`)\n }\n })\n}\n", "export type LogLevel = 'debug' | 'info' | 'warn' | 'error'\n\nexport interface DebugPolicyInput {\n configDebug: boolean\n configDebugTui: boolean\n envDebugFlag?: string\n envDebugTuiFlag?: string\n}\n\nexport interface DebugPolicy {\n debugLevel: number\n debugEnabled: boolean\n debugTuiEnabled: boolean\n verboseEnabled: boolean\n}\n\nexport function isTruthyFlag(flag?: string): boolean {\n return flag === '1' || flag?.toLowerCase() === 'true'\n}\n\nexport function parseDebugLevel(flag: string): number {\n const trimmed = flag.trim()\n if (trimmed === '2' || trimmed === 'verbose') return 2\n if (trimmed === '1' || trimmed === 'true') return 1\n return 0\n}\n\nexport function deriveDebugPolicy(input: DebugPolicyInput): DebugPolicy {\n const envDebugFlag = input.envDebugFlag ?? ''\n const debugLevel = input.configDebug\n ? envDebugFlag === '2' || envDebugFlag === 'verbose'\n ? 2\n : 1\n : parseDebugLevel(envDebugFlag)\n const debugEnabled = debugLevel >= 1\n const verboseEnabled = debugLevel >= 2\n const debugTuiEnabled =\n debugEnabled &&\n (input.configDebugTui || isTruthyFlag(input.envDebugTuiFlag))\n\n return {\n debugLevel,\n debugEnabled,\n debugTuiEnabled,\n verboseEnabled,\n }\n}\n\nexport function formatAccountLabel(\n email: string | undefined,\n accountIndex: number,\n): string {\n return email || `Account ${accountIndex + 1}`\n}\n\nexport function formatAccountContextLabel(\n email: string | undefined,\n accountIndex: number,\n): string {\n if (email) {\n return email\n }\n if (accountIndex >= 0) {\n return `Account ${accountIndex + 1}`\n }\n return 'All accounts'\n}\n\nexport function formatErrorForLog(error: unknown): string {\n if (error instanceof Error) {\n return error.stack ?? error.message\n }\n try {\n return JSON.stringify(error)\n } catch {\n return String(error)\n }\n}\n\nexport function truncateTextForLog(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text\n }\n return `${text.slice(0, maxChars)}... (truncated ${text.length - maxChars} chars)`\n}\n\nexport function formatBodyPreviewForLog(\n body: BodyInit | null | undefined,\n maxChars: number,\n): string | undefined {\n if (body == null) {\n return undefined\n }\n\n if (typeof body === 'string') {\n return truncateTextForLog(body, maxChars)\n }\n\n if (body instanceof URLSearchParams) {\n return truncateTextForLog(body.toString(), maxChars)\n }\n\n if (typeof Blob !== 'undefined' && body instanceof Blob) {\n return `[Blob size=${body.size}]`\n }\n\n if (typeof FormData !== 'undefined' && body instanceof FormData) {\n return '[FormData payload omitted]'\n }\n\n return `[${body.constructor?.name ?? typeof body} payload omitted]`\n}\n\nexport function writeConsoleLog(level: LogLevel, ...args: unknown[]): void {\n switch (level) {\n case 'debug':\n console.debug(...args)\n break\n case 'info':\n console.info(...args)\n break\n case 'warn':\n console.warn(...args)\n break\n case 'error':\n console.error(...args)\n break\n }\n}\n\n/**\n * Mask all but the first 4 and last 4 characters of `value`. A short\n * value (\u2264 8 chars) is masked entirely so the caller never leaks a\n * full identifier. Empty / non-string inputs collapse to an empty\n * marker so a debug line that ran the helper cannot accidentally\n * surface the original value.\n *\n * Pattern matches the `${start}****${end}` shape used by metrics teams\n * for opaque resource IDs; the implementation is intentionally simple\n * so a quick visual scan of a debug log still tells operators what\n * class of identifier they are looking at.\n */\nexport function redactSensitive(value: string | undefined | null): string {\n if (typeof value !== 'string' || value.length === 0) return ''\n if (value.length <= 8) return '****'\n return `${value.slice(0, 4)}****${value.slice(-4)}`\n}\n\n/**\n * Field-name pattern that flags a key as carrying a credential or\n * identifier. Matched case-insensitively against any JSON-like object\n * key the debug sink walks. `project` (not just `projectId`) so bare\n * `project` / `managedProjectId` request-body fields are masked too \u2014\n * project IDs are account-correlating identifiers.\n */\nconst SENSITIVE_FIELD_PATTERN =\n /token|refresh|access|project|fingerprint|deviceId|sessionId|sessionToken|secret|password|apiKey|clientSecret/i\n\n/**\n * Walk a JSON-like value and redact every credential-shaped field.\n * Returns a NEW value \u2014 the original is never mutated. Strings inside\n * arrays are left untouched; only object keys whose name matches the\n * sensitive pattern have their string values masked.\n */\nexport function redactSensitiveFields(value: unknown): unknown {\n if (value == null) return value\n if (Array.isArray(value)) return value.map(redactSensitiveFields)\n if (typeof value === 'object') {\n const record = value as Record<string, unknown>\n const redacted: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(record)) {\n if (SENSITIVE_FIELD_PATTERN.test(key) && typeof entry === 'string') {\n redacted[key] = redactSensitive(entry)\n } else {\n redacted[key] = redactSensitiveFields(entry)\n }\n }\n return redacted\n }\n return value\n}\n\n/**\n * Redact credential-shaped fields (project IDs, tokens, \u2026) out of a\n * serialized JSON request body. Returns the input untouched when it is\n * not parseable JSON or not an object/array \u2014 unstructured bodies have\n * no field names to key redaction off.\n */\nexport function redactJsonBodyString(body: string): string {\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n return body\n }\n if (parsed == null || typeof parsed !== 'object') return body\n return JSON.stringify(redactSensitiveFields(parsed))\n}\n\n/**\n * Redact a request body before it reaches a debug log or dump file.\n * String bodies are treated as JSON and field-redacted; every other\n * `BodyInit` shape passes through (those are summarized, not printed\n * verbatim, by the log formatters).\n */\nexport function redactBodyForLog(\n body: BodyInit | null | undefined,\n): BodyInit | null | undefined {\n if (typeof body === 'string') return redactJsonBodyString(body)\n return body\n}\n", "/**\n * Structured Logger for Antigravity Plugin\n *\n * Logging behavior:\n * - debug controls file logs only (via debug.ts)\n * - debug_tui controls TUI log panel only\n * - either sink can be enabled independently\n * - OPENCODE_ANTIGRAVITY_CONSOLE_LOG=1 \u2192 console output (independent of debug flags)\n * - operator.log_level filters the level at which log entries are emitted\n */\n\nimport { setLogSink } from '@cortexkit/antigravity-auth-core'\nimport { isDebugTuiEnabled } from './debug'\nimport { isTruthyFlag, writeConsoleLog } from './logging-utils'\nimport type { OperatorSettings } from './operator-settings'\nimport type { PluginClient } from './types'\n\ntype LogLevel = 'debug' | 'info' | 'warn' | 'error'\n\nconst ENV_CONSOLE_LOG = 'OPENCODE_ANTIGRAVITY_CONSOLE_LOG'\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n}\n\nconst LOG_LEVEL_FROM_OPERATOR: Record<OperatorSettings['log_level'], LogLevel> =\n {\n error: 'error',\n warn: 'warn',\n info: 'info',\n debug: 'debug',\n trace: 'debug',\n }\n\nexport interface Logger {\n debug(message: string, extra?: Record<string, unknown>): void\n info(message: string, extra?: Record<string, unknown>): void\n warn(message: string, extra?: Record<string, unknown>): void\n error(message: string, extra?: Record<string, unknown>): void\n}\n\nlet _client: PluginClient | null = null\nlet _configuredLevel: LogLevel = 'debug'\n\nfunction shouldEmit(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[_configuredLevel]\n}\n\n/**\n * Set the runtime log level. Reads from the operator settings controller\n * on each call so a /antigravity-logging dialog flip takes effect\n * immediately. Falls back to \"debug\" when the operator level is not\n * yet known.\n */\nexport function setRuntimeLogLevel(level: OperatorSettings['log_level']): void {\n _configuredLevel = LOG_LEVEL_FROM_OPERATOR[level] ?? 'debug'\n}\n\n/**\n * Initialize the logger with the plugin client.\n * Must be called during plugin initialization to enable TUI logging.\n */\nexport function initLogger(client: PluginClient): void {\n _client = client\n // Route core (@cortexkit/antigravity-auth-core) logs into the same TUI/console\n // sinks the OpenCode logger uses, so logs from migrated core modules surface.\n setLogSink(({ service, level, message, extra }) => {\n emitLog(service, level as LogLevel, message, extra)\n })\n}\n\n/**\n * Create a logger instance for a specific module.\n *\n * @param module - The module name (e.g., \"refresh-queue\", \"transform.claude\")\n * @returns Logger instance with debug, info, warn, error methods\n *\n * @example\n * ```typescript\n * const log = createLogger(\"refresh-queue\");\n * log.debug(\"Checking tokens\", { count: 5 });\n * log.warn(\"Token expired\", { accountIndex: 0 });\n * ```\n */\nfunction emitLog(\n service: string,\n level: LogLevel,\n message: string,\n extra?: Record<string, unknown>,\n): void {\n if (!shouldEmit(level)) return\n\n // TUI logging: controlled only by debug_tui policy\n if (isDebugTuiEnabled()) {\n const app = _client?.app\n if (app && typeof app.log === 'function') {\n app\n .log({\n body: { service, level, message, extra },\n })\n .catch(() => {\n // Silently ignore logging errors\n })\n }\n }\n\n // Console fallback: when env var is set (independent of debug flags)\n if (isConsoleLogEnabled()) {\n const prefix = `[${service}]`\n const args = extra ? [prefix, message, extra] : [prefix, message]\n writeConsoleLog(level, ...args)\n }\n // If neither TUI nor console logging is enabled, log is silently discarded\n}\n\nfunction isConsoleLogEnabled(): boolean {\n return isTruthyFlag(process.env[ENV_CONSOLE_LOG])\n}\n\nexport function createLogger(module: string): Logger {\n const service = `antigravity.${module}`\n\n const log = (\n level: LogLevel,\n message: string,\n extra?: Record<string, unknown>,\n ): void => {\n emitLog(service, level, message, extra)\n }\n\n return {\n debug: (message, extra) => log('debug', message, extra),\n info: (message, extra) => log('info', message, extra),\n warn: (message, extra) => log('warn', message, extra),\n error: (message, extra) => log('error', message, extra),\n }\n}\n", "/**\n * Account pool persistence for OAuth flows.\n *\n * Merges a batch of successful OAuth token-exchange results into the\n * persisted pool. All reads + writes happen inside the core\n * `mutateAccountStorage` callback so the mutator sees the freshest\n * state read while the lock is held \u2014 without it, a concurrent add\n * would race the read-modify-write and silently disappear.\n *\n * Two upsert keys are honored, in priority order:\n * 1. email \u2014 survives refresh-token rotation for the same Google account\n * 2. refresh token \u2014 handles the no-email case and out-of-band rotations\n *\n * Destructive (`replaceAll: true`) writes start from an empty v4 inside\n * the same locked callback so a stale merge cannot resurrect a removed\n * account.\n */\n\nimport type {\n AccountMetadataV3,\n AccountStorageV4,\n} from '@cortexkit/antigravity-auth-core'\nimport { mutateAccountStorage } from '@cortexkit/antigravity-auth-core'\n\nimport type { AntigravityTokenExchangeResult } from '../antigravity/oauth'\nimport { parseRefreshParts } from './auth'\nimport { getStoragePath } from './storage'\n\ntype TokenSuccess = Extract<AntigravityTokenExchangeResult, { type: 'success' }>\n\nfunction clampInt(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) {\n return min\n }\n return Math.min(max, Math.max(min, Math.floor(value)))\n}\n\nfunction applyUpserts(\n current: AccountStorageV4,\n results: TokenSuccess[],\n replaceAll: boolean,\n): AccountStorageV4 | undefined {\n const now = Date.now()\n\n // For fresh logins, start from empty inside the locked callback so\n // a stale merge cannot resurrect a removed account.\n const accounts: AccountMetadataV3[] = replaceAll ? [] : [...current.accounts]\n\n const indexByRefreshToken = new Map<string, number>()\n const indexByEmail = new Map<string, number>()\n for (let i = 0; i < accounts.length; i++) {\n const acc = accounts[i]\n if (!acc) continue\n if (acc.refreshToken) {\n indexByRefreshToken.set(acc.refreshToken, i)\n }\n if (acc.email) {\n indexByEmail.set(acc.email, i)\n }\n }\n\n for (const result of results) {\n const parts = parseRefreshParts(result.refresh)\n if (!parts.refreshToken) {\n continue\n }\n\n // Email match wins over token match \u2014 handles refresh-token rotation\n // for the same Google account.\n const existingByEmail = result.email\n ? indexByEmail.get(result.email)\n : undefined\n const existingByToken = indexByRefreshToken.get(parts.refreshToken)\n const existingIndex = existingByEmail ?? existingByToken\n\n if (existingIndex === undefined) {\n const newIndex = accounts.length\n indexByRefreshToken.set(parts.refreshToken, newIndex)\n if (result.email) {\n indexByEmail.set(result.email, newIndex)\n }\n accounts.push({\n email: result.email,\n label: result.label,\n refreshToken: parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId,\n addedAt: now,\n lastUsed: now,\n enabled: true,\n })\n continue\n }\n\n const existing = accounts[existingIndex]\n if (!existing) continue\n\n const oldToken = existing.refreshToken\n accounts[existingIndex] = {\n ...existing,\n email: result.email ?? existing.email,\n label: result.label ?? existing.label,\n refreshToken: parts.refreshToken,\n projectId: parts.projectId ?? existing.projectId,\n managedProjectId: parts.managedProjectId ?? existing.managedProjectId,\n lastUsed: now,\n }\n\n if (oldToken !== parts.refreshToken) {\n indexByRefreshToken.delete(oldToken)\n indexByRefreshToken.set(parts.refreshToken, existingIndex)\n }\n }\n\n if (accounts.length === 0) {\n return undefined\n }\n\n const activeIndex = replaceAll\n ? 0\n : typeof current.activeIndex === 'number' &&\n Number.isFinite(current.activeIndex)\n ? current.activeIndex\n : 0\n\n const clamped = clampInt(activeIndex, 0, accounts.length - 1)\n return {\n version: 4,\n accounts,\n activeIndex: clamped,\n activeIndexByFamily: {\n claude: clamped,\n gemini: clamped,\n },\n }\n}\n\n/**\n * Merge a batch of successful OAuth results into the persisted pool.\n *\n * - `replaceAll: true` \u2014 start from empty (fresh login)\n * - `replaceAll: false` \u2014 preserve existing accounts, upsert by email\n * then refresh token, bump `lastUsed`\n *\n * Both branches run their mutator INSIDE the locked callback. The\n * `replaceAll` branch seeds the mutator from an empty v4 rather than\n * reading the disk state, but the file lock is still required so the\n * write is atomic against concurrent writers \u2014 a deleted-account merge\n * would resurrect a stale account if we wrote without the lock.\n */\nexport async function persistAccountPool(\n results: TokenSuccess[],\n replaceAll: boolean = false,\n): Promise<void> {\n if (results.length === 0) {\n return\n }\n\n const path = getStoragePath()\n const emptyV4 = (): AccountStorageV4 => ({\n version: 4,\n accounts: [],\n activeIndex: 0,\n })\n\n if (replaceAll) {\n await mutateAccountStorage(path, () =>\n applyUpserts(emptyV4(), results, true),\n )\n return\n }\n\n await mutateAccountStorage(path, (current) =>\n applyUpserts(current, results, false),\n )\n}\n", "import os from 'os';\nimport path from 'path';\n\nconst homeDirectory = os.homedir();\nconst {env} = process;\n\nexport const xdgData = env.XDG_DATA_HOME ||\n\t(homeDirectory ? path.join(homeDirectory, '.local', 'share') : undefined);\n\nexport const xdgConfig = env.XDG_CONFIG_HOME ||\n\t(homeDirectory ? path.join(homeDirectory, '.config') : undefined);\n\nexport const xdgState = env.XDG_STATE_HOME ||\n\t(homeDirectory ? path.join(homeDirectory, '.local', 'state') : undefined);\n\nexport const xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, '.cache') : undefined);\n\nexport const xdgRuntime = env.XDG_RUNTIME_DIR || undefined;\n\nexport const xdgDataDirectories = (env.XDG_DATA_DIRS || '/usr/local/share/:/usr/share/').split(':');\n\nif (xdgData) {\n\txdgDataDirectories.unshift(xdgData);\n}\n\nexport const xdgConfigDirectories = (env.XDG_CONFIG_DIRS || '/etc/xdg').split(':');\n\nif (xdgConfig) {\n\txdgConfigDirectories.unshift(xdgConfig);\n}\n", "/**\n * Sidebar state contract for the OpenTUI sidebar.\n *\n * This module is the read-only seam between the long-running plugin and the\n * Solid/OpenTUI sidebar tree. It deliberately does NOT import account storage,\n * the account manager, OAuth code, or any other privileged host-side module:\n * the TUI is rendered inside the host's terminal and a single stray import\n * could leak credentials or pull a heavy manager into the render path.\n *\n * The plugin writes a redacted snapshot to the file resolved by\n * `getSidebarStateFile()` and the TUI polls it. The contract version is `1`:\n * any future field that the TUI cannot understand must be ignored, and any\n * broken/missing file must collapse to `DEFAULT_SIDEBAR_STATE`.\n *\n * ## Writer surface\n *\n * The plugin-side writers live here too so the read and write halves of the\n * contract evolve together. They are imported by the plugin (auth-loader,\n * quota, fetch-interceptor, event-handler, commands) but never invoked from\n * the TUI's compiled tree \u2014 that tree only calls the readers, so the\n * heavyweight core imports below never run inside the host's render path.\n *\n * Every disk mutation follows the same recipe:\n *\n * 1. Serialize through `sidebarWriteChain` so concurrent in-process calls\n * never interleave merges against the same file.\n * 2. Acquire Task 7's `acquireFencedFileLock` with bounded retry+jitter\n * (\u22642s). A live cross-process holder that does not release in time\n * surfaces as `SidebarStateLockContentionError`.\n * 3. Re-read and normalize the on-disk state while holding the lock.\n * 4. Merge the new machine or routing payload against the re-read state.\n * 5. `assertOwned()` + `writeJsonAtomic` with mode 0o600, then release.\n *\n * The merge step is deterministic: machine fields adopt only when the new\n * `checkedAt` is \u2265 the on-disk one, `routingAuthoritative` is sticky-true,\n * and `activeRouting` is merged independently and pruned to the freshest\n * 100 entries within 24h.\n */\n\nimport { mkdirSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n// Subpath imports (not the barrel): this file ships into the TUI's\n// compiled tree, and the barrel re-exports account/OAuth/quota modules\n// that must never execute inside the host's render path.\nimport { writeJsonAtomic } from '@cortexkit/antigravity-auth-core/atomic-write'\nimport {\n acquireFencedFileLock,\n type FencedFileLock,\n} from '@cortexkit/antigravity-auth-core/file-lock'\nimport { xdgState } from 'xdg-basedir'\n\nexport const SIDEBAR_STATE_VERSION = 1 as const\n\nexport type SidebarQuotaKey = 'gemini' | 'non-gemini'\n\nexport interface SidebarQuotaWindowEntry {\n window: 'weekly' | '5h'\n remainingPercent: number\n resetAt?: number\n}\n\nexport interface SidebarQuotaEntry {\n /** Most-constrained window's remaining % (derived, for back-compat). */\n remainingPercent: number\n resetAt?: number\n /** Per-window breakdown. Omitted in pre-windows snapshots. */\n windows?: SidebarQuotaWindowEntry[]\n}\n\nexport interface SidebarAccountState {\n id: string\n label: string\n enabled: boolean\n health: number\n current: boolean\n cooldownUntil?: number\n quota: Partial<Record<SidebarQuotaKey, SidebarQuotaEntry>>\n /**\n * Captured plan tier. Absent when unknown \u2014 do NOT default to `free-tier`.\n * `id` is the raw upstream string (e.g. `\"free-tier\"`); never normalised.\n */\n tier?: { id: string; paidId?: string; capturedAt: number }\n}\n\nexport interface SidebarRoutingEntry {\n accountId: string\n modelFamily: 'claude' | 'gemini'\n headerStyle: 'antigravity' | 'gemini-cli'\n strategy?: 'sticky' | 'round-robin' | 'hybrid'\n updatedAt: number\n}\n\nexport interface SidebarStateV1 {\n version: typeof SIDEBAR_STATE_VERSION\n checkedAt: number\n accounts: SidebarAccountState[]\n activeRouting: Record<string, SidebarRoutingEntry>\n routingAuthoritative: boolean\n quotaBackoffUntil?: number\n lastError?: string\n}\n\n/**\n * The subset of `SidebarStateV1` that a non-routing writer may set. The\n * fetch interceptor writes `activeRouting` directly via its own entry point\n * because routing is session-scoped, not machine-scoped.\n */\nexport interface SidebarMachineState {\n checkedAt: number\n accounts: SidebarAccountState[]\n quotaBackoffUntil?: number\n lastError?: string\n /**\n * Optional: opt in to mark the snapshot as authoritative. When `true`,\n * the merge keeps the existing `routingAuthoritative: true` even if a\n * later non-authoritative machine write lands. Sticky-true semantics.\n */\n routingAuthoritative?: boolean\n}\n\nexport const DEFAULT_SIDEBAR_STATE: SidebarStateV1 = {\n version: SIDEBAR_STATE_VERSION,\n checkedAt: 0,\n accounts: [],\n activeRouting: {},\n routingAuthoritative: false,\n}\n\nexport const SIDEBAR_STATE_ENV = 'ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE'\n\nconst SIDEBAR_STATE_DIR = 'cortexkit/antigravity-auth'\nconst SIDEBAR_STATE_FILENAME = 'sidebar-state.json'\n\n/** Active routing entries older than this are dropped on every merge. */\nconst ACTIVE_ROUTING_MAX_AGE_MS = 24 * 60 * 60 * 1000\n/** Active routing map is capped at this many newest entries. */\nconst ACTIVE_ROUTING_MAX_ENTRIES = 100\n\nconst SIDEBAR_LOCK_NAME = 'sidebar'\nconst SIDEBAR_LOCK_TTL_MS = 10_000\nconst SIDEBAR_LOCK_TIMEOUT_MS = 2_000\nconst SIDEBAR_LOCK_RETRY_BASE_MS = 25\nconst SIDEBAR_LOCK_RETRY_CAP_MS = 75\nconst SIDEBAR_LOCK_JITTER_MS = 25\nconst SIDEBAR_STATE_DIR_MODE = 0o700\nconst SIDEBAR_STATE_FILE_MODE = 0o600\n\n/**\n * Thrown by every writer when the cross-process lock cannot be acquired\n * within `SIDEBAR_LOCK_TIMEOUT_MS`. The caller decides whether to surface\n * a toast, drop the write, or retry the next tick.\n */\nexport class SidebarStateLockContentionError extends Error {\n readonly details: { stateFile: string; timeoutMs: number }\n\n constructor(stateFile: string, timeoutMs: number) {\n super(\n `Could not acquire sidebar-state lock at ${stateFile} within ${timeoutMs}ms`,\n )\n this.name = 'SidebarStateLockContentionError'\n this.details = { stateFile, timeoutMs }\n }\n}\n\n/**\n * Steps exposed to the merge hooks. Race tests pause writers here; production\n * callers leave the hooks unset (a no-op fast path).\n *\n * - `await-lock` \u2014 before invoking `acquireFencedFileLock`.\n * - `acquired-lock` \u2014 after the lock is granted and before the read.\n * - `read-state` \u2014 after the on-disk state is normalized.\n * - `merged-state` \u2014 after the merge but before `writeJsonAtomic`.\n * - `wrote-state` \u2014 after the rename but before `lock.release()`.\n */\nexport type SidebarMergeStep =\n | 'await-lock'\n | 'acquired-lock'\n | 'read-state'\n | 'merged-state'\n | 'wrote-state'\n\nexport interface SidebarMergeHooks {\n onStep?: (step: SidebarMergeStep) => Promise<void> | void\n}\n\nlet sidebarMergeHooks: SidebarMergeHooks | null = null\n\n/**\n * Install (or clear with `null`) the deterministic race hooks. Tests use\n * these to inject interleavings; production callers leave them unset. The\n * module-level state means a test must reset hooks in its own `afterEach`\n * to avoid bleeding into the next test.\n */\nexport function setSidebarMergeHooks(hooks: SidebarMergeHooks | null): void {\n sidebarMergeHooks = hooks\n}\n\nasync function emitMergeStep(step: SidebarMergeStep): Promise<void> {\n await sidebarMergeHooks?.onStep?.(step)\n}\n\n/**\n * Resolve the on-disk path the plugin writes to and the TUI reads from.\n *\n * - `ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE` wins when set (tests, packaged\n * installers, and any user override).\n * - Otherwise fall back to the XDG state directory, mirroring the path\n * conventions used elsewhere in the project.\n */\nexport function getSidebarStateFile(): string {\n const override = process.env[SIDEBAR_STATE_ENV]\n if (override && override.trim().length > 0) return override\n const base = xdgState ?? join(homedir(), '.local', 'state')\n return join(base, SIDEBAR_STATE_DIR, SIDEBAR_STATE_FILENAME)\n}\n\n/**\n * Read and normalize the sidebar state file. Returns the default state when\n * the file is missing, unreadable, malformed, or schema-incompatible \u2014 the TUI\n * must never throw out of `readSidebarState()`, the panel just shows\n * \"Awaiting Antigravity state\" and the next poll retries.\n *\n * The read is sync on purpose: the TUI polls on a 2-second timer and the file\n * is tiny (a handful of accounts); an async read here would just add race\n * surface area against Solid's reactive render cycle.\n */\nexport function readSidebarState(\n path: string = getSidebarStateFile(),\n): SidebarStateV1 {\n let raw: string\n try {\n raw = readFileSync(path, 'utf-8')\n } catch {\n return { ...DEFAULT_SIDEBAR_STATE }\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { ...DEFAULT_SIDEBAR_STATE, lastError: 'malformed-json' }\n }\n return normalizeSidebarState(parsed)\n}\n\nfunction normalizeSidebarState(input: unknown): SidebarStateV1 {\n if (!isObject(input)) {\n return { ...DEFAULT_SIDEBAR_STATE, lastError: 'shape' }\n }\n const record = input as Record<string, unknown>\n const version = record.version\n if (version !== SIDEBAR_STATE_VERSION) {\n return {\n ...DEFAULT_SIDEBAR_STATE,\n lastError: `unsupported-version:${stringifySafe(version)}`,\n }\n }\n\n const accountsRaw = record.accounts\n const accounts = Array.isArray(accountsRaw)\n ? accountsRaw\n .map((entry) => normalizeAccount(entry))\n .filter((entry): entry is SidebarAccountState => entry !== null)\n : []\n\n const routingRaw = record.activeRouting\n const activeRouting: Record<string, SidebarRoutingEntry> = {}\n if (isObject(routingRaw)) {\n for (const [sessionId, entry] of Object.entries(\n routingRaw as Record<string, unknown>,\n )) {\n const normalized = normalizeRouting(entry)\n if (normalized) activeRouting[sessionId] = normalized\n }\n }\n\n const checkedAt = toFiniteNumber(record.checkedAt)\n const routingAuthoritative = record.routingAuthoritative === true\n const quotaBackoffUntil = toFiniteNumber(record.quotaBackoffUntil)\n const lastError =\n typeof record.lastError === 'string' ? record.lastError : undefined\n\n return {\n version: SIDEBAR_STATE_VERSION,\n checkedAt: checkedAt ?? 0,\n accounts,\n activeRouting,\n routingAuthoritative,\n quotaBackoffUntil: quotaBackoffUntil ?? undefined,\n lastError,\n }\n}\n\nfunction normalizeAccount(input: unknown): SidebarAccountState | null {\n if (!isObject(input)) return null\n const record = input as Record<string, unknown>\n const id = typeof record.id === 'string' ? record.id : null\n const label = typeof record.label === 'string' ? record.label : null\n if (!id || !label) return null\n const enabled = record.enabled !== false\n const health = clampNumber(toFiniteNumber(record.health), 0, 100)\n const current = record.current === true\n const cooldownUntil = toFiniteNumber(record.cooldownUntil) ?? undefined\n const quotaRaw = record.quota\n const quota: SidebarAccountState['quota'] = {}\n if (isObject(quotaRaw)) {\n for (const key of ['gemini', 'non-gemini'] as const) {\n const entry = (quotaRaw as Record<string, unknown>)[key]\n const normalized = normalizeQuota(entry)\n if (normalized) quota[key] = normalized\n }\n }\n const tier = normalizeTier(record.tier)\n return {\n id,\n label,\n enabled,\n health,\n current,\n cooldownUntil,\n quota,\n ...(tier !== undefined ? { tier } : {}),\n }\n}\n\nfunction normalizeTier(\n input: unknown,\n): { id: string; paidId?: string; capturedAt: number } | undefined {\n if (!isObject(input)) return undefined\n const record = input as Record<string, unknown>\n const id =\n typeof record.id === 'string' && record.id.length > 0 ? record.id : null\n const capturedAt = toFiniteNumber(record.capturedAt)\n if (!id || capturedAt === null) return undefined\n const paidId =\n typeof record.paidId === 'string' && record.paidId.length > 0\n ? record.paidId\n : undefined\n return { id, ...(paidId ? { paidId } : {}), capturedAt }\n}\n\nfunction normalizeQuota(input: unknown): SidebarQuotaEntry | null {\n if (!isObject(input)) return null\n const record = input as Record<string, unknown>\n const remaining = toFiniteNumber(record.remainingPercent)\n if (remaining === null) return null\n const resetAt = toFiniteNumber(record.resetAt) ?? undefined\n\n // Tolerant: deserialize windows array if present; drop malformed entries.\n const windowsRaw = record.windows\n let windows: SidebarQuotaEntry['windows']\n if (Array.isArray(windowsRaw)) {\n const parsed = windowsRaw\n .filter(isObject)\n .map((w) => {\n const win = (w as Record<string, unknown>).window\n const rp = toFiniteNumber(\n (w as Record<string, unknown>).remainingPercent,\n )\n const ra = toFiniteNumber((w as Record<string, unknown>).resetAt)\n if ((win !== 'weekly' && win !== '5h') || rp === null) return null\n return {\n window: win as 'weekly' | '5h',\n remainingPercent: clampNumber(rp, 0, 100),\n resetAt: ra ?? undefined,\n }\n })\n .filter((v): v is NonNullable<typeof v> => v !== null)\n if (parsed.length > 0) windows = parsed\n }\n\n return {\n remainingPercent: clampNumber(remaining, 0, 100),\n resetAt,\n windows,\n }\n}\n\nfunction normalizeRouting(input: unknown): SidebarRoutingEntry | null {\n if (!isObject(input)) return null\n const record = input as Record<string, unknown>\n const accountId =\n typeof record.accountId === 'string' ? record.accountId : null\n const modelFamily = record.modelFamily\n const headerStyle = record.headerStyle\n const strategy = record.strategy\n const updatedAt = toFiniteNumber(record.updatedAt) ?? 0\n if (\n !accountId ||\n (modelFamily !== 'claude' && modelFamily !== 'gemini') ||\n (headerStyle !== 'antigravity' && headerStyle !== 'gemini-cli')\n ) {\n return null\n }\n return {\n accountId,\n modelFamily,\n headerStyle,\n strategy:\n strategy === 'sticky' ||\n strategy === 'round-robin' ||\n strategy === 'hybrid'\n ? strategy\n : undefined,\n updatedAt,\n }\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction toFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction clampNumber(value: number | null, min: number, max: number): number {\n if (value === null) return min\n if (value < min) return min\n if (value > max) return max\n return value\n}\n\nfunction stringifySafe(value: unknown): string {\n if (typeof value === 'string') return value\n try {\n return JSON.stringify(value) ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\n/**\n * Ensure the parent directory for the sidebar state file exists. Convenience\n * helper used by writers (tests, plugins) \u2014 the TUI itself does not write.\n */\nexport function ensureSidebarStateDir(\n path: string = getSidebarStateFile(),\n): void {\n mkdirSync(dirname(path), { recursive: true, mode: SIDEBAR_STATE_DIR_MODE })\n}\n\n/**\n * Drop active-routing entries older than 24h and cap the map at the freshest\n * 100. Pure helper exposed for unit testing; the writers call it on every\n * merge so the on-disk map never grows without bound.\n */\nexport function pruneActiveRouting(\n map: Record<string, SidebarRoutingEntry>,\n now: number,\n): Record<string, SidebarRoutingEntry> {\n const cutoff = now - ACTIVE_ROUTING_MAX_AGE_MS\n const filtered: Array<[string, SidebarRoutingEntry]> = []\n for (const [sessionId, entry] of Object.entries(map)) {\n if (entry.updatedAt >= cutoff) {\n filtered.push([sessionId, entry])\n }\n }\n filtered.sort((a, b) => b[1].updatedAt - a[1].updatedAt)\n if (filtered.length <= ACTIVE_ROUTING_MAX_ENTRIES) {\n return Object.fromEntries(filtered)\n }\n return Object.fromEntries(filtered.slice(0, ACTIVE_ROUTING_MAX_ENTRIES))\n}\n\n/**\n * Structural input for `redactAccountForSidebar`. Decoupled from the core\n * `ManagedAccount` type so this module never forces a type-level import\n * shape on callers (and so the TUI's compiled tree does not see the core\n * ManagedAccount shape beyond what's actually used).\n *\n * Deliberately excludes `email`: the sidebar/redaction boundary is a PII\n * firewall. Adding `email` here would re-introduce the leak this boundary\n * exists to prevent. Legacy profile labels are accepted but never serialized.\n */\nexport interface SidebarAccountRedactionInput {\n /** Position in the harness-visible account array. */\n index: number\n /** Legacy profile label; deliberately ignored by the redactor. */\n label?: string\n enabled?: boolean\n current?: boolean\n coolingDownUntil?: number\n /** Health score in `[0, 100]`. Defaults to 100 when missing. */\n healthScore?: number\n cachedQuota?: {\n gemini?: {\n remainingFraction?: number\n resetTime?: string\n windows?: Array<{\n window: 'weekly' | '5h'\n remainingFraction: number\n resetTime: string\n }>\n }\n 'non-gemini'?: {\n remainingFraction?: number\n resetTime?: string\n windows?: Array<{\n window: 'weekly' | '5h'\n remainingFraction: number\n resetTime: string\n }>\n }\n // Legacy per-pool keys written by older versions of the plugin.\n // Accepted here so the normalizer can map them to the current pool\n // keys at read time without requiring a disk migration.\n [legacyKey: string]:\n | {\n remainingFraction?: number\n resetTime?: string\n modelCount?: number\n windows?: Array<{\n window: 'weekly' | '5h'\n remainingFraction: number\n resetTime: string\n }>\n }\n | undefined\n }\n /**\n * Opaque identity stamp that was attached to the persisted quota snapshot.\n * Used together with `currentQuotaAccountId` to detect a stale cache that\n * landed on the wrong account after an index shift or token replacement.\n * PII-safe \u2014 it is a 16-char hash, not the refresh token itself.\n */\n cachedQuotaAccountId?: string\n /**\n * Opaque identity stamp for the account that is currently at this index.\n * The redactor drops `cachedQuota` when `cachedQuotaAccountId` is set\n * AND does not match this value, mirroring `toCommandAccountRow` in the\n * command-data service. Omitted in the persisted sidebar state.\n */\n currentQuotaAccountId?: string\n /**\n * Captured plan tier from `loadCodeAssist`. Absent when unknown. The `id`\n * is the raw upstream string; `capturedAt` is epoch ms. Not PII \u2014 tier\n * metadata survives the redaction boundary unchanged.\n */\n tier?: { id: string; paidId?: string; capturedAt: number }\n}\n\n/**\n * Build a `{ id, capturedAt }` tier object from account fields, or `undefined`\n * when either field is absent. Centralised so every producer (index.ts,\n * background-quota-refresh.ts, command-data.ts) uses the same guard and the\n * same shape -- the same pattern shipped as inline literals across three files\n * and produced six missed-field bugs; one helper ends that.\n */\nexport function toCapturedTier(account: {\n capturedTierId?: string\n capturedPaidTierId?: string\n capturedTierAt?: number\n}): { id: string; paidId?: string; capturedAt: number } | undefined {\n return account.capturedTierId !== undefined &&\n account.capturedTierAt !== undefined\n ? {\n id: account.capturedTierId,\n ...(account.capturedPaidTierId !== undefined\n ? { paidId: account.capturedPaidTierId }\n : {}),\n capturedAt: account.capturedTierAt,\n }\n : undefined\n}\n\n/**\n * Map pre-pool legacy quota keys to the current two-pool schema at read time.\n *\n * Legacy mappings (empirically settled from burn tests):\n * `gemini-pro` + `gemini-flash` \u2192 `gemini`\n * `claude` + `gpt-oss` \u2192 `non-gemini`\n *\n * Where multiple legacy keys collapse into one pool the MIN remainingFraction\n * and earliest resetTime are used (most-constrained-first, matching the\n * window-level rule). No `windows` arrays are invented for migrated entries;\n * the single aggregate bar is the correct render for pre-window snapshots.\n *\n * Non-legacy keys that are already canonical (`gemini`, `non-gemini`) are\n * carried through unchanged. The next real quota refresh overwrites any\n * migrated snapshot with authoritative data.\n */\nexport function normalizeLegacyCachedQuota(\n raw: SidebarAccountRedactionInput['cachedQuota'],\n): SidebarAccountRedactionInput['cachedQuota'] {\n if (!raw) return raw\n\n // Pool already carries current keys \u2014 fast path when no legacy keys present.\n const hasLegacy =\n 'gemini-pro' in raw ||\n 'gemini-flash' in raw ||\n 'claude' in raw ||\n 'gpt-oss' in raw\n if (!hasLegacy) return raw\n\n type PoolEntry = NonNullable<\n SidebarAccountRedactionInput['cachedQuota']\n >['gemini']\n\n // Pick the earlier of two ISO reset timestamps, preferring a defined\n // value over undefined. Extracted so both branches of minFraction use\n // the same merge rather than the previous asymmetry where the a-wins\n // branch dropped b's possibly-earlier resetTime.\n const earlierResetTime = (\n a: string | undefined,\n b: string | undefined,\n ): string | undefined => {\n if (!a) return b\n if (!b) return a\n return a < b ? a : b\n }\n\n const minFraction = (a: PoolEntry, b: PoolEntry): PoolEntry => {\n if (!a) return b\n if (!b) return a\n const fa = a.remainingFraction ?? 1\n const fb = b.remainingFraction ?? 1\n const winner = fa <= fb ? a : b\n const loser = fa <= fb ? b : a\n // Always merge to the earliest resetTime regardless of which pool wins\n // the fraction comparison \u2014 an earlier window expiry from the loser\n // pool must not be silently dropped.\n return {\n ...winner,\n resetTime: earlierResetTime(winner.resetTime, loser.resetTime),\n }\n }\n\n const gemini: PoolEntry =\n minFraction(\n raw.gemini,\n minFraction(\n raw['gemini-pro'] as PoolEntry,\n raw['gemini-flash'] as PoolEntry,\n ),\n ) ?? raw.gemini\n\n const nonGemini: PoolEntry =\n minFraction(\n raw['non-gemini'],\n minFraction(raw.claude as PoolEntry, raw['gpt-oss'] as PoolEntry),\n ) ?? raw['non-gemini']\n\n return {\n ...(gemini !== undefined ? { gemini } : {}),\n ...(nonGemini !== undefined ? { 'non-gemini': nonGemini } : {}),\n }\n}\n\n/**\n * Project a raw cachedQuota pool entry into the sidebar-safe shape.\n *\n * Centralized seam \u2014 every producer (quota.ts pushSidebarQuotaSnapshot,\n * auth-loader.ts materializer, command-data.ts writeSidebar) routes\n * through here so the `windows` array is never dropped by an inline\n * `{ remainingFraction, resetTime }` literal.\n *\n * Tolerant: returns `undefined` when the source is missing or the\n * `remainingFraction` is not a finite number. Legacy entries without\n * `windows` produce `{ remainingPercent, resetAt }` only \u2014 the TUI's\n * legacy path then renders a single bar.\n */\nexport function projectQuotaPoolForSidebar(source: {\n remainingFraction?: number\n resetTime?: string\n windows?: ReadonlyArray<{\n window: 'weekly' | '5h'\n remainingFraction: number\n resetTime: string\n }>\n}): SidebarQuotaEntry | undefined {\n const fraction = source.remainingFraction\n if (typeof fraction !== 'number' || !Number.isFinite(fraction))\n return undefined\n const remainingPercent = clampNumber(Math.round(fraction * 100), 0, 100)\n let resetAt: number | undefined\n if (typeof source.resetTime === 'string' && source.resetTime.length > 0) {\n const parsed = Date.parse(source.resetTime)\n if (Number.isFinite(parsed)) resetAt = parsed\n }\n\n const windows: SidebarQuotaEntry['windows'] = source.windows?.length\n ? source.windows.map((w) => ({\n window: w.window,\n remainingPercent: clampNumber(\n Math.round(w.remainingFraction * 100),\n 0,\n 100,\n ),\n resetAt:\n typeof w.resetTime === 'string' && w.resetTime.length > 0\n ? (() => {\n const parsed = Date.parse(w.resetTime)\n return Number.isFinite(parsed) ? parsed : undefined\n })()\n : undefined,\n }))\n : undefined\n\n return { remainingPercent, resetAt, windows }\n}\n\n/**\n * Returns `true` when an account at `index` is the active account for\n * at least one model family. Two accounts CAN both be current \u2014 one\n * serving claude, one serving gemini \u2014 so this must not collapse to one.\n */\nexport function isAccountCurrent(\n index: number,\n activeIndexByFamily: { claude: number; gemini: number },\n): boolean {\n return (\n index === activeIndexByFamily.claude || index === activeIndexByFamily.gemini\n )\n}\n\n/**\n * Convert a live account snapshot into the redacted shape the TUI renders.\n * The redacted `SidebarAccountState` carries no email, refresh token, access\n * token, project ID, fingerprint, OAuth profile name, or other personal or\n * credential-shaped fields. Display labels are generated ordinal account names.\n */\nexport function redactAccountForSidebar(\n source: SidebarAccountRedactionInput,\n): SidebarAccountState {\n const id = `acct-${source.index}`\n const label = `Account ${source.index + 1}`\n const enabled = source.enabled !== false\n const current = source.current === true\n const cooldownUntil =\n typeof source.coolingDownUntil === 'number' &&\n Number.isFinite(source.coolingDownUntil)\n ? source.coolingDownUntil\n : undefined\n const health = clampNumber(\n typeof source.healthScore === 'number' ? source.healthScore : 100,\n 0,\n 100,\n )\n\n const quota: SidebarAccountState['quota'] = {}\n // Stamp mismatch: the persisted quota snapshot was captured for a\n // different account than the one currently at this index. Drop the\n // stale cache rather than rendering the wrong account's quota\n // percentages. Mirrors `toCommandAccountRow` in command-data.\n const staleCachedQuota =\n typeof source.cachedQuotaAccountId === 'string' &&\n typeof source.currentQuotaAccountId === 'string' &&\n source.cachedQuotaAccountId !== source.currentQuotaAccountId\n // Normalize legacy per-pool keys to the current two-pool schema before\n // reading. This is non-destructive: the next real quota refresh will\n // overwrite the migrated snapshot with authoritative data.\n const cached = staleCachedQuota\n ? undefined\n : normalizeLegacyCachedQuota(source.cachedQuota)\n if (cached) {\n for (const key of ['gemini', 'non-gemini'] as const) {\n const entry = cached[key]\n if (!entry) continue\n const projected = projectQuotaPoolForSidebar(entry)\n if (projected) quota[key] = projected\n }\n }\n\n return {\n id,\n label,\n enabled,\n health,\n current,\n cooldownUntil,\n quota,\n // Tier is plan metadata, not PII \u2014 it passes the redaction boundary.\n ...(source.tier !== undefined ? { tier: source.tier } : {}),\n }\n}\n\n/**\n * Build a `SidebarMachineState` from a list of live account snapshots.\n * Convenience for the auth-loader / quota writer call sites that already\n * hold an array of accounts and want to push a single snapshot.\n */\nexport function buildSidebarMachineStateFromAccounts(\n accounts: SidebarAccountRedactionInput[],\n options: {\n checkedAt?: number\n quotaBackoffUntil?: number\n lastError?: string\n routingAuthoritative?: boolean\n } = {},\n): SidebarMachineState {\n return {\n checkedAt: options.checkedAt ?? Date.now(),\n accounts: accounts.map((entry) => redactAccountForSidebar(entry)),\n quotaBackoffUntil: options.quotaBackoffUntil,\n lastError: options.lastError,\n routingAuthoritative: options.routingAuthoritative,\n }\n}\n\ninterface SidebarStateWriteOptions {\n stateFile?: string\n}\n\nlet sidebarWriteChain: Promise<void> = Promise.resolve()\n\nfunction enqueueSidebarWrite<T>(work: () => Promise<T>): Promise<T> {\n // Always run `work` regardless of whether the prior link resolved or\n // rejected \u2014 a failed write must not poison the chain for the next caller.\n const next = sidebarWriteChain.then(work, work)\n sidebarWriteChain = next.then(\n () => undefined,\n () => undefined,\n )\n return next\n}\n\n/**\n * Wait for any in-flight sidebar state write to drain. The plugin lifecycle\n * calls this during `dispose()` so the file logger and RPC server are torn\n * down only after every queued write has either landed or thrown.\n */\nexport function drainSidebarWrites(): Promise<void> {\n return sidebarWriteChain\n}\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nasync function acquireSidebarLockWithRetry(\n stateFile: string,\n): Promise<FencedFileLock> {\n const start = Date.now()\n let attempt = 0\n while (true) {\n await emitMergeStep('await-lock')\n const lock = await acquireFencedFileLock({\n path: stateFile,\n name: SIDEBAR_LOCK_NAME,\n ttlMs: SIDEBAR_LOCK_TTL_MS,\n renew: true,\n })\n if (lock) {\n await emitMergeStep('acquired-lock')\n return lock\n }\n const elapsed = Date.now() - start\n if (elapsed >= SIDEBAR_LOCK_TIMEOUT_MS) {\n throw new SidebarStateLockContentionError(\n stateFile,\n SIDEBAR_LOCK_TIMEOUT_MS,\n )\n }\n const backoff = Math.min(\n SIDEBAR_LOCK_RETRY_CAP_MS,\n SIDEBAR_LOCK_RETRY_BASE_MS + attempt * 10,\n )\n const jitter = Math.floor(Math.random() * SIDEBAR_LOCK_JITTER_MS)\n await sleep(backoff + jitter)\n attempt++\n }\n}\n\nasync function performSidebarWrite(\n stateFile: string,\n merge: (existing: SidebarStateV1) => SidebarStateV1,\n): Promise<void> {\n await enqueueSidebarWrite(async () => {\n ensureSidebarStateDir(stateFile)\n const lock = await acquireSidebarLockWithRetry(stateFile)\n try {\n await lock.assertOwned()\n const existing = readSidebarState(stateFile)\n await emitMergeStep('read-state')\n const merged = merge(existing)\n await emitMergeStep('merged-state')\n await writeJsonAtomic(stateFile, merged)\n // `writeJsonAtomic` stages a tmp file with mode 0o600 and renames onto\n // the target; POSIX rename replaces the inode so the new file inherits\n // the staged mode bits. Windows ignores POSIX modes so the assertion\n // in tests is best-effort there.\n await emitMergeStep('wrote-state')\n } finally {\n await lock.release().catch(() => {})\n }\n })\n}\n\n/**\n * Merge a new machine-state payload against the on-disk state.\n *\n * Stale writes (new `checkedAt` < existing) are dropped \u2014 only newer/equal\n * `checkedAt` may replace machine fields. The merge preserves\n * `routingAuthoritative: true` once it is true (sticky-true), keeps the\n * existing `activeRouting` intact (routing is merged via its own writer),\n * and prunes any expired routing entries as a side effect.\n */\nfunction mergeMachineState(\n existing: SidebarStateV1,\n next: SidebarMachineState,\n): SidebarStateV1 {\n if (next.checkedAt < existing.checkedAt) {\n return {\n ...existing,\n activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),\n }\n }\n return {\n version: SIDEBAR_STATE_VERSION,\n checkedAt: next.checkedAt,\n accounts: next.accounts,\n quotaBackoffUntil: next.quotaBackoffUntil,\n lastError: next.lastError,\n routingAuthoritative:\n existing.routingAuthoritative === true ||\n next.routingAuthoritative === true,\n // Symmetric with the stale-write branch: every machine-write merge\n // re-prunes activeRouting so a long-running TUI session eventually\n // drops dead routes even when no fresh routing upsert lands. Cheap\n // (a single Object.entries + sort over \u2264100 entries) and bounded.\n activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),\n }\n}\n\n/**\n * Upsert a single session's active routing entry. The fetch interceptor calls\n * this with `authoritative: true` after every final route selection; the\n * `accountId`/`modelFamily`/`headerStyle` fields are already redacted by the\n * caller (the writer never sees token or project fields).\n */\nexport async function upsertSidebarActiveRouting(\n sessionId: string,\n entry: SidebarRoutingEntry,\n options: SidebarStateWriteOptions & { authoritative?: boolean } = {},\n): Promise<void> {\n const stateFile = options.stateFile ?? getSidebarStateFile()\n await performSidebarWrite(stateFile, (existing) => {\n const activeRouting = { ...existing.activeRouting, [sessionId]: entry }\n return {\n ...existing,\n routingAuthoritative:\n options.authoritative === true ? true : existing.routingAuthoritative,\n activeRouting: pruneActiveRouting(activeRouting, Date.now()),\n }\n })\n}\n\n/**\n * Remove one session's active routing entry. The event handler calls this\n * when a session is deleted so the sidebar does not retain dead routes.\n */\nexport async function removeSidebarActiveRouting(\n sessionId: string,\n options: SidebarStateWriteOptions = {},\n): Promise<void> {\n const stateFile = options.stateFile ?? getSidebarStateFile()\n await performSidebarWrite(stateFile, (existing) => {\n if (!(sessionId in existing.activeRouting)) {\n return existing\n }\n const activeRouting = { ...existing.activeRouting }\n delete activeRouting[sessionId]\n return {\n ...existing,\n activeRouting,\n }\n })\n}\n\n/**\n * Write a new machine-state snapshot. The fetch interceptor and quota\n * manager call this after each refresh; auth-loader calls it after the\n * account pool is materialized.\n */\nexport async function setSidebarMachineState(\n next: SidebarMachineState,\n options: SidebarStateWriteOptions = {},\n): Promise<void> {\n const stateFile = options.stateFile ?? getSidebarStateFile()\n await performSidebarWrite(stateFile, (existing) =>\n mergeMachineState(existing, next),\n )\n}\n\nvoid SIDEBAR_STATE_FILE_MODE\n", "import { createHash } from 'node:crypto'\nimport { accessTokenExpired } from './auth'\nimport type { OAuthAuthDetails } from './types'\n\nconst authCache = new Map<string, OAuthAuthDetails>()\n\n/**\n * Produces a stable cache key from a refresh token string.\n */\nfunction normalizeRefreshKey(refresh?: string): string | undefined {\n const key = refresh?.trim()\n return key ? key : undefined\n}\n\n/**\n * Returns a cached auth snapshot when available, favoring unexpired tokens.\n */\nexport function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {\n const key = normalizeRefreshKey(auth.refresh)\n if (!key) {\n return auth\n }\n\n const cached = authCache.get(key)\n if (!cached) {\n authCache.set(key, auth)\n return auth\n }\n\n if (!accessTokenExpired(auth)) {\n authCache.set(key, auth)\n return auth\n }\n\n if (!accessTokenExpired(cached)) {\n return cached\n }\n\n authCache.set(key, auth)\n return auth\n}\n\n/**\n * Stores the latest auth snapshot keyed by refresh token.\n */\nexport function storeCachedAuth(auth: OAuthAuthDetails): void {\n const key = normalizeRefreshKey(auth.refresh)\n if (!key) {\n return\n }\n authCache.set(key, auth)\n}\n\n/**\n * Clears cached auth globally or for a specific refresh token.\n */\nexport function clearCachedAuth(refresh?: string): void {\n if (!refresh) {\n authCache.clear()\n return\n }\n const key = normalizeRefreshKey(refresh)\n if (key) {\n authCache.delete(key)\n }\n}\n\n// ============================================================================\n// Thinking Signature Cache (for Claude multi-turn conversations)\n// ============================================================================\n\nimport {\n createSignatureCache,\n type SignatureCache,\n} from './cache/signature-cache'\nimport type { SignatureCacheConfig } from './config'\n\ninterface SignatureEntry {\n signature: string\n timestamp: number\n}\n\n// Map: sessionId -> Map<textHash, SignatureEntry>\nconst signatureCache = new Map<string, Map<string, SignatureEntry>>()\n\n// Cache entries expire after 1 hour\nconst SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1000\n\n// Maximum entries per session to prevent memory bloat\nconst MAX_ENTRIES_PER_SESSION = 100\n\n// Maximum sessions tracked in the outer Map to prevent unbounded growth\nconst MAX_CACHED_SESSIONS = 10\n// 16 hex chars = 64-bit key space; keeps memory bounded while making collisions extremely unlikely.\nconst SIGNATURE_TEXT_HASH_HEX_LEN = 16\n\n// Disk cache instance (initialized via initDiskSignatureCache)\nlet diskCache: SignatureCache | null = null\n\n/**\n * Initialize the disk-based signature cache.\n * Call this from plugin initialization when keep_thinking is enabled.\n */\nexport function initDiskSignatureCache(\n config: SignatureCacheConfig | undefined,\n): SignatureCache | null {\n diskCache = createSignatureCache(config)\n return diskCache\n}\n\n/**\n * Get the disk cache instance (for testing/debugging).\n */\nexport function getDiskSignatureCache(): SignatureCache | null {\n return diskCache\n}\n\nexport async function shutdownDiskSignatureCache(): Promise<void> {\n const cache = diskCache\n try {\n await cache?.flush()\n } finally {\n cache?.shutdown()\n diskCache = null\n clearCachedAuth()\n clearSignatureCache()\n }\n}\n\n/**\n * Hashes text content into a stable, Unicode-safe key.\n *\n * Uses SHA-256 over UTF-8 bytes and truncates to keep memory usage bounded.\n */\nfunction hashText(text: string): string {\n return createHash('sha256')\n .update(text, 'utf8')\n .digest('hex')\n .slice(0, SIGNATURE_TEXT_HASH_HEX_LEN)\n}\n\n/**\n * Create a disk cache key from sessionId and textHash.\n */\nfunction makeDiskKey(sessionId: string, textHash: string): string {\n return `${sessionId}:${textHash}`\n}\n\n/**\n * Prune stale sessions from the outer signatureCache Map.\n * Removes sessions where all entries have expired, then evicts the\n * oldest sessions if the Map still exceeds MAX_CACHED_SESSIONS.\n */\nfunction pruneSignatureSessions(): void {\n if (signatureCache.size <= MAX_CACHED_SESSIONS) return\n\n const now = Date.now()\n\n // First pass: remove sessions where ALL entries are expired\n for (const [sid, innerMap] of signatureCache) {\n let allExpired = true\n for (const entry of innerMap.values()) {\n if (now - entry.timestamp <= SIGNATURE_CACHE_TTL_MS) {\n allExpired = false\n break\n }\n }\n if (allExpired) {\n signatureCache.delete(sid)\n }\n }\n\n // Second pass: if still over cap, evict oldest sessions by newest entry timestamp\n if (signatureCache.size > MAX_CACHED_SESSIONS) {\n const sessionsByAge: Array<{ sid: string; newestTs: number }> = []\n for (const [sid, innerMap] of signatureCache) {\n let newestTs = 0\n for (const entry of innerMap.values()) {\n if (entry.timestamp > newestTs) newestTs = entry.timestamp\n }\n sessionsByAge.push({ sid, newestTs })\n }\n // Sort oldest-first, evict until at cap\n sessionsByAge.sort((a, b) => a.newestTs - b.newestTs)\n const toEvict = signatureCache.size - MAX_CACHED_SESSIONS\n for (let i = 0; i < toEvict; i++) {\n const entry = sessionsByAge[i]\n if (entry) signatureCache.delete(entry.sid)\n }\n }\n}\n\n/**\n * Caches a thinking signature for a given session and text.\n * Used for Claude models that require signed thinking blocks in multi-turn conversations.\n * Also writes to disk cache if enabled.\n */\nexport function cacheSignature(\n sessionId: string,\n text: string,\n signature: string,\n): void {\n if (!sessionId || !text || !signature) return\n\n const textHash = hashText(text)\n\n // Write to memory cache\n let sessionMemCache = signatureCache.get(sessionId)\n if (!sessionMemCache) {\n // About to add a new session \u2014 prune stale ones first\n pruneSignatureSessions()\n sessionMemCache = new Map()\n signatureCache.set(sessionId, sessionMemCache)\n }\n // Evict old entries if we're at capacity\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const now = Date.now()\n for (const [key, entry] of sessionMemCache.entries()) {\n if (now - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {\n sessionMemCache.delete(key)\n }\n }\n // If still at capacity, remove oldest entries\n if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {\n const entries = Array.from(sessionMemCache.entries()).sort(\n (a, b) => a[1].timestamp - b[1].timestamp,\n )\n const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4))\n for (const [key] of toRemove) {\n sessionMemCache.delete(key)\n }\n }\n }\n\n sessionMemCache.set(textHash, { signature, timestamp: Date.now() })\n\n // Write to disk cache if enabled\n if (diskCache) {\n const diskKey = makeDiskKey(sessionId, textHash)\n diskCache.store(diskKey, signature)\n }\n}\n\n/**\n * Retrieves a cached signature for a given session and text.\n * Checks memory first, then falls back to disk cache.\n * Returns undefined if not found or expired.\n */\nexport function getCachedSignature(\n sessionId: string,\n text: string,\n): string | undefined {\n if (!sessionId || !text) return undefined\n\n const textHash = hashText(text)\n\n // Check memory cache first\n const sessionMemCache = signatureCache.get(sessionId)\n if (sessionMemCache) {\n const entry = sessionMemCache.get(textHash)\n if (entry) {\n // Check if expired\n if (Date.now() - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {\n sessionMemCache.delete(textHash)\n } else {\n return entry.signature\n }\n }\n }\n\n // Fall back to disk cache\n if (diskCache) {\n const diskKey = makeDiskKey(sessionId, textHash)\n const diskValue = diskCache.retrieve(diskKey)\n if (diskValue) {\n // Promote to memory cache for faster subsequent access\n let memCache = signatureCache.get(sessionId)\n if (!memCache) {\n memCache = new Map()\n signatureCache.set(sessionId, memCache)\n }\n memCache.set(textHash, { signature: diskValue, timestamp: Date.now() })\n return diskValue\n }\n }\n\n return undefined\n}\n\n/**\n * Clears signature cache for a specific session or all sessions.\n * Also clears from disk cache if enabled.\n */\nexport function clearSignatureCache(sessionId?: string): void {\n if (sessionId) {\n signatureCache.delete(sessionId)\n // Note: We don't clear individual sessions from disk cache to avoid\n // expensive iteration. Disk cache entries will expire naturally.\n } else {\n signatureCache.clear()\n // For full clear, we could clear disk cache, but leaving it for now\n // since entries have TTL and will expire naturally.\n }\n}\n\n// ============================================================================\n// Disk-Persistent Signature Cache (re-export from cache/ folder)\n// ============================================================================\n\n// Re-export SignatureCache class and factory for direct use\nexport { createSignatureCache, SignatureCache } from './cache/signature-cache'\nexport type { SignatureCacheConfig } from './config'\n", "import { fetchWithActiveTimeout } from '@cortexkit/antigravity-auth-core'\nimport { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET } from '../constants'\nimport {\n calculateTokenExpiry,\n formatRefreshParts,\n parseRefreshParts,\n} from './auth'\nimport { clearCachedAuth, storeCachedAuth } from './cache'\nimport { createLogger } from './logger'\nimport { invalidateProjectContextCache } from './project'\nimport type { OAuthAuthDetails, PluginClient, RefreshParts } from './types'\n\nconst log = createLogger('token')\n\ninterface OAuthErrorPayload {\n error?:\n | string\n | {\n code?: string\n status?: string\n message?: string\n }\n error_description?: string\n}\n\n/**\n * Parses OAuth error payloads returned by Google token endpoints, tolerating varied shapes.\n */\nfunction parseOAuthErrorPayload(text: string | undefined): {\n code?: string\n description?: string\n} {\n if (!text) {\n return {}\n }\n\n try {\n const payload = JSON.parse(text) as OAuthErrorPayload\n if (!payload || typeof payload !== 'object') {\n return { description: text }\n }\n\n let code: string | undefined\n if (typeof payload.error === 'string') {\n code = payload.error\n } else if (payload.error && typeof payload.error === 'object') {\n code = payload.error.status ?? payload.error.code\n if (!payload.error_description && payload.error.message) {\n return { code, description: payload.error.message }\n }\n }\n\n const description = payload.error_description\n if (description) {\n return { code, description }\n }\n\n if (\n payload.error &&\n typeof payload.error === 'object' &&\n payload.error.message\n ) {\n return { code, description: payload.error.message }\n }\n\n return { code }\n } catch {\n return { description: text }\n }\n}\n\nexport class AntigravityTokenRefreshError extends Error {\n code?: string\n description?: string\n status: number\n statusText: string\n\n constructor(options: {\n message: string\n code?: string\n description?: string\n status: number\n statusText: string\n }) {\n super(options.message)\n this.name = 'AntigravityTokenRefreshError'\n this.code = options.code\n this.description = options.description\n this.status = options.status\n this.statusText = options.statusText\n }\n}\n\n/**\n * Refreshes an Antigravity OAuth access token, updates persisted credentials, and handles revocation.\n */\nexport async function refreshAccessToken(\n auth: OAuthAuthDetails,\n _client: PluginClient,\n _providerId: string,\n): Promise<OAuthAuthDetails | undefined> {\n const parts = parseRefreshParts(auth.refresh)\n if (!parts.refreshToken) {\n return undefined\n }\n\n try {\n const startTime = Date.now()\n const response = await fetchWithActiveTimeout(\n 'https://oauth2.googleapis.com/token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: parts.refreshToken,\n client_id: ANTIGRAVITY_CLIENT_ID,\n client_secret: ANTIGRAVITY_CLIENT_SECRET,\n }),\n },\n )\n\n if (!response.ok) {\n let errorText: string | undefined\n try {\n errorText = await response.text()\n } catch {\n errorText = undefined\n }\n\n const { code, description } = parseOAuthErrorPayload(errorText)\n const details = [code, description ?? errorText]\n .filter(Boolean)\n .join(': ')\n const baseMessage = `Antigravity token refresh failed (${response.status} ${response.statusText})`\n const message = details ? `${baseMessage} - ${details}` : baseMessage\n log.warn('Token refresh failed', {\n status: response.status,\n code,\n details,\n })\n\n if (code === 'invalid_grant') {\n log.warn(\n 'Google revoked the stored refresh token - reauthentication required',\n )\n invalidateProjectContextCache(auth.refresh)\n clearCachedAuth(auth.refresh)\n }\n\n throw new AntigravityTokenRefreshError({\n message,\n code,\n description: description ?? errorText,\n status: response.status,\n statusText: response.statusText,\n })\n }\n\n const payload = (await response.json()) as {\n access_token: string\n expires_in: number\n refresh_token?: string\n }\n\n const refreshedParts: RefreshParts = {\n refreshToken: payload.refresh_token ?? parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId,\n }\n\n const updatedAuth: OAuthAuthDetails = {\n ...auth,\n access: payload.access_token,\n expires: calculateTokenExpiry(startTime, payload.expires_in),\n refresh: formatRefreshParts(refreshedParts),\n }\n\n storeCachedAuth(updatedAuth)\n // Project context cache is intentionally not invalidated on successful token\n // refresh: managedProjectId survives access-token rotation. Invalid grants\n // still invalidate above because the refresh key is no longer usable.\n\n return updatedAuth\n } catch (error) {\n if (error instanceof AntigravityTokenRefreshError) {\n throw error\n }\n log.error('Unexpected token refresh error', { error: String(error) })\n return undefined\n }\n}\n", "/**\n * OpenCode adapter for the harness-agnostic quota manager.\n *\n * Re-exports the core `QuotaManager` types and helpers so call sites in\n * `plugin.ts` and other modules don't need to switch imports. Also wires up\n * the host-specific fetch callback that handles:\n * 1. Token refresh via the existing `refreshAccessToken` path.\n * 2. Persisting rotated refresh tokens via `client.auth.set` (matching\n * legacy behavior).\n * 3. Resolving project context via `ensureProjectContext`.\n *\n * The legacy `checkAccountsQuota(accounts, client, providerId)` export is\n * retained as a compatibility wrapper that creates a short-lived manager\n * with `force: true` \u2014 manual quota screens must always refresh, even if\n * the background manager has backed off.\n */\n\nimport {\n type AccountMetadataV3,\n type AccountQuotaResult,\n aggregateGeminiCliQuota,\n aggregateQuota,\n aggregateQuotaSummary,\n createQuotaManager,\n defaultKeyOf,\n type FetchAccountQuota,\n type FetchAvailableModelsOptions,\n fetchAvailableModels,\n fetchGeminiCliQuota,\n fetchQuotaSummary,\n type GeminiCliQuotaSummary,\n getHealthTracker,\n type QuotaManager,\n type QuotaSummary,\n} from '@cortexkit/antigravity-auth-core'\n\nimport {\n ANTIGRAVITY_ENDPOINT_FALLBACKS,\n ANTIGRAVITY_PROVIDER_ID,\n buildGeminiCliUserAgent,\n} from '../constants'\nimport {\n buildSidebarMachineStateFromAccounts,\n isAccountCurrent,\n setSidebarMachineState,\n} from '../sidebar-state'\nimport {\n accessTokenExpired,\n formatRefreshParts,\n parseRefreshParts,\n} from './auth'\nimport { logQuotaFetch, logQuotaStatus } from './debug'\nimport { buildAntigravityHarnessUserAgent } from './fingerprint'\nimport { createLogger } from './logger'\nimport { ensureProjectContext, loadManagedProject } from './project'\nimport { refreshAccessToken } from './token'\nimport type { OAuthAuthDetails, PluginClient } from './types'\n\ntype QuotaFetch = NonNullable<FetchAvailableModelsOptions['fetchVia']>\n\n// Re-export the public surface so existing imports from `./quota` keep working.\nconst log = createLogger('quota')\n\nexport type {\n AccountQuotaResult,\n AccountQuotaStatus,\n GeminiCliQuotaModel,\n GeminiCliQuotaSummary,\n PerModelQuotaEntry,\n QuotaGroup,\n QuotaGroupSummary,\n QuotaManager,\n QuotaManagerOptions,\n QuotaSummary,\n} from '@cortexkit/antigravity-auth-core'\nexport {\n classifyQuotaGroup,\n createQuotaManager,\n defaultKeyOf,\n} from '@cortexkit/antigravity-auth-core'\n\nexport interface CreateOpenCodeQuotaManagerOptions {\n /** Override the default key derivation (email \u2192 refresh-token hash). */\n keyOf?: (account: AccountMetadataV3) => string\n baseBackoffMs?: number\n maxBackoffMs?: number\n fetchTimeoutMs?: number\n}\n\n/**\n * Build an OpenCode-wired quota manager.\n *\n * The returned manager owns its cache, in-flight dedupe, and backoff state.\n * Register its `dispose()` with `PluginLifecycle` so refreshes abort on plugin\n * shutdown.\n *\n * The wrapper observes `refreshAccount` / `refreshAccounts` and pushes a\n * redacted sidebar snapshot after every refresh (success or backoff) so\n * the TUI's next poll renders the freshest cached quota. The snapshot is\n * sourced from the live AccountManager view (`getAccountsForSidebar`) so\n * it carries the just-updated percentages; before bootstrapping it is a\n * no-op.\n */\nexport function createOpenCodeQuotaManager(\n client: PluginClient,\n providerId: string = ANTIGRAVITY_PROVIDER_ID,\n options: CreateOpenCodeQuotaManagerOptions & {\n /**\n * Optional account-snapshot provider. Wired by the plugin entry to\n * the live `AccountManager.getAccounts()` so each refresh can build\n * a sidebar snapshot from the actual cached quota + cooldown. When\n * omitted, the wrapper falls back to a no-op snapshot push.\n */\n getAccountsForSidebar?: () => Array<{\n index: number\n label?: string\n enabled?: boolean\n coolingDownUntil?: number\n cachedQuota?: AccountMetadataV3['cachedQuota']\n cachedQuotaAccountId?: string\n currentQuotaAccountId?: string\n }> | null\n /**\n * Optional provider for the active-account indexes per model family.\n * Wired by the plugin entry so every quota-refresh sidebar snapshot\n * carries the real `current` flag \u2014 not a hardcoded `false`.\n */\n getActiveIndexByFamily?: () => {\n claude: number\n gemini: number\n } | null\n /**\n * Optional transport adapter used for both `fetchAvailableModels`\n * and the project-context lookup. When omitted, the production\n * `fetchWithAgyCliTransport` runs and binds to the real\n * Antigravity endpoints; the e2e harness injects a mock here so\n * quota refresh + project discovery stay on the loopback server.\n */\n fetchVia?: QuotaFetch\n } = {},\n): QuotaManager {\n const fetchAccountQuota = makeFetchAccountQuota(\n client,\n providerId,\n options.fetchVia,\n )\n const manager = createQuotaManager({\n fetchAccountQuota,\n keyOf: options.keyOf ?? defaultKeyOf,\n baseBackoffMs: options.baseBackoffMs,\n maxBackoffMs: options.maxBackoffMs,\n fetchTimeoutMs: options.fetchTimeoutMs,\n })\n const originalRefreshAccount = manager.refreshAccount\n const originalRefreshAccounts = manager.refreshAccounts\n const getAccountsForSidebar = options.getAccountsForSidebar\n const getActiveIndexByFamily = options.getActiveIndexByFamily\n let disposed = false\n const inFlight = new Set<Promise<unknown>>()\n\n const pushAfterRefresh = async (\n account: AccountMetadataV3,\n ): Promise<void> => {\n if (!getAccountsForSidebar) return\n await pushSidebarQuotaSnapshot(\n getAccountsForSidebar,\n manager.getBackoffUntil(account),\n getActiveIndexByFamily,\n ).catch(() => {\n // Sidebar persistence remains best-effort when lock contention\n // outlives its retry budget.\n })\n }\n\n const track = <T>(operation: Promise<T>): Promise<T> => {\n inFlight.add(operation)\n void operation.then(\n () => inFlight.delete(operation),\n () => inFlight.delete(operation),\n )\n return operation\n }\n\n const dispose = async (): Promise<void> => {\n if (disposed) return\n disposed = true\n await manager.dispose()\n await Promise.allSettled(inFlight)\n }\n\n return {\n ...manager,\n async refreshAccount(account, refreshOptions) {\n const shouldPush = !disposed\n return track(\n (async () => {\n const result = await originalRefreshAccount(account, refreshOptions)\n if (shouldPush) await pushAfterRefresh(account)\n return result\n })(),\n )\n },\n async refreshAccounts(accounts, refreshOptions) {\n const shouldPush = !disposed\n return track(\n (async () => {\n const results = await originalRefreshAccounts(\n accounts,\n refreshOptions,\n )\n // Push one snapshot per batch \u2014 the AccountManager's view is updated\n // by the caller (oauth-methods / fetch-interceptor) BEFORE we read\n // here, so a single post-batch snapshot captures the full diff.\n const lastAccount = accounts[accounts.length - 1]\n if (shouldPush && lastAccount) await pushAfterRefresh(lastAccount)\n return results\n })(),\n )\n },\n dispose,\n }\n}\n\n/**\n * Compatibility wrapper used by code paths that want a one-shot check across\n * the full account pool with no shared cache.\n *\n * Equivalent to spinning up a short-lived manager with `force: true` so\n * manual quota dialogs always reflect the latest data even if the background\n * manager has backed off.\n */\nexport async function checkAccountsQuotaWith(\n accounts: AccountMetadataV3[],\n fetchAccountQuota: FetchAccountQuota,\n): Promise<AccountQuotaResult[]> {\n const manager = createQuotaManager({\n fetchAccountQuota,\n keyOf: defaultKeyOf,\n })\n try {\n return await manager.refreshAccounts(accounts, {\n indexFor: (account) => accounts.indexOf(account),\n force: true,\n })\n } finally {\n manager.dispose()\n }\n}\n\nexport async function checkAccountsQuotaStandalone(\n accounts: AccountMetadataV3[],\n options: { refresh: boolean },\n): Promise<AccountQuotaResult[]> {\n if (!options.refresh) {\n return accounts.map((account, index) => ({\n index,\n email: account.email,\n status: account.enabled === false ? 'disabled' : 'ok',\n disabled: account.enabled === false,\n quota: {\n groups: account.cachedQuota ?? {},\n modelCount: Object.keys(account.cachedQuota ?? {}).length,\n },\n }))\n }\n return checkAccountsQuotaWith(\n accounts,\n makeFetchAccountQuota(undefined, ANTIGRAVITY_PROVIDER_ID),\n )\n}\n\nexport async function checkAccountsQuota(\n accounts: AccountMetadataV3[],\n client: PluginClient,\n providerId: string = ANTIGRAVITY_PROVIDER_ID,\n): Promise<AccountQuotaResult[]> {\n return checkAccountsQuotaWith(\n accounts,\n makeFetchAccountQuota(client, providerId),\n )\n}\n\n/**\n * Push a quota refresh into the sidebar. Called by every quota refresh\n * call site (manual `/antigravity-quota`, the `check` menu action, and the\n * background refresh in `fetch-interceptor`) AFTER the results have been\n * folded back into the AccountManager's cached quota. The function reads\n * the live account snapshot through `getAccounts` so the redacted entry\n * carries the just-refreshed percentages \u2014 not the previous tick's stale\n * numbers and not `undefined`.\n *\n * The mapping is deliberately tolerant: if `getAccounts` returns `null`\n * (e.g. before the plugin has finished bootstrapping) the call is a no-op.\n * On lock contention the error is logged-and-swallowed so a quota dialog\n * never fails just because the sidebar file is busy.\n */\nexport async function pushSidebarQuotaSnapshot(\n getAccounts: () => Array<{\n index: number\n label?: string\n enabled?: boolean\n coolingDownUntil?: number\n cachedQuota?: AccountMetadataV3['cachedQuota']\n cachedQuotaAccountId?: string\n currentQuotaAccountId?: string\n /** Captured plan tier to surface in the sidebar state file. */\n tier?: { id: string; paidId?: string; capturedAt: number }\n }> | null,\n backoffUntil: number = 0,\n getActiveIndexByFamily?: () => {\n claude: number\n gemini: number\n } | null,\n): Promise<void> {\n const accounts = getAccounts()\n if (!accounts || accounts.length === 0) return\n const activeByFamily = getActiveIndexByFamily?.() ?? null\n try {\n await setSidebarMachineState(\n buildSidebarMachineStateFromAccounts(\n accounts.map((entry) => ({\n index: entry.index,\n label: entry.label,\n enabled: entry.enabled,\n current: activeByFamily\n ? isAccountCurrent(entry.index, activeByFamily)\n : false,\n coolingDownUntil: entry.coolingDownUntil,\n cachedQuota: entry.cachedQuota,\n cachedQuotaAccountId: entry.cachedQuotaAccountId,\n currentQuotaAccountId: entry.currentQuotaAccountId,\n healthScore: getHealthTracker().getScore(entry.index),\n tier: entry.tier,\n })),\n {\n checkedAt: Date.now(),\n quotaBackoffUntil: backoffUntil > 0 ? backoffUntil : undefined,\n },\n ),\n )\n } catch (error) {\n log.debug('sidebar-quota-write-failed', { error: String(error) })\n }\n}\n\n/**\n * Legacy fallback: fetchAvailableModels \u2192 aggregateQuota. Used when\n * `fetchQuotaSummary` rejects (network, 403, etc.). Extracted so the\n * concurrent fetch path can reuse the same fallback logic without\n * duplicating the catch chain.\n */\nasync function fetchLegacyModelsFallback(options: {\n accessToken: string\n projectId: string\n fetchVia?: QuotaFetch\n}): Promise<QuotaSummary> {\n try {\n const modelsResponse = await fetchAvailableModels({\n accessToken: options.accessToken,\n projectId: options.projectId,\n endpoints: ANTIGRAVITY_ENDPOINT_FALLBACKS,\n userAgent: buildAntigravityHarnessUserAgent(),\n timeoutMs: 10_000,\n ...(options.fetchVia ? { fetchVia: options.fetchVia } : {}),\n })\n if (modelsResponse.models) {\n return aggregateQuota(modelsResponse.models)\n }\n return {\n groups: {},\n modelCount: 0,\n error: 'Failed to fetch Antigravity quota (legacy fallback)',\n }\n } catch {\n return {\n groups: {},\n modelCount: 0,\n error: 'Failed to fetch Antigravity quota',\n }\n }\n}\n\nfunction makeFetchAccountQuota(\n client: PluginClient | undefined,\n providerId: string,\n fetchVia?: QuotaFetch,\n): FetchAccountQuota {\n return async (account, signal) => {\n const index = 0\n const disabled = account.enabled === false\n if (disabled) {\n return {\n index,\n email: account.email,\n status: 'disabled',\n disabled: true,\n }\n }\n\n if (signal.aborted) {\n return {\n index,\n email: account.email,\n status: 'error',\n error:\n signal.reason instanceof Error ? signal.reason.message : 'aborted',\n }\n }\n\n let auth = buildAuthFromAccount(account)\n let rotatedRefresh: string | undefined\n\n try {\n if (accessTokenExpired(auth)) {\n const refreshed = await refreshAccessToken(\n auth,\n client as PluginClient,\n providerId,\n )\n if (!refreshed) {\n throw new Error('Token refresh failed')\n }\n if (refreshed.refresh !== auth.refresh) {\n rotatedRefresh = refreshed.refresh\n }\n auth = refreshed\n }\n\n const projectContext = await ensureProjectContext(auth)\n auth = projectContext.auth\n const updatedAccount = applyAccountUpdates(\n account,\n auth,\n projectContext.capturedTier,\n )\n\n if (rotatedRefresh && client) {\n await persistRotatedRefresh(client, providerId, auth).catch(() => {})\n }\n\n let quotaResult: QuotaSummary\n let fellBackToLegacy = false\n\n const authParts = parseRefreshParts(auth.refresh)\n // Bare refresh tokens have no packed project IDs \u2014 fall back to the\n // account record. The real managedProjectId lives on the persisted\n // account, not in the packed refresh string.\n const managedProjectId =\n authParts.managedProjectId ?? account.managedProjectId\n\n // Two independent payload contracts: the windowed summary\n // (with legacy fallback) and the gemini-CLI quota. They share\n // access + project but target different endpoints, so the\n // two 10s timeouts ran back-to-back for ~20s per account on\n // modal open. Run them concurrently; either rejection is\n // handled by its own branch and the result is still merged.\n const fetchSummaryPayload = (async (): Promise<{\n result: QuotaSummary\n fellBackToLegacy: boolean\n }> => {\n try {\n const summaryResult = await fetchQuotaSummary({\n accessToken: auth.access ?? '',\n managedProjectId,\n projectId: projectContext.effectiveProjectId,\n endpoints: ANTIGRAVITY_ENDPOINT_FALLBACKS,\n userAgent: buildAntigravityHarnessUserAgent(),\n timeoutMs: 10_000,\n ...(fetchVia ? { fetchVia } : {}),\n })\n return {\n result: aggregateQuotaSummary(summaryResult.summary),\n fellBackToLegacy: summaryResult.fellBackToLegacy ?? false,\n }\n } catch {\n return {\n result: await fetchLegacyModelsFallback({\n accessToken: auth.access ?? '',\n projectId: projectContext.effectiveProjectId,\n fetchVia,\n }),\n fellBackToLegacy: true,\n }\n }\n })()\n\n // CLI fetch is independent of the summary fetch. A CLI failure must\n // NOT kill the summary result, but it also must not be laundered into\n // \"No Gemini CLI quota available\" (a permanent-looking status) when\n // the real cause is a transient network error. Capture the error\n // message separately so the annotated result can carry it.\n let geminiCliFetchError: string | undefined\n const fetchGeminiCliPayload = fetchGeminiCliQuota({\n accessToken: auth.access ?? '',\n projectId: projectContext.effectiveProjectId,\n endpoints: ANTIGRAVITY_ENDPOINT_FALLBACKS,\n userAgent: buildGeminiCliUserAgent(),\n timeoutMs: 10_000,\n ...(fetchVia ? { fetchVia } : {}),\n }).catch((error: unknown) => {\n geminiCliFetchError =\n error instanceof Error ? error.message : String(error)\n log.debug('fetchGeminiCliQuota failed', { error: geminiCliFetchError })\n return { buckets: undefined } as Awaited<\n ReturnType<typeof fetchGeminiCliQuota>\n >\n })\n\n const [summary, geminiCliResponse] = await Promise.all([\n fetchSummaryPayload,\n fetchGeminiCliPayload,\n ])\n quotaResult = summary.result\n fellBackToLegacy = summary.fellBackToLegacy\n\n const geminiCliQuotaResult = aggregateGeminiCliQuota(geminiCliResponse)\n const annotated: GeminiCliQuotaSummary =\n geminiCliResponse.buckets === undefined ||\n geminiCliResponse.buckets.length === 0\n ? {\n ...geminiCliQuotaResult,\n error:\n // A real fetch exception is a transient failure, not a\n // \"no CLI configured\" scenario \u2014 propagate the actual message.\n geminiCliFetchError ??\n (geminiCliQuotaResult.models.length === 0\n ? 'No Gemini CLI quota available'\n : undefined),\n }\n : geminiCliQuotaResult\n\n for (const [family, groupQuota] of Object.entries(quotaResult.groups)) {\n const remainingPercent = (groupQuota.remainingFraction ?? 0) * 100\n logQuotaStatus(account.email, index, remainingPercent, family)\n }\n\n const legacyTag = fellBackToLegacy ? ' legacy=1' : ''\n logQuotaFetch('complete', 1, `ok=1 errors=0${legacyTag}`)\n\n return {\n index,\n email: account.email,\n status: 'ok',\n disabled: false,\n quota: quotaResult,\n geminiCliQuota: annotated,\n updatedAccount,\n }\n } catch (error) {\n logQuotaFetch(\n 'error',\n undefined,\n `account=${account.email ?? index} error=${error instanceof Error ? error.message : String(error)}`,\n )\n return {\n index,\n email: account.email,\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n disabled: false,\n }\n }\n }\n}\n\nfunction buildAuthFromAccount(account: AccountMetadataV3): OAuthAuthDetails {\n return {\n type: 'oauth',\n refresh: formatRefreshParts({\n refreshToken: account.refreshToken,\n projectId: account.projectId,\n managedProjectId: account.managedProjectId,\n }),\n access: undefined,\n expires: undefined,\n }\n}\n\nfunction applyAccountUpdates(\n account: AccountMetadataV3,\n auth: OAuthAuthDetails,\n capturedTier?: { id: string; paidId?: string; capturedAt: number },\n): AccountMetadataV3 | undefined {\n const parts = parseRefreshParts(auth.refresh)\n if (!parts.refreshToken) {\n return undefined\n }\n\n const updated: AccountMetadataV3 = {\n ...account,\n refreshToken: parts.refreshToken,\n projectId: parts.projectId ?? account.projectId,\n managedProjectId: parts.managedProjectId ?? account.managedProjectId,\n // Persist the captured tier alongside the project-context write. Only\n // present when the loadCodeAssist payload returned a non-empty currentTier.id.\n ...(capturedTier\n ? {\n capturedTierId: capturedTier.id,\n ...(capturedTier.paidId !== undefined\n ? { capturedPaidTierId: capturedTier.paidId }\n : {}),\n capturedTierAt: capturedTier.capturedAt,\n }\n : {}),\n }\n\n const changed =\n updated.refreshToken !== account.refreshToken ||\n updated.projectId !== account.projectId ||\n updated.managedProjectId !== account.managedProjectId ||\n updated.capturedTierId !== account.capturedTierId ||\n updated.capturedPaidTierId !== account.capturedPaidTierId ||\n // capturedAt represents when the tier was LAST CONFIRMED, not when it\n // changed -- always update it on a successful observation so consumers\n // can gate staleness on that timestamp even when the id stays the same.\n (capturedTier !== undefined &&\n updated.capturedTierAt !== account.capturedTierAt)\n\n return changed ? updated : undefined\n}\n\nasync function persistRotatedRefresh(\n client: PluginClient,\n providerId: string,\n auth: OAuthAuthDetails,\n): Promise<void> {\n await client.auth.set({\n path: { id: providerId },\n body: {\n type: 'oauth',\n refresh: auth.refresh,\n access: auth.access ?? '',\n expires: auth.expires ?? 0,\n },\n })\n}\n\n/**\n * Build a per-account tier-loader callback for the background poller.\n *\n * Calls `loadManagedProject` (loadCodeAssist) directly, bypassing the\n * `ensureProjectContext` cache that fast-paths on `managedProjectId` and\n * never returns a tier for existing accounts. One call per account per 24 h.\n *\n * Uses the same token-refresh infrastructure as `makeFetchAccountQuota` so\n * an expired access token does not silently fail the tier lookup.\n *\n * `loadManagedProject` uses the production TLS transport (`fetchWithAgyCliTransport`)\n * and is not interceptable via `fetchVia` -- the same design constraint applies to\n * `ensureProjectContext`. Tier lookup is best-effort; any failure resolves `null`.\n */\nexport function makeTierLoader(\n client: PluginClient | undefined,\n providerId: string,\n): (\n account: AccountMetadataV3,\n) => Promise<{ id: string; paidId?: string; capturedAt: number } | null> {\n return async (account) => {\n try {\n let auth = buildAuthFromAccount(account)\n if (accessTokenExpired(auth)) {\n const refreshed = await refreshAccessToken(\n auth,\n client as PluginClient,\n providerId,\n )\n if (!refreshed) return null\n auth = refreshed\n }\n\n const accessToken = auth.access\n if (!accessToken) return null\n\n const payload = await loadManagedProject(accessToken)\n if (!payload?.currentTier?.id) return null\n const paidTierId =\n typeof payload.paidTier === 'string'\n ? payload.paidTier\n : payload.paidTier?.id\n\n return {\n id: payload.currentTier.id,\n ...(paidTierId ? { paidId: paidTierId } : {}),\n capturedAt: Date.now(),\n }\n } catch {\n return null\n }\n }\n}\n", "import { existsSync, readFileSync } from 'node:fs'\nimport { createServer } from 'node:http'\n\nimport { ANTIGRAVITY_REDIRECT_URI } from '../constants'\n\ninterface OAuthListenerOptions {\n /**\n * How long to wait for the OAuth redirect before timing out (in milliseconds).\n */\n timeoutMs?: number\n}\n\nexport interface OAuthListener {\n /**\n * Resolves with the callback URL once Google redirects back to the local server.\n */\n waitForCallback(): Promise<URL>\n /**\n * Cleanly stop listening for callbacks.\n */\n close(): Promise<void>\n}\n\nconst redirectUri = new URL(ANTIGRAVITY_REDIRECT_URI)\nconst callbackPath = redirectUri.pathname || '/'\n\n/**\n * Detect if running in OrbStack Docker with --network host mode.\n * OrbStack's host networking only forwards ports bound to 127.0.0.1 to macOS.\n */\nfunction isOrbStackDockerHost(): boolean {\n // Check if we're in Docker\n if (!existsSync('/.dockerenv')) {\n return false\n }\n\n // Check for OrbStack-specific indicators\n // OrbStack sets specific environment variables or has identifiable characteristics\n try {\n // OrbStack containers often have /run/.containerenv or specific mount patterns\n // Also check if /proc/version contains orbstack\n if (existsSync('/proc/version')) {\n const version = readFileSync('/proc/version', 'utf8').toLowerCase()\n if (version.includes('orbstack')) {\n return true\n }\n }\n\n // Check hostname pattern (OrbStack uses specific patterns)\n const hostname = process.env.HOSTNAME || ''\n if (\n hostname.startsWith('orbstack-') ||\n hostname.endsWith('.orb') ||\n hostname === 'orbstack'\n ) {\n return true\n }\n\n // Check for OrbStack's network host mode by looking at resolv.conf\n // OrbStack with --network host has specific DNS configuration\n if (existsSync('/etc/resolv.conf')) {\n const resolv = readFileSync('/etc/resolv.conf', 'utf8')\n if (resolv.includes('orb.local') || resolv.includes('orbstack')) {\n return true\n }\n }\n\n // Fallback: Check if running on macOS/Darwin host via Docker\n // This is a heuristic - if in Docker on Linux but /proc/version shows darwin-like patterns\n if (process.platform === 'linux' && existsSync('/.dockerenv')) {\n // Most OrbStack containers will have been caught above\n // For safety, also check common OrbStack mount patterns\n if (existsSync('/run/host-services')) {\n return true\n }\n }\n } catch {\n // Ignore errors, fall through to default\n }\n\n return false\n}\n\n/**\n * Detect WSL (Windows Subsystem for Linux) environment.\n */\nfunction isWSL(): boolean {\n if (process.platform !== 'linux') return false\n try {\n const release = readFileSync('/proc/version', 'utf8').toLowerCase()\n return release.includes('microsoft') || release.includes('wsl')\n } catch {\n return false\n }\n}\n\n/**\n * Detect remote/SSH environment where localhost may not be accessible from browser.\n */\nfunction isRemoteEnvironment(): boolean {\n if (\n process.env.SSH_CLIENT ||\n process.env.SSH_TTY ||\n process.env.SSH_CONNECTION\n ) {\n return true\n }\n if (process.env.REMOTE_CONTAINERS || process.env.CODESPACES) {\n return true\n }\n return false\n}\n\n/**\n * Determine the best bind address for the OAuth callback server.\n *\n * Priority:\n * 1. OPENCODE_ANTIGRAVITY_OAUTH_BIND environment variable (user override)\n * 2. OrbStack Docker with --network host: 127.0.0.1 (required for port forwarding)\n * 3. WSL/SSH/Remote: 0.0.0.0 (needed for cross-network access)\n * 4. Default: 127.0.0.1 (most secure for local development)\n */\nfunction getBindAddress(): string {\n // Allow user override via environment variable\n const envBind = process.env.OPENCODE_ANTIGRAVITY_OAUTH_BIND\n if (envBind) {\n return envBind\n }\n\n // OrbStack Docker needs 127.0.0.1 for --network host port forwarding\n if (isOrbStackDockerHost()) {\n return '127.0.0.1'\n }\n\n // WSL and remote environments need 0.0.0.0 to be reachable\n if (isWSL() || isRemoteEnvironment()) {\n return '0.0.0.0'\n }\n\n // Default to 127.0.0.1 for security (local-only access)\n return '127.0.0.1'\n}\n\n/**\n * Starts a lightweight HTTP server that listens for the Antigravity OAuth redirect\n * and resolves with the captured callback URL.\n */\nexport async function startOAuthListener({\n timeoutMs = 5 * 60 * 1000,\n}: OAuthListenerOptions = {}): Promise<OAuthListener> {\n const port = redirectUri.port\n ? Number.parseInt(redirectUri.port, 10)\n : redirectUri.protocol === 'https:'\n ? 443\n : 80\n const origin = `${redirectUri.protocol}//${redirectUri.host}`\n\n let settled = false\n let resolveCallback: (url: URL) => void\n let rejectCallback: (error: Error) => void\n let timeoutHandle: NodeJS.Timeout\n const callbackPromise = new Promise<URL>((resolve, reject) => {\n resolveCallback = (url: URL) => {\n if (settled) return\n settled = true\n if (timeoutHandle) clearTimeout(timeoutHandle)\n resolve(url)\n }\n rejectCallback = (error: Error) => {\n if (settled) return\n settled = true\n if (timeoutHandle) clearTimeout(timeoutHandle)\n reject(error)\n }\n })\n\n const successResponse = `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Authentication Successful</title>\n <style>\n :root {\n --bg: #FAFAFA;\n --card-bg: #FFFFFF;\n --text-primary: #1F2937;\n --text-secondary: #6B7280;\n --accent: #2563EB;\n --success: #10B981;\n --border: #E5E7EB;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #111827;\n --card-bg: #1F2937;\n --text-primary: #F9FAFB;\n --text-secondary: #9CA3AF;\n --accent: #3B82F6;\n --success: #34D399;\n --border: #374151;\n }\n }\n body {\n margin: 0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n background: var(--bg);\n color: var(--text-primary);\n padding: 1rem;\n }\n .card {\n background: var(--card-bg);\n border-radius: 16px;\n padding: 3rem 2rem;\n width: 100%;\n max-width: 400px;\n text-align: center;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n border: 1px solid var(--border);\n }\n .icon-wrapper {\n width: 64px;\n height: 64px;\n background: rgba(16, 185, 129, 0.1);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0 auto 1.5rem;\n }\n .icon {\n width: 32px;\n height: 32px;\n color: var(--success);\n }\n h1 {\n font-size: 1.5rem;\n font-weight: 600;\n margin: 0 0 0.5rem;\n letter-spacing: -0.025em;\n }\n p {\n color: var(--text-secondary);\n font-size: 0.95rem;\n line-height: 1.5;\n margin: 0 0 2rem;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--text-primary);\n color: var(--card-bg);\n font-weight: 500;\n padding: 0.75rem 1.5rem;\n border-radius: 8px;\n text-decoration: none;\n transition: opacity 0.2s;\n font-size: 0.95rem;\n border: none;\n cursor: pointer;\n width: 100%;\n box-sizing: border-box;\n }\n .btn:hover {\n opacity: 0.9;\n }\n .sub-text {\n margin-top: 1rem;\n font-size: 0.8rem;\n color: var(--text-secondary);\n }\n </style>\n </head>\n <body>\n <div class=\"card\">\n <div class=\"icon-wrapper\">\n <svg class=\"icon\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </div>\n <h1>All set!</h1>\n <p>You've successfully authenticated with Antigravity. You can now return to Opencode.</p>\n <button class=\"btn\" onclick=\"closeWindow()\">Close this tab</button>\n <div class=\"sub-text\">Usage Tip: Most browsers block auto-closing. If the button doesn't work, please close the tab manually.</div>\n </div>\n <script>\n function closeWindow() {\n window.close();\n // Fallback if window.close() is blocked\n document.querySelector('.btn').textContent = \"Tab cannot be closed automatically\";\n document.querySelector('.btn').style.opacity = \"0.5\";\n document.querySelector('.btn').style.cursor = \"default\";\n }\n </script>\n </body>\n</html>`\n\n timeoutHandle = setTimeout(() => {\n rejectCallback(new Error('Timed out waiting for OAuth callback'))\n }, timeoutMs)\n timeoutHandle.unref?.()\n\n const server = createServer((request, response) => {\n if (!request.url) {\n response.writeHead(400, { 'Content-Type': 'text/plain' })\n response.end('Invalid request')\n return\n }\n\n const url = new URL(request.url, origin)\n if (url.pathname !== callbackPath) {\n response.writeHead(404, { 'Content-Type': 'text/plain' })\n response.end('Not found')\n return\n }\n\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\n response.end(successResponse)\n\n resolveCallback(url)\n\n setImmediate(() => {\n server.close()\n })\n })\n\n const bindAddress = getBindAddress()\n\n await new Promise<void>((resolve, reject) => {\n const handleError = (error: NodeJS.ErrnoException) => {\n server.off('error', handleError)\n if (error.code === 'EADDRINUSE') {\n reject(\n new Error(\n `Port ${port} is already in use. ` +\n `Another process is occupying this port. ` +\n `Please terminate the process or try again later.`,\n ),\n )\n return\n }\n reject(error)\n }\n server.once('error', handleError)\n server.listen(port, bindAddress, () => {\n server.off('error', handleError)\n resolve()\n })\n })\n\n server.on('error', (error) => {\n rejectCallback(error instanceof Error ? error : new Error(String(error)))\n })\n\n return {\n waitForCallback: () => callbackPromise,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (\n error &&\n (error as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING'\n ) {\n reject(error)\n return\n }\n if (!settled) {\n rejectCallback(new Error('OAuth listener closed before callback'))\n }\n resolve()\n })\n }),\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["mkdir", "readFile", "unlink", "path", "randomUUID", "mkdir", "rename", "rm", "writeFile", "dirname", "path", "log", "path", "path", "readFile", "sleep", "Buffer", "Buffer", "path", "log", "log", "log", "log", "log", "failureResult", "pool", "message", "snippet", "mkdirSync", "unlinkSync", "homedir", "dirname", "join", "join", "isTruthyFlag", "writeConsoleLog", "join", "xdgConfig", "isTruthyFlag", "ENV_CONSOLE_LOG", "isConsoleLogEnabled", "writeConsoleLog", "isTruthyFlag", "ENV_CONSOLE_LOG", "createLogger", "log", "log", "createLogger", "join", "log", "join", "homedir", "getConfigDir", "xdgConfig", "mkdirSync", "log", "unlinkSync", "path", "dirname", "path", "env", "log", "createLogger", "_client", "log", "createLogger", "log", "existsSync", "readFileSync", "existsSync", "readFileSync"]
7
+ }