@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
@@ -1,5 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
2
  import fsPromises from 'node:fs/promises';
3
+ import { readFileSync } from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
 
@@ -284,8 +285,57 @@ describe('todos', () => {
284
285
  });
285
286
 
286
287
  test('clamps oversized todo text', async () => {
287
- await runtime.saveTodos(PROJECT_ID, [{ id: 't1', text: 'z'.repeat(300), createdAt: 1 }]);
288
- expect((await runtime.readContext(PROJECT_ID)).todos[0].text).toHaveLength(120);
288
+ await runtime.saveTodos(PROJECT_ID, [{ id: 't1', text: 'z'.repeat(1500), createdAt: 1 }]);
289
+ expect((await runtime.readContext(PROJECT_ID)).todos[0].text).toHaveLength(1000);
290
+ });
291
+ });
292
+
293
+ describe('committed changes', () => {
294
+ test('announces the owner only after note and todo changes reach disk', async () => {
295
+ const changes = [];
296
+ const announcing = createProjectContextRuntime({
297
+ fsPromises,
298
+ path,
299
+ projectsDirPath,
300
+ createId: () => `note-${++idCounter}`,
301
+ onChanged: (projectId) => {
302
+ changes.push({ projectId, context: JSON.parse(readFileSync(announcing.contextPathFor(projectId), 'utf8')) });
303
+ },
304
+ });
305
+ await announcing.readContext(PROJECT_ID);
306
+ expect(changes).toEqual([]);
307
+ const { note } = await announcing.createNote(PROJECT_ID, { body: 'peer note' });
308
+ expect(changes.map(change => change.projectId)).toEqual([PROJECT_ID]);
309
+ expect(changes[0].context.notes[0].body).toBe('peer note');
310
+
311
+ await announcing.updateNote(PROJECT_ID, note.id, { body: 'peer edit' });
312
+ await announcing.saveTodos(PROJECT_ID, [{ id: 't1', text: 'peer todo', completed: true, createdAt: 1 }]);
313
+ expect(changes.map(change => change.projectId)).toEqual([PROJECT_ID, PROJECT_ID, PROJECT_ID]);
314
+ expect(changes[1].context.notes[0].body).toBe('peer edit');
315
+ expect(changes[2].context.todos[0].completed).toBe(true);
316
+
317
+ await announcing.deleteNote(PROJECT_ID, note.id);
318
+ expect(changes.at(-1).context.notes).toEqual([]);
319
+ const count = changes.length;
320
+ await announcing.deleteNote(PROJECT_ID, 'missing');
321
+ await expect(announcing.createNote(PROJECT_ID, { body: ' ' })).rejects.toThrow('body is required');
322
+ expect(changes).toHaveLength(count);
323
+ });
324
+
325
+ test('a failed atomic write does not announce or replace the last committed context', async () => {
326
+ await runtime.createNote(PROJECT_ID, { body: 'kept' });
327
+ const changes = [];
328
+ const failing = createProjectContextRuntime({
329
+ fsPromises: { ...fsPromises, rename: async () => { throw new Error('disk full'); } },
330
+ path,
331
+ projectsDirPath,
332
+ onChanged: (projectId) => changes.push(projectId),
333
+ });
334
+ await expect(failing.saveTodos(PROJECT_ID, [{ id: 't1', text: 'not saved' }])).rejects.toThrow('disk full');
335
+ expect(changes).toEqual([]);
336
+ const context = await runtime.readContext(PROJECT_ID);
337
+ expect(context.notes[0].body).toBe('kept');
338
+ expect(context.todos).toEqual([]);
289
339
  });
290
340
  });
291
341
 
@@ -404,6 +454,8 @@ describe('plans', () => {
404
454
  expect(read.title).toBe('My Plan');
405
455
  expect(read.body).toBe('step one');
406
456
  expect(read.raw).toBe('# My Plan\n\nstep one');
457
+ // Where the file lives, so a comment on the plan can point an agent at it.
458
+ expect(read.path).toBe(path.join(plansDir(), plan.file));
407
459
  });
408
460
 
409
461
  test('newest plan is listed first', async () => {
@@ -567,6 +619,7 @@ describe('shared plans', () => {
567
619
  const read = await sharedRuntime.readPlan(PROJECT_ID, 'shared:roadmap.md');
568
620
  expect(read.title).toBe('Roadmap');
569
621
  expect(read.raw).toBe('# Roadmap\n\n- ship it\n');
622
+ expect(read.path).toBe(path.join(sharedDir, 'roadmap.md'));
570
623
  expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:missing.md')).toBeNull();
571
624
  expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:../escape.md')).toBeNull();
572
625
  });
@@ -592,6 +645,29 @@ describe('shared plans', () => {
592
645
  expect((await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md')).deleted).toBe(false);
593
646
  });
594
647
 
648
+ test('announces shared plan updates and deletes, and stays quiet for missing files', async () => {
649
+ const changes = [];
650
+ const notifyingRuntime = createProjectContextRuntime({
651
+ fsPromises,
652
+ path,
653
+ projectsDirPath,
654
+ createId: () => `plan-${++idCounter}`,
655
+ resolveSharedPlansDir: async () => sharedDir,
656
+ onChanged: (projectId) => changes.push(projectId),
657
+ });
658
+ await fsPromises.mkdir(sharedDir, { recursive: true });
659
+ await fsPromises.writeFile(path.join(sharedDir, 'a.md'), '# A\n');
660
+
661
+ await notifyingRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: '# Renamed\n' });
662
+ expect(changes).toEqual([PROJECT_ID]);
663
+ await notifyingRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
664
+ expect(changes).toEqual([PROJECT_ID, PROJECT_ID]);
665
+
666
+ await notifyingRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: 'x' });
667
+ await notifyingRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
668
+ expect(changes).toEqual([PROJECT_ID, PROJECT_ID]);
669
+ });
670
+
595
671
  test('share moves a personal plan into the shared folder and unshare brings it back, avoiding name collisions', async () => {
596
672
  const { plan } = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'body' });
597
673
  const shared = await sharedRuntime.sharePlan(PROJECT_ID, plan.id);
@@ -38,7 +38,7 @@ const clampLength = (value, maxLength) => {
38
38
  };
39
39
 
40
40
  const normalizeStatus = (value) => {
41
- if (value === 'running' || value === 'success' || value === 'error' || value === 'idle') {
41
+ if (value === 'running' || value === 'success' || value === 'error' || value === 'idle' || value === 'queued' || value === 'sent' || value === 'skipped' || value === 'failed' || value === 'cancelled') {
42
42
  return value;
43
43
  }
44
44
  return 'idle';
@@ -218,7 +218,7 @@ const normalizeSchedule = (value, existingSchedule) => {
218
218
  return { kind, cron, timezone };
219
219
  };
220
220
 
221
- const normalizeExecution = (value) => {
221
+ const normalizeExecution = (value, targetSessionId) => {
222
222
  if (!value || typeof value !== 'object') {
223
223
  throw new Error('execution is required');
224
224
  }
@@ -230,6 +230,9 @@ const normalizeExecution = (value) => {
230
230
  const agent = asNonEmptyString(value.agent);
231
231
  const goalEnabled = value.goalEnabled === true;
232
232
  const permissionAutoAccept = value.permissionAutoAccept === true;
233
+ // Model, thinking level and agent come from the session defaults at run
234
+ // time; the stored ones are kept only for when this is turned off.
235
+ const useDefaults = value.useDefaults === true;
233
236
  const goalTokenBudget = typeof value.goalTokenBudget === 'number'
234
237
  && Number.isFinite(value.goalTokenBudget)
235
238
  && value.goalTokenBudget > 0
@@ -239,17 +242,18 @@ const normalizeExecution = (value) => {
239
242
  if (!prompt) {
240
243
  throw new Error('execution.prompt is required');
241
244
  }
242
- if (!providerID) {
245
+ if (!useDefaults && !targetSessionId && !providerID) {
243
246
  throw new Error('execution.providerID is required');
244
247
  }
245
- if (!modelID) {
248
+ if (!useDefaults && !targetSessionId && !modelID) {
246
249
  throw new Error('execution.modelID is required');
247
250
  }
251
+ if (targetSessionId && Boolean(providerID) !== Boolean(modelID)) throw new Error('execution providerID and modelID are required together');
248
252
 
249
253
  return {
250
254
  prompt,
251
- providerID,
252
- modelID,
255
+ ...(providerID && modelID ? { providerID, modelID } : {}),
256
+ ...(useDefaults ? { useDefaults: true } : {}),
253
257
  ...(variant ? { variant } : {}),
254
258
  ...(agent ? { agent } : {}),
255
259
  ...(goalEnabled ? { goalEnabled: true } : {}),
@@ -333,7 +337,10 @@ const normalizeTaskForStorage = (value, options) => {
333
337
  : (existingTask?.enabled ?? true);
334
338
 
335
339
  const schedule = normalizeSchedule(value.schedule, existingTask?.schedule);
336
- const execution = normalizeExecution(value.execution);
340
+ const targetSessionId = asNonEmptyString(value.targetSessionId);
341
+ if (value.targetSessionId != null && value.targetSessionId !== '' && !targetSessionId) throw new Error('targetSessionId is invalid');
342
+ if (targetSessionId && !/^[A-Za-z0-9_-]{4,128}$/.test(targetSessionId)) throw new Error('targetSessionId is invalid');
343
+ const execution = normalizeExecution(value.execution, targetSessionId);
337
344
 
338
345
  // Loop provenance: absolute path of the `.agents/loops/*.md` file driving
339
346
  // this task, when any. Preserved on every write so the scheduler can detect
@@ -348,7 +355,7 @@ const normalizeTaskForStorage = (value, options) => {
348
355
  updatedAt: refreshUpdatedAt ? nowMs : baseState.updatedAt ?? nowMs,
349
356
  };
350
357
 
351
- return {
358
+ const storedTask = {
352
359
  id,
353
360
  name,
354
361
  enabled,
@@ -357,6 +364,8 @@ const normalizeTaskForStorage = (value, options) => {
357
364
  state,
358
365
  ...(loopFile ? { loopFile } : {}),
359
366
  };
367
+ if (targetSessionId) storedTask.targetSessionId = targetSessionId;
368
+ return storedTask;
360
369
  };
361
370
 
362
371
  // `loopApprovals`: loop file path -> fingerprint of the version the user
@@ -865,12 +874,16 @@ export const createProjectConfigRuntime = (deps) => {
865
874
  // A project loop comes with the repository: `enabled: true` in its file
866
875
  // is the author's suggestion. It runs here only once the user enabled
867
876
  // this version of it on this machine (see setLoopApproval); a change to
868
- // what it runs or when needs a new approval.
869
- loops = loops.map((loop) => (
870
- loop?.scope === 'project' && loop.definition?.enabled === true && approvals[loop.filePath] !== loop.fingerprint
871
- ? { ...loop, definition: { ...loop.definition, enabled: false } }
872
- : loop
873
- ));
877
+ // what it runs or when needs a new approval. The returned task says
878
+ // which of the two held it back (`loopApproval`); it is never stored.
879
+ const heldBack = new Map();
880
+ loops = loops.map((loop) => {
881
+ if (loop?.scope !== 'project' || loop.definition?.enabled !== true || approvals[loop.filePath] === loop.fingerprint) {
882
+ return loop;
883
+ }
884
+ heldBack.set(loop.filePath, approvals[loop.filePath] ? 'outdated' : 'required');
885
+ return { ...loop, definition: { ...loop.definition, enabled: false } };
886
+ });
874
887
 
875
888
  const activeLoopFilePaths = new Set();
876
889
  const pendingLoops = new Map();
@@ -967,7 +980,9 @@ export const createProjectConfigRuntime = (deps) => {
967
980
  scheduledTasks: toStoredTasks(current, nextTasks, { replacedIDs }),
968
981
  });
969
982
 
970
- return nextTasks;
983
+ return nextTasks.map((task) => (
984
+ task.loopFile && heldBack.has(task.loopFile) ? { ...task, loopApproval: heldBack.get(task.loopFile) } : task
985
+ ));
971
986
  });
972
987
  };
973
988
 
@@ -23,6 +23,19 @@ const createRuntime = async () => {
23
23
  };
24
24
 
25
25
  describe('project-config runtime', () => {
26
+ it('round-trips target tasks, terminal states and agent-only overrides without changing legacy model requirements', async () => {
27
+ const { runtime, cleanup } = await createRuntime();
28
+ try {
29
+ const input = { name: 'Reuse', enabled: true, targetSessionId: 'ses_target', schedule: { kind: 'daily', time: '09:00' }, execution: { prompt: 'Continue', agent: 'plan' } };
30
+ const saved = await runtime.upsertScheduledTask('project-test', input);
31
+ for (const lastStatus of ['queued', 'sent', 'skipped', 'failed', 'cancelled']) {
32
+ await runtime.updateScheduledTaskState('project-test', saved.task.id, { lastStatus, lastSessionId: 'ses_target' });
33
+ expect((await runtime.listScheduledTasks('project-test'))[0]).toMatchObject({ targetSessionId: 'ses_target', state: { lastStatus, lastSessionId: 'ses_target' } });
34
+ }
35
+ await expect(runtime.upsertScheduledTask('project-test', { ...input, targetSessionId: '../bad' })).rejects.toThrow('targetSessionId is invalid');
36
+ await expect(runtime.upsertScheduledTask('project-test', { ...input, targetSessionId: '' })).rejects.toThrow('execution.providerID is required');
37
+ } finally { await cleanup(); }
38
+ });
26
39
  it('creates and persists a scheduled task', async () => {
27
40
  const { runtime, cleanup } = await createRuntime();
28
41
  try {
@@ -53,6 +66,23 @@ describe('project-config runtime', () => {
53
66
  }
54
67
  });
55
68
 
69
+ it('accepts a task that follows the session defaults without a model, and still needs one when pinned', async () => {
70
+ const { runtime, cleanup } = await createRuntime();
71
+ const task = (execution) => ({
72
+ name: 'Follow defaults',
73
+ enabled: true,
74
+ schedule: { kind: 'daily', time: '09:30', timezone: 'UTC' },
75
+ execution: { prompt: 'Summarize', ...execution },
76
+ });
77
+ try {
78
+ const result = await runtime.upsertScheduledTask('project-test', task({ useDefaults: true }));
79
+ expect(result.task.execution).toEqual({ prompt: 'Summarize', useDefaults: true });
80
+ await expect(runtime.upsertScheduledTask('project-test', task({}))).rejects.toThrow('execution.providerID is required');
81
+ } finally {
82
+ await cleanup();
83
+ }
84
+ });
85
+
56
86
  it('rejects invalid cron expressions', async () => {
57
87
  const { runtime, cleanup } = await createRuntime();
58
88
  try {
@@ -12,6 +12,7 @@
12
12
  // this file) use the same code paths so a value reads back the same on every
13
13
  // surface.
14
14
 
15
+ import { isRecord as isObjectRecord } from '../shared/guards.js';
15
16
  import crypto from 'node:crypto';
16
17
 
17
18
  const ACTION_NAME_MAX_LENGTH = 80;
@@ -24,8 +25,6 @@ const SETUP_COMMANDS_MAX = 50;
24
25
  const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
25
26
  const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
26
27
 
27
- const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
28
-
29
28
  const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
30
29
 
31
30
  const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
@@ -11,13 +11,12 @@
11
11
  * `req.body` is parsed here without a per-route parser.
12
12
  */
13
13
 
14
+ import { isRecord as isObjectRecord } from '../shared/guards.js';
14
15
  import path from 'node:path';
15
16
  import { z } from 'zod';
16
17
  import { createProjectIdFromPath } from './project-id.js';
17
18
  import { isProjectSetupValidationError } from './project-setup.js';
18
19
 
19
- const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
20
-
21
20
  const directoryRequestSchema = z.object({
22
21
  path: z.string().trim().catch(''),
23
22
  create: z.literal(true).catch(false),
@@ -51,7 +51,7 @@ asked, `/api/quota/providers` answers 500 instead of an empty list.
51
51
  | `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` / `providers/minimax-shared.js` | `minimax-cn-coding-plan` |
52
52
  | `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Manual cookie pasted into Settings (`aid=...; __Secure-session=...` from `ollama.com`), stored under `~/.config/openchamber/quota/` |
53
53
  | `wafer` | Wafer.ai | `providers/wafer.js` | `wafer`, `wafer-ai`, `wafer_ai`, `wafer.ai` |
54
- | `opencode-go` | OpenCode Go | `providers/opencode-go.js` | `opencode-go` API key from OpenCode `auth.json` |
54
+ | `opencode-go` | OpenCode Go | `providers/opencode-go.js` | `opencode-go` API key from OpenCode `auth.json`, or an OpenCode Console OAuth sign-in on the shared `opencode` integration |
55
55
  | `neuralwatt` | NeuralWatt | `providers/neuralwatt.js` | `neuralwatt` (API key under `key` or `token`) |
56
56
  | `kilo` | Kilo Code | `providers/kilo.js` | `kilo`, `kilocode`, `kilo-code` (API key under `key`, `token`, or OAuth `access`; organization id under `kilocodeOrganizationId`, `organizationId`, or OAuth `accountId`, else OpenCode `provider.kilo.options.kilocodeOrganizationId`) |
57
57
  | `zenmux` | ZenMux | `providers/zenmux.js` | Optional Platform API key stored under `~/.config/openchamber/quota/zenmux.json` (`platformApiKey`). The OpenCode chat key is not used. |
@@ -69,10 +69,21 @@ All providers should return results via shared helpers to preserve API shape:
69
69
  Provider modules must export `providerId`, `providerName`, `aliases`, `isConfigured(auth?)`, and `fetchQuota()`.
70
70
  `fetchQuota()` should return a quota result with `usage.windows` keyed by window name (for example `5h`, `7d`, `daily`) and optional provider-specific `usage.models` data.
71
71
 
72
- exe.dev, Ollama Cloud, Cursor, and ZenMux credentials are explicitly managed through Settings. exe.dev usage uses a separately generated HTTPS API token restricted to `billing credits usage` and aggregates every `exe-*` model provider into one monthly credit window. Generate the token with `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`. ZenMux usage uses an optional Platform API key pasted on the Usage page; the OpenCode chat key is never used, and a missing Platform API key means ZenMux quota is not fetched. OpenCode Go usage uses `GET https://opencode.ai/zen/go/v1/usage` with the `opencode-go` API key from OpenCode `auth.json` as a bearer token and the stable `x-opencode-session: openchamber-usage` workload id. The server validates managed credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
72
+ exe.dev, Ollama Cloud, Cursor, and ZenMux credentials are explicitly managed through Settings. exe.dev usage uses a separately generated HTTPS API token restricted to `billing credits usage` and aggregates every `exe-*` model provider into one monthly credit window. Generate the token with `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`. ZenMux usage uses an optional Platform API key pasted on the Usage page; the OpenCode chat key is never used, and a missing Platform API key means ZenMux quota is not fetched. The server validates managed credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
73
73
 
74
74
  Command Code usage resolves account scope through `GET /alpha/whoami`, then reads server-backed credit balances and five-hour/weekly limits from `GET /alpha/billing/credits?orgId=...`. Personal accounts return `org: null` and use `/alpha/billing/credits` without an `orgId`; organization accounts include their organization id. Web/Electron and VS Code read the standard `command-code` OpenCode auth entry (including OAuth `access`) or `COMMAND_CODE_API_KEY`; credentials remain in the owning runtime and are never returned to shared UI.
75
75
 
76
+ ## OpenCode Go credential and quota semantics
77
+
78
+ OpenCode Go has two credential shapes and they are not interchangeable. A service-account key stored under the `opencode-go` integration is a bearer token for `GET https://opencode.ai/zen/go/v1/usage` with the stable `x-opencode-session: openchamber-usage` workload id. A Console sign-in lives on the shared `opencode` integration as OAuth; its access token is a user session, not that key, and it is sent to `GET https://opencode.ai/console/api/go/status` with `x-org-id: <selected organization ID>`. That status endpoint is what the current Console Go page itself reads; it is not publicly documented, so re-check it against the live Console page before assuming it is still current.
79
+
80
+ - **Credential selection**: OpenCode marks exactly one credential active per integration and credentials are global, so the active `opencode` OAuth credential is the selected account and organization. The reader carries only the Console `server` and `orgID` metadata; a credential whose `server` is not `https://opencode.ai/console`, whose `orgID` is missing or malformed, or that is an API key does not count as a Console sign-in. When both exist, the Console sign-in serves Go and the `opencode-go` key is the fallback, both when there is no Console sign-in and when the Console read fails; the Console error surfaces only when there is no key.
81
+ - **Read-only and per-call**: the access token is read through `../opencode/auth.js` on every fetch, so switching accounts drops the previous account instead of showing stale numbers. OpenChamber never refreshes the token itself: OpenCode owns that, and an expired access token (local `expires`, or a 401/403) is surfaced as "sign in again" rather than a competing refresh.
82
+ - **Meters**: the response's `product` must be `go` or `go-plus`; anything else is an absent subscription. `access.meters.fiveHour` / `week` / `month` map to the existing `5h` / `weekly` / `monthly` windows. `limitMicroCents` and `usedMicroCents` are decimal strings; the percentage is `used / limit`, never a hardcoded plan limit. A missing meter, a non-positive or unparseable limit, or an invalid reset timestamp skips that window alone; no usable window is a failed refresh, not zero usage.
83
+ - **Trusted origin**: requests go only to the fixed Console status URL with `redirect: 'error'`, so the bearer token is never forwarded to another origin.
84
+ - **Failure classes stay distinct**: not configured (no credential), expired sign-in (expired local token or 401/403), absent subscription (unexpected `product`), malformed response, and temporary request failure each return their own configured flag and message.
85
+ - Keep `packages/web/server/lib/quota/providers/opencode-go.js` and `packages/vscode/src/opencodeGoQuota.ts` in sync — the VS Code extension duplicates this logic rather than importing the web provider.
86
+
76
87
  On the first OpenCode Go usage refresh after upgrading, OpenChamber deletes the obsolete `quota/opencode-go.json` credential file without reading its cookie value.
77
88
 
78
89
  ## Codex credit balance semantics
@@ -149,13 +160,30 @@ Web and VS Code accept finite numeric balances and non-empty numeric strings. Mi
149
160
 
150
161
  ## Kimi for Coding field semantics
151
162
 
152
- `GET https://api.kimi.com/coding/v1/usages` is inconsistent about which field carries consumption:
153
- - The weekly `usage` block returns `used` (consumed) with no `remaining` field.
154
- - Each `limits[].detail` rate-limit block returns `remaining` (available) with no `used` field.
163
+ `GET https://api.kimi.com/coding/v1/usages` has two payload shapes and the provider supports both. Which one arrives depends on whether the account's plan has been migrated; in 2026-10 Kimi moved its newer plans from a weekly allowance to a monthly one. The endpoint is not in Kimi's public Open Platform reference. Its contract is defined by Kimi's own CLI, `MoonshotAI/kimi-code`, in `packages/oauth/src/managed-usage.ts` and `apps/kimi-code/src/utils/usage/usage-format.ts`.
164
+
165
+ **Ratio payload.** A top-level `usages` object keyed by limit name, each entry `{ used_ratio, reset_time }`, where `used_ratio` is a 0-1 fraction and `reset_time` is RFC3339. Kimi documents its client as rendering whichever entries are served, so an absent key means the plan has no such window rather than missing data. An entry with no finite `used_ratio` is skipped instead of shown as an unknown percentage.
166
+
167
+ | `usages` key | Window | Notes |
168
+ | --- | --- | --- |
169
+ | `limit_5h` | `5h` | Rolling five-hour frequency window |
170
+ | `limit_7d` | `weekly` | Served only by plans that still have a weekly allowance |
171
+ | `limit_month_total` | `monthly` | The plan's monthly allowance; `reset_time` is the subscription anchor, not a calendar month boundary |
172
+ | `limit_month_code` | not shown | The code-typed share of `limit_month_total`, not a second allowance |
173
+
174
+ `limit_month_code` is not surfaced. As its own row it would read as a second monthly allowance. As a `valueLabel` on the monthly row it would replace the row's percent with fixed used figures, so the row would ignore the used/remaining display setting that every other row follows.
175
+
176
+ When `usages` is present the provider does not read `limits[]`. Its 300-minute entry describes the same five-hour window as `limit_5h`, carrying the same ratio rounded to a whole percentage against a fixed `limit` of `100`, and Kimi's own client ignores `limits[]` altogether.
177
+
178
+ The payload may also carry `boosterWallet`, the pay-as-you-go top-up wallet that Kimi's client reads as `extraUsage`. OpenChamber does not surface it.
179
+
180
+ **Counted payload.** Plans that have not migrated return a single weekly `usage` block plus `limits[]` rate-limit entries. This shape is inconsistent about which field carries consumption: the `usage` block returns `used` (consumed) with no `remaining` field, while each `limits[].detail` block returns `remaining` (available) with no `used` field. The provider computes `usedPercent` from whichever of the two is present, `used` taking precedence when both exist, rather than assuming one field name. A `limits[]` entry whose window is exactly five hours is labeled `Rate Limit (<raw duration>)` to set it apart from a plain duration.
181
+
182
+ Credentials resolve in alias order, first match wins. OpenCode's China plan id `kimi-code-plan-cn` (kimi.com) comes before the pre-split `kimi-for-coding` and `kimi` ids, because a leftover pre-split key can hold a dead credential that would otherwise shadow the live China plan key and return 401. The global plan id `kimi-code-plan-global` (kimi.ai) stays last. Kimi's client uses `https://api.kimi.ai/coding/v1/usages` for global accounts, while this provider calls `api.kimi.com` for every alias, so a global-only key gets a 401 here. That gap is known and unfixed.
155
183
 
156
- Credentials resolve in alias order, first match wins. OpenCode's China plan id `kimi-code-plan-cn` (kimi.com) comes before the pre-split `kimi-for-coding` and `kimi` ids, because a leftover pre-split key can hold a dead credential that would otherwise shadow the live China plan key and return 401. The global plan id `kimi-code-plan-global` (kimi.ai, API base `api.kimi.ai`) stays last: it is not verified that a global key works at the `api.kimi.com` usage address, so it must not outrank a working pre-split key.
184
+ Many Kimi users hold a pay-as-you-go Moonshot platform key (prepaid vouchers, no Kimi Code subscription) under `KIMI_API_KEY`. The Kimi Code usage address refuses such a key with 401. On a 401 or 403 the provider therefore asks `GET https://api.moonshot.ai/v1/users/me/balance` with the same key and, when that answers with a numeric `data.available_balance`, reports it as a `credits_balance` window (USD, remaining only). If the balance read also fails, the original `API error: <status>` is returned; other HTTP errors never try the balance address.
157
185
 
158
- The provider computes `usedPercent` from whichever of `used`/`remaining` is present (`used` takes precedence when both exist) rather than assuming one field name. Both `packages/web/server/lib/quota/providers/kimi.js` and `packages/vscode/src/quotaProviders.ts` (`fetchKimiQuota`) must stay in sync — the VS Code extension duplicates this parsing logic rather than importing it.
186
+ Both `packages/web/server/lib/quota/providers/kimi.js` and `packages/vscode/src/quotaProviders.ts` (`fetchKimiQuota`) must stay in sync — the VS Code extension duplicates this parsing logic rather than importing it.
159
187
 
160
188
  ## Ollama Cloud settings-page shapes
161
189
 
@@ -5,6 +5,7 @@
5
5
  * @module quota
6
6
  */
7
7
 
8
+ /** @public */
8
9
  export {
9
10
  listConfiguredQuotaProviders,
10
11
  fetchQuotaForProvider,
@@ -16,7 +16,7 @@ import { toClaudeUsage } from './transforms.js';
16
16
 
17
17
  export const providerId = 'claude';
18
18
  export const providerName = 'Claude';
19
- export const aliases = ['anthropic', 'claude'];
19
+ const aliases = ['anthropic', 'claude'];
20
20
 
21
21
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
22
22
  const OAUTH_BETA_HEADER = 'oauth-2025-04-20';
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  export const providerId = 'cline-pass';
14
14
  export const providerName = 'ClinePass';
15
- export const aliases = ['cline-pass'];
15
+ const aliases = ['cline-pass'];
16
16
  const CLINE_USAGE_URL = 'https://api.cline.bot/api/v1/users/me/plan/usage-limits';
17
17
 
18
18
  // Cline reports a rolling five-hour window, a rolling weekly window, and a
@@ -3,7 +3,7 @@ import { asObject, buildResult, formatMoney, toNumber, toTimestamp, toUsageWindo
3
3
 
4
4
  export const providerId = 'exe-dev';
5
5
  export const providerName = 'exe.dev';
6
- export const aliases = ['exe-dev'];
6
+ const aliases = ['exe-dev'];
7
7
  const EXEC_URL = 'https://exe.dev/exec';
8
8
  const USAGE_COMMAND = 'billing credits usage --group=day --json';
9
9
 
@@ -16,7 +16,7 @@ export { resolveGoogleAuthSources } from './auth.js';
16
16
 
17
17
  export const providerId = 'google';
18
18
  export const providerName = 'Google';
19
- export const aliases = ['google', 'google.oauth'];
19
+ const aliases = ['google', 'google.oauth'];
20
20
 
21
21
  export const isConfigured = (auth) => resolveGoogleAuthSources(auth).length > 0;
22
22
 
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  export const providerId = 'hyper';
14
14
  export const providerName = 'Charm Hyper';
15
- export const aliases = ['hyper'];
15
+ const aliases = ['hyper'];
16
16
  const HYPER_QUOTA_URL = 'https://hyper.charm.land/v1/credits';
17
17
  const CREDIT_TO_USD = 0.05;
18
18
 
@@ -8,9 +8,40 @@ import {
8
8
  toTimestamp,
9
9
  durationToLabel,
10
10
  durationToSeconds,
11
- asNonEmptyString
11
+ asNonEmptyString,
12
+ formatMoney,
13
+ asObject
12
14
  } from '../utils/index.js';
13
15
 
16
+ const MOONSHOT_BALANCE_URL = 'https://api.moonshot.ai/v1/users/me/balance';
17
+
18
+ // A pay-as-you-go Moonshot platform key (prepaid vouchers, no Kimi Code
19
+ // subscription) is refused by the Kimi Code usage address. Its balance is read
20
+ // from the platform instead: USD, what remains, with no spent figure.
21
+ // Resolves to the credits_balance windows, or null when they cannot be read.
22
+ const fetchMoonshotBalanceWindows = async (apiKey, fetchImpl) => {
23
+ try {
24
+ const response = await fetchImpl(MOONSHOT_BALANCE_URL, {
25
+ method: 'GET',
26
+ headers: { Authorization: `Bearer ${apiKey}` },
27
+ signal: AbortSignal.timeout(15_000)
28
+ });
29
+ if (!response.ok) return null;
30
+ const balance = asObject(asObject(await response.json())?.data)?.available_balance;
31
+ if (!Number.isFinite(balance)) return null;
32
+ return {
33
+ credits_balance: toUsageWindow({
34
+ usedPercent: null,
35
+ windowSeconds: null,
36
+ resetAt: null,
37
+ valueLabel: `$${formatMoney(balance)}`
38
+ })
39
+ };
40
+ } catch {
41
+ return null;
42
+ }
43
+ };
44
+
14
45
  export const providerId = 'kimi-for-coding';
15
46
  export const providerName = 'Kimi for Coding';
16
47
  // OpenCode stores the Kimi For Coding plans as `kimi-code-plan-cn` (kimi.com)
@@ -18,11 +49,47 @@ export const providerName = 'Kimi for Coding';
18
49
  // works at the api.kimi.com usage address, and a pre-split `kimi-for-coding`
19
50
  // key left behind with a dead credential must not shadow it. The global plan
20
51
  // stays last, as before, since its key is not known to work at that address.
21
- export const aliases = ['kimi-code-plan-cn', 'kimi-for-coding', 'kimi', 'kimi-code-plan-global'];
52
+ const aliases = ['kimi-code-plan-cn', 'kimi-for-coding', 'kimi', 'kimi-code-plan-global'];
53
+
54
+ // The ratio-based `usages` block, in the order Kimi's own CLI renders it.
55
+ // `limit_month_code` is not read: it is the code-typed share of
56
+ // `limit_month_total`, not a second allowance, and a value label carrying it
57
+ // would replace the monthly percent and stay a used figure when the panel shows
58
+ // what remains. `limit_7d` arrives only on plans that still have a weekly
59
+ // quota, and Kimi documents the client as rendering whichever entries are served.
60
+ const RATIO_WINDOWS = [
61
+ { key: 'limit_5h', label: '5h', windowSeconds: 5 * 60 * 60 },
62
+ { key: 'limit_7d', label: 'weekly', windowSeconds: 7 * 24 * 60 * 60 },
63
+ { key: 'limit_month_total', label: 'monthly', windowSeconds: null }
64
+ ];
65
+
66
+ // `used_ratio` is a 0-1 fraction. An entry without a finite ratio carries no
67
+ // information, so it is skipped rather than shown as an unknown percentage.
68
+ const toRatioPercent = (entry) => {
69
+ const ratio = toNumber(entry?.used_ratio);
70
+ return ratio === null ? null : Math.max(0, Math.min(100, ratio * 100));
71
+ };
72
+
73
+ const ratioWindows = (usages) => {
74
+ const windows = {};
75
+ for (const { key, label, windowSeconds } of RATIO_WINDOWS) {
76
+ const entry = asObject(usages[key]);
77
+ const usedPercent = toRatioPercent(entry);
78
+ if (usedPercent === null) continue;
79
+ windows[label] = toUsageWindow({
80
+ usedPercent,
81
+ windowSeconds,
82
+ resetAt: toTimestamp(entry.reset_time)
83
+ });
84
+ }
85
+ return windows;
86
+ };
22
87
 
23
- // Kimi's weekly `usage` block reports `used`; its rate-limit `limits[].detail`
24
- // blocks report `remaining` instead. Neither field is guaranteed present, so
25
- // derive usedPercent from whichever one the API actually returned.
88
+ // The pre-ratio payload: one counted weekly `usage` block plus `limits[]`
89
+ // rate-limit entries. Plans that have not migrated still answer this way. Its
90
+ // `usage` block reports `used`; the `limits[].detail` blocks report `remaining`
91
+ // instead. Neither field is guaranteed present, so derive usedPercent from
92
+ // whichever one the API actually returned.
26
93
  const computeUsedPercent = (total, used, remaining) => {
27
94
  if (!total) return null;
28
95
  if (used !== null) {
@@ -34,6 +101,33 @@ const computeUsedPercent = (total, used, remaining) => {
34
101
  return null;
35
102
  };
36
103
 
104
+ const legacyWindows = (payload) => {
105
+ const windows = {};
106
+ const usage = asObject(payload?.usage);
107
+ if (usage) {
108
+ windows.weekly = toUsageWindow({
109
+ usedPercent: computeUsedPercent(toNumber(usage.limit), toNumber(usage.used), toNumber(usage.remaining)),
110
+ windowSeconds: null,
111
+ resetAt: toTimestamp(usage.resetTime)
112
+ });
113
+ }
114
+
115
+ const limits = Array.isArray(payload?.limits) ? payload.limits : [];
116
+ for (const limit of limits) {
117
+ const window = limit?.window;
118
+ const detail = limit?.detail;
119
+ const rawLabel = durationToLabel(window?.duration, window?.timeUnit);
120
+ const windowSeconds = durationToSeconds(window?.duration, window?.timeUnit);
121
+ const label = windowSeconds === 5 * 60 * 60 ? `Rate Limit (${rawLabel})` : rawLabel;
122
+ windows[label] = toUsageWindow({
123
+ usedPercent: computeUsedPercent(toNumber(detail?.limit), toNumber(detail?.used), toNumber(detail?.remaining)),
124
+ windowSeconds,
125
+ resetAt: toTimestamp(detail?.resetTime)
126
+ });
127
+ }
128
+ return windows;
129
+ };
130
+
37
131
  const getApiKey = (auth) => {
38
132
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
39
133
  return asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
@@ -64,6 +158,18 @@ export const fetchQuota = async ({ readAuth = readOpenCodeCredentials, fetchImpl
64
158
  });
65
159
 
66
160
  if (!response.ok) {
161
+ if (response.status === 401 || response.status === 403) {
162
+ const balanceWindows = await fetchMoonshotBalanceWindows(apiKey, fetchImpl);
163
+ if (balanceWindows) {
164
+ return buildResult({
165
+ providerId,
166
+ providerName,
167
+ ok: true,
168
+ configured: true,
169
+ usage: { windows: balanceWindows }
170
+ });
171
+ }
172
+ }
67
173
  return buildResult({
68
174
  providerId,
69
175
  providerName,
@@ -74,37 +180,11 @@ export const fetchQuota = async ({ readAuth = readOpenCodeCredentials, fetchImpl
74
180
  }
75
181
 
76
182
  const payload = await response.json();
77
- const windows = {};
78
- const usage = payload?.usage ?? null;
79
- if (usage) {
80
- const limit = toNumber(usage.limit);
81
- const used = toNumber(usage.used);
82
- const remaining = toNumber(usage.remaining);
83
- const usedPercent = computeUsedPercent(limit, used, remaining);
84
- windows.weekly = toUsageWindow({
85
- usedPercent,
86
- windowSeconds: null,
87
- resetAt: toTimestamp(usage.resetTime)
88
- });
89
- }
90
-
91
- const limits = Array.isArray(payload?.limits) ? payload.limits : [];
92
- for (const limit of limits) {
93
- const window = limit?.window;
94
- const detail = limit?.detail;
95
- const rawLabel = durationToLabel(window?.duration, window?.timeUnit);
96
- const windowSeconds = durationToSeconds(window?.duration, window?.timeUnit);
97
- const label = windowSeconds === 5 * 60 * 60 ? `Rate Limit (${rawLabel})` : rawLabel;
98
- const total = toNumber(detail?.limit);
99
- const used = toNumber(detail?.used);
100
- const remaining = toNumber(detail?.remaining);
101
- const usedPercent = computeUsedPercent(total, used, remaining);
102
- windows[label] = toUsageWindow({
103
- usedPercent,
104
- windowSeconds,
105
- resetAt: toTimestamp(detail?.resetTime)
106
- });
107
- }
183
+ // Kimi replaced the counted payload with a ratio-based `usages` map. Its own
184
+ // client reads only `usages`, so when that block is present the `limits[]`
185
+ // entry for the same 5-hour window is a coarser duplicate and is not read.
186
+ const usages = asObject(asObject(payload)?.usages);
187
+ const windows = usages ? ratioWindows(usages) : legacyWindows(payload);
108
188
 
109
189
  return buildResult({
110
190
  providerId,