@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
@@ -1,6 +1,21 @@
1
1
  import { DateTime, IANAZone } from 'luxon';
2
2
  import parser from 'cron-parser';
3
3
 
4
+ import { projectPathFromId } from './project-id.js';
5
+ import {
6
+ DEFAULT_PLANS_DIR,
7
+ EMPTY_SHARED_PROJECT_CONFIG,
8
+ SHARED_CONFIG_RELATIVE_PATH,
9
+ applySharedProjectSetupPatch,
10
+ isSharedProjectConfigEmpty,
11
+ mergeProjectSetup,
12
+ parseSharedProjectConfig,
13
+ projectSetupPatchToStored,
14
+ projectSetupViewOf,
15
+ serializeSharedProjectConfig,
16
+ sharedTrustHashOf,
17
+ } from './project-setup.js';
18
+
4
19
  const PROJECT_CONFIG_VERSION = 1;
5
20
  export const MAX_TASK_NAME_LENGTH = 80;
6
21
  const MAX_TASK_PROMPT_LENGTH = 20_000;
@@ -255,7 +270,7 @@ const normalizeState = (value, fallback) => {
255
270
  ? Math.max(0, Math.round(source.nextRunAt))
256
271
  : undefined;
257
272
  // Absolute ms of the schedule occurrence last claimed for dispatch. Used so
258
- // two OpenChamber server instances sharing this config cannot both start a
273
+ // two AiWorker server instances sharing this config cannot both start a
259
274
  // run for the same daily/weekly/cron/once slot (see issue #2710).
260
275
  const lastScheduledFor = typeof source.lastScheduledFor === 'number' && Number.isFinite(source.lastScheduledFor)
261
276
  ? Math.max(0, Math.round(source.lastScheduledFor))
@@ -548,21 +563,16 @@ export const createProjectConfigRuntime = (deps) => {
548
563
  })
549
564
  );
550
565
 
551
- const writeProjectConfigToDisk = async (projectID, config) => {
566
+ // Atomic whole-document write; callers hand in the merged document so the
567
+ // keys they do not own survive untouched.
568
+ const writeRawProjectConfigToDisk = async (projectID, document) => {
552
569
  const filePath = resolveProjectConfigPath(projectID);
553
570
  const parentDirectory = path.dirname(filePath);
554
571
  const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
555
572
 
556
- const existing = await readRawProjectConfigFromDisk(projectID);
557
- const merged = {
558
- ...existing,
559
- version: PROJECT_CONFIG_VERSION,
560
- scheduledTasks: Array.isArray(config?.scheduledTasks) ? config.scheduledTasks : [],
561
- };
562
-
563
573
  await fsPromises.mkdir(parentDirectory, { recursive: true });
564
574
  try {
565
- await fsPromises.writeFile(temporaryPath, JSON.stringify(merged, null, 2), 'utf8');
575
+ await fsPromises.writeFile(temporaryPath, JSON.stringify(document, null, 2), 'utf8');
566
576
  await fsPromises.rename(temporaryPath, filePath);
567
577
  } catch (error) {
568
578
  await fsPromises.rm(temporaryPath, { force: true }).catch(() => {});
@@ -570,6 +580,15 @@ export const createProjectConfigRuntime = (deps) => {
570
580
  }
571
581
  };
572
582
 
583
+ const writeProjectConfigToDisk = async (projectID, config) => {
584
+ const existing = await readRawProjectConfigFromDisk(projectID);
585
+ await writeRawProjectConfigToDisk(projectID, {
586
+ ...existing,
587
+ version: PROJECT_CONFIG_VERSION,
588
+ scheduledTasks: Array.isArray(config?.scheduledTasks) ? config.scheduledTasks : [],
589
+ });
590
+ };
591
+
573
592
  const withProjectWriteLock = async (projectID, mutate) => {
574
593
  const key = sanitizeProjectID(projectID);
575
594
  const previous = writeLocks.get(key) || Promise.resolve();
@@ -717,7 +736,7 @@ export const createProjectConfigRuntime = (deps) => {
717
736
  * `predicate(currentTask)` is evaluated after the latest on-disk read; when
718
737
  * it returns false the write is skipped and `{ updated: false }` is returned.
719
738
  * Used by the scheduled-tasks runtime to claim a single schedule occurrence
720
- * across concurrent OpenChamber server instances.
739
+ * across concurrent AiWorker server instances.
721
740
  */
722
741
  const updateScheduledTaskStateIf = async (projectID, taskID, predicate, statePatch) => {
723
742
  return withProjectWriteLock(projectID, async () => {
@@ -905,7 +924,121 @@ export const createProjectConfigRuntime = (deps) => {
905
924
  });
906
925
  };
907
926
 
927
+ // The client-owned part of the file (worktree setup, project actions, draft
928
+ // starters); see `project-setup.js`. Reads are lock-free like task lists;
929
+ // an update merges the sanitized patch over the raw document under the same
930
+ // cross-process lock the task writers use, so neither side clobbers the other.
931
+ // The shared file lives in the project's checkout. The checkout path comes
932
+ // from the id itself (`path_<base64url>`), with the personal file's
933
+ // `projectPath` as the fallback for ids of another form. A missing file is
934
+ // the normal case; an unreadable or unparsable one is reported as invalid,
935
+ // never as "no shared setup".
936
+ const projectPathOf = (projectID, personalRaw) => (
937
+ projectPathFromId(projectID) || (typeof personalRaw.projectPath === 'string' ? personalRaw.projectPath.trim() : '')
938
+ );
939
+ const sharedConfigPathOf = (projectPath) => path.join(projectPath, ...SHARED_CONFIG_RELATIVE_PATH.split('/'));
940
+
941
+ const readSharedProjectConfig = async (projectID, personalRaw) => {
942
+ const projectPath = projectPathOf(projectID, personalRaw);
943
+ if (!projectPath) return { status: 'missing' };
944
+ let raw;
945
+ try {
946
+ raw = await fsPromises.readFile(sharedConfigPathOf(projectPath), 'utf8');
947
+ } catch (error) {
948
+ if (error && typeof error === 'object' && error.code === 'ENOENT') return { status: 'missing' };
949
+ return { status: 'invalid', reason: error instanceof Error ? error.message : String(error) };
950
+ }
951
+ return parseSharedProjectConfig(raw);
952
+ };
953
+
954
+ const mergedProjectSetupOf = async (projectID, personalRaw) => (
955
+ mergeProjectSetup(projectSetupViewOf(personalRaw), await readSharedProjectConfig(projectID, personalRaw))
956
+ );
957
+
958
+ const readProjectSetup = async (projectID) => mergedProjectSetupOf(projectID, await readRawProjectConfigFromDisk(projectID));
959
+
960
+ const updateProjectSetup = async (projectID, patch) => {
961
+ const stored = projectSetupPatchToStored(patch);
962
+ return withProjectWriteLock(projectID, async () => {
963
+ const existing = await readRawProjectConfigFromDisk(projectID);
964
+ const merged = { ...existing, ...stored };
965
+ for (const [key, value] of Object.entries(stored)) {
966
+ if (value === undefined) delete merged[key];
967
+ }
968
+ await writeRawProjectConfigToDisk(projectID, merged);
969
+ return mergedProjectSetupOf(projectID, merged);
970
+ });
971
+ };
972
+
973
+ /**
974
+ * Change the team's shared file in the checkout: the patch replaces the
975
+ * keys it names over the current file (a broken file counts as empty, so
976
+ * a write repairs it). A result with nothing in it removes the file (and
977
+ * the `.aiworker` folder when that leaves it empty), so unsharing the
978
+ * last item leaves no trace. The writer has seen the commands it just
979
+ * shared, so the personal trust record is set to the new hash on this
980
+ * instance; teammates still get the prompt.
981
+ */
982
+ const updateSharedProjectSetup = async (projectID, patch) => (
983
+ withProjectWriteLock(projectID, async () => {
984
+ const personalRaw = await readRawProjectConfigFromDisk(projectID);
985
+ const projectPath = projectPathOf(projectID, personalRaw);
986
+ if (!projectPath) throw new Error('project checkout not found');
987
+ try {
988
+ if (!(await fsPromises.stat(projectPath)).isDirectory()) throw new Error('project checkout not found');
989
+ } catch {
990
+ throw new Error('project checkout not found');
991
+ }
992
+ const currentRead = await readSharedProjectConfig(projectID, personalRaw);
993
+ const current = currentRead.status === 'ok' ? currentRead.config : EMPTY_SHARED_PROJECT_CONFIG;
994
+ const next = applySharedProjectSetupPatch(current, patch);
995
+
996
+ const filePath = sharedConfigPathOf(projectPath);
997
+ if (isSharedProjectConfigEmpty(next)) {
998
+ await fsPromises.rm(filePath, { force: true });
999
+ await fsPromises.rmdir(path.dirname(filePath)).catch(() => {});
1000
+ } else {
1001
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
1002
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
1003
+ try {
1004
+ await fsPromises.writeFile(temporaryPath, serializeSharedProjectConfig(next), 'utf8');
1005
+ await fsPromises.rename(temporaryPath, filePath);
1006
+ } catch (error) {
1007
+ await fsPromises.rm(temporaryPath, { force: true }).catch(() => {});
1008
+ throw error;
1009
+ }
1010
+ }
1011
+
1012
+ const hash = sharedTrustHashOf(next);
1013
+ const personalNext = { ...personalRaw };
1014
+ if (hash) personalNext.sharedTrust = { hash, trustedAt: Date.now() };
1015
+ else delete personalNext.sharedTrust;
1016
+ await writeRawProjectConfigToDisk(projectID, personalNext);
1017
+ return mergedProjectSetupOf(projectID, personalNext);
1018
+ })
1019
+ );
1020
+
1021
+ /**
1022
+ * The absolute repository plans folder of a project: `plansDir` from the
1023
+ * shared file when set, else the default `.aiworker/plans`. Setting
1024
+ * `plansDir` replaces the default outright (nothing is read from it any
1025
+ * more); moving files between the two is the user's job. Null only when the
1026
+ * checkout cannot be located.
1027
+ */
1028
+ const resolveSharedPlansDir = async (projectID) => {
1029
+ const personalRaw = await readRawProjectConfigFromDisk(projectID);
1030
+ const projectPath = projectPathOf(projectID, personalRaw);
1031
+ if (!projectPath) return null;
1032
+ const shared = await readSharedProjectConfig(projectID, personalRaw);
1033
+ const relative = shared.status === 'ok' && shared.config.plansDir ? shared.config.plansDir : DEFAULT_PLANS_DIR;
1034
+ return path.join(projectPath, ...relative.split('/'));
1035
+ };
1036
+
908
1037
  return {
1038
+ readProjectSetup,
1039
+ updateProjectSetup,
1040
+ updateSharedProjectSetup,
1041
+ resolveSharedPlansDir,
909
1042
  listScheduledTasks,
910
1043
  upsertScheduledTask,
911
1044
  deleteScheduledTask,
@@ -11,3 +11,20 @@ export const createProjectIdFromPath = (projectPath) => {
11
11
 
12
12
  return `path_${Buffer.from(normalized, 'utf8').toString('base64url')}`;
13
13
  };
14
+
15
+ /**
16
+ * The path a `path_<base64url>` id was made from, or `''` when the id is not
17
+ * of that form. The projects dir names files by this id, so the server can
18
+ * find the project's checkout (and the shared config inside it) from the id
19
+ * alone.
20
+ */
21
+ export const projectPathFromId = (projectId) => {
22
+ if (typeof projectId !== 'string' || !projectId.startsWith('path_')) return '';
23
+ const encoded = projectId.slice('path_'.length);
24
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded)) return '';
25
+ try {
26
+ return Buffer.from(encoded, 'base64url').toString('utf8');
27
+ } catch {
28
+ return '';
29
+ }
30
+ };
@@ -0,0 +1,409 @@
1
+ // A project's setup — worktree setup commands, project actions, pinned draft
2
+ // starters — comes from two files:
3
+ //
4
+ // - the personal file `~/.config/openchamber/projects/<projectId>.json`
5
+ // (client-owned keys; server-owned `version` / `scheduledTasks` live beside
6
+ // them and are never touched here), and
7
+ // - the shared file `<repo>/.aiworker/project.json`, committed by a team so
8
+ // a teammate who pulls the code gets the setup without configuring anything.
9
+ //
10
+ // This module knows both shapes and the one merge rule per field. The route
11
+ // and the VS Code bridge (`packages/vscode/src/project-setup.ts`, a mirror of
12
+ // this file) use the same code paths so a value reads back the same on every
13
+ // surface.
14
+
15
+ import crypto from 'node:crypto';
16
+
17
+ const ACTION_NAME_MAX_LENGTH = 80;
18
+ const ACTION_COMMAND_MAX_LENGTH = 4000;
19
+ const ACTION_OPEN_URL_MAX_LENGTH = 2000;
20
+ const ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
21
+ const SETUP_COMMAND_MAX_LENGTH = 4000;
22
+ const SETUP_COMMANDS_MAX = 50;
23
+
24
+ const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
25
+ const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
26
+
27
+ const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
28
+
29
+ const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
30
+
31
+ const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
32
+
33
+ /** Setup commands: non-empty trimmed strings, capped in count and length. */
34
+ export const sanitizeSetupCommands = (value) => {
35
+ if (!Array.isArray(value)) return [];
36
+ const commands = [];
37
+ for (const entry of value) {
38
+ const command = clamp(trimmedString(entry), SETUP_COMMAND_MAX_LENGTH);
39
+ if (!command) continue;
40
+ commands.push(command);
41
+ if (commands.length >= SETUP_COMMANDS_MAX) break;
42
+ }
43
+ return commands;
44
+ };
45
+
46
+ const sanitizeActionPlatforms = (value) => {
47
+ if (!Array.isArray(value)) return [];
48
+ const platforms = [];
49
+ for (const entry of value) {
50
+ const platform = trimmedString(entry).toLowerCase();
51
+ if (ACTION_PLATFORMS.has(platform) && !platforms.includes(platform)) platforms.push(platform);
52
+ }
53
+ return platforms;
54
+ };
55
+
56
+ /**
57
+ * Project actions: `id`, `name`, and `command` are required and ids are
58
+ * unique; every optional field is dropped when empty so the stored record
59
+ * carries only what the user set. `runIn` keeps only the one value the UI
60
+ * understands (`parent`); anything else means "run in the worktree".
61
+ */
62
+ export const sanitizeProjectActions = (value) => {
63
+ if (!Array.isArray(value)) return [];
64
+ const actions = [];
65
+ const seenIds = new Set();
66
+ for (const entry of value) {
67
+ if (!isObjectRecord(entry)) continue;
68
+ const id = trimmedString(entry.id);
69
+ const name = clamp(trimmedString(entry.name), ACTION_NAME_MAX_LENGTH);
70
+ const command = clamp(trimmedString(entry.command), ACTION_COMMAND_MAX_LENGTH);
71
+ if (!id || !name || !command || seenIds.has(id)) continue;
72
+ seenIds.add(id);
73
+
74
+ const icon = trimmedString(entry.icon);
75
+ const platforms = sanitizeActionPlatforms(entry.platforms);
76
+ const openUrl = clamp(trimmedString(entry.openUrl), ACTION_OPEN_URL_MAX_LENGTH);
77
+ const desktopOpenSshForward = clamp(trimmedString(entry.desktopOpenSshForward), ACTION_DESKTOP_FORWARD_MAX_LENGTH);
78
+
79
+ const action = { id, name, command, icon: icon || null };
80
+ if (entry.autoOpenUrl === true) action.autoOpenUrl = true;
81
+ if (openUrl) action.openUrl = openUrl;
82
+ if (desktopOpenSshForward) action.desktopOpenSshForward = desktopOpenSshForward;
83
+ if (platforms.length > 0) action.platforms = platforms;
84
+ if (entry.runIn === 'parent') action.runIn = 'parent';
85
+ actions.push(action);
86
+ }
87
+ return actions;
88
+ };
89
+
90
+ /** Draft starters: `{ type: 'command' | 'skill', name }`, unique by `type:name`. */
91
+ export const sanitizeDraftStarters = (value) => {
92
+ if (!Array.isArray(value)) return [];
93
+ const starters = [];
94
+ const seen = new Set();
95
+ for (const entry of value) {
96
+ if (!isObjectRecord(entry)) continue;
97
+ const type = entry.type === 'command' || entry.type === 'skill' ? entry.type : null;
98
+ const name = trimmedString(entry.name);
99
+ if (!type || !name) continue;
100
+ const key = `${type}:${name}`;
101
+ if (seen.has(key)) continue;
102
+ seen.add(key);
103
+ starters.push({ type, name });
104
+ }
105
+ return starters;
106
+ };
107
+
108
+ /**
109
+ * The client-facing view of a raw config document. A primary action id that
110
+ * names no action is reported as `null`.
111
+ */
112
+ const sanitizeIdList = (value) => {
113
+ if (!Array.isArray(value)) return [];
114
+ const ids = [];
115
+ for (const entry of value) {
116
+ const id = trimmedString(entry);
117
+ if (id && !ids.includes(id)) ids.push(id);
118
+ }
119
+ return ids;
120
+ };
121
+
122
+ /**
123
+ * The personal part of the view, straight from the personal file. The wait
124
+ * flag is `null` when the file does not set it, so the merge can let a
125
+ * shared value through; `hiddenSharedActionIds` and `setupWorktreeMode` only
126
+ * matter when a shared file exists.
127
+ */
128
+ export const projectSetupViewOf = (raw) => {
129
+ const document = isObjectRecord(raw) ? raw : {};
130
+ const projectActions = sanitizeProjectActions(document.projectActions);
131
+ const primaryRaw = trimmedString(document.projectActionsPrimaryId);
132
+ return {
133
+ setupWorktree: sanitizeSetupCommands(document['setup-worktree']),
134
+ setupWorktreeWait: typeof document['setup-worktree-wait'] === 'boolean' ? document['setup-worktree-wait'] : null,
135
+ setupWorktreeMode: SETUP_WORKTREE_MODES.has(document.setupWorktreeMode) ? document.setupWorktreeMode : 'append',
136
+ projectActions,
137
+ projectActionsPrimaryId: primaryRaw && projectActions.some((action) => action.id === primaryRaw) ? primaryRaw : null,
138
+ draftStarters: sanitizeDraftStarters(document.draftStarters),
139
+ hiddenSharedActionIds: sanitizeIdList(document.hiddenSharedActionIds),
140
+ sharedTrust: sharedTrustOf(document.sharedTrust),
141
+ };
142
+ };
143
+
144
+ /** The recorded answer to the trust prompt: which shared commands were trusted, and when. */
145
+ const sharedTrustOf = (value) => {
146
+ if (!isObjectRecord(value)) return null;
147
+ const hash = trimmedString(value.hash);
148
+ if (!hash) return null;
149
+ return { hash, trustedAt: Number.isFinite(value.trustedAt) ? value.trustedAt : 0 };
150
+ };
151
+
152
+ /**
153
+ * Turn a client patch (view keys) into the on-disk keys it changes. Only the
154
+ * keys present in the patch are returned, so a caller can merge the result
155
+ * over the raw document without clearing what the patch did not mention.
156
+ * A key with the wrong shape is a validation error, never silently dropped.
157
+ */
158
+ export const projectSetupPatchToStored = (patch) => {
159
+ if (!isObjectRecord(patch)) {
160
+ throw new Error('patch must be an object');
161
+ }
162
+ const stored = {};
163
+ if ('setupWorktree' in patch) {
164
+ if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
165
+ stored['setup-worktree'] = sanitizeSetupCommands(patch.setupWorktree);
166
+ }
167
+ if ('setupWorktreeWait' in patch) {
168
+ if (typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean');
169
+ stored['setup-worktree-wait'] = patch.setupWorktreeWait;
170
+ }
171
+ if ('projectActions' in patch) {
172
+ if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
173
+ stored.projectActions = sanitizeProjectActions(patch.projectActions);
174
+ }
175
+ if ('projectActionsPrimaryId' in patch) {
176
+ const primary = patch.projectActionsPrimaryId;
177
+ if (primary !== null && typeof primary !== 'string') throw new Error('projectActionsPrimaryId must be a string or null');
178
+ stored.projectActionsPrimaryId = trimmedString(primary) || undefined;
179
+ }
180
+ if ('draftStarters' in patch) {
181
+ if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
182
+ stored.draftStarters = sanitizeDraftStarters(patch.draftStarters);
183
+ }
184
+ if ('hiddenSharedActionIds' in patch) {
185
+ if (!Array.isArray(patch.hiddenSharedActionIds)) throw new Error('hiddenSharedActionIds must be an array');
186
+ stored.hiddenSharedActionIds = sanitizeIdList(patch.hiddenSharedActionIds);
187
+ }
188
+ if ('setupWorktreeMode' in patch) {
189
+ if (!SETUP_WORKTREE_MODES.has(patch.setupWorktreeMode)) throw new Error('setupWorktreeMode must be "append" or "replace"');
190
+ stored.setupWorktreeMode = patch.setupWorktreeMode;
191
+ }
192
+ if ('sharedTrustHash' in patch) {
193
+ const hash = patch.sharedTrustHash;
194
+ if (hash !== null && (typeof hash !== 'string' || !hash.trim())) throw new Error('sharedTrustHash must be a non-empty string or null');
195
+ stored.sharedTrust = hash === null ? undefined : { hash: hash.trim(), trustedAt: Date.now() };
196
+ }
197
+ if ('projectPath' in patch) {
198
+ if (typeof patch.projectPath !== 'string') throw new Error('projectPath must be a string');
199
+ const projectPath = patch.projectPath.trim();
200
+ if (projectPath) stored.projectPath = projectPath;
201
+ }
202
+ return stored;
203
+ };
204
+
205
+ // ── Shared file ──
206
+
207
+ export const SHARED_CONFIG_RELATIVE_PATH = '.aiworker/project.json';
208
+ /** Where repository plans live unless the shared file's `plansDir` says otherwise. */
209
+ export const DEFAULT_PLANS_DIR = '.aiworker/plans';
210
+ const SHARED_CONFIG_VERSION = 1;
211
+
212
+ /**
213
+ * A `plansDir` is a relative path inside the repo: no absolute paths, no
214
+ * drive letters, no `..` segments, forward slashes. Returns the normalized
215
+ * value or `null` when the value is not acceptable.
216
+ */
217
+ export const normalizePlansDir = (value) => {
218
+ const raw = trimmedString(value).replace(/\\/g, '/');
219
+ if (!raw) return null;
220
+ if (raw.startsWith('/') || /^[A-Za-z]:/.test(raw)) return null;
221
+ const segments = raw.split('/').filter((segment) => segment.length > 0 && segment !== '.');
222
+ if (segments.length === 0 || segments.some((segment) => segment === '..')) return null;
223
+ return segments.join('/');
224
+ };
225
+
226
+ const EMPTY_SHARED = Object.freeze({
227
+ setupWorktree: [],
228
+ setupWorktreeWait: null,
229
+ projectActions: [],
230
+ draftStarters: [],
231
+ plansDir: null,
232
+ });
233
+
234
+ /**
235
+ * Parse the text of a shared file. Anything that is not a version-1 object
236
+ * is `invalid` with a reason (never an empty config: a teammate must see that
237
+ * the file is broken, not that the project has no shared setup). A `plansDir`
238
+ * that points outside the repo is invalid for the same reason.
239
+ */
240
+ export const parseSharedProjectConfig = (raw) => {
241
+ let parsed;
242
+ try {
243
+ parsed = JSON.parse(raw);
244
+ } catch (error) {
245
+ return { status: 'invalid', reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
246
+ }
247
+ if (!isObjectRecord(parsed)) return { status: 'invalid', reason: 'not an object' };
248
+ if (parsed.version !== SHARED_CONFIG_VERSION) return { status: 'invalid', reason: `unsupported version ${JSON.stringify(parsed.version)}` };
249
+ if ('setupWorktree' in parsed && !Array.isArray(parsed.setupWorktree)) return { status: 'invalid', reason: 'setupWorktree must be an array' };
250
+ if ('setupWorktreeWait' in parsed && typeof parsed.setupWorktreeWait !== 'boolean') return { status: 'invalid', reason: 'setupWorktreeWait must be a boolean' };
251
+ if ('projectActions' in parsed && !Array.isArray(parsed.projectActions)) return { status: 'invalid', reason: 'projectActions must be an array' };
252
+ if ('draftStarters' in parsed && !Array.isArray(parsed.draftStarters)) return { status: 'invalid', reason: 'draftStarters must be an array' };
253
+ let plansDir = null;
254
+ if ('plansDir' in parsed && parsed.plansDir !== null) {
255
+ plansDir = normalizePlansDir(parsed.plansDir);
256
+ if (!plansDir) return { status: 'invalid', reason: 'plansDir must be a relative path inside the repository' };
257
+ }
258
+ return {
259
+ status: 'ok',
260
+ config: {
261
+ setupWorktree: sanitizeSetupCommands(parsed.setupWorktree),
262
+ setupWorktreeWait: typeof parsed.setupWorktreeWait === 'boolean' ? parsed.setupWorktreeWait : null,
263
+ projectActions: sanitizeProjectActions(parsed.projectActions),
264
+ draftStarters: sanitizeDraftStarters(parsed.draftStarters),
265
+ plansDir,
266
+ },
267
+ };
268
+ };
269
+
270
+ const withSource = (entries, source) => entries.map((entry) => ({ ...entry, source }));
271
+
272
+ /**
273
+ * What a trust answer covers: the shared setup commands and the shared
274
+ * actions' commands, in a canonical order, hashed. A pull that changes any
275
+ * of them changes the hash, so the prompt returns for the new commands.
276
+ * `null` when the shared config has nothing that executes.
277
+ */
278
+ export const sharedTrustHashOf = (shared) => {
279
+ const commands = shared.setupWorktree;
280
+ const actions = shared.projectActions
281
+ .map((action) => {
282
+ const executable = { id: action.id, command: action.command };
283
+ if (action.runIn) executable.runIn = action.runIn;
284
+ return executable;
285
+ })
286
+ .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
287
+ if (commands.length === 0 && actions.length === 0) return null;
288
+ const digest = crypto.createHash('sha256').update(JSON.stringify({ setupWorktree: commands, projectActions: actions })).digest('hex');
289
+ return `sha256:${digest}`;
290
+ };
291
+
292
+ /**
293
+ * One merged view from the personal view and the shared read. Rules:
294
+ * - setup commands: shared first, then personal; personal `setupWorktreeMode`
295
+ * `replace` uses only the personal list;
296
+ * - wait flag: personal when the personal file sets it, else shared, else off;
297
+ * - actions: union by id, a personal action replaces the shared one with the
298
+ * same id, hidden shared ids are dropped, primary is personal only;
299
+ * - draft starters: union by `type:name`, shared first.
300
+ * Every merged action and starter carries `source`. The `shared` and
301
+ * `personal` blocks are returned too so a page can edit one without guessing
302
+ * which entries came from where.
303
+ */
304
+ export const mergeProjectSetup = (personal, sharedRead) => {
305
+ const shared = sharedRead.status === 'ok' ? sharedRead.config : EMPTY_SHARED;
306
+ const hidden = new Set(personal.hiddenSharedActionIds);
307
+ const personalIds = new Set(personal.projectActions.map((action) => action.id));
308
+ const sharedActions = shared.projectActions.filter((action) => !hidden.has(action.id) && !personalIds.has(action.id));
309
+ const starterKeys = new Set(shared.draftStarters.map((starter) => `${starter.type}:${starter.name}`));
310
+ const personalStarters = personal.draftStarters.filter((starter) => !starterKeys.has(`${starter.type}:${starter.name}`));
311
+ const trustHash = sharedTrustHashOf(shared);
312
+ return {
313
+ // Nothing executable in the shared file means nothing to trust; otherwise
314
+ // the recorded answer must match the current commands exactly.
315
+ trust: { hash: trustHash, trusted: trustHash === null || personal.sharedTrust?.hash === trustHash },
316
+ setupWorktree: personal.setupWorktreeMode === 'replace'
317
+ ? personal.setupWorktree
318
+ : [...shared.setupWorktree, ...personal.setupWorktree],
319
+ setupWorktreeWait: personal.setupWorktreeWait !== null
320
+ ? personal.setupWorktreeWait
321
+ : shared.setupWorktreeWait === true,
322
+ projectActions: [...withSource(sharedActions, 'shared'), ...withSource(personal.projectActions, 'personal')],
323
+ projectActionsPrimaryId: personal.projectActionsPrimaryId,
324
+ draftStarters: [...withSource(shared.draftStarters, 'shared'), ...withSource(personalStarters, 'personal')],
325
+ shared: sharedBlockOf(sharedRead, shared),
326
+ personal,
327
+ };
328
+ };
329
+
330
+ const sharedBlockOf = (sharedRead, shared) => {
331
+ const block = { status: sharedRead.status, path: SHARED_CONFIG_RELATIVE_PATH, ...shared };
332
+ if (sharedRead.status === 'invalid') block.reason = sharedRead.reason;
333
+ return block;
334
+ };
335
+
336
+ /** An action without an icon is written without the key; readers fall back to the play icon. */
337
+ const withoutEmptyIcon = (action) => {
338
+ if (action.icon !== null) return action;
339
+ const { icon: _emptyIcon, ...rest } = action;
340
+ return rest;
341
+ };
342
+
343
+ /** True when the shared config carries nothing: the file should not exist. */
344
+ export const isSharedProjectConfigEmpty = (config) => (
345
+ config.setupWorktree.length === 0
346
+ && config.setupWorktreeWait === null
347
+ && config.projectActions.length === 0
348
+ && config.draftStarters.length === 0
349
+ && config.plansDir === null
350
+ );
351
+
352
+ /**
353
+ * The bytes of a shared file: version first, then only the keys that carry
354
+ * something, in a fixed order, pretty-printed — the file is committed and
355
+ * reviewed, so its diffs must stay readable. Actions lose their `source`
356
+ * mark and keep only the fields the user set.
357
+ */
358
+ export const serializeSharedProjectConfig = (config) => {
359
+ const document = { version: SHARED_CONFIG_VERSION };
360
+ if (config.setupWorktree.length > 0) document.setupWorktree = config.setupWorktree;
361
+ if (config.setupWorktreeWait !== null) document.setupWorktreeWait = config.setupWorktreeWait;
362
+ if (config.projectActions.length > 0) document.projectActions = sanitizeProjectActions(config.projectActions).map(withoutEmptyIcon);
363
+ if (config.draftStarters.length > 0) document.draftStarters = config.draftStarters;
364
+ if (config.plansDir !== null) document.plansDir = config.plansDir;
365
+ return `${JSON.stringify(document, null, 2)}\n`;
366
+ };
367
+
368
+ /**
369
+ * The next shared config after a client patch over the current one. Every
370
+ * named key replaces the current value; a wrongly shaped key is a validation
371
+ * error, and a `plansDir` outside the repo is refused rather than stored.
372
+ */
373
+ export const applySharedProjectSetupPatch = (current, patch) => {
374
+ if (!isObjectRecord(patch)) throw new Error('patch must be an object');
375
+ const next = { ...current };
376
+ if ('setupWorktree' in patch) {
377
+ if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
378
+ next.setupWorktree = sanitizeSetupCommands(patch.setupWorktree);
379
+ }
380
+ if ('setupWorktreeWait' in patch) {
381
+ if (patch.setupWorktreeWait !== null && typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean or null');
382
+ next.setupWorktreeWait = patch.setupWorktreeWait;
383
+ }
384
+ if ('projectActions' in patch) {
385
+ if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
386
+ next.projectActions = sanitizeProjectActions(patch.projectActions);
387
+ }
388
+ if ('draftStarters' in patch) {
389
+ if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
390
+ next.draftStarters = sanitizeDraftStarters(patch.draftStarters);
391
+ }
392
+ if ('plansDir' in patch) {
393
+ if (patch.plansDir === null || (typeof patch.plansDir === 'string' && !patch.plansDir.trim())) {
394
+ next.plansDir = null;
395
+ } else {
396
+ const plansDir = normalizePlansDir(patch.plansDir);
397
+ if (!plansDir) throw new Error('plansDir must be a relative path inside the repository');
398
+ next.plansDir = plansDir;
399
+ }
400
+ }
401
+ return next;
402
+ };
403
+
404
+ export const EMPTY_SHARED_PROJECT_CONFIG = EMPTY_SHARED;
405
+
406
+ export const isProjectSetupValidationError = (error) => {
407
+ const message = error instanceof Error ? error.message : '';
408
+ return message.includes('must be') || message.includes('is required') || message.includes('unsupported characters') || message.includes('not found');
409
+ };