@vantran-se/openchamber-web 2.1.1 → 2.2.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 (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -12,9 +12,11 @@ const mockResponse = (body, init = {}) => ({
12
12
  });
13
13
 
14
14
  describe('Kimi for Coding quota provider', () => {
15
- it('computes weekly usedPercent from the used field (live API shape, no remaining field)', async () => {
16
- // Captured from GET https://api.kimi.com/coding/v1/usages — the weekly
17
- // `usage` block only ever includes `used`, never `remaining`.
15
+ it('computes weekly usedPercent from the used field (pre-migration payload, no remaining field)', async () => {
16
+ // Captured from GET https://api.kimi.com/coding/v1/usages before Kimi
17
+ // migrated plans to the ratio-based `usages` map. Plans that have not
18
+ // migrated still answer this way: the weekly `usage` block only ever
19
+ // includes `used`, never `remaining`.
18
20
  const fetchImpl = async () => (
19
21
  mockResponse({
20
22
  usage: { limit: '100', used: '100', resetTime: '2026-08-04T06:21:48.514003Z' },
@@ -103,6 +105,129 @@ describe('Kimi for Coding quota provider', () => {
103
105
  expect(result.error).toBe('API error: 401');
104
106
  });
105
107
 
108
+ it('shows the pay-as-you-go balance when the Kimi Code address refuses a platform key', async () => {
109
+ const urls = [];
110
+ const fetchImpl = async (url, init) => {
111
+ urls.push({ url, authorization: init.headers.Authorization });
112
+ return url === 'https://api.moonshot.ai/v1/users/me/balance'
113
+ ? Response.json({ code: 0, data: { available_balance: 12.345 }, status: true })
114
+ : new Response('{}', { status: 401 });
115
+ };
116
+
117
+ const result = await fetchQuota({ readAuth, fetchImpl });
118
+
119
+ expect(urls.map((call) => call.url)).toEqual([
120
+ 'https://api.kimi.com/coding/v1/usages',
121
+ 'https://api.moonshot.ai/v1/users/me/balance',
122
+ ]);
123
+ expect(urls[1].authorization).toBe('Bearer test-token');
124
+ expect(result.ok).toBe(true);
125
+ expect(result.providerId).toBe('kimi-for-coding');
126
+ expect(result.providerName).toBe('Kimi for Coding');
127
+ expect(result.usage.windows.credits_balance.valueLabel).toBe('$12.35');
128
+ });
129
+
130
+ it('keeps the original 401 when the balance read fails too', async () => {
131
+ const urls = [];
132
+ const fetchImpl = async (url) => {
133
+ urls.push(url);
134
+ return new Response('{}', { status: 401 });
135
+ };
136
+
137
+ const result = await fetchQuota({ readAuth, fetchImpl });
138
+
139
+ expect(urls).toEqual(['https://api.kimi.com/coding/v1/usages', 'https://api.moonshot.ai/v1/users/me/balance']);
140
+ expect(result.ok).toBe(false);
141
+ expect(result.error).toBe('API error: 401');
142
+ });
143
+
144
+ it('treats a 403 from the Kimi Code address like a 401', async () => {
145
+ const fetchImpl = async (url) => (
146
+ url === 'https://api.moonshot.ai/v1/users/me/balance'
147
+ ? Response.json({ data: { available_balance: 5 } })
148
+ : new Response('{}', { status: 403 })
149
+ );
150
+
151
+ const result = await fetchQuota({ readAuth, fetchImpl });
152
+
153
+ expect(result.ok).toBe(true);
154
+ expect(result.usage.windows.credits_balance.valueLabel).toBe('$5.00');
155
+ });
156
+
157
+ it('does not try the balance address for errors other than authorisation', async () => {
158
+ const urls = [];
159
+ const fetchImpl = async (url) => {
160
+ urls.push(url);
161
+ return new Response('{}', { status: 500 });
162
+ };
163
+
164
+ const result = await fetchQuota({ readAuth, fetchImpl });
165
+
166
+ expect(urls).toEqual(['https://api.kimi.com/coding/v1/usages']);
167
+ expect(result.error).toBe('API error: 500');
168
+ });
169
+
170
+ describe('ratio-based usages payload', () => {
171
+ // Captured live from GET https://api.kimi.com/coding/v1/usages on
172
+ // 2026-10-08. Migrated plans answer with a ratio-based `usages` map instead
173
+ // of the counted `usage` block, and keep a coarser `limits[]` entry for the
174
+ // same 5-hour window. `limit_month_code` is the code-typed share of
175
+ // `limit_month_total`, not a second allowance, and is not shown.
176
+ const migratedPlan = {
177
+ limits: [{
178
+ window: { duration: 300, timeUnit: 'TIME_UNIT_MINUTE' },
179
+ detail: { limit: '100', used: '50', remaining: '50', resetTime: '2026-10-08T11:50:22.234058Z' },
180
+ }],
181
+ usages: {
182
+ limit_5h: { used_ratio: 0.495422, reset_time: '2026-10-08T11:50:22Z' },
183
+ limit_month_total: { used_ratio: 0.2636, reset_time: '2026-10-18T00:00:00Z' },
184
+ limit_month_code: { used_ratio: 0.1907, reset_time: '2026-10-18T00:00:00Z' },
185
+ },
186
+ };
187
+
188
+ const run = (body) => fetchQuota({ readAuth, fetchImpl: async () => mockResponse(body) });
189
+
190
+ it('reads the 5h and monthly ratios and ignores the duplicate limits[] entry', async () => {
191
+ const result = await run(migratedPlan);
192
+
193
+ expect(result.ok).toBe(true);
194
+ expect(Object.keys(result.usage.windows)).toEqual(['5h', 'monthly']);
195
+ expect(result.usage.windows['5h'].usedPercent).toBeCloseTo(49.5422, 9);
196
+ expect(result.usage.windows['5h'].windowSeconds).toBe(18000);
197
+ expect(result.usage.windows['5h'].resetAt).toBe(Date.parse('2026-10-08T11:50:22Z'));
198
+ });
199
+
200
+ it('reports the monthly row as a plain percent so it follows the used/remaining setting', async () => {
201
+ const result = await run(migratedPlan);
202
+
203
+ expect(result.usage.windows.monthly.usedPercent).toBeCloseTo(26.36, 9);
204
+ expect(result.usage.windows.monthly.remainingPercent).toBeCloseTo(73.64, 9);
205
+ expect(result.usage.windows.monthly.valueLabel).toBeUndefined();
206
+ expect(result.usage.windows.monthly.windowSeconds).toBeNull();
207
+ expect(result.usage.windows.monthly.resetAt).toBe(Date.parse('2026-10-18T00:00:00Z'));
208
+ });
209
+
210
+ it('maps limit_7d to the weekly window when the plan still serves it', async () => {
211
+ const result = await run({ usages: { limit_7d: { used_ratio: 0.25, reset_time: null } } });
212
+
213
+ expect(result.usage.windows.weekly.usedPercent).toBe(25);
214
+ expect(result.usage.windows.weekly.windowSeconds).toBe(604800);
215
+ expect(result.usage.windows.weekly.valueLabel).toBeUndefined();
216
+ });
217
+
218
+ it('keeps a zero ratio and skips an entry without one', async () => {
219
+ const result = await run({
220
+ usages: {
221
+ limit_5h: { used_ratio: 0, reset_time: null },
222
+ limit_month_total: { used_ratio: 'not-a-number', reset_time: null }
223
+ }
224
+ });
225
+
226
+ expect(result.usage.windows['5h'].usedPercent).toBe(0);
227
+ expect(Object.keys(result.usage.windows)).toEqual(['5h']);
228
+ });
229
+ });
230
+
106
231
  describe('credential lookup', () => {
107
232
  const sentKey = async (auth) => {
108
233
  let authorization;
@@ -1,31 +1,94 @@
1
1
  import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import { deleteLegacyOpenCodeGoCredential } from '../credentials/store.js';
3
- import { buildResult, getAuthEntry, normalizeAuthEntry, toUsageWindow } from '../utils/index.js';
3
+ import {
4
+ asNonEmptyString,
5
+ asObject,
6
+ buildResult,
7
+ getAuthEntry,
8
+ normalizeAuthEntry,
9
+ toNumber,
10
+ toTimestamp,
11
+ toUsageWindow,
12
+ } from '../utils/index.js';
4
13
 
5
14
  export const providerId = 'opencode-go';
6
15
  export const providerName = 'OpenCode Go';
7
- export const aliases = ['opencode-go'];
16
+ const aliases = ['opencode-go'];
8
17
 
9
- const windowsByApiKey = {
18
+ // OpenCode Go has two credential shapes. A service-account key stored under the
19
+ // `opencode-go` integration uses the legacy usage API. A Console sign-in lives
20
+ // on the shared `opencode` integration and its OAuth access token is not
21
+ // interchangeable with that key, so it uses the Console Go status endpoint.
22
+ const API_KEY_USAGE_URL = 'https://opencode.ai/zen/go/v1/usage';
23
+ const CONSOLE_STATUS_URL = 'https://opencode.ai/console/api/go/status';
24
+ const CONSOLE_SERVER = 'https://opencode.ai/console';
25
+ const CONSOLE_INTEGRATION_ID = 'opencode';
26
+ const ORGANIZATION_ID_PATTERN = /^org_[A-Za-z0-9]+$/;
27
+ const CONSOLE_PRODUCTS = new Set(['go', 'go-plus']);
28
+ const REQUEST_TIMEOUT_MS = 15_000;
29
+
30
+ const API_KEY_WINDOW_FIELDS = {
10
31
  '5h': 'rolling',
11
32
  weekly: 'weekly',
12
33
  monthly: 'monthly',
13
34
  };
14
35
 
36
+ // The Console page meters. `week` maps to the existing `weekly` window key the
37
+ // UI already labels, matching the API-key path.
38
+ const CONSOLE_WINDOW_METERS = {
39
+ '5h': 'fiveHour',
40
+ weekly: 'week',
41
+ monthly: 'month',
42
+ };
43
+
44
+ const clampPercent = (value) => Math.min(100, Math.max(0, value));
45
+
15
46
  export const parseOpenCodeGoUsage = (payload) => {
16
- const usage = payload && typeof payload === 'object' ? payload.usage : null;
17
- if (!usage || typeof usage !== 'object') return {};
47
+ const usage = asObject(payload) ? asObject(payload.usage) : null;
48
+ if (!usage) return {};
18
49
  const windows = {};
19
- for (const [key, apiKey] of Object.entries(windowsByApiKey)) {
20
- const entry = usage[apiKey];
21
- if (!entry || typeof entry !== 'object') continue;
22
- const usedPercent = entry.percent;
23
- const resetAt = entry.resetsAt;
24
- if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent)) continue;
25
- if (typeof resetAt !== 'string' || !Number.isFinite(new Date(resetAt).getTime())) continue;
50
+ for (const [key, field] of Object.entries(API_KEY_WINDOW_FIELDS)) {
51
+ const entry = asObject(usage[field]);
52
+ if (!entry) continue;
53
+ const usedPercent = toNumber(entry.percent);
54
+ const resetAt = toTimestamp(entry.resetsAt);
55
+ if (usedPercent === null || resetAt === null) continue;
26
56
  windows[key] = toUsageWindow({
27
- usedPercent: Math.min(100, Math.max(0, usedPercent)),
28
- resetAt: new Date(resetAt).getTime(),
57
+ usedPercent: clampPercent(usedPercent),
58
+ resetAt,
59
+ windowSeconds: null,
60
+ });
61
+ }
62
+ return windows;
63
+ };
64
+
65
+ /**
66
+ * The Console meters report integer micro-cents as decimal strings. A missing,
67
+ * negative, or unparseable amount is not usable data, and a zero limit would
68
+ * divide by zero, so each is rejected on its own without discarding the other
69
+ * windows.
70
+ */
71
+ const toMicroCents = (value) => {
72
+ const amount = toNumber(value);
73
+ if (amount === null || amount < 0) return null;
74
+ return amount;
75
+ };
76
+
77
+ export const parseConsoleGoUsage = (payload) => {
78
+ const access = asObject(payload) ? asObject(payload.access) : null;
79
+ const meters = access ? asObject(access.meters) : null;
80
+ if (!meters) return {};
81
+ const windows = {};
82
+ for (const [key, meterName] of Object.entries(CONSOLE_WINDOW_METERS)) {
83
+ const meter = asObject(meters[meterName]);
84
+ if (!meter) continue;
85
+ const limit = toMicroCents(meter.limitMicroCents);
86
+ const used = toMicroCents(meter.usedMicroCents);
87
+ const resetAt = toTimestamp(meter.resetsAt);
88
+ if (limit === null || limit <= 0 || used === null || resetAt === null) continue;
89
+ windows[key] = toUsageWindow({
90
+ usedPercent: clampPercent((used / limit) * 100),
91
+ resetAt,
29
92
  windowSeconds: null,
30
93
  });
31
94
  }
@@ -33,14 +96,14 @@ export const parseOpenCodeGoUsage = (payload) => {
33
96
  };
34
97
 
35
98
  export const fetchOpenCodeGoUsage = async (apiKey, fetchImpl = fetch) => {
36
- const response = await fetchImpl('https://opencode.ai/zen/go/v1/usage', {
99
+ const response = await fetchImpl(API_KEY_USAGE_URL, {
37
100
  headers: {
38
101
  Accept: 'application/json',
39
102
  Authorization: `Bearer ${apiKey}`,
40
103
  'x-opencode-session': 'openchamber-usage',
41
104
  'User-Agent': 'OpenChamber quota provider',
42
105
  },
43
- signal: AbortSignal.timeout(15_000),
106
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
44
107
  });
45
108
  if (response.status === 401 || response.status === 403) {
46
109
  throw new Error('OpenCode Go authentication failed');
@@ -51,19 +114,79 @@ export const fetchOpenCodeGoUsage = async (apiKey, fetchImpl = fetch) => {
51
114
  return windows;
52
115
  };
53
116
 
117
+ export const fetchConsoleGoUsage = async ({ access, orgID, expires }, fetchImpl = fetch) => {
118
+ if (expires !== null && expires > 0 && expires <= Date.now()) {
119
+ throw new Error('OpenCode Console sign-in expired. Sign in again in Providers.');
120
+ }
121
+ const response = await fetchImpl(CONSOLE_STATUS_URL, {
122
+ headers: {
123
+ Accept: 'application/json',
124
+ Authorization: `Bearer ${access}`,
125
+ 'x-org-id': orgID,
126
+ 'User-Agent': 'OpenChamber quota provider',
127
+ },
128
+ // The bearer token belongs to opencode.ai/console; never follow a redirect
129
+ // that would forward it to another origin.
130
+ redirect: 'error',
131
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
132
+ });
133
+ if (response.status === 401 || response.status === 403) {
134
+ throw new Error('OpenCode Console sign-in expired. Sign in again in Providers.');
135
+ }
136
+ if (!response.ok) throw new Error(`OpenCode Console Go status API returned HTTP ${response.status}`);
137
+ const payload = await response.json().catch(() => null);
138
+ if (!asObject(payload)) throw new Error('OpenCode Console Go status returned an unreadable response');
139
+ if (!CONSOLE_PRODUCTS.has(payload.product)) {
140
+ throw new Error('No active OpenCode Go subscription on the selected Console account');
141
+ }
142
+ const windows = parseConsoleGoUsage(payload);
143
+ if (Object.keys(windows).length === 0) throw new Error('OpenCode Go usage data could not be parsed');
144
+ return windows;
145
+ };
146
+
54
147
  const getApiKey = (auth) => {
55
148
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
56
- return entry?.key ?? entry?.token ?? null;
149
+ return asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
150
+ };
151
+
152
+ /**
153
+ * The active OpenCode Console OAuth credential, only when it is a Console
154
+ * sign-in (`server` is the Console origin and an organization is selected).
155
+ * OpenCode marks exactly one credential active per integration and credentials
156
+ * are global, so this is the selected account and organization; reading it per
157
+ * call follows a switch instead of caching the previous account.
158
+ */
159
+ const getConsoleCredential = (auth) => {
160
+ const entry = normalizeAuthEntry(getAuthEntry(auth, [CONSOLE_INTEGRATION_ID]));
161
+ if (!entry || entry.type !== 'oauth') return null;
162
+ const access = asNonEmptyString(entry.access);
163
+ const orgID = asNonEmptyString(entry.orgID);
164
+ if (!access || entry.server !== CONSOLE_SERVER || !orgID || !ORGANIZATION_ID_PATTERN.test(orgID)) return null;
165
+ const expires = toNumber(entry.expires);
166
+ return { access, orgID, expires };
57
167
  };
58
168
 
59
- export const isConfigured = (auth) => Boolean(getApiKey(auth));
169
+ export const isConfigured = (auth) => Boolean(getConsoleCredential(auth) || getApiKey(auth));
60
170
 
61
- export const fetchQuota = async () => {
171
+ export const fetchQuota = async ({ readAuth = readOpenCodeCredentials, fetchImpl = fetch } = {}) => {
62
172
  try {
63
173
  deleteLegacyOpenCodeGoCredential();
64
- const apiKey = getApiKey(await readOpenCodeCredentials());
174
+ const auth = await readAuth();
175
+ // A Console sign-in serves OpenCode Go once it exists; the `opencode-go`
176
+ // service key is the fallback for accounts without one, and for a Console
177
+ // read that fails (no Go in that org, an endpoint change, a hiccup).
178
+ const consoleCredential = getConsoleCredential(auth);
179
+ const apiKey = getApiKey(auth);
180
+ if (consoleCredential) {
181
+ try {
182
+ const windows = await fetchConsoleGoUsage(consoleCredential, fetchImpl);
183
+ return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows } });
184
+ } catch (consoleError) {
185
+ if (!apiKey) throw consoleError;
186
+ }
187
+ }
65
188
  if (!apiKey) return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
66
- const windows = await fetchOpenCodeGoUsage(apiKey);
189
+ const windows = await fetchOpenCodeGoUsage(apiKey, fetchImpl);
67
190
  return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows } });
68
191
  } catch (error) {
69
192
  return buildResult({ providerId, providerName, ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });
@@ -7,11 +7,16 @@ const previousDataDirectory = process.env.OPENCHAMBER_DATA_DIR;
7
7
  const temporaryDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-opencode-go-'));
8
8
  process.env.OPENCHAMBER_DATA_DIR = temporaryDataDirectory;
9
9
 
10
- vi.mock('../../opencode/auth.js', () => ({
11
- readOpenCodeCredentials: async () => ({ 'opencode-go': { key: 'test-key' } }),
12
- }));
10
+ import {
11
+ fetchConsoleGoUsage,
12
+ fetchOpenCodeGoUsage,
13
+ fetchQuota,
14
+ isConfigured,
15
+ parseConsoleGoUsage,
16
+ parseOpenCodeGoUsage,
17
+ } from './opencode-go.js';
13
18
 
14
- import { fetchOpenCodeGoUsage, fetchQuota, parseOpenCodeGoUsage } from './opencode-go.js';
19
+ const apiKeyAuth = () => ({ 'opencode-go': { key: 'test-key' } });
15
20
 
16
21
  afterEach(() => {
17
22
  vi.unstubAllGlobals();
@@ -23,7 +28,33 @@ afterAll(() => {
23
28
  fs.rmSync(temporaryDataDirectory, { recursive: true, force: true });
24
29
  });
25
30
 
26
- describe('OpenCode Go quota provider', () => {
31
+ const CONSOLE_SERVER = 'https://opencode.ai/console';
32
+ const CONSOLE_STATUS_URL = `${CONSOLE_SERVER}/api/go/status`;
33
+
34
+ const consoleAuth = (overrides = {}) => ({
35
+ opencode: {
36
+ type: 'oauth',
37
+ access: 'console-access',
38
+ refresh: 'console-refresh',
39
+ expires: Date.now() + 3_600_000,
40
+ server: CONSOLE_SERVER,
41
+ orgID: 'org_TESTORG123',
42
+ ...overrides,
43
+ },
44
+ });
45
+
46
+ const consolePayload = ({ product = 'go', meters } = {}) => ({
47
+ product,
48
+ access: {
49
+ meters: meters ?? {
50
+ fiveHour: { resetsAt: '2026-08-12T12:00:00.000Z', limitMicroCents: '1000000000', usedMicroCents: '250000000' },
51
+ week: { resetsAt: '2026-08-19T12:00:00.000Z', limitMicroCents: '2000000000', usedMicroCents: '800000000' },
52
+ month: { resetsAt: '2026-09-01T00:00:00.000Z', limitMicroCents: '6000000000', usedMicroCents: '3000000000' },
53
+ },
54
+ },
55
+ });
56
+
57
+ describe('OpenCode Go quota provider — legacy API key', () => {
27
58
  it('parses partial API usage windows', () => {
28
59
  const windows = parseOpenCodeGoUsage({ usage: { rolling: { percent: 25, resetsAt: '2026-08-12T12:00:00.000Z' }, weekly: { percent: 40, resetsAt: '2026-08-19T12:00:00.000Z' } } });
29
60
  expect(windows['5h'].usedPercent).toBe(25);
@@ -83,9 +114,201 @@ describe('OpenCode Go quota provider', () => {
83
114
  fs.writeFileSync(legacyPath, '{not valid json', { mode: 0o600 });
84
115
  const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ usage: { rolling: { percent: 25, resetsAt: '2026-08-12T12:00:00.000Z' } } })));
85
116
  vi.stubGlobal('fetch', fetchMock);
86
- const result = await fetchQuota();
117
+ const result = await fetchQuota({ readAuth: async () => apiKeyAuth() });
87
118
  expect(result).toMatchObject({ providerId: 'opencode-go', ok: true, configured: true });
88
119
  expect(fetchMock.mock.calls[0][1].headers.Authorization).toBe('Bearer test-key');
89
120
  expect(fs.existsSync(legacyPath)).toBe(false);
90
121
  });
122
+
123
+ it('still uses the API-key path when the Console integration is not a Console sign-in', async () => {
124
+ const fetchImpl = vi.fn().mockResolvedValue(new Response(JSON.stringify({ usage: { rolling: { percent: 10, resetsAt: '2026-08-12T12:00:00.000Z' } } })));
125
+ const result = await fetchQuota({
126
+ readAuth: async () => ({
127
+ ...consoleAuth({ server: 'https://example.com/console' }),
128
+ 'opencode-go': { key: 'test-key' },
129
+ }),
130
+ fetchImpl,
131
+ });
132
+ expect(result.ok).toBe(true);
133
+ expect(fetchImpl.mock.calls[0][0]).toBe('https://opencode.ai/zen/go/v1/usage');
134
+ });
135
+ });
136
+
137
+ describe('OpenCode Go quota provider — Console OAuth', () => {
138
+ it('maps fiveHour, week, and month to the existing windows from decimal strings', () => {
139
+ const windows = parseConsoleGoUsage(consolePayload());
140
+ expect(Object.keys(windows)).toEqual(['5h', 'weekly', 'monthly']);
141
+ expect(windows['5h'].usedPercent).toBe(25);
142
+ expect(windows.weekly.usedPercent).toBe(40);
143
+ expect(windows.monthly.usedPercent).toBe(50);
144
+ expect(windows['5h'].resetAt).toBe(Date.parse('2026-08-12T12:00:00.000Z'));
145
+ });
146
+
147
+ it('supports Go Plus', async () => {
148
+ const result = await fetchQuota({
149
+ readAuth: async () => consoleAuth(),
150
+ fetchImpl: async () => new Response(JSON.stringify(consolePayload({ product: 'go-plus' }))),
151
+ });
152
+ expect(result.ok).toBe(true);
153
+ expect(result.usage.windows['5h'].usedPercent).toBe(25);
154
+ });
155
+
156
+ it('skips only the unusable meters and keeps the rest', () => {
157
+ const windows = parseConsoleGoUsage(consolePayload({ meters: {
158
+ fiveHour: { resetsAt: '2026-08-12T12:00:00.000Z', limitMicroCents: '0', usedMicroCents: '10' },
159
+ week: { resetsAt: '2026-08-19T12:00:00.000Z', limitMicroCents: 'not-a-number', usedMicroCents: '10' },
160
+ month: { resetsAt: '2026-09-01T00:00:00.000Z', limitMicroCents: '1000', usedMicroCents: '250' },
161
+ } }));
162
+ expect(Object.keys(windows)).toEqual(['monthly']);
163
+ expect(windows.monthly.usedPercent).toBe(25);
164
+ });
165
+
166
+ it('skips a window with an invalid reset timestamp and handles missing meters', () => {
167
+ const windows = parseConsoleGoUsage(consolePayload({ meters: {
168
+ fiveHour: { resetsAt: 'not-a-date', limitMicroCents: '1000', usedMicroCents: '250' },
169
+ week: { resetsAt: '2026-08-19T12:00:00.000Z', limitMicroCents: '1000', usedMicroCents: '250' },
170
+ } }));
171
+ expect(Object.keys(windows)).toEqual(['weekly']);
172
+ expect(parseConsoleGoUsage({ product: 'go', access: {} })).toEqual({});
173
+ expect(parseConsoleGoUsage(null)).toEqual({});
174
+ });
175
+
176
+ it('calls the Console Go status endpoint with the Console token and organization', async () => {
177
+ let request;
178
+ const windows = await fetchConsoleGoUsage(
179
+ { access: 'console-access', orgID: 'org_TESTORG123', expires: Date.now() + 60_000 },
180
+ async (url, options) => {
181
+ request = { url, options };
182
+ return new Response(JSON.stringify(consolePayload()));
183
+ },
184
+ );
185
+ expect(request.url).toBe(CONSOLE_STATUS_URL);
186
+ expect(request.options.headers).toMatchObject({
187
+ Accept: 'application/json',
188
+ Authorization: 'Bearer console-access',
189
+ 'x-org-id': 'org_TESTORG123',
190
+ });
191
+ expect(request.options.headers['x-opencode-session']).toBeUndefined();
192
+ expect(request.options.redirect).toBe('error');
193
+ expect(windows.weekly.usedPercent).toBe(40);
194
+ });
195
+
196
+ it('prefers the selected Console account and organization over the legacy key', async () => {
197
+ const fetchImpl = vi.fn().mockResolvedValue(new Response(JSON.stringify(consolePayload())));
198
+ const result = await fetchQuota({
199
+ readAuth: async () => ({ ...consoleAuth(), 'opencode-go': { key: 'stale-key' } }),
200
+ fetchImpl,
201
+ });
202
+ expect(result.ok).toBe(true);
203
+ expect(fetchImpl.mock.calls[0][0]).toBe(CONSOLE_STATUS_URL);
204
+ expect(fetchImpl.mock.calls[0][1].headers['x-org-id']).toBe('org_TESTORG123');
205
+ });
206
+
207
+ it('falls back to the legacy key when the Console read fails', async () => {
208
+ const fetchImpl = vi.fn(async (url) => (url === CONSOLE_STATUS_URL
209
+ ? new Response(JSON.stringify(consolePayload({ product: 'zen' })))
210
+ : new Response(JSON.stringify({ usage: { rolling: { percent: 10, resetsAt: '2026-08-12T12:00:00.000Z' } } }))));
211
+ const result = await fetchQuota({
212
+ readAuth: async () => ({ ...consoleAuth(), 'opencode-go': { key: 'test-key' } }),
213
+ fetchImpl,
214
+ });
215
+ expect(result.ok).toBe(true);
216
+ expect(result.usage.windows['5h'].usedPercent).toBe(10);
217
+ expect(fetchImpl.mock.calls.map((call) => call[0])).toEqual([CONSOLE_STATUS_URL, 'https://opencode.ai/zen/go/v1/usage']);
218
+ });
219
+
220
+ it('reports the Console error when there is no key to fall back to', async () => {
221
+ const result = await fetchQuota({
222
+ readAuth: async () => consoleAuth(),
223
+ fetchImpl: async () => new Response(JSON.stringify(consolePayload({ product: 'zen' }))),
224
+ });
225
+ expect(result.ok).toBe(false);
226
+ expect(result.configured).toBe(true);
227
+ });
228
+
229
+ it('reports an unconfigured provider without requesting', async () => {
230
+ const fetchImpl = vi.fn();
231
+ const result = await fetchQuota({ readAuth: async () => ({}), fetchImpl });
232
+ expect(result).toEqual(expect.objectContaining({ ok: false, configured: false, error: 'Not configured' }));
233
+ expect(fetchImpl).not.toHaveBeenCalled();
234
+ });
235
+
236
+ it('reports an unconfigured provider when the Console sign-in is for another server or has no organization', async () => {
237
+ expect(isConfigured(consoleAuth({ server: 'https://example.com/console' }))).toBe(false);
238
+ expect(isConfigured(consoleAuth({ orgID: 'not-an-org' }))).toBe(false);
239
+ expect(isConfigured(consoleAuth())).toBe(true);
240
+ expect(isConfigured({ 'opencode-go': { key: 'test-key' } })).toBe(true);
241
+ });
242
+
243
+ it('distinguishes an expired sign-in without sending the stale token', async () => {
244
+ const fetchImpl = vi.fn();
245
+ const result = await fetchQuota({
246
+ readAuth: async () => consoleAuth({ expires: Date.now() - 1_000 }),
247
+ fetchImpl,
248
+ });
249
+ expect(fetchImpl).not.toHaveBeenCalled();
250
+ expect(result.configured).toBe(true);
251
+ expect(result.error).toMatch(/sign-in expired/i);
252
+ });
253
+
254
+ it('surfaces a 401 as an expired sign-in', async () => {
255
+ const result = await fetchQuota({
256
+ readAuth: async () => consoleAuth(),
257
+ fetchImpl: async () => new Response('', { status: 401 }),
258
+ });
259
+ expect(result.configured).toBe(true);
260
+ expect(result.error).toMatch(/sign-in expired/i);
261
+ });
262
+
263
+ it('distinguishes an absent subscription from a malformed response', async () => {
264
+ const absent = await fetchQuota({
265
+ readAuth: async () => consoleAuth(),
266
+ fetchImpl: async () => new Response(JSON.stringify({ product: 'free', access: { meters: {} } })),
267
+ });
268
+ expect(absent.configured).toBe(true);
269
+ expect(absent.error).toMatch(/subscription/i);
270
+
271
+ const malformed = await fetchQuota({
272
+ readAuth: async () => consoleAuth(),
273
+ fetchImpl: async () => new Response('not json', { status: 200 }),
274
+ });
275
+ expect(malformed.configured).toBe(true);
276
+ expect(malformed.error).toMatch(/unreadable/i);
277
+ });
278
+
279
+ it('reports a temporary request failure as configured', async () => {
280
+ const result = await fetchQuota({
281
+ readAuth: async () => consoleAuth(),
282
+ fetchImpl: async () => new Response('', { status: 502 }),
283
+ });
284
+ expect(result.configured).toBe(true);
285
+ expect(result.error).toMatch(/HTTP 502/);
286
+ });
287
+
288
+ it('follows the selected account on every read instead of caching usage', async () => {
289
+ let call = 0;
290
+ const seen = [];
291
+ const result = await fetchQuota({
292
+ readAuth: async () => {
293
+ call += 1;
294
+ return consoleAuth({ orgID: call === 1 ? 'org_FIRST123' : 'org_SECOND123' });
295
+ },
296
+ fetchImpl: async (_url, options) => {
297
+ seen.push(options.headers['x-org-id']);
298
+ return new Response(JSON.stringify(consolePayload({ meters: {
299
+ fiveHour: { resetsAt: '2026-08-12T12:00:00.000Z', limitMicroCents: '1000', usedMicroCents: call === 1 ? '250' : '900' },
300
+ } })));
301
+ },
302
+ });
303
+ expect(seen).toEqual(['org_FIRST123']);
304
+ expect(result.usage.windows['5h'].usedPercent).toBe(25);
305
+
306
+ const second = await fetchQuota({
307
+ readAuth: async () => consoleAuth({ orgID: 'org_SECOND123' }),
308
+ fetchImpl: async () => new Response(JSON.stringify(consolePayload({ meters: {
309
+ fiveHour: { resetsAt: '2026-08-12T12:00:00.000Z', limitMicroCents: '1000', usedMicroCents: '900' },
310
+ } }))),
311
+ });
312
+ expect(second.usage.windows['5h'].usedPercent).toBe(90);
313
+ });
91
314
  });
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  export const providerId = 'openrouter';
16
16
  export const providerName = 'OpenRouter';
17
- export const aliases = ['openrouter'];
17
+ const aliases = ['openrouter'];
18
18
  const OPENROUTER_API_BASE = 'https://openrouter.ai/api/v1';
19
19
  const PERIOD_SECONDS = { daily: 86400, weekly: 604800, monthly: 30 * 86400 };
20
20
 
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  export const providerId = 'zenmux';
12
12
  export const providerName = 'ZenMux';
13
- export const aliases = ['zenmux'];
13
+ const aliases = ['zenmux'];
14
14
  const ZENMUX_BALANCE_URL = 'https://zenmux.ai/api/v1/management/payg/balance';
15
15
 
16
16
  const readStoredCredential = () => readManagedCredential(providerId);
@@ -75,3 +75,4 @@ export const resolveConfigApiKey = (value, { configDir = OPENCODE_CONFIG_DIR, ho
75
75
  return null;
76
76
  }
77
77
  };
78
+
@@ -1,10 +1,6 @@
1
- export const asObject = (value) => (value && typeof value === 'object' ? value : null);
1
+ export { asNonEmptyString } from '../../shared/guards.js';
2
2
 
3
- export const asNonEmptyString = (value) => {
4
- if (typeof value !== 'string') return null;
5
- const trimmed = value.trim();
6
- return trimmed ? trimmed : null;
7
- };
3
+ export const asObject = (value) => (value && typeof value === 'object' ? value : null);
8
4
 
9
5
  export const toNumber = (value) => {
10
6
  if (typeof value === 'number' && Number.isFinite(value)) {