@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,931 @@
1
+ /**
2
+ * Privacy-safe data service for the data-first slash-command dialogs.
3
+ *
4
+ * `/antigravity-quota` (this task) and the future `/antigravity-account`
5
+ * dialogs (Tasks 10-11) read from a single shared service so they
6
+ * never touch raw account storage, never see the email PII field, and
7
+ * never run quota network I/O during the dialog's open path — the
8
+ * Refresh action is the only path that performs a live fetch.
9
+ *
10
+ * Why this is a separate module:
11
+ *
12
+ * - `commands.ts` owns the slash-command orchestration; mixing the row
13
+ * projection + quota refresh logic in there would balloon the surface
14
+ * for what is fundamentally a small read/refresh service.
15
+ * - The row type (`CommandAccountRow`) is the projection that crosses
16
+ * the PII firewall into the dialog payload. Defining it here (next
17
+ * to the code that constructs it) keeps the firewall reviewable.
18
+ * - Tests can pin the read-vs-refresh boundary, the email redaction, and
19
+ * the refresh-token-keyed persistence in one file rather than chasing
20
+ * them across the dispatcher + RPC + apply layers.
21
+ *
22
+ * Cache-only opening contract (Task 9 operator requirement):
23
+ *
24
+ * `listAccounts()` is a pure read of the live AccountManager view
25
+ * the auth-loader materialized at session start. It performs
26
+ * ZERO quota manager calls — opening the dialog must be instant even
27
+ * when the network is unreachable, and quota refresh must remain an
28
+ * explicit user-driven action so the cached percentages never quietly
29
+ * rewrite themselves behind the user's back.
30
+ *
31
+ * Refresh-token-keyed persistence (Task 9 plan trap):
32
+ *
33
+ * `refreshQuota()` runs the shared quota manager across every enabled
34
+ * account and folds the results back into the live AccountManager +
35
+ * storage. Concurrent OAuth can renumber the flat `accounts[]` array
36
+ * between read and write, so we re-read under the lock and key the
37
+ * update by `refreshToken` (the canonical identity) rather than the
38
+ * array index. This way a successful OAuth add that lands between
39
+ * the read and the write cannot cause the refresh to overwrite the
40
+ * wrong account.
41
+ */
42
+
43
+ import { createHash } from 'node:crypto'
44
+
45
+ import {
46
+ buildSidebarMachineStateFromAccounts,
47
+ normalizeLegacyCachedQuota,
48
+ type SidebarAccountRedactionInput,
49
+ setSidebarMachineState,
50
+ toCapturedTier,
51
+ } from '../sidebar-state'
52
+
53
+ /**
54
+ * Local copies of the few core types the data service touches.
55
+ *
56
+ * The shipped TUI tree cannot depend on the `@cortexkit/antigravity-auth-core`
57
+ * barrel (only subpath imports like `./file-lock` are allowed) — the
58
+ * compiled `command-data.ts` is copied verbatim into `tui-compiled/`,
59
+ * and the type-import would otherwise leak the full core surface into
60
+ * the dialog render path. Declaring the structural shape locally keeps
61
+ * the compiled tree free of the barrel while preserving duck-typed
62
+ * compatibility with the production quota manager.
63
+ */
64
+ type CommandDataAccountMetadata = {
65
+ email?: string
66
+ refreshToken: string
67
+ projectId?: string
68
+ managedProjectId?: string
69
+ addedAt: number
70
+ lastUsed: number
71
+ enabled?: boolean
72
+ label?: string
73
+ cachedQuota?: Partial<
74
+ Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
75
+ >
76
+ cachedQuotaUpdatedAt?: number
77
+ cachedQuotaAccountId?: string
78
+ accountIneligible?: boolean
79
+ }
80
+
81
+ type CommandDataQuotaGroup = 'gemini' | 'non-gemini'
82
+
83
+ type CommandDataQuotaGroupSummary = {
84
+ remainingFraction?: number
85
+ resetTime?: string
86
+ modelCount: number
87
+ windows?: Array<{
88
+ window: 'weekly' | '5h'
89
+ remainingFraction: number
90
+ resetTime: string
91
+ }>
92
+ }
93
+
94
+ type CommandDataAccountQuotaResult = {
95
+ index: number
96
+ status: 'ok' | 'disabled' | 'error'
97
+ error?: string
98
+ disabled?: boolean
99
+ quota?: {
100
+ groups: Partial<Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>>
101
+ perModel?: Array<{
102
+ modelId: string
103
+ displayName?: string
104
+ group: CommandDataQuotaGroup | null
105
+ remainingFraction: number
106
+ resetTime?: string
107
+ }>
108
+ modelCount: number
109
+ error?: string
110
+ }
111
+ updatedAccount?: CommandDataAccountMetadata
112
+ }
113
+
114
+ export interface CommandDataAccountStorage {
115
+ version: 4
116
+ activeIndex: number
117
+ activeIndexByFamily?: { claude?: number; gemini?: number }
118
+ accounts: CommandDataAccountMetadata[]
119
+ }
120
+
121
+ /**
122
+ * Privacy-safe per-account row shown in `/antigravity-*` data-first
123
+ * dialogs. Carries the cached quota percentages and the labels the
124
+ * dialog needs, but NEVER the email — the email stays in private
125
+ * account storage and is invisible to the sidebar and the dialog.
126
+ *
127
+ * `index` is the position in the live account array at the time of
128
+ * projection. It is informational only and is NOT a stable identity
129
+ * across refreshes — concurrent OAuth can renumber the array between
130
+ * the dialog opening and a refresh.
131
+ */
132
+ export interface CommandAccountRow {
133
+ /** Stable identity for dialog keying (`acct-<index>`). */
134
+ id: string
135
+ /** Position in the live account array at projection time. */
136
+ index: number
137
+ /** Privacy-safe ordinal display label (for example, `Account 1`). */
138
+ label: string
139
+ enabled: boolean
140
+ /** `true` when this row matches the harness-active account. */
141
+ current: boolean
142
+ /**
143
+ * Absolute timestamp (ms) until which this account is rate-limited.
144
+ * Carried on the row so the sidebar refresher can match cooldowns
145
+ * without relying on unstable numeric indices.
146
+ */
147
+ coolingDownUntil?: number
148
+ /** Health score in [0, 100], forwarded from the live tracker. */
149
+ healthScore?: number
150
+ quota: Array<{
151
+ key: 'gemini' | 'non-gemini'
152
+ label: string
153
+ remainingPercent: number | null
154
+ resetAt?: number
155
+ windows?: Array<{
156
+ window: 'weekly' | '5h'
157
+ remainingPercent: number
158
+ resetAt?: number
159
+ }>
160
+ }>
161
+ /** Captured plan tier. Absent when unknown; never defaulted. */
162
+ tier?: { id: string; capturedAt: number }
163
+ }
164
+
165
+ /**
166
+ * Quota display label for each supported quota group. Kept here so the
167
+ * dialog and the quota manager agree on the same vocabulary.
168
+ */
169
+ const QUOTA_GROUP_LABELS: Record<
170
+ CommandAccountRow['quota'][number]['key'],
171
+ string
172
+ > = {
173
+ gemini: 'Gemini',
174
+ 'non-gemini': 'Non-Gemini',
175
+ }
176
+
177
+ const SUPPORTED_QUOTA_KEYS = [
178
+ 'gemini',
179
+ 'non-gemini',
180
+ ] as const satisfies readonly CommandAccountRow['quota'][number]['key'][]
181
+
182
+ interface LiveAccountSnapshot {
183
+ index: number
184
+ refreshToken: string
185
+ label?: string
186
+ enabled: boolean
187
+ active: boolean
188
+ cachedQuota?: Partial<
189
+ Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
190
+ >
191
+ cachedQuotaUpdatedAt?: number
192
+ cachedQuotaAccountId?: string
193
+ accountIneligible?: boolean
194
+ coolingDownUntil?: number
195
+ healthScore?: number
196
+ /** Captured plan tier from the most recent loadCodeAssist response. */
197
+ capturedTierId?: string
198
+ /** Captured paid-tier ID from the most recent loadCodeAssist response. */
199
+ capturedPaidTierId?: string
200
+ /** Epoch ms when capturedTierId was last recorded. */
201
+ capturedTierAt?: number
202
+ }
203
+
204
+ function toCommandAccountRow(entry: LiveAccountSnapshot): CommandAccountRow {
205
+ // Stamp mismatch: the cached quota was captured for a different account
206
+ // (the refresh token changed, or an index shift placed another account's
207
+ // snapshot at this position). Drop the stale cache rather than rendering
208
+ // the wrong account's quota percentages.
209
+ const rawCached =
210
+ entry.cachedQuotaAccountId &&
211
+ entry.cachedQuotaAccountId !== quotaAccountIdentity(entry.refreshToken)
212
+ ? undefined
213
+ : entry.cachedQuota
214
+ // Normalize legacy pool keys (gemini-pro, gemini-flash, claude, gpt-oss)
215
+ // to canonical keys before the SUPPORTED_QUOTA_KEYS loop. Without this
216
+ // a legacy snapshot produces empty quota rows, and writeSidebar would
217
+ // permanently re-emit legacy keys on every dialog open until the next
218
+ // real quota refresh rewrites the on-disk snapshot.
219
+ const cached = normalizeLegacyCachedQuota(
220
+ rawCached as Parameters<typeof normalizeLegacyCachedQuota>[0],
221
+ )
222
+ const quota: CommandAccountRow['quota'] = []
223
+ for (const key of SUPPORTED_QUOTA_KEYS) {
224
+ const cachedEntry = cached?.[key]
225
+ if (!cachedEntry) continue
226
+ const fraction = cachedEntry.remainingFraction
227
+ const remainingPercent =
228
+ typeof fraction === 'number' && Number.isFinite(fraction)
229
+ ? Math.round(fraction * 100)
230
+ : null
231
+ let resetAt: number | undefined
232
+ if (
233
+ typeof cachedEntry.resetTime === 'string' &&
234
+ cachedEntry.resetTime.length > 0
235
+ ) {
236
+ const parsed = Date.parse(cachedEntry.resetTime)
237
+ if (Number.isFinite(parsed)) resetAt = parsed
238
+ }
239
+ // Carry per-window breakdown when the cache was populated from the
240
+ // windowed RUQS path — the sidebar writer projects through here.
241
+ const windows = cachedEntry.windows?.length
242
+ ? cachedEntry.windows.map((w) => ({
243
+ window: w.window,
244
+ remainingPercent: Math.round(w.remainingFraction * 100),
245
+ resetAt:
246
+ typeof w.resetTime === 'string' && w.resetTime.length > 0
247
+ ? (() => {
248
+ const parsed = Date.parse(w.resetTime)
249
+ return Number.isFinite(parsed) ? parsed : undefined
250
+ })()
251
+ : undefined,
252
+ }))
253
+ : undefined
254
+ quota.push({
255
+ key,
256
+ label: QUOTA_GROUP_LABELS[key],
257
+ remainingPercent,
258
+ resetAt,
259
+ windows,
260
+ })
261
+ }
262
+ const label = `Account ${entry.index + 1}`
263
+ return {
264
+ id: `acct-${entry.index}`,
265
+ index: entry.index,
266
+ label,
267
+ enabled: entry.enabled,
268
+ current: entry.active,
269
+ coolingDownUntil: entry.coolingDownUntil,
270
+ healthScore: entry.healthScore,
271
+ quota,
272
+ tier: toCapturedTier(entry),
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Opaque identity derived from a refresh token. Antigravity refresh tokens
278
+ * are stable (they do not rotate), so this hash is a durable, prunable
279
+ * identity to detect a stale cached quota after an account-index shift.
280
+ */
281
+ function quotaAccountIdentity(refreshToken: string): string {
282
+ return createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)
283
+ }
284
+
285
+ export function projectCommandAccountRows(
286
+ storage: CommandDataAccountStorage | null | undefined,
287
+ ): CommandAccountRow[] {
288
+ if (!storage) return []
289
+ const activeIndex = storage.activeIndexByFamily?.claude ?? storage.activeIndex
290
+ return storage.accounts.map((entry, index) =>
291
+ toCommandAccountRow({
292
+ index,
293
+ refreshToken: entry.refreshToken,
294
+ label: entry.label,
295
+ enabled: entry.enabled !== false,
296
+ active: index === activeIndex,
297
+ cachedQuota: entry.cachedQuota,
298
+ cachedQuotaUpdatedAt: entry.cachedQuotaUpdatedAt,
299
+ cachedQuotaAccountId: entry.cachedQuotaAccountId,
300
+ }),
301
+ )
302
+ }
303
+
304
+ /**
305
+ * Live AccountManager view the data service needs.
306
+ *
307
+ * - `getAccounts()` returns the in-memory snapshot — used for cache-only
308
+ * reads during dialog open and for re-reading the post-mutation state.
309
+ * `getAccountsForQuotaCheck()` returns the freshest `AccountMetadataV3`
310
+ * (the canonical shape the quota manager expects).
311
+ * - `updateQuotaCache(index, groups)` + `requestSaveToDisk()` fold the
312
+ * refreshed quota back into the live view and persist it. The service
313
+ * calls them together after the network fetch resolves.
314
+ * - `setAccountEnabled`, `setAccountCurrent`, `removeAccountByIndex`
315
+ * mirror the CLI menu's mutation primitives — the dialog actions
316
+ * (Task 10) reuse them so the TUI never invents its own mutation
317
+ * logic. Each method returns `true` when the live view changed.
318
+ * - `getRefreshTokenAt(index)` lets the service identify the canonical
319
+ * account identity before it reaches the locked storage mutator; a
320
+ * concurrent OAuth could renumber the flat array between the dialog
321
+ * opening and the apply, so keying by refresh token is mandatory.
322
+ * - `flushSaveToDisk()` drains the AccountManager's debounced save so a
323
+ * dialog-triggered mutation lands on disk before the dialog's response
324
+ * returns. Without it, the dialog could toast "Account removed" while
325
+ * the file still carries the old pool.
326
+ */
327
+ export interface CommandDataAccountManagerView {
328
+ getAccounts(): LiveAccountSnapshot[]
329
+ getAccountsForQuotaCheck(): CommandDataAccountMetadata[]
330
+ updateQuotaCache(
331
+ index: number,
332
+ groups: Partial<
333
+ Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
334
+ >,
335
+ expectedRefreshToken?: string,
336
+ ): void
337
+ requestSaveToDisk(): void
338
+ flushSaveToDisk(): Promise<void>
339
+ /**
340
+ * Per-family live cursor. Used by the remove path so the persisted
341
+ * `activeIndexByFamily` follows each model's currently-active account
342
+ * independently — collapsing both families to a single index would
343
+ * silently unelect one family on every restart.
344
+ */
345
+ getActiveIndexByFamily(): { claude: number; gemini: number }
346
+ /** Enable/disable the account at `index`. Returns true when it changed. */
347
+ setAccountEnabled(index: number, enabled: boolean): boolean
348
+ /** Pin `index` as the active account for every family the dialog cares about. */
349
+ setAccountCurrent(index: number): boolean
350
+ /** Remove the account at `index` from the live view. Returns true when it changed. */
351
+ removeAccountByIndex(index: number): boolean
352
+ /** Canonical refresh token for the account at `index`, or undefined. */
353
+ getRefreshTokenAt(index: number): string | undefined
354
+ }
355
+
356
+ /**
357
+ * Storage adapter the data service uses for re-read-under-lock writes.
358
+ * `mutate` runs the supplied callback against the latest snapshot under
359
+ * the file lock — the callback receives the current storage and may
360
+ * return a replacement; returning the input unchanged is a no-op.
361
+ *
362
+ * The promise resolves with the (possibly mutated) storage snapshot so
363
+ * the data service can await it. A rejected promise signals a failed
364
+ * write — `AccountStorageUnreadableError`, lock contention, or any
365
+ * other I/O failure — which the data service surfaces to the dialog
366
+ * as a friendly error toast.
367
+ */
368
+ export interface CommandDataStorage {
369
+ mutate(
370
+ mutator: (
371
+ current: CommandDataAccountStorage,
372
+ ) =>
373
+ | CommandDataAccountStorage
374
+ | undefined
375
+ | Promise<CommandDataAccountStorage | undefined>,
376
+ ): Promise<CommandDataAccountStorage | undefined> | undefined
377
+ }
378
+
379
+ /**
380
+ * Options for `createCommandDataService`. Each field is required so
381
+ * production wiring is explicit — a missing dependency is a startup
382
+ * error, not a silent no-op at dialog-open time.
383
+ */
384
+ export interface CommandDataServiceOptions {
385
+ accountManagerView: CommandDataAccountManagerView
386
+ quotaManager: {
387
+ refreshAccounts(
388
+ accounts: CommandDataAccountMetadata[],
389
+ options: {
390
+ indexFor?: (account: CommandDataAccountMetadata) => number
391
+ force?: boolean
392
+ },
393
+ ): Promise<CommandDataAccountQuotaResult[]>
394
+ }
395
+ /** Path to the label-only sidebar state file. */
396
+ sidebarStateFile: string
397
+ /**
398
+ * Optional storage adapter. When provided, the service persists the
399
+ * refreshed quota to disk via the lock-held mutator (best-effort —
400
+ * a lock contention never breaks the dialog response). When omitted,
401
+ * the service still folds results into the live AccountManager view;
402
+ * the auth-loader is responsible for the on-disk write.
403
+ */
404
+ storage?: CommandDataStorage
405
+ /** Clock for `cachedQuotaUpdatedAt`. Tests inject a fixed clock. */
406
+ now?: () => number
407
+ }
408
+
409
+ /**
410
+ * Public surface of the command-data service. Each method is the
411
+ * smallest possible projection so the dialog layer never has to know
412
+ * how the underlying quota manager or storage adapter is wired.
413
+ */
414
+ export interface CommandDataService {
415
+ /**
416
+ * Cache-only snapshot. Performs zero quota manager calls — safe to
417
+ * call as part of the dialog's open path.
418
+ */
419
+ listAccounts(): Promise<CommandAccountRow[]>
420
+ /**
421
+ * Force-refresh quota through the shared quota manager, persist by
422
+ * refresh token, bump `cachedQuotaUpdatedAt`, and push a label-only
423
+ * sidebar snapshot. Returns the freshly persisted rows so the
424
+ * dialog can re-render in place.
425
+ *
426
+ * Bypass flag is intentional: explicit user-triggered refreshes
427
+ * ("Refresh" button, direct slash-command with `refresh` argument)
428
+ * must always fetch, even if the quota manager has backed off.
429
+ */
430
+ refreshQuota(): Promise<CommandAccountRow[]>
431
+ /**
432
+ * Non-forced quota check that RESPECTS the quota manager's per-account
433
+ * backoff. Used by the dialog OPEN path — which is a VIEW, not a user
434
+ * request to fetch. Accounts already fresh (or in backoff) are skipped;
435
+ * the result is discarded (the sidebar poller carries the freshness
436
+ * guarantee for idle accounts).
437
+ */
438
+ refreshQuotaRespectingBackoff(): Promise<void>
439
+ /**
440
+ * Pin `index` as the active account for every family the dialog
441
+ * tracks (claude + gemini). Mutates the live AccountManager AND the
442
+ * locked storage so the new active index survives a restart. Returns
443
+ * the freshly projected rows so the dialog can re-render in place.
444
+ * Returns `null` when the index is out of range or the account has
445
+ * no refresh token — the dialog surfaces the null as a toast.
446
+ */
447
+ setCurrentAccount(index: number): Promise<CommandAccountRow[] | null>
448
+ /**
449
+ * Flip the `enabled` flag on the account at `index`. Mirrors the CLI
450
+ * menu's "manage" toggle so the on-disk state matches what the CLI
451
+ * would produce. Returns the freshly projected rows (or `null` when
452
+ * the index is invalid or the account is ineligible).
453
+ */
454
+ toggleAccountEnabled(index: number): Promise<CommandAccountRow[] | null>
455
+ /**
456
+ * Remove the account at `index` from both the live view and the
457
+ * locked storage. Removal renumbers the flat `accounts[]` array so
458
+ * the returned rows use the freshest indices; callers MUST re-key
459
+ * their transient dialog IDs (`acct-${index}`) by the row's `id`
460
+ * field after this method returns.
461
+ *
462
+ * Returns `null` when the index is out of range — the dialog
463
+ * surfaces the null as a toast.
464
+ */
465
+ removeAccount(index: number): Promise<CommandAccountRow[] | null>
466
+ }
467
+
468
+ /**
469
+ * Build the data service.
470
+ *
471
+ * The factory form (instead of a module-level singleton) keeps the
472
+ * service unit-testable: each test constructs its own dependencies
473
+ * (storage stub, quota manager stub, fixed clock) without touching
474
+ * the production quota path.
475
+ */
476
+ export function createCommandDataService(
477
+ options: CommandDataServiceOptions,
478
+ ): CommandDataService {
479
+ const {
480
+ accountManagerView,
481
+ quotaManager,
482
+ sidebarStateFile,
483
+ storage,
484
+ now = () => Date.now(),
485
+ } = options
486
+
487
+ const projectRows = (): CommandAccountRow[] =>
488
+ accountManagerView.getAccounts().map(toCommandAccountRow)
489
+
490
+ // Map a CommandAccountRow back to the SidebarAccountRedactionInput shape.
491
+ // Extracted to prevent the "sixth dropped field" pattern: every field in
492
+ // CommandAccountRow that must reach the sidebar lives here once, not as an
493
+ // inline literal that a future author has to remember to update in two places.
494
+ const toRedactionInput = (
495
+ row: CommandAccountRow,
496
+ ): SidebarAccountRedactionInput => {
497
+ const gemini = row.quota.find((q) => q.key === 'gemini')
498
+ const nonGemini = row.quota.find((q) => q.key === 'non-gemini')
499
+ // Map a CommandAccountRow quota entry to cachedQuota pool shape
500
+ // so projectQuotaPoolForSidebar (the canonical projection seam)
501
+ // carries the per-window breakdown into the sidebar state.
502
+ const toPool = (
503
+ q:
504
+ | {
505
+ remainingPercent: number | null
506
+ resetAt?: number
507
+ windows?: CommandAccountRow['quota'][number]['windows']
508
+ }
509
+ | undefined,
510
+ ) => {
511
+ if (!q || q.remainingPercent == null) return undefined
512
+ return {
513
+ remainingFraction: q.remainingPercent / 100,
514
+ resetTime:
515
+ typeof q.resetAt === 'number' && Number.isFinite(q.resetAt)
516
+ ? new Date(q.resetAt).toISOString()
517
+ : undefined,
518
+ windows: q.windows?.map((w) => ({
519
+ window: w.window,
520
+ remainingFraction: w.remainingPercent / 100,
521
+ resetTime:
522
+ typeof w.resetAt === 'number' && Number.isFinite(w.resetAt)
523
+ ? new Date(w.resetAt).toISOString()
524
+ : '',
525
+ })),
526
+ }
527
+ }
528
+ return {
529
+ index: row.index,
530
+ label: row.label,
531
+ enabled: row.enabled,
532
+ current: row.current,
533
+ healthScore: row.healthScore,
534
+ // Tier passes the redaction boundary unchanged (plan metadata, not PII).
535
+ // The stamp check was already done by toCommandAccountRow.
536
+ tier: row.tier,
537
+ cachedQuota: {
538
+ gemini: toPool(gemini),
539
+ 'non-gemini': toPool(nonGemini),
540
+ },
541
+ }
542
+ }
543
+
544
+ const writeSidebar = (rows: CommandAccountRow[]): void => {
545
+ const accounts = rows.map(toRedactionInput)
546
+ // Fire-and-forget -- the sidebar writer is fenced by its own queue,
547
+ // so a transient lock contention cannot block the dialog response.
548
+ void setSidebarMachineState(
549
+ buildSidebarMachineStateFromAccounts(accounts, { checkedAt: now() }),
550
+ { stateFile: sidebarStateFile },
551
+ ).catch(() => {
552
+ // Sidebar writes are best-effort; the next command or quota refresh
553
+ // will publish the current snapshot.
554
+ })
555
+ }
556
+
557
+ return {
558
+ async listAccounts() {
559
+ return projectRows()
560
+ },
561
+
562
+ async refreshQuota() {
563
+ const accountsForQuota = accountManagerView.getAccountsForQuotaCheck()
564
+ if (accountsForQuota.length === 0) {
565
+ // Empty pool: still push a clean sidebar so the TUI drops any
566
+ // stale snapshot left over from a previous session.
567
+ writeSidebar([])
568
+ return []
569
+ }
570
+
571
+ const results = await quotaManager.refreshAccounts(accountsForQuota, {
572
+ indexFor: (account) => accountsForQuota.indexOf(account),
573
+ force: true,
574
+ })
575
+
576
+ const refreshedAt = now()
577
+ const updates: Array<{
578
+ refreshToken: string
579
+ groups?: Partial<
580
+ Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
581
+ >
582
+ }> = []
583
+ for (const result of results) {
584
+ const refreshToken =
585
+ result.updatedAccount?.refreshToken ??
586
+ accountsForQuota[result.index]?.refreshToken
587
+ if (!refreshToken) continue
588
+ const groups =
589
+ result.status === 'ok' && result.quota?.groups
590
+ ? result.quota.groups
591
+ : undefined
592
+ updates.push({ refreshToken, groups })
593
+ }
594
+
595
+ // Resolve live indexes only after the network request. Numeric indexes
596
+ // from the quota result refer to the original input array and may now
597
+ // identify a different account after a concurrent add/remove.
598
+ const liveIndexByRefreshToken = new Map<string, number>()
599
+ for (const entry of accountManagerView.getAccounts()) {
600
+ liveIndexByRefreshToken.set(entry.refreshToken, entry.index)
601
+ }
602
+ let liveQuotaChanged = false
603
+ for (const update of updates) {
604
+ const liveIndex = liveIndexByRefreshToken.get(update.refreshToken)
605
+ if (liveIndex === undefined || !update.groups) continue
606
+ accountManagerView.updateQuotaCache(
607
+ liveIndex,
608
+ update.groups,
609
+ update.refreshToken,
610
+ )
611
+ liveQuotaChanged = true
612
+ }
613
+ if (liveQuotaChanged) accountManagerView.requestSaveToDisk()
614
+
615
+ // Persist by canonical refresh token against the latest locked snapshot.
616
+ // A removed account is skipped rather than falling back to its old index.
617
+ if (storage) {
618
+ const updateByRefreshToken = new Map(
619
+ updates.map((update) => [update.refreshToken, update]),
620
+ )
621
+ const writeResult = storage.mutate((current) => ({
622
+ ...current,
623
+ accounts: current.accounts.map((entry) => {
624
+ const update = updateByRefreshToken.get(entry.refreshToken)
625
+ if (!update) return entry
626
+ if (update.groups) {
627
+ return {
628
+ ...entry,
629
+ cachedQuota: update.groups,
630
+ // Stamp the persisted quota with an opaque identity derived
631
+ // from the refresh token so a later projection can detect
632
+ // a stale snapshot after an account-index shift.
633
+ cachedQuotaAccountId: quotaAccountIdentity(entry.refreshToken),
634
+ cachedQuotaUpdatedAt: refreshedAt,
635
+ }
636
+ }
637
+ // Error result keeps the previous cached percentage and only
638
+ // records that a refresh was attempted.
639
+ return {
640
+ ...entry,
641
+ cachedQuotaUpdatedAt: refreshedAt,
642
+ }
643
+ }),
644
+ }))
645
+ await Promise.resolve(writeResult).catch(() => {
646
+ // The live AccountManager already carries successful refreshes; its
647
+ // next save can reconcile a transient storage-lock failure.
648
+ })
649
+ }
650
+
651
+ // Re-read the live view post-mutate so the rows reflect the
652
+ // freshly persisted percentages.
653
+ const rows = projectRows()
654
+ writeSidebar(rows)
655
+ return rows
656
+ },
657
+
658
+ async refreshQuotaRespectingBackoff() {
659
+ const accountsForQuota = accountManagerView.getAccountsForQuotaCheck()
660
+ if (accountsForQuota.length === 0) return
661
+
662
+ // Non-forced: the quota manager's per-account backoff and dedup
663
+ // apply. Accounts that are fresh or in backoff are skipped. Unlike
664
+ // the earlier fire-and-forget path, successful results ARE folded
665
+ // into the live cache so the sidebar snapshot reflects fresh data
666
+ // immediately — the dialog renders ONLY from the sidebar file, and
667
+ // the quota manager's push path does not update AccountManager
668
+ // entries for this caller.
669
+ let results: Awaited<ReturnType<typeof quotaManager.refreshAccounts>>
670
+ try {
671
+ results = await quotaManager.refreshAccounts(accountsForQuota, {
672
+ indexFor: (account) => accountsForQuota.indexOf(account),
673
+ force: false,
674
+ })
675
+ } catch {
676
+ // Non-critical: dialog open must never fail because a background
677
+ // refresh check encountered backoff or a transient error.
678
+ return
679
+ }
680
+
681
+ // Build updates akin to the forced refresh path — resolve live
682
+ // indexes after the network call (a concurrent add/remove may have
683
+ // shifted positions), fold successful results into the live cache,
684
+ // then push a sidebar snapshot.
685
+ const updates: Array<{
686
+ refreshToken: string
687
+ groups?: Partial<
688
+ Record<CommandDataQuotaGroup, CommandDataQuotaGroupSummary>
689
+ >
690
+ }> = []
691
+ for (const result of results) {
692
+ const refreshToken =
693
+ result.updatedAccount?.refreshToken ??
694
+ accountsForQuota[result.index]?.refreshToken
695
+ if (!refreshToken) continue
696
+ const groups =
697
+ result.status === 'ok' && result.quota?.groups
698
+ ? result.quota.groups
699
+ : undefined
700
+ updates.push({ refreshToken, groups })
701
+ }
702
+
703
+ if (updates.length === 0) return
704
+
705
+ const liveIndexByRefreshToken = new Map<string, number>()
706
+ for (const entry of accountManagerView.getAccounts()) {
707
+ liveIndexByRefreshToken.set(entry.refreshToken, entry.index)
708
+ }
709
+ let liveQuotaChanged = false
710
+ for (const update of updates) {
711
+ const liveIndex = liveIndexByRefreshToken.get(update.refreshToken)
712
+ if (liveIndex === undefined || !update.groups) continue
713
+ accountManagerView.updateQuotaCache(
714
+ liveIndex,
715
+ update.groups,
716
+ update.refreshToken,
717
+ )
718
+ liveQuotaChanged = true
719
+ }
720
+ if (liveQuotaChanged) accountManagerView.requestSaveToDisk()
721
+
722
+ // Push a fresh sidebar snapshot — the live view now carries the
723
+ // just-refreshed quota plus any skipped (backoff/fresh) accounts
724
+ // whose cached percentages are unchanged.
725
+ const rows = projectRows()
726
+ writeSidebar(rows)
727
+ },
728
+
729
+ async setCurrentAccount(index) {
730
+ return mutateLiveAndStorage({ action: 'setCurrent', index })
731
+ },
732
+
733
+ async toggleAccountEnabled(index) {
734
+ return mutateLiveAndStorage({ action: 'toggleEnabled', index })
735
+ },
736
+
737
+ async removeAccount(index) {
738
+ return mutateLiveAndStorage({ action: 'remove', index })
739
+ },
740
+ }
741
+
742
+ async function mutateLiveAndStorage(args: {
743
+ action: 'setCurrent' | 'toggleEnabled' | 'remove'
744
+ index: number
745
+ }): Promise<CommandAccountRow[] | null> {
746
+ const { index, action } = args
747
+ const target = accountManagerView.getAccounts()[index]
748
+ if (!target) return null
749
+ const refreshToken =
750
+ accountManagerView.getRefreshTokenAt(index) ?? target.refreshToken
751
+ if (!refreshToken) return null
752
+ if (
753
+ action === 'toggleEnabled' &&
754
+ target.enabled === false &&
755
+ target.accountIneligible === true
756
+ ) {
757
+ throw new Error(
758
+ 'This account is ineligible and cannot be enabled until eligibility is rechecked.',
759
+ )
760
+ }
761
+
762
+ if (!storage) {
763
+ throw new Error(
764
+ 'CommandDataService is missing a locked-storage adapter; account mutations are disabled.',
765
+ )
766
+ }
767
+
768
+ // Capture the live view's current-account identity per family BEFORE
769
+ // the storage mutation. The remove action must persist the index that
770
+ // the same account will occupy AFTER the removal — unconditionally
771
+ // resetting to 0 made a non-current removal promote whichever account
772
+ // shifted into slot 0 to "active" on the next restart. Each model's
773
+ // cursor is tracked independently, so collapse-tokens does not work
774
+ // here: a Claude-active account and a Gemini-active account can
775
+ // point at different rows.
776
+ const liveCurrentTokens: { claude?: string; gemini?: string } = {}
777
+ if (action === 'remove') {
778
+ const liveAccounts = accountManagerView.getAccounts()
779
+ const liveIndexes = accountManagerView.getActiveIndexByFamily()
780
+ liveCurrentTokens.claude =
781
+ liveAccounts[liveIndexes.claude]?.refreshToken ?? undefined
782
+ liveCurrentTokens.gemini =
783
+ liveAccounts[liveIndexes.gemini]?.refreshToken ?? undefined
784
+ }
785
+
786
+ let foundInStorage = false
787
+ let desiredEnabled: boolean | undefined
788
+ let previousEnabled: boolean | undefined
789
+ let nextActiveIndex = 0
790
+ let nextActiveIndexByFamily: { claude?: number; gemini?: number } = {
791
+ claude: 0,
792
+ gemini: 0,
793
+ }
794
+ await storage.mutate((current) => {
795
+ const tokenIdx = current.accounts.findIndex(
796
+ (entry) => entry.refreshToken === refreshToken,
797
+ )
798
+ if (tokenIdx === -1) return current
799
+ foundInStorage = true
800
+
801
+ if (action === 'setCurrent') {
802
+ return {
803
+ ...current,
804
+ activeIndex: tokenIdx,
805
+ activeIndexByFamily: { claude: tokenIdx, gemini: tokenIdx },
806
+ }
807
+ }
808
+
809
+ if (action === 'toggleEnabled') {
810
+ const entry = current.accounts[tokenIdx]
811
+ if (!entry) return current
812
+ previousEnabled = entry.enabled !== false
813
+ desiredEnabled = entry.enabled === false
814
+ if (desiredEnabled && entry.accountIneligible === true) {
815
+ throw new Error(
816
+ 'This account is ineligible and cannot be enabled until eligibility is rechecked.',
817
+ )
818
+ }
819
+ return {
820
+ ...current,
821
+ accounts: current.accounts.map((account) =>
822
+ account.refreshToken === refreshToken
823
+ ? { ...account, enabled: desiredEnabled }
824
+ : account,
825
+ ),
826
+ }
827
+ }
828
+
829
+ // remove: build the post-removal account list, then resolve the
830
+ // current-account's NEW index in that list. If the removed
831
+ // account was the live current, the captured token falls out of
832
+ // the list — the live AccountManager leaves the numeric current
833
+ // cursor at the SAME index (now occupied by whichever account
834
+ // shifted in), so the persisted index must follow that numeric
835
+ // slot rather than resetting to 0.
836
+ const nextAccounts = current.accounts.filter(
837
+ (account) => account.refreshToken !== refreshToken,
838
+ )
839
+ const resolveNextIndex = (
840
+ liveToken: string | undefined,
841
+ legacyIndex: number,
842
+ ): number => {
843
+ if (!liveToken)
844
+ return Math.max(0, Math.min(legacyIndex, nextAccounts.length - 1))
845
+ const found = nextAccounts.findIndex(
846
+ (account) => account.refreshToken === liveToken,
847
+ )
848
+ if (found !== -1) return found
849
+ // The captured current token was the one removed. Mirror the
850
+ // core's in-memory semantics: keep the cursor at the removed
851
+ // slot when it still exists in the new array; when the removed
852
+ // slot was the last position and no longer exists, persist 0
853
+ // (the core's buildStorageSnapshot clamps negative sentinels
854
+ // to 0, and auth-doctor treats a negative activeIndex as
855
+ // corruption — cf. auth-doctor.ts:149-156).
856
+ //
857
+ // Use tokenIdx (the storage-snapshot position) rather than the
858
+ // outer-scope `index` (live-read position): a concurrent mutation
859
+ // between the live read and the lock can diverge the two, and the
860
+ // storage-level cursor must match the storage-level array.
861
+ return tokenIdx < nextAccounts.length ? tokenIdx : 0
862
+ }
863
+ const legacyClaude =
864
+ current.activeIndexByFamily?.claude ?? current.activeIndex
865
+ const legacyGemini =
866
+ current.activeIndexByFamily?.gemini ?? current.activeIndex
867
+ nextActiveIndex = resolveNextIndex(liveCurrentTokens.claude, legacyClaude)
868
+ nextActiveIndexByFamily = {
869
+ claude: resolveNextIndex(liveCurrentTokens.claude, legacyClaude),
870
+ gemini: resolveNextIndex(liveCurrentTokens.gemini, legacyGemini),
871
+ }
872
+ return {
873
+ ...current,
874
+ accounts: nextAccounts,
875
+ activeIndex: nextActiveIndex,
876
+ activeIndexByFamily: nextActiveIndexByFamily,
877
+ }
878
+ })
879
+
880
+ if (!foundInStorage) return null
881
+
882
+ // Re-resolve the live index by canonical identity after the awaited disk
883
+ // transaction. A concurrent OAuth add/remove may have shifted every index.
884
+ const liveIndex = accountManagerView
885
+ .getAccounts()
886
+ .findIndex((account) => account.refreshToken === refreshToken)
887
+ let applied = action === 'remove' && liveIndex === -1
888
+ if (liveIndex !== -1) {
889
+ if (action === 'setCurrent') {
890
+ applied = accountManagerView.setAccountCurrent(liveIndex)
891
+ } else if (action === 'toggleEnabled') {
892
+ applied = accountManagerView.setAccountEnabled(
893
+ liveIndex,
894
+ desiredEnabled === true,
895
+ )
896
+ if (!applied) {
897
+ applied =
898
+ accountManagerView.getAccounts()[liveIndex]?.enabled ===
899
+ desiredEnabled
900
+ }
901
+ } else {
902
+ applied = accountManagerView.removeAccountByIndex(liveIndex)
903
+ }
904
+ }
905
+
906
+ if (!applied && action !== 'remove') {
907
+ if (action === 'toggleEnabled' && previousEnabled !== undefined) {
908
+ await storage.mutate((current) => ({
909
+ ...current,
910
+ accounts: current.accounts.map((account) =>
911
+ account.refreshToken === refreshToken
912
+ ? { ...account, enabled: previousEnabled }
913
+ : account,
914
+ ),
915
+ }))
916
+ }
917
+ throw new Error(
918
+ 'The account changed while the operation was being applied; reopen the dialog and try again.',
919
+ )
920
+ }
921
+
922
+ await accountManagerView.flushSaveToDisk().catch(() => {
923
+ // The locked storage mutation already committed; a later periodic flush
924
+ // can reconcile transient AccountManager lock contention.
925
+ })
926
+
927
+ const rows = projectRows()
928
+ writeSidebar(rows)
929
+ return rows
930
+ }
931
+ }