@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,991 @@
1
+ /**
2
+ * Sidebar state contract for the OpenTUI sidebar.
3
+ *
4
+ * This module is the read-only seam between the long-running plugin and the
5
+ * Solid/OpenTUI sidebar tree. It deliberately does NOT import account storage,
6
+ * the account manager, OAuth code, or any other privileged host-side module:
7
+ * the TUI is rendered inside the host's terminal and a single stray import
8
+ * could leak credentials or pull a heavy manager into the render path.
9
+ *
10
+ * The plugin writes a redacted snapshot to the file resolved by
11
+ * `getSidebarStateFile()` and the TUI polls it. The contract version is `1`:
12
+ * any future field that the TUI cannot understand must be ignored, and any
13
+ * broken/missing file must collapse to `DEFAULT_SIDEBAR_STATE`.
14
+ *
15
+ * ## Writer surface
16
+ *
17
+ * The plugin-side writers live here too so the read and write halves of the
18
+ * contract evolve together. They are imported by the plugin (auth-loader,
19
+ * quota, fetch-interceptor, event-handler, commands) but never invoked from
20
+ * the TUI's compiled tree — that tree only calls the readers, so the
21
+ * heavyweight core imports below never run inside the host's render path.
22
+ *
23
+ * Every disk mutation follows the same recipe:
24
+ *
25
+ * 1. Serialize through `sidebarWriteChain` so concurrent in-process calls
26
+ * never interleave merges against the same file.
27
+ * 2. Acquire Task 7's `acquireFencedFileLock` with bounded retry+jitter
28
+ * (≤2s). A live cross-process holder that does not release in time
29
+ * surfaces as `SidebarStateLockContentionError`.
30
+ * 3. Re-read and normalize the on-disk state while holding the lock.
31
+ * 4. Merge the new machine or routing payload against the re-read state.
32
+ * 5. `assertOwned()` + `writeJsonAtomic` with mode 0o600, then release.
33
+ *
34
+ * The merge step is deterministic: machine fields adopt only when the new
35
+ * `checkedAt` is ≥ the on-disk one, `routingAuthoritative` is sticky-true,
36
+ * and `activeRouting` is merged independently and pruned to the freshest
37
+ * 100 entries within 24h.
38
+ */
39
+
40
+ import { mkdirSync, readFileSync } from 'node:fs'
41
+ import { homedir } from 'node:os'
42
+ import { dirname, join } from 'node:path'
43
+ // Subpath imports (not the barrel): this file ships into the TUI's
44
+ // compiled tree, and the barrel re-exports account/OAuth/quota modules
45
+ // that must never execute inside the host's render path.
46
+ import { writeJsonAtomic } from '@cortexkit/antigravity-auth-core/atomic-write'
47
+ import {
48
+ acquireFencedFileLock,
49
+ type FencedFileLock,
50
+ } from '@cortexkit/antigravity-auth-core/file-lock'
51
+ import { xdgState } from 'xdg-basedir'
52
+
53
+ export const SIDEBAR_STATE_VERSION = 1 as const
54
+
55
+ export type SidebarQuotaKey = 'gemini' | 'non-gemini'
56
+
57
+ export interface SidebarQuotaWindowEntry {
58
+ window: 'weekly' | '5h'
59
+ remainingPercent: number
60
+ resetAt?: number
61
+ }
62
+
63
+ export interface SidebarQuotaEntry {
64
+ /** Most-constrained window's remaining % (derived, for back-compat). */
65
+ remainingPercent: number
66
+ resetAt?: number
67
+ /** Per-window breakdown. Omitted in pre-windows snapshots. */
68
+ windows?: SidebarQuotaWindowEntry[]
69
+ }
70
+
71
+ export interface SidebarAccountState {
72
+ id: string
73
+ label: string
74
+ enabled: boolean
75
+ health: number
76
+ current: boolean
77
+ cooldownUntil?: number
78
+ quota: Partial<Record<SidebarQuotaKey, SidebarQuotaEntry>>
79
+ /**
80
+ * Captured plan tier. Absent when unknown — do NOT default to `free-tier`.
81
+ * `id` is the raw upstream string (e.g. `"free-tier"`); never normalised.
82
+ */
83
+ tier?: { id: string; paidId?: string; capturedAt: number }
84
+ }
85
+
86
+ export interface SidebarRoutingEntry {
87
+ accountId: string
88
+ modelFamily: 'claude' | 'gemini'
89
+ headerStyle: 'antigravity' | 'gemini-cli'
90
+ strategy?: 'sticky' | 'round-robin' | 'hybrid'
91
+ updatedAt: number
92
+ }
93
+
94
+ export interface SidebarStateV1 {
95
+ version: typeof SIDEBAR_STATE_VERSION
96
+ checkedAt: number
97
+ accounts: SidebarAccountState[]
98
+ activeRouting: Record<string, SidebarRoutingEntry>
99
+ routingAuthoritative: boolean
100
+ quotaBackoffUntil?: number
101
+ lastError?: string
102
+ }
103
+
104
+ /**
105
+ * The subset of `SidebarStateV1` that a non-routing writer may set. The
106
+ * fetch interceptor writes `activeRouting` directly via its own entry point
107
+ * because routing is session-scoped, not machine-scoped.
108
+ */
109
+ export interface SidebarMachineState {
110
+ checkedAt: number
111
+ accounts: SidebarAccountState[]
112
+ quotaBackoffUntil?: number
113
+ lastError?: string
114
+ /**
115
+ * Optional: opt in to mark the snapshot as authoritative. When `true`,
116
+ * the merge keeps the existing `routingAuthoritative: true` even if a
117
+ * later non-authoritative machine write lands. Sticky-true semantics.
118
+ */
119
+ routingAuthoritative?: boolean
120
+ }
121
+
122
+ export const DEFAULT_SIDEBAR_STATE: SidebarStateV1 = {
123
+ version: SIDEBAR_STATE_VERSION,
124
+ checkedAt: 0,
125
+ accounts: [],
126
+ activeRouting: {},
127
+ routingAuthoritative: false,
128
+ }
129
+
130
+ export const SIDEBAR_STATE_ENV = 'ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE'
131
+
132
+ const SIDEBAR_STATE_DIR = 'cortexkit/antigravity-auth'
133
+ const SIDEBAR_STATE_FILENAME = 'sidebar-state.json'
134
+
135
+ /** Active routing entries older than this are dropped on every merge. */
136
+ const ACTIVE_ROUTING_MAX_AGE_MS = 24 * 60 * 60 * 1000
137
+ /** Active routing map is capped at this many newest entries. */
138
+ const ACTIVE_ROUTING_MAX_ENTRIES = 100
139
+
140
+ const SIDEBAR_LOCK_NAME = 'sidebar'
141
+ const SIDEBAR_LOCK_TTL_MS = 10_000
142
+ const SIDEBAR_LOCK_TIMEOUT_MS = 2_000
143
+ const SIDEBAR_LOCK_RETRY_BASE_MS = 25
144
+ const SIDEBAR_LOCK_RETRY_CAP_MS = 75
145
+ const SIDEBAR_LOCK_JITTER_MS = 25
146
+ const SIDEBAR_STATE_DIR_MODE = 0o700
147
+ const SIDEBAR_STATE_FILE_MODE = 0o600
148
+
149
+ /**
150
+ * Thrown by every writer when the cross-process lock cannot be acquired
151
+ * within `SIDEBAR_LOCK_TIMEOUT_MS`. The caller decides whether to surface
152
+ * a toast, drop the write, or retry the next tick.
153
+ */
154
+ export class SidebarStateLockContentionError extends Error {
155
+ readonly details: { stateFile: string; timeoutMs: number }
156
+
157
+ constructor(stateFile: string, timeoutMs: number) {
158
+ super(
159
+ `Could not acquire sidebar-state lock at ${stateFile} within ${timeoutMs}ms`,
160
+ )
161
+ this.name = 'SidebarStateLockContentionError'
162
+ this.details = { stateFile, timeoutMs }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Steps exposed to the merge hooks. Race tests pause writers here; production
168
+ * callers leave the hooks unset (a no-op fast path).
169
+ *
170
+ * - `await-lock` — before invoking `acquireFencedFileLock`.
171
+ * - `acquired-lock` — after the lock is granted and before the read.
172
+ * - `read-state` — after the on-disk state is normalized.
173
+ * - `merged-state` — after the merge but before `writeJsonAtomic`.
174
+ * - `wrote-state` — after the rename but before `lock.release()`.
175
+ */
176
+ export type SidebarMergeStep =
177
+ | 'await-lock'
178
+ | 'acquired-lock'
179
+ | 'read-state'
180
+ | 'merged-state'
181
+ | 'wrote-state'
182
+
183
+ export interface SidebarMergeHooks {
184
+ onStep?: (step: SidebarMergeStep) => Promise<void> | void
185
+ }
186
+
187
+ let sidebarMergeHooks: SidebarMergeHooks | null = null
188
+
189
+ /**
190
+ * Install (or clear with `null`) the deterministic race hooks. Tests use
191
+ * these to inject interleavings; production callers leave them unset. The
192
+ * module-level state means a test must reset hooks in its own `afterEach`
193
+ * to avoid bleeding into the next test.
194
+ */
195
+ export function setSidebarMergeHooks(hooks: SidebarMergeHooks | null): void {
196
+ sidebarMergeHooks = hooks
197
+ }
198
+
199
+ async function emitMergeStep(step: SidebarMergeStep): Promise<void> {
200
+ await sidebarMergeHooks?.onStep?.(step)
201
+ }
202
+
203
+ /**
204
+ * Resolve the on-disk path the plugin writes to and the TUI reads from.
205
+ *
206
+ * - `ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE` wins when set (tests, packaged
207
+ * installers, and any user override).
208
+ * - Otherwise fall back to the XDG state directory, mirroring the path
209
+ * conventions used elsewhere in the project.
210
+ */
211
+ export function getSidebarStateFile(): string {
212
+ const override = process.env[SIDEBAR_STATE_ENV]
213
+ if (override && override.trim().length > 0) return override
214
+ const base = xdgState ?? join(homedir(), '.local', 'state')
215
+ return join(base, SIDEBAR_STATE_DIR, SIDEBAR_STATE_FILENAME)
216
+ }
217
+
218
+ /**
219
+ * Read and normalize the sidebar state file. Returns the default state when
220
+ * the file is missing, unreadable, malformed, or schema-incompatible — the TUI
221
+ * must never throw out of `readSidebarState()`, the panel just shows
222
+ * "Awaiting Antigravity state" and the next poll retries.
223
+ *
224
+ * The read is sync on purpose: the TUI polls on a 2-second timer and the file
225
+ * is tiny (a handful of accounts); an async read here would just add race
226
+ * surface area against Solid's reactive render cycle.
227
+ */
228
+ export function readSidebarState(
229
+ path: string = getSidebarStateFile(),
230
+ ): SidebarStateV1 {
231
+ let raw: string
232
+ try {
233
+ raw = readFileSync(path, 'utf-8')
234
+ } catch {
235
+ return { ...DEFAULT_SIDEBAR_STATE }
236
+ }
237
+ let parsed: unknown
238
+ try {
239
+ parsed = JSON.parse(raw)
240
+ } catch {
241
+ return { ...DEFAULT_SIDEBAR_STATE, lastError: 'malformed-json' }
242
+ }
243
+ return normalizeSidebarState(parsed)
244
+ }
245
+
246
+ function normalizeSidebarState(input: unknown): SidebarStateV1 {
247
+ if (!isObject(input)) {
248
+ return { ...DEFAULT_SIDEBAR_STATE, lastError: 'shape' }
249
+ }
250
+ const record = input as Record<string, unknown>
251
+ const version = record.version
252
+ if (version !== SIDEBAR_STATE_VERSION) {
253
+ return {
254
+ ...DEFAULT_SIDEBAR_STATE,
255
+ lastError: `unsupported-version:${stringifySafe(version)}`,
256
+ }
257
+ }
258
+
259
+ const accountsRaw = record.accounts
260
+ const accounts = Array.isArray(accountsRaw)
261
+ ? accountsRaw
262
+ .map((entry) => normalizeAccount(entry))
263
+ .filter((entry): entry is SidebarAccountState => entry !== null)
264
+ : []
265
+
266
+ const routingRaw = record.activeRouting
267
+ const activeRouting: Record<string, SidebarRoutingEntry> = {}
268
+ if (isObject(routingRaw)) {
269
+ for (const [sessionId, entry] of Object.entries(
270
+ routingRaw as Record<string, unknown>,
271
+ )) {
272
+ const normalized = normalizeRouting(entry)
273
+ if (normalized) activeRouting[sessionId] = normalized
274
+ }
275
+ }
276
+
277
+ const checkedAt = toFiniteNumber(record.checkedAt)
278
+ const routingAuthoritative = record.routingAuthoritative === true
279
+ const quotaBackoffUntil = toFiniteNumber(record.quotaBackoffUntil)
280
+ const lastError =
281
+ typeof record.lastError === 'string' ? record.lastError : undefined
282
+
283
+ return {
284
+ version: SIDEBAR_STATE_VERSION,
285
+ checkedAt: checkedAt ?? 0,
286
+ accounts,
287
+ activeRouting,
288
+ routingAuthoritative,
289
+ quotaBackoffUntil: quotaBackoffUntil ?? undefined,
290
+ lastError,
291
+ }
292
+ }
293
+
294
+ function normalizeAccount(input: unknown): SidebarAccountState | null {
295
+ if (!isObject(input)) return null
296
+ const record = input as Record<string, unknown>
297
+ const id = typeof record.id === 'string' ? record.id : null
298
+ const label = typeof record.label === 'string' ? record.label : null
299
+ if (!id || !label) return null
300
+ const enabled = record.enabled !== false
301
+ const health = clampNumber(toFiniteNumber(record.health), 0, 100)
302
+ const current = record.current === true
303
+ const cooldownUntil = toFiniteNumber(record.cooldownUntil) ?? undefined
304
+ const quotaRaw = record.quota
305
+ const quota: SidebarAccountState['quota'] = {}
306
+ if (isObject(quotaRaw)) {
307
+ for (const key of ['gemini', 'non-gemini'] as const) {
308
+ const entry = (quotaRaw as Record<string, unknown>)[key]
309
+ const normalized = normalizeQuota(entry)
310
+ if (normalized) quota[key] = normalized
311
+ }
312
+ }
313
+ const tier = normalizeTier(record.tier)
314
+ return {
315
+ id,
316
+ label,
317
+ enabled,
318
+ health,
319
+ current,
320
+ cooldownUntil,
321
+ quota,
322
+ ...(tier !== undefined ? { tier } : {}),
323
+ }
324
+ }
325
+
326
+ function normalizeTier(
327
+ input: unknown,
328
+ ): { id: string; paidId?: string; capturedAt: number } | undefined {
329
+ if (!isObject(input)) return undefined
330
+ const record = input as Record<string, unknown>
331
+ const id =
332
+ typeof record.id === 'string' && record.id.length > 0 ? record.id : null
333
+ const capturedAt = toFiniteNumber(record.capturedAt)
334
+ if (!id || capturedAt === null) return undefined
335
+ const paidId =
336
+ typeof record.paidId === 'string' && record.paidId.length > 0
337
+ ? record.paidId
338
+ : undefined
339
+ return { id, ...(paidId ? { paidId } : {}), capturedAt }
340
+ }
341
+
342
+ function normalizeQuota(input: unknown): SidebarQuotaEntry | null {
343
+ if (!isObject(input)) return null
344
+ const record = input as Record<string, unknown>
345
+ const remaining = toFiniteNumber(record.remainingPercent)
346
+ if (remaining === null) return null
347
+ const resetAt = toFiniteNumber(record.resetAt) ?? undefined
348
+
349
+ // Tolerant: deserialize windows array if present; drop malformed entries.
350
+ const windowsRaw = record.windows
351
+ let windows: SidebarQuotaEntry['windows']
352
+ if (Array.isArray(windowsRaw)) {
353
+ const parsed = windowsRaw
354
+ .filter(isObject)
355
+ .map((w) => {
356
+ const win = (w as Record<string, unknown>).window
357
+ const rp = toFiniteNumber(
358
+ (w as Record<string, unknown>).remainingPercent,
359
+ )
360
+ const ra = toFiniteNumber((w as Record<string, unknown>).resetAt)
361
+ if ((win !== 'weekly' && win !== '5h') || rp === null) return null
362
+ return {
363
+ window: win as 'weekly' | '5h',
364
+ remainingPercent: clampNumber(rp, 0, 100),
365
+ resetAt: ra ?? undefined,
366
+ }
367
+ })
368
+ .filter((v): v is NonNullable<typeof v> => v !== null)
369
+ if (parsed.length > 0) windows = parsed
370
+ }
371
+
372
+ return {
373
+ remainingPercent: clampNumber(remaining, 0, 100),
374
+ resetAt,
375
+ windows,
376
+ }
377
+ }
378
+
379
+ function normalizeRouting(input: unknown): SidebarRoutingEntry | null {
380
+ if (!isObject(input)) return null
381
+ const record = input as Record<string, unknown>
382
+ const accountId =
383
+ typeof record.accountId === 'string' ? record.accountId : null
384
+ const modelFamily = record.modelFamily
385
+ const headerStyle = record.headerStyle
386
+ const strategy = record.strategy
387
+ const updatedAt = toFiniteNumber(record.updatedAt) ?? 0
388
+ if (
389
+ !accountId ||
390
+ (modelFamily !== 'claude' && modelFamily !== 'gemini') ||
391
+ (headerStyle !== 'antigravity' && headerStyle !== 'gemini-cli')
392
+ ) {
393
+ return null
394
+ }
395
+ return {
396
+ accountId,
397
+ modelFamily,
398
+ headerStyle,
399
+ strategy:
400
+ strategy === 'sticky' ||
401
+ strategy === 'round-robin' ||
402
+ strategy === 'hybrid'
403
+ ? strategy
404
+ : undefined,
405
+ updatedAt,
406
+ }
407
+ }
408
+
409
+ function isObject(value: unknown): value is Record<string, unknown> {
410
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
411
+ }
412
+
413
+ function toFiniteNumber(value: unknown): number | null {
414
+ if (typeof value === 'number' && Number.isFinite(value)) return value
415
+ if (typeof value === 'string') {
416
+ const parsed = Number(value)
417
+ return Number.isFinite(parsed) ? parsed : null
418
+ }
419
+ return null
420
+ }
421
+
422
+ function clampNumber(value: number | null, min: number, max: number): number {
423
+ if (value === null) return min
424
+ if (value < min) return min
425
+ if (value > max) return max
426
+ return value
427
+ }
428
+
429
+ function stringifySafe(value: unknown): string {
430
+ if (typeof value === 'string') return value
431
+ try {
432
+ return JSON.stringify(value) ?? 'unknown'
433
+ } catch {
434
+ return 'unknown'
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Ensure the parent directory for the sidebar state file exists. Convenience
440
+ * helper used by writers (tests, plugins) — the TUI itself does not write.
441
+ */
442
+ export function ensureSidebarStateDir(
443
+ path: string = getSidebarStateFile(),
444
+ ): void {
445
+ mkdirSync(dirname(path), { recursive: true, mode: SIDEBAR_STATE_DIR_MODE })
446
+ }
447
+
448
+ /**
449
+ * Drop active-routing entries older than 24h and cap the map at the freshest
450
+ * 100. Pure helper exposed for unit testing; the writers call it on every
451
+ * merge so the on-disk map never grows without bound.
452
+ */
453
+ export function pruneActiveRouting(
454
+ map: Record<string, SidebarRoutingEntry>,
455
+ now: number,
456
+ ): Record<string, SidebarRoutingEntry> {
457
+ const cutoff = now - ACTIVE_ROUTING_MAX_AGE_MS
458
+ const filtered: Array<[string, SidebarRoutingEntry]> = []
459
+ for (const [sessionId, entry] of Object.entries(map)) {
460
+ if (entry.updatedAt >= cutoff) {
461
+ filtered.push([sessionId, entry])
462
+ }
463
+ }
464
+ filtered.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
465
+ if (filtered.length <= ACTIVE_ROUTING_MAX_ENTRIES) {
466
+ return Object.fromEntries(filtered)
467
+ }
468
+ return Object.fromEntries(filtered.slice(0, ACTIVE_ROUTING_MAX_ENTRIES))
469
+ }
470
+
471
+ /**
472
+ * Structural input for `redactAccountForSidebar`. Decoupled from the core
473
+ * `ManagedAccount` type so this module never forces a type-level import
474
+ * shape on callers (and so the TUI's compiled tree does not see the core
475
+ * ManagedAccount shape beyond what's actually used).
476
+ *
477
+ * Deliberately excludes `email`: the sidebar/redaction boundary is a PII
478
+ * firewall. Adding `email` here would re-introduce the leak this boundary
479
+ * exists to prevent. Legacy profile labels are accepted but never serialized.
480
+ */
481
+ export interface SidebarAccountRedactionInput {
482
+ /** Position in the harness-visible account array. */
483
+ index: number
484
+ /** Legacy profile label; deliberately ignored by the redactor. */
485
+ label?: string
486
+ enabled?: boolean
487
+ current?: boolean
488
+ coolingDownUntil?: number
489
+ /** Health score in `[0, 100]`. Defaults to 100 when missing. */
490
+ healthScore?: number
491
+ cachedQuota?: {
492
+ gemini?: {
493
+ remainingFraction?: number
494
+ resetTime?: string
495
+ windows?: Array<{
496
+ window: 'weekly' | '5h'
497
+ remainingFraction: number
498
+ resetTime: string
499
+ }>
500
+ }
501
+ 'non-gemini'?: {
502
+ remainingFraction?: number
503
+ resetTime?: string
504
+ windows?: Array<{
505
+ window: 'weekly' | '5h'
506
+ remainingFraction: number
507
+ resetTime: string
508
+ }>
509
+ }
510
+ // Legacy per-pool keys written by older versions of the plugin.
511
+ // Accepted here so the normalizer can map them to the current pool
512
+ // keys at read time without requiring a disk migration.
513
+ [legacyKey: string]:
514
+ | {
515
+ remainingFraction?: number
516
+ resetTime?: string
517
+ modelCount?: number
518
+ windows?: Array<{
519
+ window: 'weekly' | '5h'
520
+ remainingFraction: number
521
+ resetTime: string
522
+ }>
523
+ }
524
+ | undefined
525
+ }
526
+ /**
527
+ * Opaque identity stamp that was attached to the persisted quota snapshot.
528
+ * Used together with `currentQuotaAccountId` to detect a stale cache that
529
+ * landed on the wrong account after an index shift or token replacement.
530
+ * PII-safe — it is a 16-char hash, not the refresh token itself.
531
+ */
532
+ cachedQuotaAccountId?: string
533
+ /**
534
+ * Opaque identity stamp for the account that is currently at this index.
535
+ * The redactor drops `cachedQuota` when `cachedQuotaAccountId` is set
536
+ * AND does not match this value, mirroring `toCommandAccountRow` in the
537
+ * command-data service. Omitted in the persisted sidebar state.
538
+ */
539
+ currentQuotaAccountId?: string
540
+ /**
541
+ * Captured plan tier from `loadCodeAssist`. Absent when unknown. The `id`
542
+ * is the raw upstream string; `capturedAt` is epoch ms. Not PII — tier
543
+ * metadata survives the redaction boundary unchanged.
544
+ */
545
+ tier?: { id: string; paidId?: string; capturedAt: number }
546
+ }
547
+
548
+ /**
549
+ * Build a `{ id, capturedAt }` tier object from account fields, or `undefined`
550
+ * when either field is absent. Centralised so every producer (index.ts,
551
+ * background-quota-refresh.ts, command-data.ts) uses the same guard and the
552
+ * same shape -- the same pattern shipped as inline literals across three files
553
+ * and produced six missed-field bugs; one helper ends that.
554
+ */
555
+ export function toCapturedTier(account: {
556
+ capturedTierId?: string
557
+ capturedPaidTierId?: string
558
+ capturedTierAt?: number
559
+ }): { id: string; paidId?: string; capturedAt: number } | undefined {
560
+ return account.capturedTierId !== undefined &&
561
+ account.capturedTierAt !== undefined
562
+ ? {
563
+ id: account.capturedTierId,
564
+ ...(account.capturedPaidTierId !== undefined
565
+ ? { paidId: account.capturedPaidTierId }
566
+ : {}),
567
+ capturedAt: account.capturedTierAt,
568
+ }
569
+ : undefined
570
+ }
571
+
572
+ /**
573
+ * Map pre-pool legacy quota keys to the current two-pool schema at read time.
574
+ *
575
+ * Legacy mappings (empirically settled from burn tests):
576
+ * `gemini-pro` + `gemini-flash` → `gemini`
577
+ * `claude` + `gpt-oss` → `non-gemini`
578
+ *
579
+ * Where multiple legacy keys collapse into one pool the MIN remainingFraction
580
+ * and earliest resetTime are used (most-constrained-first, matching the
581
+ * window-level rule). No `windows` arrays are invented for migrated entries;
582
+ * the single aggregate bar is the correct render for pre-window snapshots.
583
+ *
584
+ * Non-legacy keys that are already canonical (`gemini`, `non-gemini`) are
585
+ * carried through unchanged. The next real quota refresh overwrites any
586
+ * migrated snapshot with authoritative data.
587
+ */
588
+ export function normalizeLegacyCachedQuota(
589
+ raw: SidebarAccountRedactionInput['cachedQuota'],
590
+ ): SidebarAccountRedactionInput['cachedQuota'] {
591
+ if (!raw) return raw
592
+
593
+ // Pool already carries current keys — fast path when no legacy keys present.
594
+ const hasLegacy =
595
+ 'gemini-pro' in raw ||
596
+ 'gemini-flash' in raw ||
597
+ 'claude' in raw ||
598
+ 'gpt-oss' in raw
599
+ if (!hasLegacy) return raw
600
+
601
+ type PoolEntry = NonNullable<
602
+ SidebarAccountRedactionInput['cachedQuota']
603
+ >['gemini']
604
+
605
+ // Pick the earlier of two ISO reset timestamps, preferring a defined
606
+ // value over undefined. Extracted so both branches of minFraction use
607
+ // the same merge rather than the previous asymmetry where the a-wins
608
+ // branch dropped b's possibly-earlier resetTime.
609
+ const earlierResetTime = (
610
+ a: string | undefined,
611
+ b: string | undefined,
612
+ ): string | undefined => {
613
+ if (!a) return b
614
+ if (!b) return a
615
+ return a < b ? a : b
616
+ }
617
+
618
+ const minFraction = (a: PoolEntry, b: PoolEntry): PoolEntry => {
619
+ if (!a) return b
620
+ if (!b) return a
621
+ const fa = a.remainingFraction ?? 1
622
+ const fb = b.remainingFraction ?? 1
623
+ const winner = fa <= fb ? a : b
624
+ const loser = fa <= fb ? b : a
625
+ // Always merge to the earliest resetTime regardless of which pool wins
626
+ // the fraction comparison — an earlier window expiry from the loser
627
+ // pool must not be silently dropped.
628
+ return {
629
+ ...winner,
630
+ resetTime: earlierResetTime(winner.resetTime, loser.resetTime),
631
+ }
632
+ }
633
+
634
+ const gemini: PoolEntry =
635
+ minFraction(
636
+ raw.gemini,
637
+ minFraction(
638
+ raw['gemini-pro'] as PoolEntry,
639
+ raw['gemini-flash'] as PoolEntry,
640
+ ),
641
+ ) ?? raw.gemini
642
+
643
+ const nonGemini: PoolEntry =
644
+ minFraction(
645
+ raw['non-gemini'],
646
+ minFraction(raw.claude as PoolEntry, raw['gpt-oss'] as PoolEntry),
647
+ ) ?? raw['non-gemini']
648
+
649
+ return {
650
+ ...(gemini !== undefined ? { gemini } : {}),
651
+ ...(nonGemini !== undefined ? { 'non-gemini': nonGemini } : {}),
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Project a raw cachedQuota pool entry into the sidebar-safe shape.
657
+ *
658
+ * Centralized seam — every producer (quota.ts pushSidebarQuotaSnapshot,
659
+ * auth-loader.ts materializer, command-data.ts writeSidebar) routes
660
+ * through here so the `windows` array is never dropped by an inline
661
+ * `{ remainingFraction, resetTime }` literal.
662
+ *
663
+ * Tolerant: returns `undefined` when the source is missing or the
664
+ * `remainingFraction` is not a finite number. Legacy entries without
665
+ * `windows` produce `{ remainingPercent, resetAt }` only — the TUI's
666
+ * legacy path then renders a single bar.
667
+ */
668
+ export function projectQuotaPoolForSidebar(source: {
669
+ remainingFraction?: number
670
+ resetTime?: string
671
+ windows?: ReadonlyArray<{
672
+ window: 'weekly' | '5h'
673
+ remainingFraction: number
674
+ resetTime: string
675
+ }>
676
+ }): SidebarQuotaEntry | undefined {
677
+ const fraction = source.remainingFraction
678
+ if (typeof fraction !== 'number' || !Number.isFinite(fraction))
679
+ return undefined
680
+ const remainingPercent = clampNumber(Math.round(fraction * 100), 0, 100)
681
+ let resetAt: number | undefined
682
+ if (typeof source.resetTime === 'string' && source.resetTime.length > 0) {
683
+ const parsed = Date.parse(source.resetTime)
684
+ if (Number.isFinite(parsed)) resetAt = parsed
685
+ }
686
+
687
+ const windows: SidebarQuotaEntry['windows'] = source.windows?.length
688
+ ? source.windows.map((w) => ({
689
+ window: w.window,
690
+ remainingPercent: clampNumber(
691
+ Math.round(w.remainingFraction * 100),
692
+ 0,
693
+ 100,
694
+ ),
695
+ resetAt:
696
+ typeof w.resetTime === 'string' && w.resetTime.length > 0
697
+ ? (() => {
698
+ const parsed = Date.parse(w.resetTime)
699
+ return Number.isFinite(parsed) ? parsed : undefined
700
+ })()
701
+ : undefined,
702
+ }))
703
+ : undefined
704
+
705
+ return { remainingPercent, resetAt, windows }
706
+ }
707
+
708
+ /**
709
+ * Returns `true` when an account at `index` is the active account for
710
+ * at least one model family. Two accounts CAN both be current — one
711
+ * serving claude, one serving gemini — so this must not collapse to one.
712
+ */
713
+ export function isAccountCurrent(
714
+ index: number,
715
+ activeIndexByFamily: { claude: number; gemini: number },
716
+ ): boolean {
717
+ return (
718
+ index === activeIndexByFamily.claude || index === activeIndexByFamily.gemini
719
+ )
720
+ }
721
+
722
+ /**
723
+ * Convert a live account snapshot into the redacted shape the TUI renders.
724
+ * The redacted `SidebarAccountState` carries no email, refresh token, access
725
+ * token, project ID, fingerprint, OAuth profile name, or other personal or
726
+ * credential-shaped fields. Display labels are generated ordinal account names.
727
+ */
728
+ export function redactAccountForSidebar(
729
+ source: SidebarAccountRedactionInput,
730
+ ): SidebarAccountState {
731
+ const id = `acct-${source.index}`
732
+ const label = `Account ${source.index + 1}`
733
+ const enabled = source.enabled !== false
734
+ const current = source.current === true
735
+ const cooldownUntil =
736
+ typeof source.coolingDownUntil === 'number' &&
737
+ Number.isFinite(source.coolingDownUntil)
738
+ ? source.coolingDownUntil
739
+ : undefined
740
+ const health = clampNumber(
741
+ typeof source.healthScore === 'number' ? source.healthScore : 100,
742
+ 0,
743
+ 100,
744
+ )
745
+
746
+ const quota: SidebarAccountState['quota'] = {}
747
+ // Stamp mismatch: the persisted quota snapshot was captured for a
748
+ // different account than the one currently at this index. Drop the
749
+ // stale cache rather than rendering the wrong account's quota
750
+ // percentages. Mirrors `toCommandAccountRow` in command-data.
751
+ const staleCachedQuota =
752
+ typeof source.cachedQuotaAccountId === 'string' &&
753
+ typeof source.currentQuotaAccountId === 'string' &&
754
+ source.cachedQuotaAccountId !== source.currentQuotaAccountId
755
+ // Normalize legacy per-pool keys to the current two-pool schema before
756
+ // reading. This is non-destructive: the next real quota refresh will
757
+ // overwrite the migrated snapshot with authoritative data.
758
+ const cached = staleCachedQuota
759
+ ? undefined
760
+ : normalizeLegacyCachedQuota(source.cachedQuota)
761
+ if (cached) {
762
+ for (const key of ['gemini', 'non-gemini'] as const) {
763
+ const entry = cached[key]
764
+ if (!entry) continue
765
+ const projected = projectQuotaPoolForSidebar(entry)
766
+ if (projected) quota[key] = projected
767
+ }
768
+ }
769
+
770
+ return {
771
+ id,
772
+ label,
773
+ enabled,
774
+ health,
775
+ current,
776
+ cooldownUntil,
777
+ quota,
778
+ // Tier is plan metadata, not PII — it passes the redaction boundary.
779
+ ...(source.tier !== undefined ? { tier: source.tier } : {}),
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Build a `SidebarMachineState` from a list of live account snapshots.
785
+ * Convenience for the auth-loader / quota writer call sites that already
786
+ * hold an array of accounts and want to push a single snapshot.
787
+ */
788
+ export function buildSidebarMachineStateFromAccounts(
789
+ accounts: SidebarAccountRedactionInput[],
790
+ options: {
791
+ checkedAt?: number
792
+ quotaBackoffUntil?: number
793
+ lastError?: string
794
+ routingAuthoritative?: boolean
795
+ } = {},
796
+ ): SidebarMachineState {
797
+ return {
798
+ checkedAt: options.checkedAt ?? Date.now(),
799
+ accounts: accounts.map((entry) => redactAccountForSidebar(entry)),
800
+ quotaBackoffUntil: options.quotaBackoffUntil,
801
+ lastError: options.lastError,
802
+ routingAuthoritative: options.routingAuthoritative,
803
+ }
804
+ }
805
+
806
+ interface SidebarStateWriteOptions {
807
+ stateFile?: string
808
+ }
809
+
810
+ let sidebarWriteChain: Promise<void> = Promise.resolve()
811
+
812
+ function enqueueSidebarWrite<T>(work: () => Promise<T>): Promise<T> {
813
+ // Always run `work` regardless of whether the prior link resolved or
814
+ // rejected — a failed write must not poison the chain for the next caller.
815
+ const next = sidebarWriteChain.then(work, work)
816
+ sidebarWriteChain = next.then(
817
+ () => undefined,
818
+ () => undefined,
819
+ )
820
+ return next
821
+ }
822
+
823
+ /**
824
+ * Wait for any in-flight sidebar state write to drain. The plugin lifecycle
825
+ * calls this during `dispose()` so the file logger and RPC server are torn
826
+ * down only after every queued write has either landed or thrown.
827
+ */
828
+ export function drainSidebarWrites(): Promise<void> {
829
+ return sidebarWriteChain
830
+ }
831
+
832
+ async function sleep(ms: number): Promise<void> {
833
+ await new Promise((resolve) => setTimeout(resolve, ms))
834
+ }
835
+
836
+ async function acquireSidebarLockWithRetry(
837
+ stateFile: string,
838
+ ): Promise<FencedFileLock> {
839
+ const start = Date.now()
840
+ let attempt = 0
841
+ while (true) {
842
+ await emitMergeStep('await-lock')
843
+ const lock = await acquireFencedFileLock({
844
+ path: stateFile,
845
+ name: SIDEBAR_LOCK_NAME,
846
+ ttlMs: SIDEBAR_LOCK_TTL_MS,
847
+ renew: true,
848
+ })
849
+ if (lock) {
850
+ await emitMergeStep('acquired-lock')
851
+ return lock
852
+ }
853
+ const elapsed = Date.now() - start
854
+ if (elapsed >= SIDEBAR_LOCK_TIMEOUT_MS) {
855
+ throw new SidebarStateLockContentionError(
856
+ stateFile,
857
+ SIDEBAR_LOCK_TIMEOUT_MS,
858
+ )
859
+ }
860
+ const backoff = Math.min(
861
+ SIDEBAR_LOCK_RETRY_CAP_MS,
862
+ SIDEBAR_LOCK_RETRY_BASE_MS + attempt * 10,
863
+ )
864
+ const jitter = Math.floor(Math.random() * SIDEBAR_LOCK_JITTER_MS)
865
+ await sleep(backoff + jitter)
866
+ attempt++
867
+ }
868
+ }
869
+
870
+ async function performSidebarWrite(
871
+ stateFile: string,
872
+ merge: (existing: SidebarStateV1) => SidebarStateV1,
873
+ ): Promise<void> {
874
+ await enqueueSidebarWrite(async () => {
875
+ ensureSidebarStateDir(stateFile)
876
+ const lock = await acquireSidebarLockWithRetry(stateFile)
877
+ try {
878
+ await lock.assertOwned()
879
+ const existing = readSidebarState(stateFile)
880
+ await emitMergeStep('read-state')
881
+ const merged = merge(existing)
882
+ await emitMergeStep('merged-state')
883
+ await writeJsonAtomic(stateFile, merged)
884
+ // `writeJsonAtomic` stages a tmp file with mode 0o600 and renames onto
885
+ // the target; POSIX rename replaces the inode so the new file inherits
886
+ // the staged mode bits. Windows ignores POSIX modes so the assertion
887
+ // in tests is best-effort there.
888
+ await emitMergeStep('wrote-state')
889
+ } finally {
890
+ await lock.release().catch(() => {})
891
+ }
892
+ })
893
+ }
894
+
895
+ /**
896
+ * Merge a new machine-state payload against the on-disk state.
897
+ *
898
+ * Stale writes (new `checkedAt` < existing) are dropped — only newer/equal
899
+ * `checkedAt` may replace machine fields. The merge preserves
900
+ * `routingAuthoritative: true` once it is true (sticky-true), keeps the
901
+ * existing `activeRouting` intact (routing is merged via its own writer),
902
+ * and prunes any expired routing entries as a side effect.
903
+ */
904
+ function mergeMachineState(
905
+ existing: SidebarStateV1,
906
+ next: SidebarMachineState,
907
+ ): SidebarStateV1 {
908
+ if (next.checkedAt < existing.checkedAt) {
909
+ return {
910
+ ...existing,
911
+ activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),
912
+ }
913
+ }
914
+ return {
915
+ version: SIDEBAR_STATE_VERSION,
916
+ checkedAt: next.checkedAt,
917
+ accounts: next.accounts,
918
+ quotaBackoffUntil: next.quotaBackoffUntil,
919
+ lastError: next.lastError,
920
+ routingAuthoritative:
921
+ existing.routingAuthoritative === true ||
922
+ next.routingAuthoritative === true,
923
+ // Symmetric with the stale-write branch: every machine-write merge
924
+ // re-prunes activeRouting so a long-running TUI session eventually
925
+ // drops dead routes even when no fresh routing upsert lands. Cheap
926
+ // (a single Object.entries + sort over ≤100 entries) and bounded.
927
+ activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Upsert a single session's active routing entry. The fetch interceptor calls
933
+ * this with `authoritative: true` after every final route selection; the
934
+ * `accountId`/`modelFamily`/`headerStyle` fields are already redacted by the
935
+ * caller (the writer never sees token or project fields).
936
+ */
937
+ export async function upsertSidebarActiveRouting(
938
+ sessionId: string,
939
+ entry: SidebarRoutingEntry,
940
+ options: SidebarStateWriteOptions & { authoritative?: boolean } = {},
941
+ ): Promise<void> {
942
+ const stateFile = options.stateFile ?? getSidebarStateFile()
943
+ await performSidebarWrite(stateFile, (existing) => {
944
+ const activeRouting = { ...existing.activeRouting, [sessionId]: entry }
945
+ return {
946
+ ...existing,
947
+ routingAuthoritative:
948
+ options.authoritative === true ? true : existing.routingAuthoritative,
949
+ activeRouting: pruneActiveRouting(activeRouting, Date.now()),
950
+ }
951
+ })
952
+ }
953
+
954
+ /**
955
+ * Remove one session's active routing entry. The event handler calls this
956
+ * when a session is deleted so the sidebar does not retain dead routes.
957
+ */
958
+ export async function removeSidebarActiveRouting(
959
+ sessionId: string,
960
+ options: SidebarStateWriteOptions = {},
961
+ ): Promise<void> {
962
+ const stateFile = options.stateFile ?? getSidebarStateFile()
963
+ await performSidebarWrite(stateFile, (existing) => {
964
+ if (!(sessionId in existing.activeRouting)) {
965
+ return existing
966
+ }
967
+ const activeRouting = { ...existing.activeRouting }
968
+ delete activeRouting[sessionId]
969
+ return {
970
+ ...existing,
971
+ activeRouting,
972
+ }
973
+ })
974
+ }
975
+
976
+ /**
977
+ * Write a new machine-state snapshot. The fetch interceptor and quota
978
+ * manager call this after each refresh; auth-loader calls it after the
979
+ * account pool is materialized.
980
+ */
981
+ export async function setSidebarMachineState(
982
+ next: SidebarMachineState,
983
+ options: SidebarStateWriteOptions = {},
984
+ ): Promise<void> {
985
+ const stateFile = options.stateFile ?? getSidebarStateFile()
986
+ await performSidebarWrite(stateFile, (existing) =>
987
+ mergeMachineState(existing, next),
988
+ )
989
+ }
990
+
991
+ void SIDEBAR_STATE_FILE_MODE