@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
@@ -3,21 +3,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
3
3
  import { createGlobalMessageStreamHub } from '../event-stream/global-hub.js';
4
4
  import { createOpenCodeWatcherRuntime } from './watcher.js';
5
5
 
6
- async function waitForAssertion(assertion) {
7
- const deadline = Date.now() + 1000;
8
- let lastError;
9
- while (Date.now() < deadline) {
10
- try {
11
- assertion();
12
- return;
13
- } catch (error) {
14
- lastError = error;
15
- await new Promise((resolve) => setTimeout(resolve, 10));
16
- }
17
- }
18
- throw lastError;
19
- }
20
-
21
6
  function createSseResponse({ blocks = [], signal, holdOpen = false }) {
22
7
  const encoder = new TextEncoder();
23
8
  let index = 0;
@@ -204,45 +189,6 @@ describe('createOpenCodeWatcherRuntime', () => {
204
189
  });
205
190
  });
206
191
 
207
- it('keeps its shared subscription across a hub rebind', async () => {
208
- vi.spyOn(console, 'log').mockImplementation(() => {});
209
- let generation = 0;
210
- const payloads = [];
211
- const globalEventHub = createGlobalMessageStreamHub({
212
- buildOpenCodeUrl: () => `http://127.0.0.1:${generation === 0 ? 4096 : 5096}/api/event`,
213
- getOpenCodeAuthHeaders: () => ({ Authorization: generation === 0 ? 'Basic first' : 'Basic second' }),
214
- upstreamReconnectDelayMs: 60_000,
215
- fetchImpl: async (_url, options) => createSseResponse({
216
- signal: options.signal,
217
- holdOpen: true,
218
- blocks: [generation === 0
219
- ? 'data: {"id":"evt-1","type":"server.connected","data":{}}\n\n'
220
- : 'data: {"id":"evt-2","type":"session.renamed","location":{"directory":"/tmp/project"},"data":{"sessionID":"ses_1","title":"Recovered"}}\n\n'],
221
- }),
222
- });
223
- const watcher = createOpenCodeWatcherRuntime({
224
- waitForOpenCodePort: async () => {},
225
- buildOpenCodeUrl: () => '',
226
- getOpenCodeAuthHeaders: () => ({}),
227
- globalEventHub,
228
- onPayload: (payload) => payloads.push(payload),
229
- });
230
-
231
- try {
232
- await watcher.start();
233
- await waitForAssertion(() => expect(payloads.map((payload) => payload.type)).toContain('server.connected'));
234
- generation = 1;
235
- globalEventHub.rebind();
236
- await waitForAssertion(() => expect(payloads).toContainEqual(expect.objectContaining({
237
- type: 'session.updated',
238
- properties: expect.objectContaining({ sessionID: 'ses_1' }),
239
- })));
240
- } finally {
241
- watcher.stop();
242
- globalEventHub.stop();
243
- }
244
- });
245
-
246
192
  it('does not stop a shared global event hub when the watcher stops', async () => {
247
193
  const events = new Set();
248
194
  const statuses = new Set();
@@ -0,0 +1,52 @@
1
+ import http from 'node:http';
2
+
3
+ // Outbound requests from the server follow the standard proxy variables
4
+ // (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, either case), so usage quotas, catalogs
5
+ // and other upstream calls work behind a corporate or local proxy without
6
+ // NODE_USE_ENV_PROXY. Loopback always stays direct: OAuth callbacks, the
7
+ // managed OpenCode server and SSH forwards live there, and a proxy that
8
+ // cannot reach this machine's 127.0.0.1 would break them.
9
+
10
+ const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '::1'];
11
+
12
+ const readVariable = (env, name) => String(env[name] ?? env[name.toLowerCase()] ?? '').trim();
13
+
14
+ /**
15
+ * The proxy settings the environment asks for, with loopback added to the
16
+ * bypass list, or null when no proxy is configured.
17
+ */
18
+ export const resolveOutboundProxyEnv = (env) => {
19
+ const httpProxy = readVariable(env, 'HTTP_PROXY');
20
+ const httpsProxy = readVariable(env, 'HTTPS_PROXY');
21
+ if (!httpProxy && !httpsProxy) return null;
22
+ const bypass = readVariable(env, 'NO_PROXY').split(',').map((entry) => entry.trim()).filter(Boolean);
23
+ for (const host of LOOPBACK_HOSTS) {
24
+ if (!bypass.includes(host)) bypass.push(host);
25
+ }
26
+ const proxyEnv = { NO_PROXY: bypass.join(',') };
27
+ if (httpProxy) proxyEnv.HTTP_PROXY = httpProxy;
28
+ if (httpsProxy) proxyEnv.HTTPS_PROXY = httpsProxy;
29
+ return proxyEnv;
30
+ };
31
+
32
+ /**
33
+ * Route this process's fetch and http(s) requests through the configured
34
+ * proxy. NO_PROXY is written back to the environment so clients that read it
35
+ * themselves (Bun's fetch, child processes) keep loopback direct too.
36
+ * Returns whether a proxy was configured.
37
+ */
38
+ export const applyOutboundProxyFromEnv = ({ env = process.env, httpModule = http } = {}) => {
39
+ const proxyEnv = resolveOutboundProxyEnv(env);
40
+ if (!proxyEnv) return false;
41
+ env.NO_PROXY = proxyEnv.NO_PROXY;
42
+ env.no_proxy = proxyEnv.NO_PROXY;
43
+ // Node 24 and Electron's Node apply this to fetch and http(s); Bun's fetch
44
+ // reads the variables directly.
45
+ try {
46
+ httpModule.setGlobalProxyFromEnv?.(proxyEnv);
47
+ } catch (error) {
48
+ console.warn('Could not apply the proxy from HTTP_PROXY/HTTPS_PROXY:', error?.message || error);
49
+ return false;
50
+ }
51
+ return true;
52
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ import { applyOutboundProxyFromEnv, resolveOutboundProxyEnv } from './outbound-proxy.js';
5
+
6
+ describe('resolveOutboundProxyEnv', () => {
7
+ it('is null without a proxy', () => {
8
+ expect(resolveOutboundProxyEnv({ NO_PROXY: 'internal.example' })).toBeNull();
9
+ });
10
+
11
+ it('reads either case and always bypasses loopback', () => {
12
+ expect(resolveOutboundProxyEnv({ https_proxy: 'http://proxy:7897', no_proxy: 'corp.example, 127.0.0.1' })).toEqual({
13
+ HTTPS_PROXY: 'http://proxy:7897',
14
+ NO_PROXY: 'corp.example,127.0.0.1,localhost,::1',
15
+ });
16
+ });
17
+ });
18
+
19
+ describe('applyOutboundProxyFromEnv', () => {
20
+ it('leaves the process alone without a proxy', () => {
21
+ const setGlobalProxyFromEnv = vi.fn();
22
+ expect(applyOutboundProxyFromEnv({ env: {}, httpModule: { setGlobalProxyFromEnv } })).toBe(false);
23
+ expect(setGlobalProxyFromEnv).not.toHaveBeenCalled();
24
+ });
25
+
26
+ it('writes the loopback bypass back for clients that read the environment', () => {
27
+ const env = { HTTP_PROXY: 'http://proxy:3128' };
28
+ const setGlobalProxyFromEnv = vi.fn();
29
+ expect(applyOutboundProxyFromEnv({ env, httpModule: { setGlobalProxyFromEnv } })).toBe(true);
30
+ expect(env.NO_PROXY).toBe('localhost,127.0.0.1,::1');
31
+ expect(env.no_proxy).toBe('localhost,127.0.0.1,::1');
32
+ expect(setGlobalProxyFromEnv).toHaveBeenCalledWith({ HTTP_PROXY: 'http://proxy:3128', NO_PROXY: 'localhost,127.0.0.1,::1' });
33
+ });
34
+
35
+ it('sends fetch through the proxy and keeps loopback direct in a real Node process', () => {
36
+ const moduleUrl = new URL('./outbound-proxy.js', import.meta.url).href;
37
+ const result = spawnSync('node', ['--input-type=module', '--eval', `
38
+ import http from 'node:http';
39
+ import assert from 'node:assert/strict';
40
+ const seen = [];
41
+ const proxy = http.createServer((req, res) => res.end());
42
+ proxy.on('connect', (req, socket) => { seen.push('proxy ' + req.url); socket.end('HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n'); });
43
+ const local = http.createServer((req, res) => { seen.push('direct'); res.end('ok'); });
44
+ await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
45
+ await new Promise((resolve) => local.listen(0, '127.0.0.1', resolve));
46
+ const { applyOutboundProxyFromEnv } = await import(${JSON.stringify(moduleUrl)});
47
+ assert.equal(applyOutboundProxyFromEnv({ env: { HTTPS_PROXY: 'http://127.0.0.1:' + proxy.address().port, HTTP_PROXY: 'http://127.0.0.1:' + proxy.address().port } }), true);
48
+ await fetch('http://upstream.invalid/usage').catch(() => {});
49
+ assert.equal(await (await fetch('http://127.0.0.1:' + local.address().port + '/')).text(), 'ok');
50
+ assert.deepEqual(seen, ['proxy upstream.invalid:80', 'direct']);
51
+ process.exit(0);
52
+ `], { encoding: 'utf8', timeout: 15_000, windowsHide: true });
53
+ expect(result.status, result.stderr).toBe(0);
54
+ });
55
+ });
@@ -16,6 +16,7 @@ const PACKAGE_NAME = '@vantran-se/openchamber-web';
16
16
  const PACKAGE_PATH_SEGMENTS = PACKAGE_NAME.split('/');
17
17
  const GITHUB_RELEASES_URL = 'https://github.com/vantran-se/openchamber/releases';
18
18
  const GITHUB_RELEASES_API_URL = 'https://api.github.com/repos/vantran-se/openchamber/releases';
19
+ const UPSTREAM_RELEASES_API_URL = 'https://api.github.com/repos/openchamber/openchamber/releases';
19
20
  let cachedDetectedPm = null;
20
21
 
21
22
  function getSpawnSyncBaseOptions() {
@@ -98,7 +99,7 @@ async function resolveAndroidApkUrl(version, candidateUrl) {
98
99
  }
99
100
 
100
101
  try {
101
- const response = await fetch(`${GITHUB_RELEASES_API_URL}/tags/v${version}`, {
102
+ const response = await fetch(`${UPSTREAM_RELEASES_API_URL}/tags/v${version}`, {
102
103
  headers: {
103
104
  Accept: 'application/vnd.github+json',
104
105
  'User-Agent': 'openchamber-update-check',
@@ -600,6 +601,15 @@ function resolvePackageManagerCommand(pm) {
600
601
  return pm;
601
602
  }
602
603
 
604
+ function quoteCommand(command) {
605
+ if (!command) return command;
606
+ if (!/\s/.test(command)) return command;
607
+ if (process.platform === 'win32') {
608
+ return `"${command.replace(/"/g, '""')}"`;
609
+ }
610
+ return `'${command.replace(/'/g, "'\\''")}'`;
611
+ }
612
+
603
613
  function isCommandAvailable(command) {
604
614
  try {
605
615
  const result = spawnSync(command, ['--version'], {
@@ -648,7 +658,7 @@ function isPackageInstalledWith(pm) {
648
658
 
649
659
  // npm, bun and yarn print `name@version`; pnpm separates the name and the
650
660
  // version with whitespace. An optional `v` covers yarn listing formats.
651
- const GLOBAL_VERSION_PATTERN = /@vantran-se\/openchamber-web[@\s]+v?(\d[\w.+-]*)/;
661
+ const GLOBAL_VERSION_PATTERN = /(?:@vantran-se\/openchamber-web|@openchamber\/web)[@\s]+v?(\d[\w.+-]*)/;
652
662
 
653
663
  /**
654
664
  * Read the globally installed version of the package as reported by the
@@ -687,30 +697,25 @@ function normalizeTargetVersion(value) {
687
697
  * package manager see different metadata, and dist-tag resolution can lag the
688
698
  * check behind a fresh release (stale packument cache, pnpm minimumReleaseAge).
689
699
  */
690
- function getUpdateInvocation(pm = detectPackageManager(), options = {}) {
700
+ export function getUpdateCommand(pm = detectPackageManager(), options = {}) {
691
701
  const targetVersion = normalizeTargetVersion(options.targetVersion);
692
702
  if (options.targetVersion != null && !targetVersion) {
693
703
  throw new Error(`Invalid target version for update: ${String(options.targetVersion)}`);
694
704
  }
695
- const packageSpec = `${PACKAGE_NAME}@${targetVersion || 'latest'}`;
696
- const command = resolvePackageManagerCommand(pm);
705
+ const versionSpec = targetVersion ? `@${targetVersion}` : '@latest';
706
+ const pmCommand = quoteCommand(resolvePackageManagerCommand(pm));
697
707
  switch (pm) {
698
708
  case 'pnpm':
699
- return { command, args: ['add', '-g', packageSpec] };
709
+ return `${pmCommand} add -g ${PACKAGE_NAME}${versionSpec}`;
700
710
  case 'yarn':
701
- return { command, args: ['global', 'add', packageSpec] };
711
+ return `${pmCommand} global add ${PACKAGE_NAME}${versionSpec}`;
702
712
  case 'bun':
703
- return { command, args: ['add', '-g', packageSpec] };
713
+ return `${pmCommand} add -g ${PACKAGE_NAME}${versionSpec}`;
704
714
  default:
705
- return { command, args: ['install', '-g', packageSpec] };
715
+ return `${pmCommand} install -g ${PACKAGE_NAME}${versionSpec}`;
706
716
  }
707
717
  }
708
718
 
709
- export function getUpdateCommand(pm = detectPackageManager(), options = {}) {
710
- const invocation = getUpdateInvocation(pm, options);
711
- return [invocation.command, ...invocation.args].join(' ');
712
- }
713
-
714
719
  /**
715
720
  * Get current installed version from package.json
716
721
  */
@@ -850,14 +855,15 @@ export async function checkForUpdates(options = {}) {
850
855
  * and the caller must not report success without the version matching.
851
856
  */
852
857
  export function executeUpdate(pm = detectPackageManager(), options = {}) {
853
- const invocation = getUpdateInvocation(pm, { targetVersion: options.targetVersion });
858
+ const command = getUpdateCommand(pm, { targetVersion: options.targetVersion });
854
859
  if (!options?.silent) {
855
860
  console.log(`Updating ${PACKAGE_NAME} using ${pm}...`);
856
- console.log(`Running: ${[invocation.command, ...invocation.args].join(' ')}`);
861
+ console.log(`Running: ${command}`);
857
862
  }
858
863
 
859
- const result = spawnSync(invocation.command, invocation.args, {
864
+ const result = spawnSync(command, {
860
865
  stdio: 'inherit',
866
+ shell: true,
861
867
  ...getSpawnSyncBaseOptions(),
862
868
  });
863
869
 
@@ -39,22 +39,6 @@ function createFetchMock() {
39
39
  return mock;
40
40
  }
41
41
 
42
- describe('fork package update command', () => {
43
- it('targets the published fork without a runtime environment override', () => {
44
- expect(getUpdateCommand('npm')).toBe('npm install -g @vantran-se/openchamber-web@latest');
45
- });
46
-
47
- it('executes the package manager without a shell', () => {
48
- spawnSync.mockClear();
49
- executeUpdate('npm', { silent: true });
50
- expect(spawnSync).toHaveBeenCalledWith(
51
- 'npm',
52
- ['install', '-g', '@vantran-se/openchamber-web@latest'],
53
- expect.not.objectContaining({ shell: true }),
54
- );
55
- });
56
- });
57
-
58
42
  describe('checkForUpdates', () => {
59
43
  let fetchMock;
60
44
  let originalFetch;
@@ -277,7 +261,7 @@ describe('checkForUpdates', () => {
277
261
  downloadUrl: 'https://github.com/openchamber/openchamber/releases/download/v1.10.0/OpenChamber-1.10.0-42-android.aab',
278
262
  }),
279
263
  })
280
- .when('api.github.com/repos/vantran-se/openchamber/releases/tags/v1.10.0', {
264
+ .when('api.github.com/repos/openchamber/openchamber/releases/tags/v1.10.0', {
281
265
  ok: true,
282
266
  json: async () => ({
283
267
  assets: [
@@ -463,13 +447,16 @@ describe('getUpdateCommand', () => {
463
447
  describe('executeUpdate', () => {
464
448
  function stubSpawnSync({ installStatus = 0, listingStdout = '', listingStatus = 0 } = {}) {
465
449
  spawnSync.mockImplementation((command, args) => {
450
+ if (!Array.isArray(args)) {
451
+ return { status: installStatus, stdout: '', stderr: '' };
452
+ }
466
453
  if (args.includes('--version')) {
467
454
  return { status: 0, stdout: '10.0.0', stderr: '' };
468
455
  }
469
456
  if (args[0] === 'list' || args[0] === 'pm' || args[0] === 'global') {
470
457
  return { status: listingStatus, stdout: listingStdout, stderr: '' };
471
458
  }
472
- return { status: installStatus, stdout: '', stderr: '' };
459
+ return { status: 0, stdout: '', stderr: '' };
473
460
  });
474
461
  }
475
462
 
@@ -18,7 +18,7 @@ Policies written before the modes stored booleans. The first read converts them
18
18
 
19
19
  ## Default mode
20
20
 
21
- `permissionDefaultMode` (Settings → Sessions) is written onto each top-level session when OpenChamber adopts it (`metadata.openchamber.adopted === true` arrives through `session.created` or `session.updated`), and only when no policy exists for it yet: a mode the creating flow already set wins, and changing the default never reaches back into older sessions. A session seen only through the shared local OpenCode service remains TUI-owned, so Web settings cannot silently change its permission policy. OpenChamber creation and the first message sent from an OpenChamber surface persist the adoption marker; merely viewing a session does not. Subagents inherit instead. `ask` is persisted too, so a later change to the global default cannot retroactively change an existing session. Mode resolution waits for pending writes on the session and its ancestors, so an adopted session's first permission request sees its default.
21
+ `permissionDefaultMode` (Settings → Sessions) is written onto each new top-level session when `session.created` arrives, and only when no policy exists for it yet: a mode the creating flow already set wins, and changing the default never reaches back into older sessions. Subagents inherit instead. `ask` writes nothing. Mode resolution waits for pending writes, so a session's first permission request sees its default.
22
22
 
23
23
  ## Runtime
24
24
 
@@ -18,13 +18,7 @@ const storedPolicySchema = z.object({
18
18
 
19
19
  const readStoredPolicy = (value) => storedPolicySchema.parse(value ?? {});
20
20
 
21
- const createdSessionSchema = z.object({
22
- id: z.string().min(1),
23
- parentID: z.string().nullish(),
24
- metadata: z.object({
25
- openchamber: z.object({ adopted: z.literal(true).optional() }).optional(),
26
- }).optional(),
27
- });
21
+ const createdSessionSchema = z.object({ id: z.string().min(1), parentID: z.string().nullish() });
28
22
 
29
23
  const hasLegacyEntries = (stored) => Object.values(stored.sessions).some((entry) => entry === true || entry === false);
30
24
 
@@ -148,29 +142,17 @@ export function createPermissionAutoAcceptRuntime({
148
142
  * sessions; a policy the creating flow already set wins. Subagents inherit
149
143
  * from their parent instead.
150
144
  */
151
- const defaultModePromises = new Map();
152
- const applyDefaultMode = (sessionId, resolveParentID = async () => null) => {
153
- const existing = defaultModePromises.get(sessionId);
154
- if (existing) return existing;
155
- const pending = (async () => {
156
- const parentID = await resolveParentID();
157
- if (parentID !== null) return;
158
- await load();
159
- if (Object.hasOwn(policy.sessions, sessionId)) return;
160
- const settings = await readSettingsFromDiskMigrated();
161
- const configuredMode = settings?.[DEFAULT_MODE_SETTINGS_KEY];
162
- const mode = isPermissionMode(configuredMode) ? configuredMode : 'ask';
163
- await persistUpdate((current) => (Object.hasOwn(current.sessions, sessionId) ? current : {
164
- ...current,
165
- sessions: { ...current.sessions, [sessionId]: mode },
166
- revision: current.revision + 1,
167
- }));
168
- })();
169
- defaultModePromises.set(sessionId, pending);
170
- void pending.finally(() => {
171
- if (defaultModePromises.get(sessionId) === pending) defaultModePromises.delete(sessionId);
172
- });
173
- return pending;
145
+ const applyDefaultMode = async (sessionId) => {
146
+ await load();
147
+ if (Object.hasOwn(policy.sessions, sessionId)) return;
148
+ const settings = await readSettingsFromDiskMigrated();
149
+ const mode = settings?.[DEFAULT_MODE_SETTINGS_KEY];
150
+ if (!isPermissionMode(mode) || mode === 'ask') return;
151
+ await persistUpdate((current) => (Object.hasOwn(current.sessions, sessionId) ? current : {
152
+ ...current,
153
+ sessions: { ...current.sessions, [sessionId]: mode },
154
+ revision: current.revision + 1,
155
+ }));
174
156
  };
175
157
 
176
158
  const rememberSession = (info, directoryHint) => {
@@ -178,9 +160,7 @@ export function createPermissionAutoAcceptRuntime({
178
160
  // v2 session updates are partial (a rename carries only the title), so a
179
161
  // field the update does not name keeps what an earlier record said.
180
162
  const previous = sessions.get(info.id);
181
- const parentID = typeof info.parentID === 'string' && info.parentID
182
- ? info.parentID
183
- : (previous ? previous.parentID : undefined);
163
+ const parentID = typeof info.parentID === 'string' && info.parentID ? info.parentID : previous?.parentID ?? null;
184
164
  // v2 keeps the directory on `location`; translated events already flatten it.
185
165
  const directory = typeof info.directory === 'string' && info.directory
186
166
  ? info.directory
@@ -219,7 +199,7 @@ export function createPermissionAutoAcceptRuntime({
219
199
 
220
200
  const getSession = async (sessionId, directory) => {
221
201
  const cached = sessions.get(sessionId);
222
- if (cached?.parentID !== undefined) return cached;
202
+ if (cached) return cached;
223
203
  const info = await request(`/api/session/${encodeURIComponent(sessionId)}`, { directory });
224
204
  rememberSession(info?.data ?? info, directory);
225
205
  return sessions.get(sessionId) ?? null;
@@ -228,16 +208,13 @@ export function createPermissionAutoAcceptRuntime({
228
208
  /** The nearest explicit mode up the session's lineage; unknown lineage fails closed to `ask`. */
229
209
  const resolveSessionMode = async (sessionId, directory) => {
230
210
  await load();
231
- // A default being selected and written for a newly adopted session must be
232
- // visible to its first permission request.
233
- await defaultModePromises.get(sessionId)?.catch(() => undefined);
211
+ // A default being written for a just-created session must be visible to
212
+ // its first permission request.
234
213
  await writePromise.catch(() => undefined);
235
214
  const seen = new Set();
236
215
  let current = sessionId;
237
216
  let currentDirectory = directory;
238
217
  while (current && !seen.has(current)) {
239
- await defaultModePromises.get(current)?.catch(() => undefined);
240
- await writePromise.catch(() => undefined);
241
218
  if (Object.hasOwn(policy.sessions, current)) return policy.sessions[current];
242
219
  seen.add(current);
243
220
  let info;
@@ -375,14 +352,9 @@ export function createPermissionAutoAcceptRuntime({
375
352
  if (payload.type === 'session.created' || payload.type === 'session.updated') {
376
353
  const info = payload.properties?.info;
377
354
  rememberSession(info, directory ?? payload.properties?.directory);
378
- const adopted = createdSessionSchema.safeParse(info);
379
- const remembered = adopted.success ? sessions.get(adopted.data.id) : null;
380
- if (adopted.success && adopted.data.metadata?.openchamber?.adopted === true) {
381
- void applyDefaultMode(adopted.data.id, async () => {
382
- const parentID = remembered?.parentID;
383
- if (parentID !== undefined) return parentID;
384
- return (await getSession(adopted.data.id, remembered?.directory))?.parentID ?? null;
385
- }).catch((error) => {
355
+ const created = payload.type === 'session.created' ? createdSessionSchema.safeParse(info) : null;
356
+ if (created?.success && !created.data.parentID) {
357
+ void applyDefaultMode(created.data.id).catch((error) => {
386
358
  console.warn('[permission-auto-accept] failed to apply the default mode:', error?.message ?? error);
387
359
  });
388
360
  }
@@ -77,11 +77,11 @@ describe('permission auto-accept runtime', () => {
77
77
  expect(resolveLegacyEnabledMode).toHaveBeenCalledTimes(1);
78
78
  });
79
79
 
80
- it('writes the default mode onto a new OpenChamber-owned top-level session only', async () => {
80
+ it('writes the default mode onto a new top-level session only', async () => {
81
81
  const { runtime, emit, getSettings } = createRuntime();
82
82
  await runtime.load();
83
83
  getSettings().permissionDefaultMode = 'safety';
84
- emit({ type: 'session.created', properties: { info: { id: 'root', metadata: { openchamber: { adopted: true } } } } });
84
+ emit({ type: 'session.created', properties: { info: { id: 'root' } } });
85
85
  emit({ type: 'session.created', properties: { info: { id: 'child', parentID: 'root' } } });
86
86
  await flush();
87
87
  await expect(runtime.resolveSessionMode('root', '/project')).resolves.toBe('safety');
@@ -100,7 +100,7 @@ collision gets a numeric suffix. Sharing is refused only when the checkout canno
100
100
  | PATCH | `/api/project-context/:projectId/notes/:noteId` | patches `body`; legacy `pinned` input is ignored by session knowledge; `404` when unknown |
101
101
  | DELETE | `/api/project-context/:projectId/notes/:noteId` | `404` when unknown |
102
102
  | PATCH | `/api/project-context/:projectId/plans/:planId` | legacy project pin state only; session attachment uses session knowledge; `404` when unknown |
103
- | GET | `/api/project-context/:projectId/plans/:planId` | `404` when the link or its markdown is gone |
103
+ | GET | `/api/project-context/:projectId/plans/:planId` | `404` when the link or its markdown is gone; the answer carries `path`, the markdown's absolute path, so comments on the plan name the file to the agent (reads and writes stay id-addressed) |
104
104
  | POST | `/api/project-context/:projectId/plans` | `201`; takes `{title, body}`, never a path |
105
105
  | PUT | `/api/project-context/:projectId/plans/:planId` | takes the whole `{raw}` document; `404` when the link or its markdown is gone |
106
106
  | DELETE | `/api/project-context/:projectId/plans/:planId` | `404` when unknown |
@@ -119,6 +119,12 @@ on a bare express app so that failure mode fails the suite instead of the user.
119
119
  separators and traversal. Validation failures are `400`; malformed stored data
120
120
  and I/O failures are `500`.
121
121
 
122
+ ## Change notifications
123
+
124
+ After `writeContext` atomically commits `context.json`, the runtime calls its optional `onChanged(projectId)` dependency. The server broadcasts `openchamber:project-context-changed` with `{ projectId }` on the existing OpenChamber control stream. It carries the path-derived owner id, not the bounded storage stem or any note content. Failed writes and missing-entry no-ops announce nothing. Updating or deleting a shared plan through the API announces the same way after the file write, since shared plans have no manifest entry.
125
+
126
+ Clients re-read authoritative context for the visible owner. This is a commit notification, not a filesystem watcher; direct changes to shared plan files outside the API retain their existing read-on-open behavior.
127
+
122
128
  ## Invariants
123
129
 
124
130
  - **Missing is not malformed.** A missing `context.json` is authoritative empty
@@ -13,12 +13,11 @@
13
13
  * undefined and rejects every write as a malformed body.
14
14
  */
15
15
 
16
+ import { isRecord as isObjectRecord } from '../shared/guards.js';
16
17
  import express from 'express';
17
18
 
18
19
  const parseJsonBody = express.json({ limit: '1mb' });
19
20
 
20
- const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
21
-
22
21
  const isValidationError = (error) => {
23
22
  const message = error instanceof Error ? error.message : '';
24
23
  return message.includes('is required') || message.includes('unsupported characters');
@@ -12,12 +12,13 @@
12
12
  * directory never invalidates a reference.
13
13
  */
14
14
 
15
+ import { asNonEmptyString, isRecord as isObjectRecord } from '../shared/guards.js';
15
16
  import { projectConfigFileStemOf } from '../projects/project-id.js';
16
17
 
17
18
  const PROJECT_CONTEXT_VERSION = 2;
18
19
  const PROJECT_NOTE_BODY_MAX_LENGTH = 3000;
19
20
  const PROJECT_NOTE_MAX_ITEMS = 200;
20
- const PROJECT_TODO_TEXT_MAX_LENGTH = 120;
21
+ const PROJECT_TODO_TEXT_MAX_LENGTH = 1000;
21
22
  const PROJECT_PLAN_TITLE_MAX_LENGTH = 160;
22
23
  const PROJECT_PLAN_BODY_MAX_LENGTH = 200_000;
23
24
  const PROJECT_TODO_MAX_ITEMS = 500;
@@ -26,19 +27,11 @@ const PROJECT_PLAN_MAX_ITEMS = 500;
26
27
  const PROJECT_ID_PATTERN = /^[a-zA-Z0-9._:-]+$/;
27
28
  const PLAN_FILE_PATTERN = /^[a-zA-Z0-9._-]+\.md$/;
28
29
 
29
- const asNonEmptyString = (value) => {
30
- if (typeof value !== 'string') return null;
31
- const trimmed = value.trim();
32
- return trimmed.length > 0 ? trimmed : null;
33
- };
34
-
35
30
  const clampLength = (value, maxLength) => {
36
31
  if (typeof value !== 'string') return '';
37
32
  return value.length > maxLength ? value.slice(0, maxLength) : value;
38
33
  };
39
34
 
40
- const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
41
-
42
35
  const NOTE_SOURCES = new Set(['manual', 'selection', 'agent']);
43
36
 
44
37
  const sanitizeNoteOrigin = (value) => {
@@ -197,7 +190,7 @@ const createEmptyContext = () => ({
197
190
  const SHARED_PLAN_ID_PREFIX = 'shared:';
198
191
 
199
192
  export const createProjectContextRuntime = (deps) => {
200
- const { fsPromises, path, projectsDirPath, createId, resolveSharedPlansDir } = deps;
193
+ const { fsPromises, path, projectsDirPath, createId, resolveSharedPlansDir, onChanged } = deps;
201
194
  // The team's shared plans folder for a project (absolute path) or null; the
202
195
  // project config runtime owns that answer (`plansDir` in the shared file).
203
196
  const sharedPlansDirFor = typeof resolveSharedPlansDir === 'function'
@@ -473,6 +466,7 @@ export const createProjectContextRuntime = (deps) => {
473
466
  todos: context.todos,
474
467
  plans: context.plans,
475
468
  });
469
+ onChanged?.(sanitizeProjectId(projectId));
476
470
  };
477
471
 
478
472
  const saveTodos = async (projectId, todos) => {
@@ -586,15 +580,16 @@ export const createProjectContextRuntime = (deps) => {
586
580
  if (sharedFile) {
587
581
  const dir = await sharedPlansDirFor(projectId);
588
582
  if (!dir) return null;
583
+ const planPath = path.join(dir, sharedFile);
589
584
  let raw;
590
585
  try {
591
- raw = await fsPromises.readFile(path.join(dir, sharedFile), 'utf8');
586
+ raw = await fsPromises.readFile(planPath, 'utf8');
592
587
  } catch (error) {
593
588
  if (error && error.code === 'ENOENT') return null;
594
589
  throw error;
595
590
  }
596
591
  const parsed = parsePlanMarkdown(raw);
597
- return { id, file: sharedFile, createdAt: 0, title: parsed.title, body: parsed.body, raw, source: 'shared' };
592
+ return { id, file: sharedFile, path: planPath, createdAt: 0, title: parsed.title, body: parsed.body, raw, source: 'shared' };
598
593
  }
599
594
  const context = await readStoredContext(projectId);
600
595
  const link = context.plans.find((entry) => entry.id === id);
@@ -604,16 +599,17 @@ export const createProjectContextRuntime = (deps) => {
604
599
 
605
600
  const folder = await folderOfLink(projectId, link);
606
601
  if (!folder) return null;
602
+ const planPath = path.join(folder, link.file);
607
603
  let raw;
608
604
  try {
609
- raw = await fsPromises.readFile(path.join(folder, link.file), 'utf8');
605
+ raw = await fsPromises.readFile(planPath, 'utf8');
610
606
  } catch (error) {
611
607
  if (error && error.code === 'ENOENT') return null;
612
608
  throw error;
613
609
  }
614
610
 
615
611
  const parsed = parsePlanMarkdown(raw);
616
- return { id: link.id, file: link.file, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw, source: link.shared ? 'shared' : 'personal' };
612
+ return { id: link.id, file: link.file, path: planPath, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw, source: link.shared ? 'shared' : 'personal' };
617
613
  };
618
614
 
619
615
  /**
@@ -652,6 +648,7 @@ export const createProjectContextRuntime = (deps) => {
652
648
  throw error;
653
649
  }
654
650
  await fsPromises.writeFile(filePath, raw, 'utf8');
651
+ onChanged?.(sanitizeProjectId(projectId));
655
652
  const parsed = parsePlanMarkdown(raw);
656
653
  const context = await readContext(projectId);
657
654
  const plan = context.plans.find((entry) => entry.id === id) ?? { id, file: sharedFile, title: parsed.title, createdAt: Date.now(), pinned: false, source: 'shared' };
@@ -771,6 +768,7 @@ export const createProjectContextRuntime = (deps) => {
771
768
  const exists = filePath ? await fsPromises.access(filePath).then(() => true, () => false) : false;
772
769
  if (!exists) return { deleted: false, context: await readContext(projectId) };
773
770
  await fsPromises.rm(filePath, { force: true });
771
+ onChanged?.(sanitizeProjectId(projectId));
774
772
  return { deleted: true, context: await readContext(projectId) };
775
773
  });
776
774
  }