@aiworker/web 1.22.0 → 1.23.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 (415) hide show
  1. package/README.md +4 -4
  2. package/bin/cli.js +14 -4
  3. package/bin/cli.test.js +165 -73
  4. package/bin/lib/cli-api-target.js +2 -2
  5. package/bin/lib/cli-args.js +119 -119
  6. package/bin/lib/cli-lifecycle.js +9 -9
  7. package/bin/lib/cli-network.js +3 -3
  8. package/bin/lib/cli-paths.js +3 -3
  9. package/bin/lib/cli-ports.js +2 -2
  10. package/bin/lib/cli-process.js +4 -4
  11. package/bin/lib/cli-startup.js +64 -15
  12. package/bin/lib/cli-tunnel-profiles.js +1 -1
  13. package/bin/lib/cli-tunnel-utils.js +2 -2
  14. package/bin/lib/commands-connect-url.js +19 -19
  15. package/bin/lib/commands-lifecycle.js +27 -27
  16. package/bin/lib/commands-logs.js +5 -5
  17. package/bin/lib/commands-models.js +1 -1
  18. package/bin/lib/commands-projects.js +1 -1
  19. package/bin/lib/commands-schedule.js +1 -1
  20. package/bin/lib/commands-serve.js +29 -29
  21. package/bin/lib/commands-session.js +1 -1
  22. package/bin/lib/commands-startup.js +45 -10
  23. package/bin/lib/commands-status.js +1 -1
  24. package/bin/lib/commands-tunnel.js +40 -40
  25. package/bin/lib/commands-update.js +1 -1
  26. package/bin/lib/commands-update.test.js +6 -6
  27. package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
  28. package/dist/assets/DiffView-DXjVsEOw.js +4 -0
  29. package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
  30. package/dist/assets/FilesView-CJdG0-wk.js +17 -0
  31. package/dist/assets/GitView-BXsS3jG6.js +6 -0
  32. package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
  33. package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
  34. package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
  35. package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
  36. package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
  37. package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
  38. package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
  39. package/dist/assets/PlanView-CJTmOF6I.js +5 -0
  40. package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
  41. package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
  42. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  43. package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
  44. package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
  45. package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
  46. package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
  47. package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
  48. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  49. package/dist/assets/de-DCrsQ5zS.js +10 -0
  50. package/dist/assets/es-Dwv4YfH6.js +16 -0
  51. package/dist/assets/fr-BCWg4Cnx.js +14 -0
  52. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  53. package/dist/assets/index-B68h5S5B.css +1 -0
  54. package/dist/assets/index-DVwMarHI.js +39 -0
  55. package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
  56. package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
  57. package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
  58. package/dist/assets/ja-Bruw3RYC.js +16 -0
  59. package/dist/assets/ko-idhXGF8j.js +16 -0
  60. package/dist/assets/main-BkyaQhJX.js +930 -0
  61. package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
  62. package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
  63. package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
  64. package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
  65. package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
  66. package/dist/assets/pl-CGZWUqZJ.js +16 -0
  67. package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
  68. package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
  69. package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
  70. package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
  71. package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
  72. package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
  73. package/dist/assets/surface-oMvhsog3.js +1 -0
  74. package/dist/assets/tr-BK_KiO2V.js +7 -0
  75. package/dist/assets/uk-CG74xV8N.js +16 -0
  76. package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
  77. package/dist/assets/useGitComparison-DH220WmO.js +1 -0
  78. package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
  79. package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
  80. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
  82. package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
  83. package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
  84. package/dist/assets/zh-CN-T4AXofZI.js +16 -0
  85. package/dist/assets/zh-TW-Dte40sxp.js +16 -0
  86. package/dist/index.html +21 -59
  87. package/dist/mini-chat.html +7 -7
  88. package/dist/mobile.html +5 -5
  89. package/package.json +3 -4
  90. package/server/index.d.ts +15 -0
  91. package/server/index.js +158 -89
  92. package/server/lib/agent-memory/actions.js +2 -2
  93. package/server/lib/agent-memory/feature-flag.js +1 -1
  94. package/server/lib/agent-memory/feature-flag.test.js +8 -8
  95. package/server/lib/agent-memory/routes.js +2 -2
  96. package/server/lib/agent-tool/runtime.js +37 -54
  97. package/server/lib/agent-tool/runtime.test.js +59 -59
  98. package/server/lib/browser-control/broker.js +3 -3
  99. package/server/lib/browser-control/broker.test.js +1 -1
  100. package/server/lib/changelog/update-notes.js +82 -0
  101. package/server/lib/changelog/update-notes.test.js +59 -0
  102. package/server/lib/client-auth/remote-clients.test.js +5 -5
  103. package/server/lib/context-obligatory/runtime.js +7 -7
  104. package/server/lib/context-obligatory/runtime.test.js +5 -5
  105. package/server/lib/data-dir-migration.js +40 -0
  106. package/server/lib/data-dir-migration.test.js +59 -0
  107. package/server/lib/dev-servers/parse.js +2 -2
  108. package/server/lib/dev-servers/parse.test.js +2 -2
  109. package/server/lib/dev-servers/routes.js +1 -1
  110. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  111. package/server/lib/dev-tunnel/client.js +1 -1
  112. package/server/lib/dev-tunnel/runtime.js +5 -5
  113. package/server/lib/dev-tunnel/tunnel.test.js +21 -1
  114. package/server/lib/dictation/local/model-catalog.js +1 -1
  115. package/server/lib/dictation/local/worker-process.js +2 -2
  116. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  117. package/server/lib/event-stream/global-ws-bridge.js +1 -1
  118. package/server/lib/event-stream/protocol.test.js +4 -4
  119. package/server/lib/event-stream/runtime.test.js +18 -18
  120. package/server/lib/fs/DOCUMENTATION.md +5 -0
  121. package/server/lib/fs/routes.js +68 -26
  122. package/server/lib/fs/routes.test.js +208 -22
  123. package/server/lib/git/DOCUMENTATION.md +32 -6
  124. package/server/lib/git/identity-storage.js +1 -1
  125. package/server/lib/git/issue-2746-longpaths.test.js +4 -4
  126. package/server/lib/git/routes.js +41 -5
  127. package/server/lib/git/service.js +300 -224
  128. package/server/lib/git/service.test.js +596 -28
  129. package/server/lib/github/auth.js +8 -8
  130. package/server/lib/github/pr-status.js +1 -1
  131. package/server/lib/github/routes.js +2 -2
  132. package/server/lib/inherited-env.js +17 -10
  133. package/server/lib/inherited-env.test.js +10 -11
  134. package/server/lib/linear/auth.js +8 -8
  135. package/server/lib/linear/auth.test.js +24 -24
  136. package/server/lib/linear/issues.test.js +6 -6
  137. package/server/lib/linear/mapping.test.js +4 -4
  138. package/server/lib/linear/oauth.js +2 -2
  139. package/server/lib/linear/oauth.test.js +14 -14
  140. package/server/lib/linear/routes.js +5 -5
  141. package/server/lib/linear/routes.test.js +16 -16
  142. package/server/lib/linear/status-runtime.test.js +9 -9
  143. package/server/lib/linear/status.js +1 -1
  144. package/server/lib/linear/status.test.js +15 -15
  145. package/server/lib/linear/teams.test.js +4 -4
  146. package/server/lib/magic-prompts/routes.js +2 -2
  147. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  148. package/server/lib/mcp-reconnect/runtime.js +122 -0
  149. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  150. package/server/lib/message-queue/DOCUMENTATION.md +161 -0
  151. package/server/lib/message-queue/runtime.js +870 -0
  152. package/server/lib/message-queue/runtime.test.js +482 -0
  153. package/server/lib/network-defaults.js +23 -0
  154. package/server/lib/network-defaults.test.js +52 -0
  155. package/server/lib/notifications/apns-runtime.js +17 -17
  156. package/server/lib/notifications/apns-runtime.test.js +11 -11
  157. package/server/lib/notifications/emitter-runtime.js +1 -1
  158. package/server/lib/notifications/emitter-runtime.test.js +1 -1
  159. package/server/lib/notifications/push-runtime.js +7 -7
  160. package/server/lib/notifications/routes.js +1 -1
  161. package/server/lib/notifications/runtime.js +1 -1
  162. package/server/lib/openchamber-control/action-resolution.test.js +10 -10
  163. package/server/lib/openchamber-control/actions.js +24 -24
  164. package/server/lib/openchamber-control/error.js +4 -4
  165. package/server/lib/openchamber-control/routes.js +2 -2
  166. package/server/lib/openchamber-control/routes.test.js +5 -5
  167. package/server/lib/openchamber-control/screenshots.js +1 -1
  168. package/server/lib/openchamber-control/screenshots.test.js +1 -1
  169. package/server/lib/openchamber-control/service.js +50 -50
  170. package/server/lib/openchamber-control/service.test.js +5 -5
  171. package/server/lib/openchamber-sessions/routes.js +124 -22
  172. package/server/lib/openchamber-sessions/routes.test.js +99 -11
  173. package/server/lib/opencode/DOCUMENTATION.md +30 -6
  174. package/server/lib/opencode/auth.js +1 -1
  175. package/server/lib/opencode/bootstrap-runtime.js +8 -6
  176. package/server/lib/opencode/cli-options.js +8 -8
  177. package/server/lib/opencode/config-paths.test.js +116 -0
  178. package/server/lib/opencode/core-routes.js +8 -7
  179. package/server/lib/opencode/core-routes.test.js +11 -11
  180. package/server/lib/opencode/env-config.js +5 -5
  181. package/server/lib/opencode/env-config.test.js +7 -7
  182. package/server/lib/opencode/env-runtime.js +10 -10
  183. package/server/lib/opencode/env-runtime.test.js +18 -18
  184. package/server/lib/opencode/feature-routes-runtime.js +19 -12
  185. package/server/lib/opencode/hmr-state-runtime.js +2 -2
  186. package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
  187. package/server/lib/opencode/input-history-scope.js +14 -0
  188. package/server/lib/opencode/lifecycle.js +8 -8
  189. package/server/lib/opencode/lifecycle.test.js +4 -4
  190. package/server/lib/opencode/managed-plugin-config.js +35 -0
  191. package/server/lib/opencode/managed-process-registry.js +3 -3
  192. package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
  193. package/server/lib/opencode/openchamber-routes.js +79 -19
  194. package/server/lib/opencode/openchamber-routes.test.js +139 -12
  195. package/server/lib/opencode/plugins.js +2 -2
  196. package/server/lib/opencode/plugins.test.js +1 -1
  197. package/server/lib/opencode/project-icon-routes.js +2 -2
  198. package/server/lib/opencode/project-icon-routes.test.js +1 -1
  199. package/server/lib/opencode/providers.js +44 -2
  200. package/server/lib/opencode/providers.test.js +131 -4
  201. package/server/lib/opencode/proxy.js +2 -2
  202. package/server/lib/opencode/pwa-manifest-routes.js +1 -1
  203. package/server/lib/opencode/restart-session-recovery.test.js +1 -1
  204. package/server/lib/opencode/routes-upgrade.test.js +4 -4
  205. package/server/lib/opencode/routes.js +19 -17
  206. package/server/lib/opencode/server-startup-runtime.js +6 -6
  207. package/server/lib/opencode/session-runtime.js +3 -3
  208. package/server/lib/opencode/session-runtime.test.js +6 -6
  209. package/server/lib/opencode/settings-files.js +214 -0
  210. package/server/lib/opencode/settings-helpers.js +116 -44
  211. package/server/lib/opencode/settings-helpers.test.js +354 -12
  212. package/server/lib/opencode/settings-registry.json +747 -0
  213. package/server/lib/opencode/settings-runtime.js +141 -20
  214. package/server/lib/opencode/settings-runtime.test.js +191 -4
  215. package/server/lib/opencode/shared.js +5 -2
  216. package/server/lib/opencode/shared.test.js +7 -7
  217. package/server/lib/opencode/shutdown-runtime.js +2 -0
  218. package/server/lib/opencode/skill-routes.js +2 -1
  219. package/server/lib/opencode/snippets.js +1 -2
  220. package/server/lib/opencode/startup-performance.js +1 -1
  221. package/server/lib/opencode/startup-performance.test.js +6 -6
  222. package/server/lib/opencode/static-routes-runtime.js +8 -8
  223. package/server/lib/opencode/static-routes-runtime.test.js +7 -7
  224. package/server/lib/opencode/upgrade-capability.js +2 -2
  225. package/server/lib/opencode/upgrade-capability.test.js +3 -3
  226. package/server/lib/package-manager.js +17 -42
  227. package/server/lib/package-manager.test.js +19 -19
  228. package/server/lib/permission-auto-accept/runtime.js +1 -1
  229. package/server/lib/project-context/DOCUMENTATION.md +25 -1
  230. package/server/lib/project-context/routes.http.test.js +18 -0
  231. package/server/lib/project-context/routes.js +16 -1
  232. package/server/lib/project-context/runtime.js +248 -23
  233. package/server/lib/project-context/runtime.test.js +106 -3
  234. package/server/lib/projects/DOCUMENTATION.md +76 -0
  235. package/server/lib/projects/project-config.js +144 -11
  236. package/server/lib/projects/project-id.js +17 -0
  237. package/server/lib/projects/project-setup.js +409 -0
  238. package/server/lib/projects/project-setup.test.js +557 -0
  239. package/server/lib/projects/routes.js +59 -0
  240. package/server/lib/quota/DOCUMENTATION.md +40 -2
  241. package/server/lib/quota/credentials/providers.js +4 -0
  242. package/server/lib/quota/credentials/store.js +4 -4
  243. package/server/lib/quota/credentials/store.test.js +8 -8
  244. package/server/lib/quota/index.js +1 -0
  245. package/server/lib/quota/providers/cline-pass.js +125 -0
  246. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  247. package/server/lib/quota/providers/exe-dev.js +75 -0
  248. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  249. package/server/lib/quota/providers/hyper.js +118 -0
  250. package/server/lib/quota/providers/hyper.test.js +126 -0
  251. package/server/lib/quota/providers/index.js +22 -0
  252. package/server/lib/quota/providers/neuralwatt.js +3 -6
  253. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  254. package/server/lib/quota/providers/ollama-cloud.js +32 -1
  255. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  256. package/server/lib/quota/providers/opencode-go.js +3 -2
  257. package/server/lib/quota/providers/opencode-go.test.js +35 -6
  258. package/server/lib/quota/providers/openrouter.js +119 -24
  259. package/server/lib/quota/providers/openrouter.test.js +330 -0
  260. package/server/lib/quota/providers/xai.js +1 -1
  261. package/server/lib/quota/routes.js +2 -0
  262. package/server/lib/quota/utils/auth.js +1 -1
  263. package/server/lib/relay/DOCUMENTATION.md +56 -1
  264. package/server/lib/relay/cross-compat.test.js +26 -0
  265. package/server/lib/relay/downstream-scheduler.js +154 -0
  266. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  267. package/server/lib/relay/e2ee.js +10 -8
  268. package/server/lib/relay/flow-control.test.js +177 -0
  269. package/server/lib/relay/host-client.js +38 -9
  270. package/server/lib/relay/host-client.test.js +9 -1
  271. package/server/lib/relay/host-lock.js +1 -1
  272. package/server/lib/relay/service.js +5 -5
  273. package/server/lib/relay/tunnel-codec.js +9 -0
  274. package/server/lib/relay/tunnel-host.js +69 -18
  275. package/server/lib/relay/tunnel-host.test.js +9 -1
  276. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
  277. package/server/lib/scheduled-tasks/routes.js +5 -5
  278. package/server/lib/scheduled-tasks/service.js +25 -25
  279. package/server/lib/scheduled-tasks/service.test.js +1 -1
  280. package/server/lib/security/bind-host.js +4 -4
  281. package/server/lib/security/bind-host.test.js +1 -1
  282. package/server/lib/security/request-security.js +11 -5
  283. package/server/lib/security/request-security.test.js +14 -14
  284. package/server/lib/session-assist/runtime.js +15 -18
  285. package/server/lib/session-folders/routes.js +2 -2
  286. package/server/lib/session-folders/routes.test.js +8 -8
  287. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  288. package/server/lib/session-goal/create.js +4 -2
  289. package/server/lib/session-goal/create.test.js +1 -1
  290. package/server/lib/session-goal/objectives.js +4 -4
  291. package/server/lib/session-goal/routes.js +1 -1
  292. package/server/lib/session-goal/runtime.js +91 -30
  293. package/server/lib/session-goal/runtime.test.js +428 -2
  294. package/server/lib/session-knowledge/runtime.js +11 -11
  295. package/server/lib/session-knowledge/runtime.test.js +5 -5
  296. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  297. package/server/lib/skills-catalog/cache.test.js +2 -2
  298. package/server/lib/skills-catalog/disk-cache.js +5 -5
  299. package/server/lib/skills-catalog/github-meta.test.js +2 -2
  300. package/server/lib/skills-catalog/install.js +3 -2
  301. package/server/lib/small-model/DOCUMENTATION.md +7 -2
  302. package/server/lib/small-model/call.js +18 -7
  303. package/server/lib/small-model/call.test.js +58 -6
  304. package/server/lib/small-model/index.js +12 -16
  305. package/server/lib/small-model/index.test.js +1 -1
  306. package/server/lib/small-model/resolve.js +1 -1
  307. package/server/lib/small-model/resolve.test.js +1 -1
  308. package/server/lib/small-model/routes.js +2 -1
  309. package/server/lib/small-model/runtime-providers.js +15 -7
  310. package/server/lib/small-model/runtime-providers.test.js +7 -1
  311. package/server/lib/system-prompt/runtime.js +3 -22
  312. package/server/lib/system-prompt/runtime.test.js +3 -3
  313. package/server/lib/terminal/DOCUMENTATION.md +14 -10
  314. package/server/lib/terminal/runtime.js +190 -31
  315. package/server/lib/terminal/runtime.test.js +572 -38
  316. package/server/lib/terminal/shells.js +20 -3
  317. package/server/lib/terminal/shells.test.js +33 -2
  318. package/server/lib/tts/base-url.js +3 -3
  319. package/server/lib/tts/routes.test.js +20 -20
  320. package/server/lib/ui-auth/ui-auth.js +12 -9
  321. package/server/lib/ui-auth/ui-auth.test.js +19 -3
  322. package/server/lib/ui-auth/ui-passkeys.js +7 -7
  323. package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
  324. package/server/lib/walkthrough/index.js +1 -0
  325. package/server/lib/walkthrough/jobs.test.js +1 -1
  326. package/server/lib/walkthrough/language.test.js +1 -1
  327. package/server/lib/walkthrough/model-settings.js +11 -15
  328. package/server/lib/walkthrough/model-settings.test.js +1 -1
  329. package/server/lib/walkthrough/prompt.js +3 -1
  330. package/server/lib/walkthrough/pull-request.test.js +5 -5
  331. package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
  332. package/server/lib/walkthrough/routes.test.js +26 -5
  333. package/server/lib/walkthrough/sources.js +23 -2
  334. package/server/lib/walkthrough/store.js +3 -3
  335. package/server/lib/walkthrough/store.test.js +1 -1
  336. package/server/opencode-proxy.test.js +2 -2
  337. package/server/proxy-headers.js +1 -1
  338. package/server/sse-routes.test.js +4 -4
  339. package/README.md.bak +0 -250
  340. package/bin/lib/DOCUMENTATION.md.bak +0 -163
  341. package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
  342. package/dist/assets/DiffView-DluiRSla.js +0 -4
  343. package/dist/assets/FilesView-Bdq8ppva.js +0 -17
  344. package/dist/assets/GitView-Znn7VJcP.js +0 -6
  345. package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
  346. package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
  347. package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
  348. package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
  349. package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
  350. package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
  351. package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
  352. package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
  353. package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
  354. package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
  355. package/dist/assets/de-C1P6X5Rs.js +0 -9
  356. package/dist/assets/es-CDgaF1_t.js +0 -15
  357. package/dist/assets/fr-Bgxqvvz_.js +0 -13
  358. package/dist/assets/index-C57jT0Sa.css +0 -1
  359. package/dist/assets/index-DW_E3rnL.js +0 -7
  360. package/dist/assets/ja-BDRrEcNP.js +0 -15
  361. package/dist/assets/ko-Ba8sQV79.js +0 -15
  362. package/dist/assets/main-Bv5Pkngh.js +0 -928
  363. package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
  364. package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
  365. package/dist/assets/pl-Dioz29BI.js +0 -15
  366. package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
  367. package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
  368. package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
  369. package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
  370. package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
  371. package/dist/assets/tr-DFnMlPxY.js +0 -6
  372. package/dist/assets/uk-ZrCNjM7Y.js +0 -15
  373. package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
  374. package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
  375. package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
  376. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  377. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
  378. package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
  379. package/dist/assets/zh-TW-CzEUk38j.js +0 -15
  380. package/server/index.d.ts.bak +0 -39
  381. package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
  382. package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
  383. package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
  384. package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
  385. package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
  386. package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
  387. package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
  388. package/server/lib/git/DOCUMENTATION.md.bak +0 -193
  389. package/server/lib/github/DOCUMENTATION.md.bak +0 -183
  390. package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
  391. package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
  392. package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
  393. package/server/lib/notifications/APNS.md.bak +0 -135
  394. package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
  395. package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
  396. package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
  397. package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
  398. package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
  399. package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
  400. package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
  401. package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
  402. package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
  403. package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
  404. package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
  405. package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
  406. package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
  407. package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
  408. package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
  409. package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
  410. package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
  411. package/server/lib/text/DOCUMENTATION.md.bak +0 -35
  412. package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
  413. package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
  414. package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
  415. package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
@@ -164,15 +164,22 @@ const sanitizePlanLinks = (value, now) => {
164
164
  const id = asNonEmptyString(entry.id);
165
165
  const file = asNonEmptyString(entry.file);
166
166
  if (!id || !file || !PLAN_FILE_PATTERN.test(file)) continue;
167
- if (seenIds.has(id) || seenFiles.has(file)) continue;
167
+ // A personal file and a shared file may carry the same name: they live in different folders.
168
+ const shared = entry.shared === true;
169
+ const fileKey = `${shared ? 'shared' : 'personal'}/${file}`;
170
+ if (seenIds.has(id) || seenFiles.has(fileKey)) continue;
168
171
  seenIds.add(id);
169
- seenFiles.add(file);
172
+ seenFiles.add(fileKey);
170
173
  result.push({
171
174
  id,
172
175
  file,
173
176
  title: sanitizePlanTitle(entry.title) || 'Plan',
174
177
  createdAt: Number.isFinite(entry.createdAt) && entry.createdAt >= 0 ? entry.createdAt : now,
175
178
  pinned: entry.pinned === true,
179
+ // The user's own plan that was moved into the team's folder: it keeps
180
+ // its id (session attachments still point at it) and this flag says
181
+ // which folder holds the file.
182
+ shared,
176
183
  });
177
184
  }
178
185
  return result.sort((a, b) => b.createdAt - a.createdAt);
@@ -185,8 +192,15 @@ const createEmptyContext = () => ({
185
192
  plans: [],
186
193
  });
187
194
 
195
+ const SHARED_PLAN_ID_PREFIX = 'shared:';
196
+
188
197
  export const createProjectContextRuntime = (deps) => {
189
- const { fsPromises, path, projectsDirPath, createId } = deps;
198
+ const { fsPromises, path, projectsDirPath, createId, resolveSharedPlansDir } = deps;
199
+ // The team's shared plans folder for a project (absolute path) or null; the
200
+ // project config runtime owns that answer (`plansDir` in the shared file).
201
+ const sharedPlansDirFor = typeof resolveSharedPlansDir === 'function'
202
+ ? resolveSharedPlansDir
203
+ : async () => null;
190
204
 
191
205
  const idFactory = typeof createId === 'function'
192
206
  ? createId
@@ -356,7 +370,7 @@ export const createProjectContextRuntime = (deps) => {
356
370
  * of identical content, so concurrent migrations converge instead of
357
371
  * interleaving.
358
372
  */
359
- const readContext = async (projectId) => {
373
+ const readStoredContext = async (projectId) => {
360
374
  const now = Date.now();
361
375
  const stored = await readJson(contextPathFor(projectId));
362
376
 
@@ -385,6 +399,67 @@ export const createProjectContextRuntime = (deps) => {
385
399
  };
386
400
  };
387
401
 
402
+ /** The file name behind a shared plan id, or null when the id is not one. */
403
+ const sharedPlanFileOf = (planId) => {
404
+ if (typeof planId !== 'string' || !planId.startsWith(SHARED_PLAN_ID_PREFIX)) return null;
405
+ const file = planId.slice(SHARED_PLAN_ID_PREFIX.length);
406
+ return PLAN_FILE_PATTERN.test(file) ? file : null;
407
+ };
408
+
409
+ /**
410
+ * Plans in the team's shared folder: every `.md` file there, newest first,
411
+ * addressed by `shared:<file>`. Plans written by other tools have no
412
+ * manifest entry, so the title comes from the file each time.
413
+ */
414
+ const listSharedPlans = async (projectId, claimedFiles = new Set()) => {
415
+ const dir = await sharedPlansDirFor(projectId);
416
+ if (!dir) return { dir: null, plans: [] };
417
+ let entries;
418
+ try {
419
+ entries = await fsPromises.readdir(dir, { withFileTypes: true });
420
+ } catch (error) {
421
+ if (error && error.code === 'ENOENT') return { dir, plans: [] };
422
+ throw error;
423
+ }
424
+ const plans = [];
425
+ for (const entry of entries) {
426
+ if (!entry.isFile() || !PLAN_FILE_PATTERN.test(entry.name)) continue;
427
+ // Listed under its own id by the manifest entry that moved it there.
428
+ if (claimedFiles.has(entry.name)) continue;
429
+ const filePath = path.join(dir, entry.name);
430
+ const [raw, stat] = await Promise.all([fsPromises.readFile(filePath, 'utf8'), fsPromises.stat(filePath)]);
431
+ plans.push({
432
+ id: `${SHARED_PLAN_ID_PREFIX}${entry.name}`,
433
+ file: entry.name,
434
+ title: parsePlanMarkdown(raw).title,
435
+ createdAt: Math.round(stat.mtimeMs),
436
+ pinned: false,
437
+ source: 'shared',
438
+ });
439
+ }
440
+ plans.sort((left, right) => right.createdAt - left.createdAt);
441
+ return { dir, plans };
442
+ };
443
+
444
+ /** What clients see: the stored context plus the team's shared plans, each marked with its source. */
445
+ const readContext = async (projectId) => {
446
+ const stored = await readStoredContext(projectId);
447
+ const claimed = new Set(stored.plans.filter((plan) => plan.shared).map((plan) => plan.file));
448
+ const shared = await listSharedPlans(projectId, claimed);
449
+ const own = stored.plans
450
+ // A moved plan is only reachable while the project still has a shared folder.
451
+ .filter((plan) => !plan.shared || shared.dir)
452
+ .map(({ shared: isShared, ...plan }) => ({ ...plan, source: isShared ? 'shared' : 'personal' }));
453
+ return {
454
+ ...stored,
455
+ plans: [...own, ...shared.plans],
456
+ sharedPlansDir: shared.dir,
457
+ };
458
+ };
459
+
460
+ /** The folder a manifest plan's file lives in; null for a moved plan when the project lost its shared folder. */
461
+ const folderOfLink = async (projectId, link) => (link.shared ? sharedPlansDirFor(projectId) : plansDirFor(projectId));
462
+
388
463
  const writeContext = async (projectId, context) => {
389
464
  await writeJsonAtomic(contextPathFor(projectId), {
390
465
  version: PROJECT_CONTEXT_VERSION,
@@ -397,7 +472,7 @@ export const createProjectContextRuntime = (deps) => {
397
472
  const saveTodos = async (projectId, todos) => {
398
473
  return withWriteLock(projectId, async () => {
399
474
  const now = Date.now();
400
- const current = await readContext(projectId);
475
+ const current = await readStoredContext(projectId);
401
476
  const next = { ...current, todos: sanitizeTodos(todos, now) };
402
477
  await writeContext(projectId, next);
403
478
  return next;
@@ -420,7 +495,7 @@ export const createProjectContextRuntime = (deps) => {
420
495
 
421
496
  return withWriteLock(projectId, async () => {
422
497
  const now = Date.now();
423
- const current = await readContext(projectId);
498
+ const current = await readStoredContext(projectId);
424
499
  if (current.notes.length >= PROJECT_NOTE_MAX_ITEMS) {
425
500
  throw new Error(`A project can hold at most ${PROJECT_NOTE_MAX_ITEMS} notes`);
426
501
  }
@@ -462,7 +537,7 @@ export const createProjectContextRuntime = (deps) => {
462
537
 
463
538
  return withWriteLock(projectId, async () => {
464
539
  const now = Date.now();
465
- const current = await readContext(projectId);
540
+ const current = await readStoredContext(projectId);
466
541
  const existing = current.notes.find((note) => note.id === id);
467
542
  if (!existing) {
468
543
  return null;
@@ -486,7 +561,7 @@ export const createProjectContextRuntime = (deps) => {
486
561
  }
487
562
 
488
563
  return withWriteLock(projectId, async () => {
489
- const current = await readContext(projectId);
564
+ const current = await readStoredContext(projectId);
490
565
  if (!current.notes.some((note) => note.id === id)) {
491
566
  return { deleted: false, context: current };
492
567
  }
@@ -501,22 +576,38 @@ export const createProjectContextRuntime = (deps) => {
501
576
  if (!id) {
502
577
  throw new Error('planId is required');
503
578
  }
504
- const context = await readContext(projectId);
579
+ const sharedFile = sharedPlanFileOf(id);
580
+ if (sharedFile) {
581
+ const dir = await sharedPlansDirFor(projectId);
582
+ if (!dir) return null;
583
+ let raw;
584
+ try {
585
+ raw = await fsPromises.readFile(path.join(dir, sharedFile), 'utf8');
586
+ } catch (error) {
587
+ if (error && error.code === 'ENOENT') return null;
588
+ throw error;
589
+ }
590
+ const parsed = parsePlanMarkdown(raw);
591
+ return { id, file: sharedFile, createdAt: 0, title: parsed.title, body: parsed.body, raw, source: 'shared' };
592
+ }
593
+ const context = await readStoredContext(projectId);
505
594
  const link = context.plans.find((entry) => entry.id === id);
506
595
  if (!link) {
507
596
  return null;
508
597
  }
509
598
 
599
+ const folder = await folderOfLink(projectId, link);
600
+ if (!folder) return null;
510
601
  let raw;
511
602
  try {
512
- raw = await fsPromises.readFile(path.join(plansDirFor(projectId), link.file), 'utf8');
603
+ raw = await fsPromises.readFile(path.join(folder, link.file), 'utf8');
513
604
  } catch (error) {
514
605
  if (error && error.code === 'ENOENT') return null;
515
606
  throw error;
516
607
  }
517
608
 
518
609
  const parsed = parsePlanMarkdown(raw);
519
- return { id: link.id, file: link.file, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw };
610
+ return { id: link.id, file: link.file, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw, source: link.shared ? 'shared' : 'personal' };
520
611
  };
521
612
 
522
613
  /**
@@ -541,14 +632,37 @@ export const createProjectContextRuntime = (deps) => {
541
632
  }
542
633
  const raw = clampLength(value.raw, PROJECT_PLAN_BODY_MAX_LENGTH);
543
634
 
635
+ const sharedFile = sharedPlanFileOf(id);
636
+ if (sharedFile) {
637
+ // A shared plan is the file itself: written verbatim, no manifest.
638
+ return withWriteLock(projectId, async () => {
639
+ const dir = await sharedPlansDirFor(projectId);
640
+ if (!dir) return null;
641
+ const filePath = path.join(dir, sharedFile);
642
+ try {
643
+ await fsPromises.access(filePath);
644
+ } catch (error) {
645
+ if (error && error.code === 'ENOENT') return null;
646
+ throw error;
647
+ }
648
+ await fsPromises.writeFile(filePath, raw, 'utf8');
649
+ const parsed = parsePlanMarkdown(raw);
650
+ const context = await readContext(projectId);
651
+ const plan = context.plans.find((entry) => entry.id === id) ?? { id, file: sharedFile, title: parsed.title, createdAt: Date.now(), pinned: false, source: 'shared' };
652
+ return { plan, context, title: parsed.title, body: parsed.body, raw };
653
+ });
654
+ }
655
+
544
656
  return withWriteLock(projectId, async () => {
545
- const current = await readContext(projectId);
657
+ const current = await readStoredContext(projectId);
546
658
  const link = current.plans.find((entry) => entry.id === id);
547
659
  if (!link) {
548
660
  return null;
549
661
  }
550
662
 
551
- const filePath = path.join(plansDirFor(projectId), link.file);
663
+ const folder = await folderOfLink(projectId, link);
664
+ if (!folder) return null;
665
+ const filePath = path.join(folder, link.file);
552
666
  // Refuse to recreate a file that was deleted underneath us: the link is
553
667
  // already dead, and writing here would resurrect it with editor content
554
668
  // the user believed was discarded.
@@ -569,7 +683,8 @@ export const createProjectContextRuntime = (deps) => {
569
683
  };
570
684
  await writeContext(projectId, next);
571
685
 
572
- return { plan: nextLink, context: next, title: parsed.title, body: parsed.body, raw };
686
+ const { shared: isShared, ...publicLink } = nextLink;
687
+ return { plan: { ...publicLink, source: isShared ? 'shared' : 'personal' }, context: await readContext(projectId), title: parsed.title, body: parsed.body, raw };
573
688
  });
574
689
  };
575
690
 
@@ -586,7 +701,7 @@ export const createProjectContextRuntime = (deps) => {
586
701
  const body = clampLength(typeof value?.body === 'string' ? value.body : '', PROJECT_PLAN_BODY_MAX_LENGTH);
587
702
 
588
703
  return withWriteLock(projectId, async () => {
589
- const current = await readContext(projectId);
704
+ const current = await readStoredContext(projectId);
590
705
  const createdAt = Date.now();
591
706
  const plansDir = plansDirFor(projectId);
592
707
  await fsPromises.mkdir(plansDir, { recursive: true });
@@ -594,7 +709,7 @@ export const createProjectContextRuntime = (deps) => {
594
709
  const baseName = `${createdAt}-${slugifyPlanTitle(title)}`;
595
710
  let file = `${baseName}.md`;
596
711
  let attempt = 1;
597
- while (current.plans.some((entry) => entry.file === file)) {
712
+ while (current.plans.some((entry) => !entry.shared && entry.file === file)) {
598
713
  file = `${baseName}-${attempt}.md`;
599
714
  attempt += 1;
600
715
  }
@@ -604,7 +719,7 @@ export const createProjectContextRuntime = (deps) => {
604
719
  const link = { id: idFactory(), file, title, createdAt, pinned: false };
605
720
  const next = { ...current, plans: [link, ...current.plans] };
606
721
  await writeContext(projectId, next);
607
- return { plan: link, context: next };
722
+ return { plan: { ...link, source: 'personal' }, context: await readContext(projectId) };
608
723
  });
609
724
  };
610
725
 
@@ -623,7 +738,7 @@ export const createProjectContextRuntime = (deps) => {
623
738
  }
624
739
 
625
740
  return withWriteLock(projectId, async () => {
626
- const current = await readContext(projectId);
741
+ const current = await readStoredContext(projectId);
627
742
  const existing = current.plans.find((entry) => entry.id === id);
628
743
  if (!existing) {
629
744
  return null;
@@ -631,7 +746,8 @@ export const createProjectContextRuntime = (deps) => {
631
746
  const plan = { ...existing, pinned: pinned === true };
632
747
  const next = { ...current, plans: current.plans.map((entry) => (entry.id === id ? plan : entry)) };
633
748
  await writeContext(projectId, next);
634
- return { plan, context: next };
749
+ const { shared: isShared, ...publicPlan } = plan;
750
+ return { plan: { ...publicPlan, source: isShared ? 'shared' : 'personal' }, context: await readContext(projectId) };
635
751
  });
636
752
  };
637
753
 
@@ -641,17 +757,124 @@ export const createProjectContextRuntime = (deps) => {
641
757
  throw new Error('planId is required');
642
758
  }
643
759
 
760
+ const sharedFile = sharedPlanFileOf(id);
761
+ if (sharedFile) {
762
+ return withWriteLock(projectId, async () => {
763
+ const dir = await sharedPlansDirFor(projectId);
764
+ const filePath = dir ? path.join(dir, sharedFile) : null;
765
+ const exists = filePath ? await fsPromises.access(filePath).then(() => true, () => false) : false;
766
+ if (!exists) return { deleted: false, context: await readContext(projectId) };
767
+ await fsPromises.rm(filePath, { force: true });
768
+ return { deleted: true, context: await readContext(projectId) };
769
+ });
770
+ }
771
+
644
772
  return withWriteLock(projectId, async () => {
645
- const current = await readContext(projectId);
773
+ const current = await readStoredContext(projectId);
646
774
  const link = current.plans.find((entry) => entry.id === id);
647
775
  if (!link) {
648
- return { deleted: false, context: current };
776
+ return { deleted: false, context: await readContext(projectId) };
649
777
  }
650
778
 
651
779
  const next = { ...current, plans: current.plans.filter((entry) => entry.id !== id) };
652
780
  await writeContext(projectId, next);
653
- await fsPromises.rm(path.join(plansDirFor(projectId), link.file), { force: true });
654
- return { deleted: true, context: next };
781
+ const folder = await folderOfLink(projectId, link);
782
+ if (folder) await fsPromises.rm(path.join(folder, link.file), { force: true });
783
+ return { deleted: true, context: await readContext(projectId) };
784
+ });
785
+ };
786
+
787
+ /** A free file name in `dir`, keeping the wanted base name unless it is taken. */
788
+ const freeFileNameIn = async (dir, wanted, taken = new Set()) => {
789
+ const base = wanted.replace(/\.md$/, '');
790
+ let file = wanted;
791
+ let attempt = 1;
792
+ while (taken.has(file) || await fsPromises.access(path.join(dir, file)).then(() => true, () => false)) {
793
+ file = `${base}-${attempt}.md`;
794
+ attempt += 1;
795
+ }
796
+ return file;
797
+ };
798
+
799
+ const moveFile = async (from, to) => {
800
+ try {
801
+ await fsPromises.rename(from, to);
802
+ } catch (error) {
803
+ if (!error || error.code !== 'EXDEV') throw error;
804
+ await fsPromises.copyFile(from, to);
805
+ await fsPromises.rm(from, { force: true });
806
+ }
807
+ };
808
+
809
+ /**
810
+ * Move one of the user's plans into the team's shared folder. The plan
811
+ * keeps its id: the manifest entry stays and gets the `shared` flag, so a
812
+ * session that attached the plan still finds it. The markdown moves first,
813
+ * then the manifest is written; a failure in between leaves the file in the
814
+ * shared folder (listed there as `shared:<file>`) and a dead personal entry
815
+ * that `readPlan` reports as gone. Needs a shared plans folder; refused
816
+ * (validation error) when the project has none.
817
+ */
818
+ const sharePlan = async (projectId, planId) => {
819
+ const id = asNonEmptyString(planId);
820
+ if (!id) throw new Error('planId is required');
821
+ return withWriteLock(projectId, async () => {
822
+ const dir = await sharedPlansDirFor(projectId);
823
+ if (!dir) throw new Error('shared plans folder is required');
824
+ const current = await readStoredContext(projectId);
825
+ const link = current.plans.find((entry) => entry.id === id);
826
+ if (!link || link.shared) return null;
827
+ const from = path.join(plansDirFor(projectId), link.file);
828
+ const exists = await fsPromises.access(from).then(() => true, () => false);
829
+ if (!exists) return null;
830
+ await fsPromises.mkdir(dir, { recursive: true });
831
+ const file = await freeFileNameIn(dir, link.file);
832
+ await moveFile(from, path.join(dir, file));
833
+ const moved = { ...link, file, shared: true };
834
+ await writeContext(projectId, { ...current, plans: current.plans.map((entry) => (entry.id === id ? moved : entry)) });
835
+ const context = await readContext(projectId);
836
+ return { plan: context.plans.find((entry) => entry.id === id), context };
837
+ });
838
+ };
839
+
840
+ /**
841
+ * Move a shared plan back into the user's own plans; the reverse of
842
+ * `sharePlan`. A plan the user moved keeps its id; a plan that only ever
843
+ * lived in the team's folder (`shared:<file>`) gets a manifest entry now.
844
+ */
845
+ const unsharePlan = async (projectId, planId) => {
846
+ const id = asNonEmptyString(planId);
847
+ if (!id) throw new Error('planId is required');
848
+ return withWriteLock(projectId, async () => {
849
+ const dir = await sharedPlansDirFor(projectId);
850
+ if (!dir) return null;
851
+ const current = await readStoredContext(projectId);
852
+ const ownLink = current.plans.find((entry) => entry.id === id && entry.shared);
853
+ const sharedFile = ownLink ? ownLink.file : sharedPlanFileOf(id);
854
+ if (!sharedFile) return null;
855
+ const from = path.join(dir, sharedFile);
856
+ let raw;
857
+ try {
858
+ raw = await fsPromises.readFile(from, 'utf8');
859
+ } catch (error) {
860
+ if (error && error.code === 'ENOENT') return null;
861
+ throw error;
862
+ }
863
+ const plansDir = plansDirFor(projectId);
864
+ await fsPromises.mkdir(plansDir, { recursive: true });
865
+ const taken = new Set(current.plans.filter((entry) => !entry.shared).map((entry) => entry.file));
866
+ const file = await freeFileNameIn(plansDir, sharedFile, taken);
867
+ await moveFile(from, path.join(plansDir, file));
868
+ const title = parsePlanMarkdown(raw).title;
869
+ const link = ownLink
870
+ ? { ...ownLink, file, title, shared: false }
871
+ : { id: idFactory(), file, title, createdAt: Date.now(), pinned: false, shared: false };
872
+ const plans = ownLink
873
+ ? current.plans.map((entry) => (entry.id === id ? link : entry))
874
+ : [link, ...current.plans];
875
+ await writeContext(projectId, { ...current, plans });
876
+ const { shared: _movedFlag, ...publicLink } = link;
877
+ return { plan: { ...publicLink, source: 'personal' }, context: await readContext(projectId) };
655
878
  });
656
879
  };
657
880
 
@@ -666,6 +889,8 @@ export const createProjectContextRuntime = (deps) => {
666
889
  createPlan,
667
890
  setPlanPinned,
668
891
  deletePlan,
892
+ sharePlan,
893
+ unsharePlan,
669
894
  contextPathFor,
670
895
  plansDirFor,
671
896
  };
@@ -52,6 +52,7 @@ describe('readContext', () => {
52
52
  notes: [],
53
53
  todos: [],
54
54
  plans: [],
55
+ sharedPlansDir: null,
55
56
  });
56
57
  });
57
58
 
@@ -134,7 +135,7 @@ describe('legacy migration', () => {
134
135
  const context = await runtime.readContext(PROJECT_ID);
135
136
  expect(context.notes.map((note) => note.body)).toEqual(['legacy notes']);
136
137
  expect(context.todos).toEqual([{ id: 't1', text: 'legacy todo', completed: true, createdAt: 5 }]);
137
- expect(context.plans).toEqual([{ id: 'p1', file: '10-old.md', title: 'Old plan', createdAt: 10, pinned: false }]);
138
+ expect(context.plans).toEqual([{ id: 'p1', file: '10-old.md', title: 'Old plan', createdAt: 10, pinned: false, source: 'personal' }]);
138
139
 
139
140
  const remaining = await readJson(legacyConfigPath());
140
141
  expect(remaining).toEqual({
@@ -152,7 +153,7 @@ describe('legacy migration', () => {
152
153
  });
153
154
 
154
155
  const context = await runtime.readContext(PROJECT_ID);
155
- expect(context.plans).toEqual([{ id: 'p1', file: 'stray.md', title: 'Stray', createdAt: 10, pinned: false }]);
156
+ expect(context.plans).toEqual([{ id: 'p1', file: 'stray.md', title: 'Stray', createdAt: 10, pinned: false, source: 'personal' }]);
156
157
  expect(await fsPromises.readFile(path.join(plansDir(), 'stray.md'), 'utf8')).toContain('recovered');
157
158
  });
158
159
 
@@ -170,7 +171,7 @@ describe('legacy migration', () => {
170
171
  test('does not run when the legacy config holds no context keys', async () => {
171
172
  await writeJson(legacyConfigPath(), { 'setup-worktree': ['bun install'] });
172
173
 
173
- expect(await runtime.readContext(PROJECT_ID)).toEqual({ version: 2, notes: [], todos: [], plans: [] });
174
+ expect(await runtime.readContext(PROJECT_ID)).toEqual({ version: 2, notes: [], todos: [], plans: [], sharedPlansDir: null });
174
175
  await expect(fsPromises.access(contextPath())).rejects.toThrow();
175
176
  expect(await readJson(legacyConfigPath())).toEqual({ 'setup-worktree': ['bun install'] });
176
177
  });
@@ -479,6 +480,108 @@ describe('plans', () => {
479
480
  });
480
481
  });
481
482
 
483
+ describe('shared plans', () => {
484
+ let sharedDir;
485
+ let sharedRuntime;
486
+
487
+ beforeEach(async () => {
488
+ sharedDir = path.join(projectsDirPath, 'repo', 'docs', 'plans');
489
+ sharedRuntime = createProjectContextRuntime({
490
+ fsPromises,
491
+ path,
492
+ projectsDirPath,
493
+ createId: () => `plan-${++idCounter}`,
494
+ resolveSharedPlansDir: async () => sharedDir,
495
+ });
496
+ });
497
+
498
+ test('lists the shared folder\'s markdown files after the personal plans, marked shared, and reads them by file', async () => {
499
+ await fsPromises.mkdir(sharedDir, { recursive: true });
500
+ await fsPromises.writeFile(path.join(sharedDir, 'roadmap.md'), '# Roadmap\n\n- ship it\n');
501
+ await fsPromises.writeFile(path.join(sharedDir, 'notes.txt'), 'not a plan');
502
+ await fsPromises.writeFile(path.join(sharedDir, 'untitled.md'), 'first line only');
503
+ const mine = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'x' });
504
+
505
+ const context = await sharedRuntime.readContext(PROJECT_ID);
506
+ expect(context.sharedPlansDir).toBe(sharedDir);
507
+ expect(context.plans.map((plan) => `${plan.id}:${plan.source}`)).toEqual(expect.arrayContaining([
508
+ `${mine.plan.id}:personal`, 'shared:roadmap.md:shared', 'shared:untitled.md:shared',
509
+ ]));
510
+ expect(context.plans[0].id).toBe(mine.plan.id);
511
+ expect(context.plans.find((plan) => plan.id === 'shared:untitled.md').title).toBe('first line only');
512
+
513
+ const read = await sharedRuntime.readPlan(PROJECT_ID, 'shared:roadmap.md');
514
+ expect(read.title).toBe('Roadmap');
515
+ expect(read.raw).toBe('# Roadmap\n\n- ship it\n');
516
+ expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:missing.md')).toBeNull();
517
+ expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:../escape.md')).toBeNull();
518
+ });
519
+
520
+ test('a missing shared folder lists nothing; no folder configured lists nothing', async () => {
521
+ expect((await sharedRuntime.readContext(PROJECT_ID)).plans).toEqual([]);
522
+ expect((await runtime.readContext(PROJECT_ID)).sharedPlansDir).toBeNull();
523
+ });
524
+
525
+ test('updates and deletes a shared plan in place, verbatim', async () => {
526
+ await fsPromises.mkdir(sharedDir, { recursive: true });
527
+ await fsPromises.writeFile(path.join(sharedDir, 'a.md'), '# A\n');
528
+ const updated = await sharedRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: '# Renamed\n\nbody\n' });
529
+ expect(updated.plan.title).toBe('Renamed');
530
+ expect(updated.plan.source).toBe('shared');
531
+ expect(await fsPromises.readFile(path.join(sharedDir, 'a.md'), 'utf8')).toBe('# Renamed\n\nbody\n');
532
+ expect(await sharedRuntime.updatePlan(PROJECT_ID, 'shared:gone.md', { raw: 'x' })).toBeNull();
533
+ expect(await sharedRuntime.setPlanPinned(PROJECT_ID, 'shared:a.md', true)).toBeNull();
534
+
535
+ const deleted = await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
536
+ expect(deleted.deleted).toBe(true);
537
+ await expect(fsPromises.access(path.join(sharedDir, 'a.md'))).rejects.toThrow();
538
+ expect((await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md')).deleted).toBe(false);
539
+ });
540
+
541
+ test('share moves a personal plan into the shared folder and unshare brings it back, avoiding name collisions', async () => {
542
+ const { plan } = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'body' });
543
+ const shared = await sharedRuntime.sharePlan(PROJECT_ID, plan.id);
544
+ // The id survives the move: a session that attached the plan still finds it.
545
+ expect(shared.plan.id).toBe(plan.id);
546
+ expect(shared.plan.source).toBe('shared');
547
+ expect(shared.context.plans.map((entry) => `${entry.id}:${entry.source}`)).toEqual([`${plan.id}:shared`]);
548
+ await expect(fsPromises.access(path.join(plansDir(), plan.file))).rejects.toThrow();
549
+ expect(await fsPromises.readFile(path.join(sharedDir, plan.file), 'utf8')).toBe('# Mine\n\nbody');
550
+ expect((await readJson(path.join(projectsDirPath, PROJECT_ID, 'context.json'))).plans[0]).toMatchObject({ id: plan.id, shared: true });
551
+ const readMoved = await sharedRuntime.readPlan(PROJECT_ID, plan.id);
552
+ expect(readMoved.source).toBe('shared');
553
+ expect(readMoved.raw).toBe('# Mine\n\nbody');
554
+ expect((await sharedRuntime.updatePlan(PROJECT_ID, plan.id, { raw: '# Mine v2\n' })).plan).toMatchObject({ id: plan.id, title: 'Mine v2', source: 'shared' });
555
+ expect(await fsPromises.readFile(path.join(sharedDir, plan.file), 'utf8')).toBe('# Mine v2\n');
556
+
557
+ // A personal file with the same name already exists: the returning plan gets a suffix, same id.
558
+ await fsPromises.mkdir(plansDir(), { recursive: true });
559
+ await fsPromises.writeFile(path.join(plansDir(), plan.file), 'squatter');
560
+ const back = await sharedRuntime.unsharePlan(PROJECT_ID, plan.id);
561
+ expect(back.plan.id).toBe(plan.id);
562
+ expect(back.plan.source).toBe('personal');
563
+ expect(back.plan.file).toBe(plan.file.replace(/\.md$/, '-1.md'));
564
+ expect(back.plan.title).toBe('Mine v2');
565
+ expect(back.context.plans.map((entry) => `${entry.id}:${entry.source}`)).toEqual([`${plan.id}:personal`]);
566
+ await expect(fsPromises.access(path.join(sharedDir, plan.file))).rejects.toThrow();
567
+ expect(await sharedRuntime.unsharePlan(PROJECT_ID, plan.id)).toBeNull();
568
+ expect(await sharedRuntime.sharePlan(PROJECT_ID, 'nope')).toBeNull();
569
+
570
+ // A plan that only ever lived in the team's folder gets an id of its own on the way in.
571
+ await fsPromises.writeFile(path.join(sharedDir, 'foreign.md'), '# Foreign\n');
572
+ const adopted = await sharedRuntime.unsharePlan(PROJECT_ID, 'shared:foreign.md');
573
+ expect(adopted.plan.id).not.toMatch(/^shared:/);
574
+ expect(adopted.plan.title).toBe('Foreign');
575
+ expect(adopted.plan.source).toBe('personal');
576
+ });
577
+
578
+ test('share is refused without a shared plans folder', async () => {
579
+ const { plan } = await runtime.createPlan(PROJECT_ID, { title: 'Mine', body: 'x' });
580
+ await expect(runtime.sharePlan(PROJECT_ID, plan.id)).rejects.toThrow('shared plans folder is required');
581
+ expect((await runtime.readContext(PROJECT_ID)).plans.map((entry) => entry.id)).toEqual([plan.id]);
582
+ });
583
+ });
584
+
482
585
  describe('parsePlanMarkdown', () => {
483
586
  test('reads the leading heading as the title', () => {
484
587
  expect(parsePlanMarkdown('# Title\n\nbody')).toEqual({ title: 'Title', body: 'body' });
@@ -0,0 +1,76 @@
1
+ # Projects
2
+
3
+ ## Purpose
4
+
5
+ Server-owned storage for a project's per-user config file,
6
+ `~/.config/openchamber/projects/<projectId>.json`. The file holds two
7
+ families of keys with different writers, and this module is the only place
8
+ that writes it:
9
+
10
+ | Keys | Owner | Reached through |
11
+ |---|---|---|
12
+ | `version`, `scheduledTasks` | `project-config.js` (scheduled-task runtime) | `/api/projects/:projectId/scheduled-tasks/*` |
13
+ | `setup-worktree`, `setup-worktree-wait`, `projectActions`, `projectActionsPrimaryId`, `draftStarters`, `projectPath` | `project-setup.js` via `readProjectSetup` / `updateProjectSetup` on the same runtime | `GET/PUT /api/projects/:projectId/config` (`routes.js`) |
14
+
15
+ Notes, todos, and plans moved out of this file to `packages/web/server/lib/project-context`.
16
+
17
+ A second, optional source is the team's shared file, `<repo>/.aiworker/project.json`
18
+ (`version: 1`; `setupWorktree`, `setupWorktreeWait`, `projectActions`, `draftStarters`,
19
+ `plansDir`). The server reads it from the checkout the project id names
20
+ (`projectPathFromId`). `GET /api/projects/:projectId/config`
21
+ returns one merged view: what runs at the top level, plus `shared` and `personal`
22
+ blocks so a page can edit the personal file without copying a teammate's entry into it.
23
+
24
+ | Field | Merge rule |
25
+ |---|---|
26
+ | `setupWorktree` | shared first, then personal; personal `setupWorktreeMode: "replace"` uses the personal list only |
27
+ | `setupWorktreeWait` | personal when the personal file sets it, else shared, else `false` |
28
+ | `projectActions` | union by `id`; a personal action replaces the shared one with the same id; ids in personal `hiddenSharedActionIds` are dropped; every entry carries `source` |
29
+ | `projectActionsPrimaryId` | personal only |
30
+ | `draftStarters` | union by `type:name`, shared first, every entry carries `source` |
31
+ | `plansDir` | shared only |
32
+
33
+ A shared file that exists but cannot be parsed (or names a `plansDir` outside the
34
+ repo) is `shared.status: "invalid"` with a `reason`; the personal setup is still
35
+ served. It is never treated as "no shared setup".
36
+
37
+ ### Writing the shared file
38
+
39
+ `PUT /api/projects/:projectId/config/shared` (`updateSharedProjectSetup`) is
40
+ the only writer. The patch replaces the keys it names over the current file
41
+ (a broken file counts as empty, so a write repairs it); the result is written
42
+ pretty-printed with `version` first and only the keys that carry something
43
+ (`serializeSharedProjectConfig`), because the file is committed and reviewed.
44
+ A result with nothing in it removes the file and the `.aiworker` folder
45
+ when that leaves it empty, so unsharing the last item leaves no trace. The
46
+ write refuses a checkout that does not exist and a `plansDir` outside the
47
+ repo. The writer has seen what it shared, so its personal trust record is set
48
+ to the new hash; teammates still get the prompt. The shared UI composes
49
+ "share" and "make personal" as a shared write followed by a personal write.
50
+
51
+ ### Trust
52
+
53
+ Shared setup commands and shared actions run on the machine of whoever pulls
54
+ the repo, so they run only after the user has seen them. The view carries
55
+ `trust: { hash, trusted }`: `hash` is `sharedTrustHashOf(shared)`, a SHA-256
56
+ over the executable parts (`setupWorktree` and each action's `id`, `command`,
57
+ `runIn`, actions sorted by id; names and icons do not count), or `null` when
58
+ nothing executes. `trusted` is true when nothing executes or the personal
59
+ file's `sharedTrust.hash` equals the current hash, so a pull that changes a
60
+ command brings the prompt back. The client records an answer with a PUT of
61
+ `sharedTrustHash` (`null` forgets it). The prompt itself lives in the shared
62
+ UI (`packages/ui/src/lib/sharedTrustConfirmation.ts`).
63
+
64
+ ## Modules
65
+
66
+ - `project-id.js` — `createProjectIdFromPath` / `projectPathFromId`: the path-derived id (`path_<base64url>`) that names the file, and the checkout path back from it. The shared UI derives the same id (`packages/ui/src/lib/projectId.ts`); both sides must agree.
67
+ - `project-config.js` — `createProjectConfigRuntime`: raw read, atomic write, the cross-process file lock (Electron and a CLI `serve` can share one projects dir), scheduled-task normalization, and the project-setup read/update.
68
+ - `project-setup.js` — sanitizers, the shared-file parser (`parseSharedProjectConfig`, `normalizePlansDir`), the merge (`mergeProjectSetup`), and the personal view for the setup keys. Mirrored in the VS Code extension host (`packages/vscode/src/project-setup.ts`), which owns the same file when the webview has no AiWorker server; keep the two in sync.
69
+ - `routes.js` — the setup routes. `/api/projects` is on the JSON-body allowlist in `opencode/core-routes.js`.
70
+
71
+ ## Invariants
72
+
73
+ - **Every write is a locked read-modify-write of the whole document.** Keys the writer does not own, and keys from newer builds, come back out unchanged. A setup update and a scheduled-task update never clobber each other.
74
+ - **A wrongly shaped key is a 400, not a silent drop.** `projectSetupPatchToStored` throws; the file is untouched. Values inside a well-shaped key are sanitized (trimmed, capped, deduplicated) rather than rejected.
75
+ - **The client never composes the path.** `packages/ui/src/lib/openchamberConfig.ts` speaks only HTTP; the same code serves web, desktop, VS Code, and the phone, including a phone on a remote instance.
76
+ - **`AIWORKER_DATA_DIR` moves this directory too.** Every AiWorker folder hangs off the one root; a custom root gets `projects/`, `themes/`, and `speech-models/` copied in from `~/.config/openchamber` once at startup (copied, not moved: a second instance beside the default one must not strip it) (`lib/data-dir-migration.js`). A scratch server started with its own data dir therefore never touches the real project configs.