@cortexkit/opencode-antigravity-auth 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/README.md +225 -553
  2. package/dist/cli.js +3790 -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 +18951 -13145
  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/accounts.d.ts +7 -236
  38. package/dist/src/plugin/accounts.d.ts.map +1 -1
  39. package/dist/src/plugin/accounts.js +31 -1166
  40. package/dist/src/plugin/accounts.js.map +1 -1
  41. package/dist/src/plugin/agy-request-metadata.d.ts +2 -0
  42. package/dist/src/plugin/agy-request-metadata.d.ts.map +1 -0
  43. package/dist/src/plugin/agy-request-metadata.js +2 -0
  44. package/dist/src/plugin/agy-request-metadata.js.map +1 -0
  45. package/dist/src/plugin/agy-transport.d.ts +1 -1
  46. package/dist/src/plugin/agy-transport.js +1 -1
  47. package/dist/src/plugin/auth-doctor.d.ts +6 -6
  48. package/dist/src/plugin/auth-doctor.d.ts.map +1 -1
  49. package/dist/src/plugin/auth-doctor.js +79 -62
  50. package/dist/src/plugin/auth-doctor.js.map +1 -1
  51. package/dist/src/plugin/auth-drift.d.ts +4 -4
  52. package/dist/src/plugin/auth-drift.d.ts.map +1 -1
  53. package/dist/src/plugin/auth-drift.js +15 -15
  54. package/dist/src/plugin/auth-drift.js.map +1 -1
  55. package/dist/src/plugin/auth-loader.d.ts +35 -0
  56. package/dist/src/plugin/auth-loader.d.ts.map +1 -0
  57. package/dist/src/plugin/auth-loader.js +144 -0
  58. package/dist/src/plugin/auth-loader.js.map +1 -0
  59. package/dist/src/plugin/auth.d.ts +1 -1
  60. package/dist/src/plugin/auth.js +1 -1
  61. package/dist/src/plugin/cache/index.d.ts +1 -1
  62. package/dist/src/plugin/cache/index.d.ts.map +1 -1
  63. package/dist/src/plugin/cache/index.js +1 -1
  64. package/dist/src/plugin/cache/index.js.map +1 -1
  65. package/dist/src/plugin/cache/signature-cache.d.ts +1 -1
  66. package/dist/src/plugin/cache/signature-cache.d.ts.map +1 -1
  67. package/dist/src/plugin/cache/signature-cache.js +23 -24
  68. package/dist/src/plugin/cache/signature-cache.js.map +1 -1
  69. package/dist/src/plugin/cache.d.ts +6 -5
  70. package/dist/src/plugin/cache.d.ts.map +1 -1
  71. package/dist/src/plugin/cache.js +21 -7
  72. package/dist/src/plugin/cache.js.map +1 -1
  73. package/dist/src/plugin/catalog.d.ts +23 -0
  74. package/dist/src/plugin/catalog.d.ts.map +1 -0
  75. package/dist/src/plugin/catalog.js +60 -0
  76. package/dist/src/plugin/catalog.js.map +1 -0
  77. package/dist/src/plugin/cli.d.ts +7 -7
  78. package/dist/src/plugin/cli.d.ts.map +1 -1
  79. package/dist/src/plugin/cli.js +62 -55
  80. package/dist/src/plugin/cli.js.map +1 -1
  81. package/dist/src/plugin/command-data.d.ts +276 -0
  82. package/dist/src/plugin/command-data.d.ts.map +1 -0
  83. package/dist/src/plugin/command-data.js +325 -0
  84. package/dist/src/plugin/command-data.js.map +1 -0
  85. package/dist/src/plugin/commands.d.ts +151 -0
  86. package/dist/src/plugin/commands.d.ts.map +1 -0
  87. package/dist/src/plugin/commands.js +639 -0
  88. package/dist/src/plugin/commands.js.map +1 -0
  89. package/dist/src/plugin/config/index.d.ts +2 -2
  90. package/dist/src/plugin/config/index.d.ts.map +1 -1
  91. package/dist/src/plugin/config/index.js +2 -2
  92. package/dist/src/plugin/config/index.js.map +1 -1
  93. package/dist/src/plugin/config/loader.d.ts +1 -1
  94. package/dist/src/plugin/config/loader.d.ts.map +1 -1
  95. package/dist/src/plugin/config/loader.js +18 -16
  96. package/dist/src/plugin/config/loader.js.map +1 -1
  97. package/dist/src/plugin/config/models.d.ts +2 -2
  98. package/dist/src/plugin/config/models.d.ts.map +1 -1
  99. package/dist/src/plugin/config/models.js +1 -1
  100. package/dist/src/plugin/config/models.js.map +1 -1
  101. package/dist/src/plugin/config/operator-settings-schema.d.ts +29 -0
  102. package/dist/src/plugin/config/operator-settings-schema.d.ts.map +1 -0
  103. package/dist/src/plugin/config/operator-settings-schema.js +28 -0
  104. package/dist/src/plugin/config/operator-settings-schema.js.map +1 -0
  105. package/dist/src/plugin/config/schema.d.ts +21 -2
  106. package/dist/src/plugin/config/schema.d.ts.map +1 -1
  107. package/dist/src/plugin/config/schema.js +85 -25
  108. package/dist/src/plugin/config/schema.js.map +1 -1
  109. package/dist/src/plugin/config/updater.d.ts.map +1 -1
  110. package/dist/src/plugin/config/updater.js +15 -15
  111. package/dist/src/plugin/config/updater.js.map +1 -1
  112. package/dist/src/plugin/config/writer.d.ts +34 -0
  113. package/dist/src/plugin/config/writer.d.ts.map +1 -0
  114. package/dist/src/plugin/config/writer.js +81 -0
  115. package/dist/src/plugin/config/writer.js.map +1 -0
  116. package/dist/src/plugin/core/streaming/index.d.ts +1 -1
  117. package/dist/src/plugin/core/streaming/index.d.ts.map +1 -1
  118. package/dist/src/plugin/core/streaming/index.js +1 -1
  119. package/dist/src/plugin/core/streaming/index.js.map +1 -1
  120. package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
  121. package/dist/src/plugin/core/streaming/transformer.js +85 -44
  122. package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
  123. package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
  124. package/dist/src/plugin/debug.d.ts +3 -3
  125. package/dist/src/plugin/debug.d.ts.map +1 -1
  126. package/dist/src/plugin/debug.js +85 -47
  127. package/dist/src/plugin/debug.js.map +1 -1
  128. package/dist/src/plugin/dependencies.d.ts +129 -0
  129. package/dist/src/plugin/dependencies.d.ts.map +1 -0
  130. package/dist/src/plugin/dependencies.js +107 -0
  131. package/dist/src/plugin/dependencies.js.map +1 -0
  132. package/dist/src/plugin/errors.d.ts +27 -0
  133. package/dist/src/plugin/errors.d.ts.map +1 -1
  134. package/dist/src/plugin/errors.js +29 -3
  135. package/dist/src/plugin/errors.js.map +1 -1
  136. package/dist/src/plugin/event-handler.d.ts +53 -0
  137. package/dist/src/plugin/event-handler.d.ts.map +1 -0
  138. package/dist/src/plugin/event-handler.js +101 -0
  139. package/dist/src/plugin/event-handler.js.map +1 -0
  140. package/dist/src/plugin/fetch/retry-state.d.ts +61 -0
  141. package/dist/src/plugin/fetch/retry-state.d.ts.map +1 -0
  142. package/dist/src/plugin/fetch/retry-state.js +248 -0
  143. package/dist/src/plugin/fetch/retry-state.js.map +1 -0
  144. package/dist/src/plugin/fetch/warmup.d.ts +30 -0
  145. package/dist/src/plugin/fetch/warmup.d.ts.map +1 -0
  146. package/dist/src/plugin/fetch/warmup.js +76 -0
  147. package/dist/src/plugin/fetch/warmup.js.map +1 -0
  148. package/dist/src/plugin/fetch-interceptor.d.ts +59 -0
  149. package/dist/src/plugin/fetch-interceptor.d.ts.map +1 -0
  150. package/dist/src/plugin/fetch-interceptor.js +1387 -0
  151. package/dist/src/plugin/fetch-interceptor.js.map +1 -0
  152. package/dist/src/plugin/fetch-routing.d.ts +26 -0
  153. package/dist/src/plugin/fetch-routing.d.ts.map +1 -0
  154. package/dist/src/plugin/fetch-routing.js +99 -0
  155. package/dist/src/plugin/fetch-routing.js.map +1 -0
  156. package/dist/src/plugin/fingerprint.d.ts +1 -1
  157. package/dist/src/plugin/fingerprint.js +1 -1
  158. package/dist/src/plugin/gemini-dump.d.ts +5 -5
  159. package/dist/src/plugin/gemini-dump.d.ts.map +1 -1
  160. package/dist/src/plugin/gemini-dump.js +109 -55
  161. package/dist/src/plugin/gemini-dump.js.map +1 -1
  162. package/dist/src/plugin/google-search-tool.d.ts +9 -0
  163. package/dist/src/plugin/google-search-tool.d.ts.map +1 -0
  164. package/dist/src/plugin/google-search-tool.js +56 -0
  165. package/dist/src/plugin/google-search-tool.js.map +1 -0
  166. package/dist/src/plugin/image-saver.d.ts +2 -21
  167. package/dist/src/plugin/image-saver.d.ts.map +1 -1
  168. package/dist/src/plugin/image-saver.js +22 -56
  169. package/dist/src/plugin/image-saver.js.map +1 -1
  170. package/dist/src/plugin/index.d.ts +23 -0
  171. package/dist/src/plugin/index.d.ts.map +1 -0
  172. package/dist/src/plugin/index.js +308 -0
  173. package/dist/src/plugin/index.js.map +1 -0
  174. package/dist/src/plugin/killswitch.d.ts +94 -0
  175. package/dist/src/plugin/killswitch.d.ts.map +1 -0
  176. package/dist/src/plugin/killswitch.js +194 -0
  177. package/dist/src/plugin/killswitch.js.map +1 -0
  178. package/dist/src/plugin/lifecycle.d.ts +40 -0
  179. package/dist/src/plugin/lifecycle.d.ts.map +1 -0
  180. package/dist/src/plugin/lifecycle.js +73 -0
  181. package/dist/src/plugin/lifecycle.js.map +1 -0
  182. package/dist/src/plugin/logger.d.ts +10 -1
  183. package/dist/src/plugin/logger.d.ts.map +1 -1
  184. package/dist/src/plugin/logger.js +38 -12
  185. package/dist/src/plugin/logger.js.map +1 -1
  186. package/dist/src/plugin/logging-utils.d.ts +35 -1
  187. package/dist/src/plugin/logging-utils.d.ts.map +1 -1
  188. package/dist/src/plugin/logging-utils.js +98 -15
  189. package/dist/src/plugin/logging-utils.js.map +1 -1
  190. package/dist/src/plugin/model-registry.d.ts +1 -1
  191. package/dist/src/plugin/model-registry.js +1 -1
  192. package/dist/src/plugin/oauth-login.d.ts +22 -0
  193. package/dist/src/plugin/oauth-login.d.ts.map +1 -0
  194. package/dist/src/plugin/oauth-login.js +37 -0
  195. package/dist/src/plugin/oauth-login.js.map +1 -0
  196. package/dist/src/plugin/oauth-methods.d.ts +42 -0
  197. package/dist/src/plugin/oauth-methods.d.ts.map +1 -0
  198. package/dist/src/plugin/oauth-methods.js +1284 -0
  199. package/dist/src/plugin/oauth-methods.js.map +1 -0
  200. package/dist/src/plugin/operator-settings.d.ts +40 -0
  201. package/dist/src/plugin/operator-settings.d.ts.map +1 -0
  202. package/dist/src/plugin/operator-settings.js +148 -0
  203. package/dist/src/plugin/operator-settings.js.map +1 -0
  204. package/dist/src/plugin/persist-account-pool.d.ts +37 -0
  205. package/dist/src/plugin/persist-account-pool.d.ts.map +1 -0
  206. package/dist/src/plugin/persist-account-pool.js +142 -0
  207. package/dist/src/plugin/persist-account-pool.js.map +1 -0
  208. package/dist/src/plugin/project.d.ts +1 -1
  209. package/dist/src/plugin/project.js +1 -1
  210. package/dist/src/plugin/prompt-context.d.ts.map +1 -1
  211. package/dist/src/plugin/prompt-context.js +11 -11
  212. package/dist/src/plugin/prompt-context.js.map +1 -1
  213. package/dist/src/plugin/quota.d.ts +97 -41
  214. package/dist/src/plugin/quota.d.ts.map +1 -1
  215. package/dist/src/plugin/quota.js +273 -285
  216. package/dist/src/plugin/quota.js.map +1 -1
  217. package/dist/src/plugin/recovery/constants.d.ts.map +1 -1
  218. package/dist/src/plugin/recovery/constants.js +24 -15
  219. package/dist/src/plugin/recovery/constants.js.map +1 -1
  220. package/dist/src/plugin/recovery/index.d.ts +3 -3
  221. package/dist/src/plugin/recovery/index.d.ts.map +1 -1
  222. package/dist/src/plugin/recovery/index.js +3 -3
  223. package/dist/src/plugin/recovery/index.js.map +1 -1
  224. package/dist/src/plugin/recovery/storage.d.ts +1 -1
  225. package/dist/src/plugin/recovery/storage.d.ts.map +1 -1
  226. package/dist/src/plugin/recovery/storage.js +31 -39
  227. package/dist/src/plugin/recovery/storage.js.map +1 -1
  228. package/dist/src/plugin/recovery/types.d.ts +12 -12
  229. package/dist/src/plugin/recovery/types.d.ts.map +1 -1
  230. package/dist/src/plugin/recovery.d.ts +3 -3
  231. package/dist/src/plugin/recovery.d.ts.map +1 -1
  232. package/dist/src/plugin/recovery.js +62 -58
  233. package/dist/src/plugin/recovery.js.map +1 -1
  234. package/dist/src/plugin/refresh-queue.d.ts +5 -2
  235. package/dist/src/plugin/refresh-queue.d.ts.map +1 -1
  236. package/dist/src/plugin/refresh-queue.js +37 -15
  237. package/dist/src/plugin/refresh-queue.js.map +1 -1
  238. package/dist/src/plugin/request-helpers.d.ts +2 -1
  239. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  240. package/dist/src/plugin/request-helpers.js +458 -354
  241. package/dist/src/plugin/request-helpers.js.map +1 -1
  242. package/dist/src/plugin/request.d.ts +12 -6
  243. package/dist/src/plugin/request.d.ts.map +1 -1
  244. package/dist/src/plugin/request.js +801 -390
  245. package/dist/src/plugin/request.js.map +1 -1
  246. package/dist/src/plugin/rotation.d.ts +1 -168
  247. package/dist/src/plugin/rotation.d.ts.map +1 -1
  248. package/dist/src/plugin/rotation.js +1 -327
  249. package/dist/src/plugin/rotation.js.map +1 -1
  250. package/dist/src/plugin/search.d.ts.map +1 -1
  251. package/dist/src/plugin/search.js +39 -35
  252. package/dist/src/plugin/search.js.map +1 -1
  253. package/dist/src/plugin/server.d.ts +1 -1
  254. package/dist/src/plugin/server.d.ts.map +1 -1
  255. package/dist/src/plugin/server.js +42 -37
  256. package/dist/src/plugin/server.js.map +1 -1
  257. package/dist/src/plugin/session-context.d.ts +22 -0
  258. package/dist/src/plugin/session-context.d.ts.map +1 -0
  259. package/dist/src/plugin/session-context.js +67 -0
  260. package/dist/src/plugin/session-context.js.map +1 -0
  261. package/dist/src/plugin/storage.d.ts +59 -114
  262. package/dist/src/plugin/storage.d.ts.map +1 -1
  263. package/dist/src/plugin/storage.js +140 -474
  264. package/dist/src/plugin/storage.js.map +1 -1
  265. package/dist/src/plugin/stores/signature-store.d.ts.map +1 -1
  266. package/dist/src/plugin/stores/signature-store.js.map +1 -1
  267. package/dist/src/plugin/thinking-recovery.d.ts.map +1 -1
  268. package/dist/src/plugin/thinking-recovery.js +35 -33
  269. package/dist/src/plugin/thinking-recovery.js.map +1 -1
  270. package/dist/src/plugin/token.d.ts +2 -2
  271. package/dist/src/plugin/token.d.ts.map +1 -1
  272. package/dist/src/plugin/token.js +30 -21
  273. package/dist/src/plugin/token.js.map +1 -1
  274. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts +1 -1
  275. package/dist/src/plugin/transform/cross-model-sanitizer.js +1 -1
  276. package/dist/src/plugin/transform/index.d.ts +1 -1
  277. package/dist/src/plugin/transform/index.js +1 -1
  278. package/dist/src/plugin/transform/model-resolver.d.ts +1 -1
  279. package/dist/src/plugin/transform/model-resolver.js +1 -1
  280. package/dist/src/plugin/transform/types.d.ts +1 -1
  281. package/dist/src/plugin/transform/types.js +1 -1
  282. package/dist/src/plugin/types.d.ts +25 -69
  283. package/dist/src/plugin/types.d.ts.map +1 -1
  284. package/dist/src/plugin/ui/ansi.d.ts.map +1 -1
  285. package/dist/src/plugin/ui/ansi.js.map +1 -1
  286. package/dist/src/plugin/ui/auth-menu.d.ts +2 -2
  287. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
  288. package/dist/src/plugin/ui/auth-menu.js +84 -31
  289. package/dist/src/plugin/ui/auth-menu.js.map +1 -1
  290. package/dist/src/plugin/ui/confirm.d.ts.map +1 -1
  291. package/dist/src/plugin/ui/confirm.js.map +1 -1
  292. package/dist/src/plugin/ui/model-status.d.ts +3 -3
  293. package/dist/src/plugin/ui/model-status.d.ts.map +1 -1
  294. package/dist/src/plugin/ui/model-status.js +3 -3
  295. package/dist/src/plugin/ui/model-status.js.map +1 -1
  296. package/dist/src/plugin/ui/quota-status.d.ts +4 -4
  297. package/dist/src/plugin/ui/quota-status.d.ts.map +1 -1
  298. package/dist/src/plugin/ui/quota-status.js +51 -48
  299. package/dist/src/plugin/ui/quota-status.js.map +1 -1
  300. package/dist/src/plugin/ui/select.d.ts.map +1 -1
  301. package/dist/src/plugin/ui/select.js +22 -12
  302. package/dist/src/plugin/ui/select.js.map +1 -1
  303. package/dist/src/plugin/version.d.ts +1 -1
  304. package/dist/src/plugin/version.js +1 -1
  305. package/dist/src/rpc/notifications.d.ts +6 -0
  306. package/dist/src/rpc/notifications.d.ts.map +1 -0
  307. package/dist/src/rpc/notifications.js +46 -0
  308. package/dist/src/rpc/notifications.js.map +1 -0
  309. package/dist/src/rpc/port-file.d.ts +13 -0
  310. package/dist/src/rpc/port-file.d.ts.map +1 -0
  311. package/dist/src/rpc/port-file.js +109 -0
  312. package/dist/src/rpc/port-file.js.map +1 -0
  313. package/dist/src/rpc/protocol.d.ts +22 -0
  314. package/dist/src/rpc/protocol.d.ts.map +1 -0
  315. package/dist/src/rpc/protocol.js +1 -0
  316. package/dist/src/rpc/protocol.js.map +1 -0
  317. package/dist/src/rpc/rpc-client.d.ts +10 -0
  318. package/dist/src/rpc/rpc-client.d.ts.map +1 -0
  319. package/dist/src/rpc/rpc-client.js +58 -0
  320. package/dist/src/rpc/rpc-client.js.map +1 -0
  321. package/dist/src/rpc/rpc-dir.d.ts +4 -0
  322. package/dist/src/rpc/rpc-dir.d.ts.map +1 -0
  323. package/dist/src/rpc/rpc-dir.js +19 -0
  324. package/dist/src/rpc/rpc-dir.js.map +1 -0
  325. package/dist/src/rpc/rpc-server.d.ts +13 -0
  326. package/dist/src/rpc/rpc-server.d.ts.map +1 -0
  327. package/dist/src/rpc/rpc-server.js +231 -0
  328. package/dist/src/rpc/rpc-server.js.map +1 -0
  329. package/dist/src/sidebar-state.d.ts +240 -0
  330. package/dist/src/sidebar-state.d.ts.map +1 -0
  331. package/dist/src/sidebar-state.js +520 -0
  332. package/dist/src/sidebar-state.js.map +1 -0
  333. package/dist/src/tui/command-dialogs.d.ts +61 -0
  334. package/dist/src/tui/command-dialogs.d.ts.map +1 -0
  335. package/dist/src/tui/command-dialogs.js +497 -0
  336. package/dist/src/tui/command-dialogs.js.map +1 -0
  337. package/dist/src/tui/file-logger.d.ts +41 -0
  338. package/dist/src/tui/file-logger.d.ts.map +1 -0
  339. package/dist/src/tui/file-logger.js +114 -0
  340. package/dist/src/tui/file-logger.js.map +1 -0
  341. package/dist/src/tui-preferences.d.ts +40 -0
  342. package/dist/src/tui-preferences.d.ts.map +1 -0
  343. package/dist/src/tui-preferences.js +328 -0
  344. package/dist/src/tui-preferences.js.map +1 -0
  345. package/dist/src/tui.d.ts +68 -0
  346. package/dist/src/tui.d.ts.map +1 -0
  347. package/dist/src/tui.js +662 -0
  348. package/dist/src/tui.js.map +1 -0
  349. package/package.json +46 -22
  350. package/src/plugin/command-data.ts +651 -0
  351. package/src/rpc/port-file.ts +143 -0
  352. package/src/rpc/protocol.ts +31 -0
  353. package/src/rpc/rpc-client.ts +102 -0
  354. package/src/rpc/rpc-dir.ts +24 -0
  355. package/src/sidebar-state.ts +717 -0
  356. package/src/tui/command-dialogs.tsx +699 -0
  357. package/src/tui/entry.mjs +74 -0
  358. package/src/tui/file-logger.ts +145 -0
  359. package/src/tui-compiled/plugin/command-data.ts +651 -0
  360. package/src/tui-compiled/rpc/port-file.ts +143 -0
  361. package/src/tui-compiled/rpc/protocol.ts +31 -0
  362. package/src/tui-compiled/rpc/rpc-client.ts +102 -0
  363. package/src/tui-compiled/rpc/rpc-dir.ts +24 -0
  364. package/src/tui-compiled/sidebar-state.ts +717 -0
  365. package/src/tui-compiled/tui/command-dialogs.tsx +659 -0
  366. package/src/tui-compiled/tui/file-logger.ts +145 -0
  367. package/src/tui-compiled/tui-preferences.ts +416 -0
  368. package/src/tui-compiled/tui.tsx +1150 -0
  369. package/src/tui-preferences.ts +416 -0
  370. package/src/tui.tsx +1117 -0
  371. package/dist/src/plugin.d.ts +0 -32
  372. package/dist/src/plugin.d.ts.map +0 -1
  373. package/dist/src/plugin.js +0 -3151
  374. package/dist/src/plugin.js.map +0 -1
@@ -0,0 +1,717 @@
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 =
56
+ | 'claude'
57
+ | 'gemini-pro'
58
+ | 'gemini-flash'
59
+ | 'gpt-oss'
60
+
61
+ export interface SidebarQuotaEntry {
62
+ remainingPercent: number
63
+ resetAt?: number
64
+ }
65
+
66
+ export interface SidebarAccountState {
67
+ id: string
68
+ label: string
69
+ enabled: boolean
70
+ health: number
71
+ current: boolean
72
+ cooldownUntil?: number
73
+ quota: Partial<Record<SidebarQuotaKey, SidebarQuotaEntry>>
74
+ }
75
+
76
+ export interface SidebarRoutingEntry {
77
+ accountId: string
78
+ modelFamily: 'claude' | 'gemini'
79
+ headerStyle: 'antigravity' | 'gemini-cli'
80
+ strategy?: 'sticky' | 'round-robin' | 'hybrid'
81
+ updatedAt: number
82
+ }
83
+
84
+ export interface SidebarStateV1 {
85
+ version: typeof SIDEBAR_STATE_VERSION
86
+ checkedAt: number
87
+ accounts: SidebarAccountState[]
88
+ activeRouting: Record<string, SidebarRoutingEntry>
89
+ routingAuthoritative: boolean
90
+ quotaBackoffUntil?: number
91
+ lastError?: string
92
+ }
93
+
94
+ /**
95
+ * The subset of `SidebarStateV1` that a non-routing writer may set. The
96
+ * fetch interceptor writes `activeRouting` directly via its own entry point
97
+ * because routing is session-scoped, not machine-scoped.
98
+ */
99
+ export interface SidebarMachineState {
100
+ checkedAt: number
101
+ accounts: SidebarAccountState[]
102
+ quotaBackoffUntil?: number
103
+ lastError?: string
104
+ /**
105
+ * Optional: opt in to mark the snapshot as authoritative. When `true`,
106
+ * the merge keeps the existing `routingAuthoritative: true` even if a
107
+ * later non-authoritative machine write lands. Sticky-true semantics.
108
+ */
109
+ routingAuthoritative?: boolean
110
+ }
111
+
112
+ export const DEFAULT_SIDEBAR_STATE: SidebarStateV1 = {
113
+ version: SIDEBAR_STATE_VERSION,
114
+ checkedAt: 0,
115
+ accounts: [],
116
+ activeRouting: {},
117
+ routingAuthoritative: false,
118
+ }
119
+
120
+ export const SIDEBAR_STATE_ENV = 'ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE'
121
+
122
+ const SIDEBAR_STATE_DIR = 'cortexkit/antigravity-auth'
123
+ const SIDEBAR_STATE_FILENAME = 'sidebar-state.json'
124
+
125
+ /** Active routing entries older than this are dropped on every merge. */
126
+ const ACTIVE_ROUTING_MAX_AGE_MS = 24 * 60 * 60 * 1000
127
+ /** Active routing map is capped at this many newest entries. */
128
+ const ACTIVE_ROUTING_MAX_ENTRIES = 100
129
+
130
+ const SIDEBAR_LOCK_NAME = 'sidebar'
131
+ const SIDEBAR_LOCK_TTL_MS = 10_000
132
+ const SIDEBAR_LOCK_TIMEOUT_MS = 2_000
133
+ const SIDEBAR_LOCK_RETRY_BASE_MS = 25
134
+ const SIDEBAR_LOCK_RETRY_CAP_MS = 75
135
+ const SIDEBAR_LOCK_JITTER_MS = 25
136
+ const SIDEBAR_STATE_DIR_MODE = 0o700
137
+ const SIDEBAR_STATE_FILE_MODE = 0o600
138
+
139
+ /**
140
+ * Thrown by every writer when the cross-process lock cannot be acquired
141
+ * within `SIDEBAR_LOCK_TIMEOUT_MS`. The caller decides whether to surface
142
+ * a toast, drop the write, or retry the next tick.
143
+ */
144
+ export class SidebarStateLockContentionError extends Error {
145
+ readonly details: { stateFile: string; timeoutMs: number }
146
+
147
+ constructor(stateFile: string, timeoutMs: number) {
148
+ super(
149
+ `Could not acquire sidebar-state lock at ${stateFile} within ${timeoutMs}ms`,
150
+ )
151
+ this.name = 'SidebarStateLockContentionError'
152
+ this.details = { stateFile, timeoutMs }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Steps exposed to the merge hooks. Race tests pause writers here; production
158
+ * callers leave the hooks unset (a no-op fast path).
159
+ *
160
+ * - `await-lock` — before invoking `acquireFencedFileLock`.
161
+ * - `acquired-lock` — after the lock is granted and before the read.
162
+ * - `read-state` — after the on-disk state is normalized.
163
+ * - `merged-state` — after the merge but before `writeJsonAtomic`.
164
+ * - `wrote-state` — after the rename but before `lock.release()`.
165
+ */
166
+ export type SidebarMergeStep =
167
+ | 'await-lock'
168
+ | 'acquired-lock'
169
+ | 'read-state'
170
+ | 'merged-state'
171
+ | 'wrote-state'
172
+
173
+ export interface SidebarMergeHooks {
174
+ onStep?: (step: SidebarMergeStep) => Promise<void> | void
175
+ }
176
+
177
+ let sidebarMergeHooks: SidebarMergeHooks | null = null
178
+
179
+ /**
180
+ * Install (or clear with `null`) the deterministic race hooks. Tests use
181
+ * these to inject interleavings; production callers leave them unset. The
182
+ * module-level state means a test must reset hooks in its own `afterEach`
183
+ * to avoid bleeding into the next test.
184
+ */
185
+ export function setSidebarMergeHooks(hooks: SidebarMergeHooks | null): void {
186
+ sidebarMergeHooks = hooks
187
+ }
188
+
189
+ async function emitMergeStep(step: SidebarMergeStep): Promise<void> {
190
+ await sidebarMergeHooks?.onStep?.(step)
191
+ }
192
+
193
+ /**
194
+ * Resolve the on-disk path the plugin writes to and the TUI reads from.
195
+ *
196
+ * - `ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE` wins when set (tests, packaged
197
+ * installers, and any user override).
198
+ * - Otherwise fall back to the XDG state directory, mirroring the path
199
+ * conventions used elsewhere in the project.
200
+ */
201
+ export function getSidebarStateFile(): string {
202
+ const override = process.env[SIDEBAR_STATE_ENV]
203
+ if (override && override.trim().length > 0) return override
204
+ const base = xdgState ?? join(homedir(), '.local', 'state')
205
+ return join(base, SIDEBAR_STATE_DIR, SIDEBAR_STATE_FILENAME)
206
+ }
207
+
208
+ /**
209
+ * Read and normalize the sidebar state file. Returns the default state when
210
+ * the file is missing, unreadable, malformed, or schema-incompatible — the TUI
211
+ * must never throw out of `readSidebarState()`, the panel just shows
212
+ * "Awaiting Antigravity state" and the next poll retries.
213
+ *
214
+ * The read is sync on purpose: the TUI polls on a 2-second timer and the file
215
+ * is tiny (a handful of accounts); an async read here would just add race
216
+ * surface area against Solid's reactive render cycle.
217
+ */
218
+ export function readSidebarState(
219
+ path: string = getSidebarStateFile(),
220
+ ): SidebarStateV1 {
221
+ let raw: string
222
+ try {
223
+ raw = readFileSync(path, 'utf-8')
224
+ } catch {
225
+ return { ...DEFAULT_SIDEBAR_STATE }
226
+ }
227
+ let parsed: unknown
228
+ try {
229
+ parsed = JSON.parse(raw)
230
+ } catch {
231
+ return { ...DEFAULT_SIDEBAR_STATE, lastError: 'malformed-json' }
232
+ }
233
+ return normalizeSidebarState(parsed)
234
+ }
235
+
236
+ function normalizeSidebarState(input: unknown): SidebarStateV1 {
237
+ if (!isObject(input)) {
238
+ return { ...DEFAULT_SIDEBAR_STATE, lastError: 'shape' }
239
+ }
240
+ const record = input as Record<string, unknown>
241
+ const version = record.version
242
+ if (version !== SIDEBAR_STATE_VERSION) {
243
+ return {
244
+ ...DEFAULT_SIDEBAR_STATE,
245
+ lastError: `unsupported-version:${stringifySafe(version)}`,
246
+ }
247
+ }
248
+
249
+ const accountsRaw = record.accounts
250
+ const accounts = Array.isArray(accountsRaw)
251
+ ? accountsRaw
252
+ .map((entry) => normalizeAccount(entry))
253
+ .filter((entry): entry is SidebarAccountState => entry !== null)
254
+ : []
255
+
256
+ const routingRaw = record.activeRouting
257
+ const activeRouting: Record<string, SidebarRoutingEntry> = {}
258
+ if (isObject(routingRaw)) {
259
+ for (const [sessionId, entry] of Object.entries(
260
+ routingRaw as Record<string, unknown>,
261
+ )) {
262
+ const normalized = normalizeRouting(entry)
263
+ if (normalized) activeRouting[sessionId] = normalized
264
+ }
265
+ }
266
+
267
+ const checkedAt = toFiniteNumber(record.checkedAt)
268
+ const routingAuthoritative = record.routingAuthoritative === true
269
+ const quotaBackoffUntil = toFiniteNumber(record.quotaBackoffUntil)
270
+ const lastError =
271
+ typeof record.lastError === 'string' ? record.lastError : undefined
272
+
273
+ return {
274
+ version: SIDEBAR_STATE_VERSION,
275
+ checkedAt: checkedAt ?? 0,
276
+ accounts,
277
+ activeRouting,
278
+ routingAuthoritative,
279
+ quotaBackoffUntil: quotaBackoffUntil ?? undefined,
280
+ lastError,
281
+ }
282
+ }
283
+
284
+ function normalizeAccount(input: unknown): SidebarAccountState | null {
285
+ if (!isObject(input)) return null
286
+ const record = input as Record<string, unknown>
287
+ const id = typeof record.id === 'string' ? record.id : null
288
+ const label = typeof record.label === 'string' ? record.label : null
289
+ if (!id || !label) return null
290
+ const enabled = record.enabled !== false
291
+ const health = clampNumber(toFiniteNumber(record.health), 0, 100)
292
+ const current = record.current === true
293
+ const cooldownUntil = toFiniteNumber(record.cooldownUntil) ?? undefined
294
+ const quotaRaw = record.quota
295
+ const quota: SidebarAccountState['quota'] = {}
296
+ if (isObject(quotaRaw)) {
297
+ for (const key of [
298
+ 'claude',
299
+ 'gemini-pro',
300
+ 'gemini-flash',
301
+ 'gpt-oss',
302
+ ] as const) {
303
+ const entry = (quotaRaw as Record<string, unknown>)[key]
304
+ const normalized = normalizeQuota(entry)
305
+ if (normalized) quota[key] = normalized
306
+ }
307
+ }
308
+ return {
309
+ id,
310
+ label,
311
+ enabled,
312
+ health,
313
+ current,
314
+ cooldownUntil,
315
+ quota,
316
+ }
317
+ }
318
+
319
+ function normalizeQuota(input: unknown): SidebarQuotaEntry | null {
320
+ if (!isObject(input)) return null
321
+ const record = input as Record<string, unknown>
322
+ const remaining = toFiniteNumber(record.remainingPercent)
323
+ if (remaining === null) return null
324
+ const resetAt = toFiniteNumber(record.resetAt) ?? undefined
325
+ return {
326
+ remainingPercent: clampNumber(remaining, 0, 100),
327
+ resetAt,
328
+ }
329
+ }
330
+
331
+ function normalizeRouting(input: unknown): SidebarRoutingEntry | null {
332
+ if (!isObject(input)) return null
333
+ const record = input as Record<string, unknown>
334
+ const accountId =
335
+ typeof record.accountId === 'string' ? record.accountId : null
336
+ const modelFamily = record.modelFamily
337
+ const headerStyle = record.headerStyle
338
+ const strategy = record.strategy
339
+ const updatedAt = toFiniteNumber(record.updatedAt) ?? 0
340
+ if (
341
+ !accountId ||
342
+ (modelFamily !== 'claude' && modelFamily !== 'gemini') ||
343
+ (headerStyle !== 'antigravity' && headerStyle !== 'gemini-cli')
344
+ ) {
345
+ return null
346
+ }
347
+ return {
348
+ accountId,
349
+ modelFamily,
350
+ headerStyle,
351
+ strategy:
352
+ strategy === 'sticky' ||
353
+ strategy === 'round-robin' ||
354
+ strategy === 'hybrid'
355
+ ? strategy
356
+ : undefined,
357
+ updatedAt,
358
+ }
359
+ }
360
+
361
+ function isObject(value: unknown): value is Record<string, unknown> {
362
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
363
+ }
364
+
365
+ function toFiniteNumber(value: unknown): number | null {
366
+ if (typeof value === 'number' && Number.isFinite(value)) return value
367
+ if (typeof value === 'string') {
368
+ const parsed = Number(value)
369
+ return Number.isFinite(parsed) ? parsed : null
370
+ }
371
+ return null
372
+ }
373
+
374
+ function clampNumber(value: number | null, min: number, max: number): number {
375
+ if (value === null) return min
376
+ if (value < min) return min
377
+ if (value > max) return max
378
+ return value
379
+ }
380
+
381
+ function stringifySafe(value: unknown): string {
382
+ if (typeof value === 'string') return value
383
+ try {
384
+ return JSON.stringify(value) ?? 'unknown'
385
+ } catch {
386
+ return 'unknown'
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Ensure the parent directory for the sidebar state file exists. Convenience
392
+ * helper used by writers (tests, plugins) — the TUI itself does not write.
393
+ */
394
+ export function ensureSidebarStateDir(
395
+ path: string = getSidebarStateFile(),
396
+ ): void {
397
+ mkdirSync(dirname(path), { recursive: true, mode: SIDEBAR_STATE_DIR_MODE })
398
+ }
399
+
400
+ /**
401
+ * Drop active-routing entries older than 24h and cap the map at the freshest
402
+ * 100. Pure helper exposed for unit testing; the writers call it on every
403
+ * merge so the on-disk map never grows without bound.
404
+ */
405
+ export function pruneActiveRouting(
406
+ map: Record<string, SidebarRoutingEntry>,
407
+ now: number,
408
+ ): Record<string, SidebarRoutingEntry> {
409
+ const cutoff = now - ACTIVE_ROUTING_MAX_AGE_MS
410
+ const filtered: Array<[string, SidebarRoutingEntry]> = []
411
+ for (const [sessionId, entry] of Object.entries(map)) {
412
+ if (entry.updatedAt >= cutoff) {
413
+ filtered.push([sessionId, entry])
414
+ }
415
+ }
416
+ filtered.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
417
+ if (filtered.length <= ACTIVE_ROUTING_MAX_ENTRIES) {
418
+ return Object.fromEntries(filtered)
419
+ }
420
+ return Object.fromEntries(filtered.slice(0, ACTIVE_ROUTING_MAX_ENTRIES))
421
+ }
422
+
423
+ /**
424
+ * Structural input for `redactAccountForSidebar`. Decoupled from the core
425
+ * `ManagedAccount` type so this module never forces a type-level import
426
+ * shape on callers (and so the TUI's compiled tree does not see the core
427
+ * ManagedAccount shape beyond what's actually used).
428
+ *
429
+ * Deliberately excludes `email`: the sidebar/redaction boundary is a PII
430
+ * firewall. Adding `email` here would re-introduce the leak this boundary
431
+ * exists to prevent. Legacy profile labels are accepted but never serialized.
432
+ */
433
+ export interface SidebarAccountRedactionInput {
434
+ /** Position in the harness-visible account array. */
435
+ index: number
436
+ /** Legacy profile label; deliberately ignored by the redactor. */
437
+ label?: string
438
+ enabled?: boolean
439
+ current?: boolean
440
+ coolingDownUntil?: number
441
+ /** Health score in `[0, 100]`. Defaults to 100 when missing. */
442
+ healthScore?: number
443
+ cachedQuota?: {
444
+ claude?: { remainingFraction?: number; resetTime?: string }
445
+ 'gemini-pro'?: { remainingFraction?: number; resetTime?: string }
446
+ 'gemini-flash'?: { remainingFraction?: number; resetTime?: string }
447
+ 'gpt-oss'?: { remainingFraction?: number; resetTime?: string }
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Convert a live account snapshot into the redacted shape the TUI renders.
453
+ * The redacted `SidebarAccountState` carries no email, refresh token, access
454
+ * token, project ID, fingerprint, OAuth profile name, or other personal or
455
+ * credential-shaped fields. Display labels are generated ordinal account names.
456
+ */
457
+ export function redactAccountForSidebar(
458
+ source: SidebarAccountRedactionInput,
459
+ ): SidebarAccountState {
460
+ const id = `acct-${source.index}`
461
+ const label = `Account ${source.index + 1}`
462
+ const enabled = source.enabled !== false
463
+ const current = source.current === true
464
+ const cooldownUntil =
465
+ typeof source.coolingDownUntil === 'number' &&
466
+ Number.isFinite(source.coolingDownUntil)
467
+ ? source.coolingDownUntil
468
+ : undefined
469
+ const health = clampNumber(
470
+ typeof source.healthScore === 'number' ? source.healthScore : null,
471
+ 0,
472
+ 100,
473
+ )
474
+
475
+ const quota: SidebarAccountState['quota'] = {}
476
+ const cached = source.cachedQuota
477
+ if (cached) {
478
+ for (const key of [
479
+ 'claude',
480
+ 'gemini-pro',
481
+ 'gemini-flash',
482
+ 'gpt-oss',
483
+ ] as const) {
484
+ const entry = cached[key]
485
+ if (!entry) continue
486
+ const fraction = entry.remainingFraction
487
+ if (typeof fraction !== 'number' || !Number.isFinite(fraction)) continue
488
+ const remainingPercent = clampNumber(Math.round(fraction * 100), 0, 100)
489
+ let resetAt: number | undefined
490
+ if (typeof entry.resetTime === 'string' && entry.resetTime.length > 0) {
491
+ const parsed = Date.parse(entry.resetTime)
492
+ if (Number.isFinite(parsed)) resetAt = parsed
493
+ }
494
+ quota[key] = { remainingPercent, resetAt }
495
+ }
496
+ }
497
+
498
+ return {
499
+ id,
500
+ label,
501
+ enabled,
502
+ health,
503
+ current,
504
+ cooldownUntil,
505
+ quota,
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Build a `SidebarMachineState` from a list of live account snapshots.
511
+ * Convenience for the auth-loader / quota writer call sites that already
512
+ * hold an array of accounts and want to push a single snapshot.
513
+ */
514
+ export function buildSidebarMachineStateFromAccounts(
515
+ accounts: SidebarAccountRedactionInput[],
516
+ options: {
517
+ checkedAt?: number
518
+ quotaBackoffUntil?: number
519
+ lastError?: string
520
+ routingAuthoritative?: boolean
521
+ } = {},
522
+ ): SidebarMachineState {
523
+ return {
524
+ checkedAt: options.checkedAt ?? Date.now(),
525
+ accounts: accounts.map((entry) => redactAccountForSidebar(entry)),
526
+ quotaBackoffUntil: options.quotaBackoffUntil,
527
+ lastError: options.lastError,
528
+ routingAuthoritative: options.routingAuthoritative,
529
+ }
530
+ }
531
+
532
+ interface SidebarStateWriteOptions {
533
+ stateFile?: string
534
+ }
535
+
536
+ let sidebarWriteChain: Promise<void> = Promise.resolve()
537
+
538
+ function enqueueSidebarWrite<T>(work: () => Promise<T>): Promise<T> {
539
+ // Always run `work` regardless of whether the prior link resolved or
540
+ // rejected — a failed write must not poison the chain for the next caller.
541
+ const next = sidebarWriteChain.then(work, work)
542
+ sidebarWriteChain = next.then(
543
+ () => undefined,
544
+ () => undefined,
545
+ )
546
+ return next
547
+ }
548
+
549
+ /**
550
+ * Wait for any in-flight sidebar state write to drain. The plugin lifecycle
551
+ * calls this during `dispose()` so the file logger and RPC server are torn
552
+ * down only after every queued write has either landed or thrown.
553
+ */
554
+ export function drainSidebarWrites(): Promise<void> {
555
+ return sidebarWriteChain
556
+ }
557
+
558
+ async function sleep(ms: number): Promise<void> {
559
+ await new Promise((resolve) => setTimeout(resolve, ms))
560
+ }
561
+
562
+ async function acquireSidebarLockWithRetry(
563
+ stateFile: string,
564
+ ): Promise<FencedFileLock> {
565
+ const start = Date.now()
566
+ let attempt = 0
567
+ while (true) {
568
+ await emitMergeStep('await-lock')
569
+ const lock = await acquireFencedFileLock({
570
+ path: stateFile,
571
+ name: SIDEBAR_LOCK_NAME,
572
+ ttlMs: SIDEBAR_LOCK_TTL_MS,
573
+ renew: true,
574
+ })
575
+ if (lock) {
576
+ await emitMergeStep('acquired-lock')
577
+ return lock
578
+ }
579
+ const elapsed = Date.now() - start
580
+ if (elapsed >= SIDEBAR_LOCK_TIMEOUT_MS) {
581
+ throw new SidebarStateLockContentionError(
582
+ stateFile,
583
+ SIDEBAR_LOCK_TIMEOUT_MS,
584
+ )
585
+ }
586
+ const backoff = Math.min(
587
+ SIDEBAR_LOCK_RETRY_CAP_MS,
588
+ SIDEBAR_LOCK_RETRY_BASE_MS + attempt * 10,
589
+ )
590
+ const jitter = Math.floor(Math.random() * SIDEBAR_LOCK_JITTER_MS)
591
+ await sleep(backoff + jitter)
592
+ attempt++
593
+ }
594
+ }
595
+
596
+ async function performSidebarWrite(
597
+ stateFile: string,
598
+ merge: (existing: SidebarStateV1) => SidebarStateV1,
599
+ ): Promise<void> {
600
+ await enqueueSidebarWrite(async () => {
601
+ ensureSidebarStateDir(stateFile)
602
+ const lock = await acquireSidebarLockWithRetry(stateFile)
603
+ try {
604
+ await lock.assertOwned()
605
+ const existing = readSidebarState(stateFile)
606
+ await emitMergeStep('read-state')
607
+ const merged = merge(existing)
608
+ await emitMergeStep('merged-state')
609
+ await writeJsonAtomic(stateFile, merged)
610
+ // `writeJsonAtomic` stages a tmp file with mode 0o600 and renames onto
611
+ // the target; POSIX rename replaces the inode so the new file inherits
612
+ // the staged mode bits. Windows ignores POSIX modes so the assertion
613
+ // in tests is best-effort there.
614
+ await emitMergeStep('wrote-state')
615
+ } finally {
616
+ await lock.release().catch(() => {})
617
+ }
618
+ })
619
+ }
620
+
621
+ /**
622
+ * Merge a new machine-state payload against the on-disk state.
623
+ *
624
+ * Stale writes (new `checkedAt` < existing) are dropped — only newer/equal
625
+ * `checkedAt` may replace machine fields. The merge preserves
626
+ * `routingAuthoritative: true` once it is true (sticky-true), keeps the
627
+ * existing `activeRouting` intact (routing is merged via its own writer),
628
+ * and prunes any expired routing entries as a side effect.
629
+ */
630
+ function mergeMachineState(
631
+ existing: SidebarStateV1,
632
+ next: SidebarMachineState,
633
+ ): SidebarStateV1 {
634
+ if (next.checkedAt < existing.checkedAt) {
635
+ return {
636
+ ...existing,
637
+ activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),
638
+ }
639
+ }
640
+ return {
641
+ version: SIDEBAR_STATE_VERSION,
642
+ checkedAt: next.checkedAt,
643
+ accounts: next.accounts,
644
+ quotaBackoffUntil: next.quotaBackoffUntil,
645
+ lastError: next.lastError,
646
+ routingAuthoritative:
647
+ existing.routingAuthoritative === true ||
648
+ next.routingAuthoritative === true,
649
+ // Symmetric with the stale-write branch: every machine-write merge
650
+ // re-prunes activeRouting so a long-running TUI session eventually
651
+ // drops dead routes even when no fresh routing upsert lands. Cheap
652
+ // (a single Object.entries + sort over ≤100 entries) and bounded.
653
+ activeRouting: pruneActiveRouting(existing.activeRouting, Date.now()),
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Upsert a single session's active routing entry. The fetch interceptor calls
659
+ * this with `authoritative: true` after every final route selection; the
660
+ * `accountId`/`modelFamily`/`headerStyle` fields are already redacted by the
661
+ * caller (the writer never sees token or project fields).
662
+ */
663
+ export async function upsertSidebarActiveRouting(
664
+ sessionId: string,
665
+ entry: SidebarRoutingEntry,
666
+ options: SidebarStateWriteOptions & { authoritative?: boolean } = {},
667
+ ): Promise<void> {
668
+ const stateFile = options.stateFile ?? getSidebarStateFile()
669
+ await performSidebarWrite(stateFile, (existing) => {
670
+ const activeRouting = { ...existing.activeRouting, [sessionId]: entry }
671
+ return {
672
+ ...existing,
673
+ routingAuthoritative:
674
+ options.authoritative === true ? true : existing.routingAuthoritative,
675
+ activeRouting: pruneActiveRouting(activeRouting, Date.now()),
676
+ }
677
+ })
678
+ }
679
+
680
+ /**
681
+ * Remove one session's active routing entry. The event handler calls this
682
+ * when a session is deleted so the sidebar does not retain dead routes.
683
+ */
684
+ export async function removeSidebarActiveRouting(
685
+ sessionId: string,
686
+ options: SidebarStateWriteOptions = {},
687
+ ): Promise<void> {
688
+ const stateFile = options.stateFile ?? getSidebarStateFile()
689
+ await performSidebarWrite(stateFile, (existing) => {
690
+ if (!(sessionId in existing.activeRouting)) {
691
+ return existing
692
+ }
693
+ const activeRouting = { ...existing.activeRouting }
694
+ delete activeRouting[sessionId]
695
+ return {
696
+ ...existing,
697
+ activeRouting,
698
+ }
699
+ })
700
+ }
701
+
702
+ /**
703
+ * Write a new machine-state snapshot. The fetch interceptor and quota
704
+ * manager call this after each refresh; auth-loader calls it after the
705
+ * account pool is materialized.
706
+ */
707
+ export async function setSidebarMachineState(
708
+ next: SidebarMachineState,
709
+ options: SidebarStateWriteOptions = {},
710
+ ): Promise<void> {
711
+ const stateFile = options.stateFile ?? getSidebarStateFile()
712
+ await performSidebarWrite(stateFile, (existing) =>
713
+ mergeMachineState(existing, next),
714
+ )
715
+ }
716
+
717
+ void SIDEBAR_STATE_FILE_MODE