@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
@@ -7,6 +7,7 @@ import {
7
7
  SKILL_SCOPE,
8
8
  ensureDirs,
9
9
  parseMdFile,
10
+ parseMdFileAsync,
10
11
  writeMdFile,
11
12
  readConfigLayers,
12
13
  readConfig,
@@ -92,8 +93,8 @@ function getProjectAgentsSkillPath(workingDirectory, skillName) {
92
93
  return path.join(getProjectAgentsSkillDir(workingDirectory, skillName), 'SKILL.md');
93
94
  }
94
95
 
95
- function getSkillScope(skillName, workingDirectory) {
96
- const discovered = discoverSkills(workingDirectory).find((skill) => skill.name === skillName);
96
+ async function getSkillScope(skillName, workingDirectory) {
97
+ const discovered = (await discoverSkills(workingDirectory)).find((skill) => skill.name === skillName);
97
98
  if (discovered?.path) {
98
99
  return { scope: discovered.scope || null, path: discovered.path, source: discovered.source || null };
99
100
  }
@@ -128,8 +129,8 @@ function getSkillScope(skillName, workingDirectory) {
128
129
  return { scope: null, path: null, source: null };
129
130
  }
130
131
 
131
- function getSkillWritePath(skillName, workingDirectory, requestedScope) {
132
- const existing = getSkillScope(skillName, workingDirectory);
132
+ async function getSkillWritePath(skillName, workingDirectory, requestedScope) {
133
+ const existing = await getSkillScope(skillName, workingDirectory);
133
134
  if (existing.path) {
134
135
  return existing;
135
136
  }
@@ -150,15 +151,34 @@ function getSkillWritePath(skillName, workingDirectory, requestedScope) {
150
151
  };
151
152
  }
152
153
 
153
- function discoverSkills(workingDirectory) {
154
+ async function discoverSkills(workingDirectory) {
154
155
  const skills = new Map();
155
156
 
157
+ // A root can come up twice in one discovery: a home directory opened as a
158
+ // project lists `~/.claude/skills` and `~/.agents/skills` once as user
159
+ // skills and again as project skills. Each root is walked and each SKILL.md
160
+ // read once; the second pass still re-adds them in the same order, so the
161
+ // project scope wins as before.
162
+ const walkedRoots = new Map();
163
+ const parsedFiles = new Map();
164
+ const walkRoot = (root) => {
165
+ const key = path.resolve(root);
166
+ if (!walkedRoots.has(key)) walkedRoots.set(key, walkSkillMdFiles(root));
167
+ return walkedRoots.get(key);
168
+ };
169
+ const addSkills = async (root, scope, source) => {
170
+ for (const skillMdPath of await walkRoot(root)) {
171
+ if (!parsedFiles.has(skillMdPath)) {
172
+ parsedFiles.set(skillMdPath, await parseMdFileAsync(skillMdPath).catch(() => null));
173
+ }
174
+ addSkillFromMdFile(skills, skillMdPath, parsedFiles.get(skillMdPath), scope, source);
175
+ }
176
+ };
177
+
156
178
  for (const externalRootName of ['.claude', '.agents']) {
157
179
  const homeRoot = path.join(os.homedir(), externalRootName, 'skills');
158
180
  const source = externalRootName === '.agents' ? 'agents' : 'claude';
159
- for (const skillMdPath of walkSkillMdFiles(homeRoot)) {
160
- addSkillFromMdFile(skills, skillMdPath, SKILL_SCOPE.USER, source);
161
- }
181
+ await addSkills(homeRoot, SKILL_SCOPE.USER, source);
162
182
  }
163
183
 
164
184
  if (workingDirectory) {
@@ -168,9 +188,7 @@ function discoverSkills(workingDirectory) {
168
188
  for (const externalRootName of ['.claude', '.agents']) {
169
189
  const source = externalRootName === '.agents' ? 'agents' : 'claude';
170
190
  const externalSkillsRoot = path.join(ancestor, externalRootName, 'skills');
171
- for (const skillMdPath of walkSkillMdFiles(externalSkillsRoot)) {
172
- addSkillFromMdFile(skills, skillMdPath, SKILL_SCOPE.PROJECT, source);
173
- }
191
+ await addSkills(externalSkillsRoot, SKILL_SCOPE.PROJECT, source);
174
192
  }
175
193
  }
176
194
  }
@@ -183,13 +201,11 @@ function discoverSkills(workingDirectory) {
183
201
  for (const dir of configDirectories) {
184
202
  for (const subDir of ['skill', 'skills']) {
185
203
  const root = path.join(dir, subDir);
186
- for (const skillMdPath of walkSkillMdFiles(root)) {
187
- const isUserConfigDir = dir === OPENCODE_CONFIG_DIR
188
- || dir === homeOpencodeDir
189
- || (customConfigDir && dir === customConfigDir);
190
- const scope = isUserConfigDir ? SKILL_SCOPE.USER : SKILL_SCOPE.PROJECT;
191
- addSkillFromMdFile(skills, skillMdPath, scope, 'opencode');
192
- }
204
+ const isUserConfigDir = dir === OPENCODE_CONFIG_DIR
205
+ || dir === homeOpencodeDir
206
+ || (customConfigDir && dir === customConfigDir);
207
+ const scope = isUserConfigDir ? SKILL_SCOPE.USER : SKILL_SCOPE.PROJECT;
208
+ await addSkills(root, scope, 'opencode');
193
209
  }
194
210
  }
195
211
 
@@ -214,9 +230,7 @@ function discoverSkills(workingDirectory) {
214
230
  const resolved = path.isAbsolute(expanded)
215
231
  ? path.resolve(expanded)
216
232
  : path.resolve(workingDirectory || process.cwd(), expanded);
217
- for (const skillMdPath of walkSkillMdFiles(resolved)) {
218
- addSkillFromMdFile(skills, skillMdPath, SKILL_SCOPE.PROJECT, 'opencode');
219
- }
233
+ await addSkills(resolved, SKILL_SCOPE.PROJECT, 'opencode');
220
234
  }
221
235
 
222
236
  const cacheCandidates = [];
@@ -232,9 +246,7 @@ function discoverSkills(workingDirectory) {
232
246
  for (const entry of entries) {
233
247
  if (!entry.isDirectory()) continue;
234
248
  const skillRoot = path.join(cacheRoot, entry.name);
235
- for (const skillMdPath of walkSkillMdFiles(skillRoot)) {
236
- addSkillFromMdFile(skills, skillMdPath, SKILL_SCOPE.USER, 'opencode');
237
- }
249
+ await addSkills(skillRoot, SKILL_SCOPE.USER, 'opencode');
238
250
  }
239
251
  }
240
252
 
@@ -308,7 +320,10 @@ function applyModelInvocation(frontmatter, disabled) {
308
320
  }
309
321
  }
310
322
 
311
- function getSkillSources(skillName, workingDirectory, discoveredSkill = null) {
323
+ // `includeSupportingFiles: false` leaves `md.supportingFiles` out instead of
324
+ // walking the skill folder: the skills list has no use for it, and a folder
325
+ // with a bundled `.venv` or `node_modules` costs seconds to walk.
326
+ async function getSkillSources(skillName, workingDirectory, discoveredSkill = null, { includeSupportingFiles = true } = {}) {
312
327
  const isReadableFile = (filePath) => {
313
328
  if (!filePath) return false;
314
329
  try {
@@ -339,7 +354,7 @@ function getSkillSources(skillName, workingDirectory, discoveredSkill = null) {
339
354
 
340
355
  const matchedDiscovered = discoveredSkill && discoveredSkill.name === skillName
341
356
  ? discoveredSkill
342
- : discoverSkills(workingDirectory).find((skill) => skill.name === skillName);
357
+ : (await discoverSkills(workingDirectory)).find((skill) => skill.name === skillName);
343
358
  const discoveredDescription =
344
359
  matchedDiscovered && typeof matchedDiscovered.description === 'string'
345
360
  ? matchedDiscovered.description
@@ -410,7 +425,6 @@ function getSkillSources(skillName, workingDirectory, discoveredSkill = null) {
410
425
  scope: mdScope,
411
426
  source: mdSource,
412
427
  fields: isBuiltInDiscovered ? ['description', 'instructions'] : [],
413
- supportingFiles: [],
414
428
  name: matchedDiscovered?.name || skillName,
415
429
  description: discoveredDescription,
416
430
  instructions: isBuiltInDiscovered ? discoveredContent : '',
@@ -444,7 +458,7 @@ function getSkillSources(skillName, workingDirectory, discoveredSkill = null) {
444
458
  };
445
459
 
446
460
  if (mdExists && mdDir) {
447
- const { frontmatter, body } = parseMdFile(mdPath);
461
+ const { frontmatter, body } = await parseMdFileAsync(mdPath);
448
462
  sources.md.fields = Object.keys(frontmatter);
449
463
  sources.md.description = frontmatter.description || '';
450
464
  sources.md.name = frontmatter.name || skillName;
@@ -455,7 +469,9 @@ function getSkillSources(skillName, workingDirectory, discoveredSkill = null) {
455
469
  } else {
456
470
  sources.md.instructions = '';
457
471
  }
458
- sources.md.supportingFiles = listSkillSupportingFiles(mdDir);
472
+ }
473
+ if (includeSupportingFiles) {
474
+ sources.md.supportingFiles = mdExists && mdDir ? await listSkillSupportingFiles(mdDir) : [];
459
475
  }
460
476
 
461
477
  return sources;
@@ -474,11 +490,25 @@ function assertValidSkillName(skillName) {
474
490
  }
475
491
  }
476
492
 
493
+ // Create, edit and rename check the disk, wait on discovery, then write. Run
494
+ // one at a time per process, the way they did while discovery was
495
+ // synchronous, so a check cannot go stale before its own write.
496
+ let skillWriteQueue = Promise.resolve();
497
+ function serializeSkillWrite(write) {
498
+ const run = skillWriteQueue.then(write, write);
499
+ skillWriteQueue = run.catch(() => {});
500
+ return run;
501
+ }
502
+
477
503
  function createSkill(skillName, config, workingDirectory, scope) {
504
+ return serializeSkillWrite(() => createSkillNow(skillName, config, workingDirectory, scope));
505
+ }
506
+
507
+ async function createSkillNow(skillName, config, workingDirectory, scope) {
478
508
  ensureDirs();
479
509
  assertValidSkillName(skillName);
480
510
 
481
- const existing = getSkillScope(skillName, workingDirectory);
511
+ const existing = await getSkillScope(skillName, workingDirectory);
482
512
  if (existing.path) {
483
513
  throw new Error(`Skill ${skillName} already exists at ${existing.path}`);
484
514
  }
@@ -534,7 +564,16 @@ function createSkill(skillName, config, workingDirectory, scope) {
534
564
  applyModelInvocation(frontmatter, true);
535
565
  }
536
566
 
537
- writeMdFile(targetPath, frontmatter, instructions || '');
567
+ // `wx` fails if SKILL.md appeared since the check above, from another
568
+ // process or a client racing this one, instead of overwriting it.
569
+ try {
570
+ writeMdFile(targetPath, frontmatter, instructions || '', { flag: 'wx' });
571
+ } catch (error) {
572
+ if (error?.code === 'EEXIST') {
573
+ throw new Error(`Skill ${skillName} already exists at ${targetPath}`);
574
+ }
575
+ throw error;
576
+ }
538
577
 
539
578
  if (supportingFiles && Array.isArray(supportingFiles)) {
540
579
  for (const file of supportingFiles) {
@@ -548,6 +587,10 @@ function createSkill(skillName, config, workingDirectory, scope) {
548
587
  }
549
588
 
550
589
  function updateSkill(skillName, updates, workingDirectory, targetPath = null) {
590
+ return serializeSkillWrite(() => updateSkillNow(skillName, updates, workingDirectory, targetPath));
591
+ }
592
+
593
+ async function updateSkillNow(skillName, updates, workingDirectory, targetPath = null) {
551
594
  ensureDirs();
552
595
 
553
596
  const requestedPath = typeof targetPath === 'string' && targetPath.trim()
@@ -555,7 +598,7 @@ function updateSkill(skillName, updates, workingDirectory, targetPath = null) {
555
598
  : null;
556
599
  const existing = requestedPath && fs.existsSync(requestedPath)
557
600
  ? { scope: null, path: requestedPath, source: null }
558
- : getSkillScope(skillName, workingDirectory);
601
+ : await getSkillScope(skillName, workingDirectory);
559
602
  if (!existing.path) {
560
603
  throw new Error(`Skill "${skillName}" not found`);
561
604
  }
@@ -724,6 +767,10 @@ function isManagedSkillPath(skillMdPath, workingDirectory) {
724
767
  }
725
768
 
726
769
  function renameSkill(oldName, newName, workingDirectory) {
770
+ return serializeSkillWrite(() => renameSkillNow(oldName, newName, workingDirectory));
771
+ }
772
+
773
+ async function renameSkillNow(oldName, newName, workingDirectory) {
727
774
  ensureDirs();
728
775
  assertValidSkillName(newName);
729
776
 
@@ -731,7 +778,7 @@ function renameSkill(oldName, newName, workingDirectory) {
731
778
  return;
732
779
  }
733
780
 
734
- const existing = getSkillScope(oldName, workingDirectory);
781
+ const existing = await getSkillScope(oldName, workingDirectory);
735
782
  if (!existing.path) {
736
783
  throw new Error(`Skill "${oldName}" not found`);
737
784
  }
@@ -753,7 +800,7 @@ function renameSkill(oldName, newName, workingDirectory) {
753
800
  throw new Error(`Skill "${oldName}" does not match ${existing.path}`);
754
801
  }
755
802
 
756
- const conflict = getSkillScope(newName, workingDirectory);
803
+ const conflict = await getSkillScope(newName, workingDirectory);
757
804
  if (conflict.path) {
758
805
  throw new Error(`Skill ${newName} already exists at ${conflict.path}`);
759
806
  }
@@ -4,7 +4,7 @@ import fsPromises from 'fs/promises';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
6
  import yaml from 'yaml';
7
- import { discoverSkills, getSkillSources, mergeDiscoveredSkills, renameSkill, updateSkill } from './skills.js';
7
+ import { createSkill, discoverSkills, getSkillSources, mergeDiscoveredSkills, renameSkill, updateSkill } from './skills.js';
8
8
 
9
9
  describe('skills', () => {
10
10
  it('merges locally discovered skills missing from OpenCode live discovery', () => {
@@ -48,7 +48,7 @@ describe('skills', () => {
48
48
  'utf8',
49
49
  );
50
50
 
51
- const discovered = discoverSkills(tempRoot);
51
+ const discovered = await discoverSkills(tempRoot);
52
52
  const match = discovered.find((skill) => skill.name === 'repo-local-skill');
53
53
 
54
54
  expect(match).toEqual({
@@ -63,8 +63,163 @@ describe('skills', () => {
63
63
  }
64
64
  });
65
65
 
66
- it('resolves built-in OpenCode skill content without parsing virtual locations as files', () => {
67
- const sources = getSkillSources(
66
+ describe('discovery over a tree with nested skills and links', () => {
67
+ // Expected values were recorded from the synchronous discovery on main
68
+ // before it became async; they must not move.
69
+ const buildTree = async () => {
70
+ const root = await fsPromises.realpath(await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skill-equiv-')));
71
+ const write = async (relative, frontmatter) => {
72
+ const file = path.join(root, relative);
73
+ await fsPromises.mkdir(path.dirname(file), { recursive: true });
74
+ await fsPromises.writeFile(file, `---\n${frontmatter}\n---\nBody\n`, 'utf8');
75
+ };
76
+ const skill = (relative, name) => write(relative, `name: ${name}\ndescription: ${name} description`);
77
+ // A junction needs no elevation on Windows; the type is ignored on POSIX.
78
+ const link = async (target, relative) => {
79
+ await fsPromises.mkdir(path.dirname(path.join(root, relative)), { recursive: true });
80
+ await fsPromises.symlink(path.join(root, target), path.join(root, relative), process.platform === 'win32' ? 'junction' : 'dir');
81
+ };
82
+ await skill('home/.agents/skills/alpha/SKILL.md', 'alpha');
83
+ await skill('home/.agents/skills/alpha/nested/inner/SKILL.md', 'alpha-inner');
84
+ await skill('home/.agents/skills/group/beta/SKILL.md', 'beta');
85
+ await write('home/.agents/skills/broken/SKILL.md', 'description: no name');
86
+ await skill('outside/gamma/SKILL.md', 'gamma');
87
+ await link('outside/gamma', 'home/.agents/skills/linked-gamma');
88
+ await link('home/.agents/skills', 'home/.agents/skills/loop/back');
89
+ await link('home/.agents/skills/alpha', 'home/.claude/skills/alpha');
90
+ await skill('home/.claude/skills/delta/SKILL.md', 'delta');
91
+ await skill('home/.claude/skills/delta/.venv/lib/pkg/SKILL.md', 'venv-skill');
92
+ await fsPromises.mkdir(path.join(root, 'home/work/project/.git'), { recursive: true });
93
+ await skill('home/work/project/.agents/skills/proj/SKILL.md', 'proj');
94
+ await skill('home/work/project/.opencode/skills/oc/SKILL.md', 'oc');
95
+ await skill('home/work/project/.claude/skills/delta/SKILL.md', 'delta');
96
+ return root;
97
+ };
98
+
99
+ const withHome = async (home, run) => {
100
+ const previous = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE };
101
+ process.env.HOME = home;
102
+ process.env.USERPROFILE = home;
103
+ try {
104
+ return await run();
105
+ } finally {
106
+ for (const [key, value] of Object.entries(previous)) {
107
+ if (value === undefined) delete process.env[key];
108
+ else process.env[key] = value;
109
+ }
110
+ }
111
+ };
112
+
113
+ // Skills of the machine running the tests (global config, caches) are
114
+ // outside the tree and left out.
115
+ const discoveredIn = async (root, directory) => (await discoverSkills(directory))
116
+ .filter((skill) => skill.path.startsWith(root))
117
+ .map((skill) => [skill.name, path.relative(root, skill.path).split(path.sep).join('/'), skill.scope, skill.source])
118
+ .sort((a, b) => a[0].localeCompare(b[0]));
119
+
120
+ const userSkills = (scope) => [
121
+ ['alpha', 'home/.agents/skills/alpha/SKILL.md', scope, 'agents'],
122
+ ['alpha-inner', 'home/.agents/skills/alpha/nested/inner/SKILL.md', scope, 'agents'],
123
+ ['beta', 'home/.agents/skills/group/beta/SKILL.md', scope, 'agents'],
124
+ ['delta', 'home/.claude/skills/delta/SKILL.md', scope, 'claude'],
125
+ ['gamma', 'home/.agents/skills/linked-gamma/SKILL.md', scope, 'agents'],
126
+ ['venv-skill', 'home/.claude/skills/delta/.venv/lib/pkg/SKILL.md', scope, 'claude'],
127
+ ];
128
+
129
+ it('finds the same skills as before for a project, a home-directory project and no project', async () => {
130
+ const root = await buildTree();
131
+ const home = path.join(root, 'home');
132
+ try {
133
+ await withHome(home, async () => {
134
+ expect(await discoveredIn(root, path.join(home, 'work', 'project'))).toEqual([
135
+ ['alpha', 'home/.agents/skills/alpha/SKILL.md', 'user', 'agents'],
136
+ ['alpha-inner', 'home/.agents/skills/alpha/nested/inner/SKILL.md', 'user', 'agents'],
137
+ ['beta', 'home/.agents/skills/group/beta/SKILL.md', 'user', 'agents'],
138
+ ['delta', 'home/work/project/.claude/skills/delta/SKILL.md', 'project', 'claude'],
139
+ ['gamma', 'home/.agents/skills/linked-gamma/SKILL.md', 'user', 'agents'],
140
+ ['oc', 'home/work/project/.opencode/skills/oc/SKILL.md', 'project', 'opencode'],
141
+ ['proj', 'home/work/project/.agents/skills/proj/SKILL.md', 'project', 'agents'],
142
+ ['venv-skill', 'home/.claude/skills/delta/.venv/lib/pkg/SKILL.md', 'user', 'claude'],
143
+ ]);
144
+ // The home directory's own skill roots are read once but still
145
+ // land as project skills, the way the second pass always left them.
146
+ expect(await discoveredIn(root, home)).toEqual(userSkills('project'));
147
+ expect(await discoveredIn(root, null)).toEqual(userSkills('user'));
148
+ });
149
+ } finally {
150
+ await fsPromises.rm(root, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ it('lets other work run while it reads the tree', async () => {
155
+ const root = await buildTree();
156
+ const events = [];
157
+ try {
158
+ await withHome(path.join(root, 'home'), async () => {
159
+ const discovery = discoverSkills(path.join(root, 'home', 'work', 'project')).then(() => events.push('discovered'));
160
+ setImmediate(() => events.push('other work'));
161
+ await discovery;
162
+ });
163
+ expect(events).toEqual(['other work', 'discovered']);
164
+ } finally {
165
+ await fsPromises.rm(root, { recursive: true, force: true });
166
+ }
167
+ });
168
+ });
169
+
170
+ describe('writes that race each other', () => {
171
+ const makeProject = async () => {
172
+ const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skills-race-'));
173
+ const projectRoot = path.join(tempRoot, 'project');
174
+ await fsPromises.mkdir(path.join(projectRoot, '.git'), { recursive: true });
175
+ return { tempRoot, projectRoot };
176
+ };
177
+ const skillPath = (projectRoot, name) => path.join(projectRoot, '.opencode', 'skills', name, 'SKILL.md');
178
+ // `.agents` project skills: the existence check only finds them through
179
+ // discovery, so two creates both pass it unless the write itself refuses.
180
+ const agentsSkillPath = (projectRoot, name) => path.join(projectRoot, '.agents', 'skills', name, 'SKILL.md');
181
+
182
+ it('lets exactly one of two simultaneous creates of the same name succeed', async () => {
183
+ const { tempRoot, projectRoot } = await makeProject();
184
+ try {
185
+ const results = await Promise.allSettled([
186
+ createSkill('race-skill', { description: 'First', source: 'agents' }, projectRoot, 'project'),
187
+ createSkill('race-skill', { description: 'Second', source: 'agents' }, projectRoot, 'project'),
188
+ ]);
189
+
190
+ expect(results.map((result) => result.status).sort()).toEqual(['fulfilled', 'rejected']);
191
+ expect(results.find((result) => result.status === 'rejected').reason.message).toMatch(/already exists/);
192
+ const written = await fsPromises.readFile(agentsSkillPath(projectRoot, 'race-skill'), 'utf8');
193
+ expect(written).toContain(`description: ${results[0].status === 'fulfilled' ? 'First' : 'Second'}`);
194
+ } finally {
195
+ await fsPromises.rm(tempRoot, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ it('does not write an edit into the folder of a skill renamed meanwhile', async () => {
200
+ const { tempRoot, projectRoot } = await makeProject();
201
+ const oldDir = path.dirname(skillPath(projectRoot, 'moving-skill'));
202
+ try {
203
+ await createSkill('moving-skill', { description: 'Before' }, projectRoot, 'project');
204
+
205
+ const [renamed, edited] = await Promise.allSettled([
206
+ renameSkill('moving-skill', 'moved-skill', projectRoot),
207
+ updateSkill('moving-skill', { description: 'Edited' }, projectRoot),
208
+ ]);
209
+
210
+ expect(renamed.status).toBe('fulfilled');
211
+ expect(edited.status).toBe('rejected');
212
+ expect(edited.reason.message).toMatch(/not found/);
213
+ expect(fs.existsSync(oldDir)).toBe(false);
214
+ expect(await fsPromises.readFile(skillPath(projectRoot, 'moved-skill'), 'utf8')).toContain('description: Before');
215
+ } finally {
216
+ await fsPromises.rm(tempRoot, { recursive: true, force: true });
217
+ }
218
+ });
219
+ });
220
+
221
+ it('resolves built-in OpenCode skill content without parsing virtual locations as files', async () => {
222
+ const sources = await getSkillSources(
68
223
  'customize-opencode',
69
224
  '/tmp/openchamber-skills-test-missing-project',
70
225
  {
@@ -87,9 +242,9 @@ describe('skills', () => {
87
242
  expect(sources.md.fields).toEqual(['description', 'instructions']);
88
243
  });
89
244
 
90
- it('clears file metadata when a discovered skill path is unreadable', () => {
245
+ it('clears file metadata when a discovered skill path is unreadable', async () => {
91
246
  const missingPath = path.join(os.tmpdir(), 'openchamber-skills-test-missing-file', 'SKILL.md');
92
- const sources = getSkillSources(
247
+ const sources = await getSkillSources(
93
248
  'missing-agent-skill',
94
249
  '/tmp/openchamber-skills-test-missing-project',
95
250
  {
@@ -131,7 +286,7 @@ describe('skills', () => {
131
286
  'utf8',
132
287
  );
133
288
 
134
- const sources = getSkillSources('example-skill', tempRoot, {
289
+ const sources = await getSkillSources('example-skill', tempRoot, {
135
290
  name: 'example-skill',
136
291
  path: skillPath,
137
292
  scope: 'user',
@@ -167,24 +322,24 @@ describe('skills', () => {
167
322
  ['---', 'name: manual-skill', 'description: Manual', 'metadata:', ' team: core', '---', '', 'Body.', ''].join('\n'),
168
323
  'utf8',
169
324
  );
170
- expect(getSkillSources('manual-skill', tempRoot, discovered).md.disableModelInvocation).toBe(false);
325
+ expect((await getSkillSources('manual-skill', tempRoot, discovered)).md.disableModelInvocation).toBe(false);
171
326
 
172
- updateSkill('manual-skill', { disableModelInvocation: true }, tempRoot, skillPath);
327
+ await updateSkill('manual-skill', { disableModelInvocation: true }, tempRoot, skillPath);
173
328
  expect(await readFrontmatter()).toEqual({
174
329
  name: 'manual-skill',
175
330
  description: 'Manual',
176
331
  metadata: { team: 'core', 'opencode/autoinvoke': false },
177
332
  'disable-model-invocation': true,
178
333
  });
179
- expect(getSkillSources('manual-skill', tempRoot, discovered).md.disableModelInvocation).toBe(true);
334
+ expect((await getSkillSources('manual-skill', tempRoot, discovered)).md.disableModelInvocation).toBe(true);
180
335
 
181
- updateSkill('manual-skill', { disableModelInvocation: false }, tempRoot, skillPath);
336
+ await updateSkill('manual-skill', { disableModelInvocation: false }, tempRoot, skillPath);
182
337
  expect(await readFrontmatter()).toEqual({
183
338
  name: 'manual-skill',
184
339
  description: 'Manual',
185
340
  metadata: { team: 'core' },
186
341
  });
187
- expect(getSkillSources('manual-skill', tempRoot, discovered).md.disableModelInvocation).toBe(false);
342
+ expect((await getSkillSources('manual-skill', tempRoot, discovered)).md.disableModelInvocation).toBe(false);
188
343
  } finally {
189
344
  await fsPromises.rm(tempRoot, { recursive: true, force: true });
190
345
  }
@@ -201,7 +356,7 @@ describe('skills', () => {
201
356
  ['---', 'name: read-skill', 'description: Read', ...frontmatterLines, '---', '', 'Body.', ''].join('\n'),
202
357
  'utf8',
203
358
  );
204
- return getSkillSources('read-skill', tempRoot, discovered).md.disableModelInvocation;
359
+ return (await getSkillSources('read-skill', tempRoot, discovered)).md.disableModelInvocation;
205
360
  };
206
361
 
207
362
  try {
@@ -249,7 +404,7 @@ describe('skills', () => {
249
404
  );
250
405
  await fsPromises.writeFile(supportPath, 'supporting file contents\n', 'utf8');
251
406
 
252
- renameSkill('original-skill', 'renamed-skill', projectRoot);
407
+ await renameSkill('original-skill', 'renamed-skill', projectRoot);
253
408
 
254
409
  const renamedDir = path.join(projectRoot, '.opencode', 'skills', 'renamed-skill');
255
410
  const renamedPath = path.join(renamedDir, 'SKILL.md');
@@ -259,7 +414,7 @@ describe('skills', () => {
259
414
  expect(fs.existsSync(renamedPath)).toBe(true);
260
415
  expect(fs.existsSync(renamedSupportPath)).toBe(true);
261
416
 
262
- const sources = getSkillSources('renamed-skill', projectRoot, {
417
+ const sources = await getSkillSources('renamed-skill', projectRoot, {
263
418
  name: 'renamed-skill',
264
419
  path: renamedPath,
265
420
  scope: 'project',
@@ -305,7 +460,7 @@ describe('skills', () => {
305
460
  );
306
461
  await fsPromises.chmod(skillPath, 0o444);
307
462
 
308
- expect(() => renameSkill('rollback-skill', 'rollback-skill-renamed', projectRoot)).toThrow();
463
+ await expect(renameSkill('rollback-skill', 'rollback-skill-renamed', projectRoot)).rejects.toThrow();
309
464
 
310
465
  expect(fs.existsSync(skillDir)).toBe(true);
311
466
  expect(fs.existsSync(path.join(projectRoot, '.opencode', 'skills', 'rollback-skill-renamed'))).toBe(false);
@@ -390,11 +545,11 @@ describe('skills', () => {
390
545
  'utf8',
391
546
  );
392
547
 
393
- expect(() => renameSkill('managed-skill', 'Invalid_Name', projectRoot)).toThrow(/Invalid skill name/);
394
- expect(() => renameSkill('missing-skill', 'new-skill', projectRoot)).toThrow(/not found/);
395
- expect(() => renameSkill('managed-skill', 'taken-name', projectRoot)).toThrow(/already exists/);
396
- expect(() => renameSkill('folder-name', 'renamed-mismatch', projectRoot)).toThrow(/does not match/);
397
- expect(() => renameSkill('cache-skill', 'cache-skill-renamed', projectRoot)).toThrow(/managed skill directories/);
548
+ await expect(renameSkill('managed-skill', 'Invalid_Name', projectRoot)).rejects.toThrow(/Invalid skill name/);
549
+ await expect(renameSkill('missing-skill', 'new-skill', projectRoot)).rejects.toThrow(/not found/);
550
+ await expect(renameSkill('managed-skill', 'taken-name', projectRoot)).rejects.toThrow(/already exists/);
551
+ await expect(renameSkill('folder-name', 'renamed-mismatch', projectRoot)).rejects.toThrow(/does not match/);
552
+ await expect(renameSkill('cache-skill', 'cache-skill-renamed', projectRoot)).rejects.toThrow(/managed skill directories/);
398
553
 
399
554
  expect(fs.existsSync(managedDir)).toBe(true);
400
555
  expect(fs.existsSync(cacheDir)).toBe(true);
@@ -33,8 +33,11 @@ export const createStartupPipelineRuntime = (dependencies) => {
33
33
  terminalHeartbeatIntervalMs,
34
34
  terminalRebindWindowMs,
35
35
  terminalMaxRebindsPerWindow,
36
+ environmentRuntime = null,
36
37
  setupProxy,
38
+ scheduleOpenCodeApiDetection,
37
39
  bootstrapOpenCodeAtStartup,
40
+ onListenerReady = null,
38
41
  staticRoutesRuntime,
39
42
  process,
40
43
  crypto,
@@ -53,7 +56,6 @@ export const createStartupPipelineRuntime = (dependencies) => {
53
56
  port,
54
57
  startupTunnelRequest,
55
58
  onTunnelReady,
56
- onListenerReady,
57
59
  tunnelRuntimeContext,
58
60
  attachSignals,
59
61
  apiOnly,
@@ -75,6 +77,7 @@ export const createStartupPipelineRuntime = (dependencies) => {
75
77
  TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS: terminalHeartbeatIntervalMs,
76
78
  TERMINAL_INPUT_WS_REBIND_WINDOW_MS: terminalRebindWindowMs,
77
79
  TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW: terminalMaxRebindsPerWindow,
80
+ environmentRuntime,
78
81
  });
79
82
 
80
83
  const dictationRuntime = createDictationRuntime({
@@ -134,7 +137,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
134
137
  onTunnelReady,
135
138
  afterListening: async ({ activePort }) => {
136
139
  tunnelRuntimeContext.setActivePort(activePort);
137
- if (onListenerReady) await onListenerReady();
140
+ await onListenerReady?.({ activePort });
141
+ scheduleOpenCodeApiDetection();
138
142
  await bootstrapOpenCodeAtStartup();
139
143
  },
140
144
  });
@@ -142,7 +146,6 @@ export const createStartupPipelineRuntime = (dependencies) => {
142
146
  durationMs: performance.now() - pipelineStartedAt,
143
147
  });
144
148
 
145
-
146
149
  serverStartupRuntime.attachProcessHandlers({ attachSignals });
147
150
 
148
151
  return {
@@ -31,6 +31,7 @@ describe('startup pipeline runtime', () => {
31
31
  tunnelRuntimeContext: {
32
32
  setActivePort: (port) => order.push(`port:${port}`),
33
33
  },
34
+ scheduleOpenCodeApiDetection: vi.fn(),
34
35
  bootstrapOpenCodeAtStartup: async () => {
35
36
  order.push('bootstrap');
36
37
  await bootstrapGate;
@@ -41,7 +42,7 @@ describe('startup pipeline runtime', () => {
41
42
  attachSignals: false,
42
43
  });
43
44
 
44
- await Promise.resolve();
45
+ await new Promise((resolve) => setTimeout(resolve, 0));
45
46
  expect(order).toEqual(['listen', 'port:3901', 'bootstrap']);
46
47
  releaseBootstrap();
47
48
  await run;