@thevinci/web 1.0.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 (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { classifyPreviewNavigation, classifyPreviewResourceError, rewritePreviewBody } from './proxy-runtime.js';
4
+
5
+ const rewrite = (bodyText, kind) => rewritePreviewBody({
6
+ bodyText,
7
+ kind,
8
+ proxyBasePath: '/api/preview/proxy/abc123',
9
+ targetOrigin: 'http://127.0.0.1:3000',
10
+ });
11
+
12
+ describe('preview resource error classification', () => {
13
+ it('suppresses Astro/Vite stylesheet modules reported as failed scripts', () => {
14
+ expect(classifyPreviewResourceError({
15
+ tagName: 'script',
16
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/src/styles/global.css',
17
+ })).toBe('suppress');
18
+
19
+ expect(classifyPreviewResourceError({
20
+ tagName: 'script',
21
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/src/pages/support.astro?astro&type=style&index=0&lang.css',
22
+ })).toBe('suppress');
23
+ });
24
+
25
+ it('suppresses framework virtual modules reported by dev servers', () => {
26
+ expect(classifyPreviewResourceError({
27
+ tagName: 'script',
28
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/src/layouts/BaseLayout.astro?astro&type=script&index=0&lang.ts',
29
+ })).toBe('suppress');
30
+
31
+ expect(classifyPreviewResourceError({
32
+ tagName: 'script',
33
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/@vite/client',
34
+ })).toBe('suppress');
35
+
36
+ expect(classifyPreviewResourceError({
37
+ tagName: 'link',
38
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/@id/astro:scripts/page.js',
39
+ })).toBe('suppress');
40
+ });
41
+
42
+ it('suppresses conservative ecosystem dev-runtime resources', () => {
43
+ const noisyResources = [
44
+ '/_next/static/chunks/webpack.js',
45
+ '/_next/static/chunks/react-refresh.js',
46
+ '/.svelte-kit/generated/client/app.js',
47
+ '/@id/__x00__virtual:sveltekit:browser',
48
+ '/@remix-run/dev/dist/browser.js',
49
+ '/__hmr?runtime=remix',
50
+ '/_nuxt/@vite/client',
51
+ '/_nuxt/@id/virtual:nuxt:%2FUsers%2Fapp',
52
+ '/webpack-dev-server/client/index.js',
53
+ '/webpack/hot/dev-server.js',
54
+ '/__webpack_hmr',
55
+ ];
56
+
57
+ for (const resource of noisyResources) {
58
+ expect(classifyPreviewResourceError({
59
+ tagName: 'script',
60
+ url: `http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc${resource}`,
61
+ })).toBe('suppress');
62
+ }
63
+ });
64
+
65
+ it('keeps ordinary application resource failures visible', () => {
66
+ expect(classifyPreviewResourceError({
67
+ tagName: 'script',
68
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/assets/app.js',
69
+ })).toBe('report');
70
+
71
+ expect(classifyPreviewResourceError({
72
+ tagName: 'img',
73
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/missing.png',
74
+ })).toBe('report');
75
+
76
+ expect(classifyPreviewResourceError({
77
+ tagName: 'link',
78
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/styles/missing.css',
79
+ })).toBe('report');
80
+ });
81
+ });
82
+
83
+ describe('preview body URL rewriting', () => {
84
+ it('rewrites only HTML resource attributes in HTML responses', () => {
85
+ const input = '<img src="/logo.png"><a href="/docs">Docs</a><script>const url = "/api/data";</script>';
86
+ const output = rewrite(input, 'html');
87
+
88
+ expect(output).toContain('src="/api/preview/proxy/abc123/logo.png"');
89
+ expect(output).toContain('href="/api/preview/proxy/abc123/docs"');
90
+ expect(output).toContain('const url = "/api/data";');
91
+ });
92
+
93
+ it('rewrites only CSS imports and url references in CSS responses', () => {
94
+ const input = '@import "/theme.css"; .hero { background: url(/hero.png); } .copy::after { content: "/not-a-url"; }';
95
+ const output = rewrite(input, 'css');
96
+
97
+ expect(output).toContain('@import "/api/preview/proxy/abc123/theme.css"');
98
+ expect(output).toContain('url(/api/preview/proxy/abc123/hero.png)');
99
+ expect(output).toContain('content: "/not-a-url"');
100
+ });
101
+
102
+ it('rewrites only JavaScript static import specifiers in JavaScript responses', () => {
103
+ const input = 'import "/entry.js"; import value from "/module.js"; const url = "/api/data"; fetch("/api/data");';
104
+ const output = rewrite(input, 'javascript');
105
+
106
+ expect(output).toContain('import "/api/preview/proxy/abc123/entry.js"');
107
+ expect(output).toContain('from "/api/preview/proxy/abc123/module.js"');
108
+ expect(output).toContain('const url = "/api/data"');
109
+ expect(output).toContain('fetch("/api/data")');
110
+ });
111
+ });
112
+
113
+ describe('preview navigation policy', () => {
114
+ const currentUrl = 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/docs';
115
+
116
+ it('keeps same-page hash and already-proxied links in the iframe', () => {
117
+ expect(classifyPreviewNavigation({ url: '#section', currentUrl }).action).toBe('allow');
118
+ expect(classifyPreviewNavigation({
119
+ url: 'http://127.0.0.1:57123/api/preview/proxy/f4af70b4261d77706743959516f9cecc/roadmap',
120
+ currentUrl,
121
+ }).action).toBe('allow');
122
+ });
123
+
124
+ it('routes loopback absolute links through the preview proxy', () => {
125
+ expect(classifyPreviewNavigation({ url: 'http://localhost:3000/roadmap', currentUrl })).toEqual({
126
+ action: 'proxy',
127
+ url: 'http://localhost:3000/roadmap',
128
+ });
129
+ });
130
+
131
+ it('sends non-loopback http links outside the preview iframe', () => {
132
+ expect(classifyPreviewNavigation({ url: 'https://example.com/docs', currentUrl })).toEqual({
133
+ action: 'external',
134
+ url: 'https://example.com/docs',
135
+ });
136
+ });
137
+
138
+ it('leaves non-http links to browser defaults', () => {
139
+ expect(classifyPreviewNavigation({ url: 'mailto:test@example.com', currentUrl })).toEqual({
140
+ action: 'allow',
141
+ url: 'mailto:test@example.com',
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,567 @@
1
+ import { DateTime, IANAZone } from 'luxon';
2
+ import parser from 'cron-parser';
3
+
4
+ const PROJECT_CONFIG_VERSION = 1;
5
+ const MAX_TASK_NAME_LENGTH = 80;
6
+ const MAX_TASK_PROMPT_LENGTH = 20_000;
7
+ const MAX_CRON_LENGTH = 200;
8
+ const MAX_LAST_ERROR_LENGTH = 2_000;
9
+
10
+ const asNonEmptyString = (value) => {
11
+ if (typeof value !== 'string') {
12
+ return null;
13
+ }
14
+ const trimmed = value.trim();
15
+ return trimmed.length > 0 ? trimmed : null;
16
+ };
17
+
18
+ const clampLength = (value, maxLength) => {
19
+ if (typeof value !== 'string') {
20
+ return '';
21
+ }
22
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
23
+ };
24
+
25
+ const normalizeStatus = (value) => {
26
+ if (value === 'running' || value === 'success' || value === 'error' || value === 'idle') {
27
+ return value;
28
+ }
29
+ return 'idle';
30
+ };
31
+
32
+ const normalizeTimeValue = (value) => {
33
+ const time = asNonEmptyString(value);
34
+ if (!time) {
35
+ return null;
36
+ }
37
+ if (!/^([01]\d|2[0-3]):([0-5]\d)$/.test(time)) {
38
+ return null;
39
+ }
40
+ return time;
41
+ };
42
+
43
+ const normalizeDateValue = (value) => {
44
+ const date = asNonEmptyString(value);
45
+ if (!date) {
46
+ return null;
47
+ }
48
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
49
+ return null;
50
+ }
51
+ const parsed = DateTime.fromISO(date, { zone: 'UTC' });
52
+ if (!parsed.isValid || parsed.toFormat('yyyy-LL-dd') !== date) {
53
+ return null;
54
+ }
55
+ return date;
56
+ };
57
+
58
+ const normalizeWeekdays = (value) => {
59
+ if (!Array.isArray(value)) {
60
+ return null;
61
+ }
62
+
63
+ const unique = new Set();
64
+ for (const entry of value) {
65
+ if (!Number.isInteger(entry)) {
66
+ return null;
67
+ }
68
+ if (entry < 0 || entry > 6) {
69
+ return null;
70
+ }
71
+ unique.add(entry);
72
+ }
73
+
74
+ if (unique.size === 0) {
75
+ return null;
76
+ }
77
+
78
+ return Array.from(unique).sort((a, b) => a - b);
79
+ };
80
+
81
+ const resolveScheduleTimes = (value, existingSchedule) => {
82
+ const times = [];
83
+
84
+ if (Array.isArray(value?.times)) {
85
+ for (const item of value.times) {
86
+ const normalized = normalizeTimeValue(item);
87
+ if (!normalized) {
88
+ throw new Error('schedule.times must contain HH:mm values');
89
+ }
90
+ times.push(normalized);
91
+ }
92
+ }
93
+
94
+ const legacySingleTime = normalizeTimeValue(value?.time);
95
+ if (legacySingleTime) {
96
+ times.push(legacySingleTime);
97
+ }
98
+
99
+ if (times.length === 0 && Array.isArray(existingSchedule?.times)) {
100
+ for (const item of existingSchedule.times) {
101
+ const normalized = normalizeTimeValue(item);
102
+ if (normalized) {
103
+ times.push(normalized);
104
+ }
105
+ }
106
+ }
107
+
108
+ const uniqueSorted = Array.from(new Set(times)).sort((a, b) => a.localeCompare(b));
109
+ if (uniqueSorted.length === 0) {
110
+ return null;
111
+ }
112
+ return uniqueSorted;
113
+ };
114
+
115
+ const resolveDefaultTimezone = () => {
116
+ const resolved = DateTime.local().zoneName;
117
+ if (resolved && IANAZone.isValidZone(resolved)) {
118
+ return resolved;
119
+ }
120
+ return 'UTC';
121
+ };
122
+
123
+ const normalizeTimezone = (value, fallback = resolveDefaultTimezone()) => {
124
+ const timezone = asNonEmptyString(value);
125
+ if (!timezone) {
126
+ return fallback;
127
+ }
128
+ return IANAZone.isValidZone(timezone) ? timezone : null;
129
+ };
130
+
131
+ const validateCronExpression = (expression, timezone) => {
132
+ try {
133
+ const iterator = parser.parseExpression(expression, {
134
+ tz: timezone,
135
+ currentDate: new Date(),
136
+ });
137
+ iterator.next();
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ };
143
+
144
+ const normalizeSchedule = (value, existingSchedule) => {
145
+ if (!value || typeof value !== 'object') {
146
+ throw new Error('schedule is required');
147
+ }
148
+
149
+ const kind = asNonEmptyString(value.kind);
150
+ if (kind !== 'daily' && kind !== 'weekly' && kind !== 'once' && kind !== 'cron') {
151
+ throw new Error('schedule.kind must be daily, weekly, once, or cron');
152
+ }
153
+
154
+ const fallbackTimezone = existingSchedule?.timezone || resolveDefaultTimezone();
155
+ const timezone = normalizeTimezone(value.timezone, fallbackTimezone);
156
+ if (!timezone) {
157
+ throw new Error('schedule.timezone must be a valid IANA timezone');
158
+ }
159
+
160
+ if (kind === 'daily') {
161
+ const times = resolveScheduleTimes(value, existingSchedule);
162
+ if (!times) {
163
+ throw new Error('schedule.times must include at least one HH:mm value for daily schedule');
164
+ }
165
+ return { kind, times, timezone };
166
+ }
167
+
168
+ if (kind === 'weekly') {
169
+ const times = resolveScheduleTimes(value, existingSchedule);
170
+ if (!times) {
171
+ throw new Error('schedule.times must include at least one HH:mm value for weekly schedule');
172
+ }
173
+ const weekdays = normalizeWeekdays(value.weekdays);
174
+ if (!weekdays) {
175
+ throw new Error('schedule.weekdays must include values from 0 to 6 for weekly schedule');
176
+ }
177
+ return { kind, times, weekdays, timezone };
178
+ }
179
+
180
+ if (kind === 'once') {
181
+ const date = normalizeDateValue(value.date);
182
+ if (!date) {
183
+ throw new Error('schedule.date must be YYYY-MM-DD for once schedule');
184
+ }
185
+
186
+ const time = normalizeTimeValue(value.time);
187
+ if (!time) {
188
+ throw new Error('schedule.time must be HH:mm for once schedule');
189
+ }
190
+
191
+ return { kind, date, time, timezone };
192
+ }
193
+
194
+ const cron = clampLength(asNonEmptyString(value.cron) || '', MAX_CRON_LENGTH);
195
+ if (!cron) {
196
+ throw new Error('schedule.cron is required for cron schedule');
197
+ }
198
+
199
+ if (!validateCronExpression(cron, timezone)) {
200
+ throw new Error('schedule.cron is invalid');
201
+ }
202
+
203
+ return { kind, cron, timezone };
204
+ };
205
+
206
+ const normalizeExecution = (value) => {
207
+ if (!value || typeof value !== 'object') {
208
+ throw new Error('execution is required');
209
+ }
210
+
211
+ const prompt = clampLength(asNonEmptyString(value.prompt) || '', MAX_TASK_PROMPT_LENGTH);
212
+ const providerID = asNonEmptyString(value.providerID);
213
+ const modelID = asNonEmptyString(value.modelID);
214
+ const variant = asNonEmptyString(value.variant);
215
+ const agent = asNonEmptyString(value.agent);
216
+
217
+ if (!prompt) {
218
+ throw new Error('execution.prompt is required');
219
+ }
220
+ if (!providerID) {
221
+ throw new Error('execution.providerID is required');
222
+ }
223
+ if (!modelID) {
224
+ throw new Error('execution.modelID is required');
225
+ }
226
+
227
+ return {
228
+ prompt,
229
+ providerID,
230
+ modelID,
231
+ ...(variant ? { variant } : {}),
232
+ ...(agent ? { agent } : {}),
233
+ };
234
+ };
235
+
236
+ const normalizeState = (value, fallback) => {
237
+ const source = value && typeof value === 'object' ? value : fallback || {};
238
+ const lastRunAt = typeof source.lastRunAt === 'number' && Number.isFinite(source.lastRunAt)
239
+ ? Math.max(0, Math.round(source.lastRunAt))
240
+ : undefined;
241
+ const lastDurationMs = typeof source.lastDurationMs === 'number' && Number.isFinite(source.lastDurationMs)
242
+ ? Math.max(0, Math.round(source.lastDurationMs))
243
+ : undefined;
244
+ const nextRunAt = typeof source.nextRunAt === 'number' && Number.isFinite(source.nextRunAt)
245
+ ? Math.max(0, Math.round(source.nextRunAt))
246
+ : undefined;
247
+ const lastSessionId = asNonEmptyString(source.lastSessionId);
248
+ const lastErrorRaw = asNonEmptyString(source.lastError);
249
+ const lastError = lastErrorRaw ? clampLength(lastErrorRaw, MAX_LAST_ERROR_LENGTH) : undefined;
250
+
251
+ return {
252
+ createdAt: typeof source.createdAt === 'number' && Number.isFinite(source.createdAt)
253
+ ? Math.max(0, Math.round(source.createdAt))
254
+ : Date.now(),
255
+ updatedAt: typeof source.updatedAt === 'number' && Number.isFinite(source.updatedAt)
256
+ ? Math.max(0, Math.round(source.updatedAt))
257
+ : Date.now(),
258
+ lastStatus: normalizeStatus(source.lastStatus),
259
+ ...(typeof lastRunAt === 'number' ? { lastRunAt } : {}),
260
+ ...(typeof lastDurationMs === 'number' ? { lastDurationMs } : {}),
261
+ ...(typeof nextRunAt === 'number' ? { nextRunAt } : {}),
262
+ ...(lastSessionId ? { lastSessionId } : {}),
263
+ ...(lastError ? { lastError } : {}),
264
+ };
265
+ };
266
+
267
+ const normalizeTaskForStorage = (value, options) => {
268
+ const {
269
+ now,
270
+ createId,
271
+ existingTask,
272
+ allowCreate,
273
+ refreshUpdatedAt = true,
274
+ } = options;
275
+
276
+ if (!value || typeof value !== 'object') {
277
+ throw new Error('task is required');
278
+ }
279
+
280
+ const incomingId = asNonEmptyString(value.id);
281
+ const existingId = asNonEmptyString(existingTask?.id);
282
+
283
+ if (existingTask) {
284
+ if (incomingId && incomingId !== existingId) {
285
+ throw new Error('task.id is immutable');
286
+ }
287
+ }
288
+
289
+ if (!existingTask && incomingId && !allowCreate) {
290
+ throw new Error('task.id does not exist');
291
+ }
292
+
293
+ const id = existingId || incomingId || createId();
294
+ const name = clampLength(asNonEmptyString(value.name) || '', MAX_TASK_NAME_LENGTH);
295
+ if (!name) {
296
+ throw new Error('task.name is required');
297
+ }
298
+
299
+ const enabled = typeof value.enabled === 'boolean'
300
+ ? value.enabled
301
+ : (existingTask?.enabled ?? true);
302
+
303
+ const schedule = normalizeSchedule(value.schedule, existingTask?.schedule);
304
+ const execution = normalizeExecution(value.execution);
305
+
306
+ const nowMs = Math.max(0, Math.round(now));
307
+ const baseState = normalizeState(value.state, existingTask?.state);
308
+ const state = {
309
+ ...baseState,
310
+ createdAt: existingTask?.state?.createdAt ?? baseState.createdAt ?? nowMs,
311
+ updatedAt: refreshUpdatedAt ? nowMs : baseState.updatedAt ?? nowMs,
312
+ };
313
+
314
+ return {
315
+ id,
316
+ name,
317
+ enabled,
318
+ schedule,
319
+ execution,
320
+ state,
321
+ };
322
+ };
323
+
324
+ const createEmptyProjectConfig = () => ({
325
+ version: PROJECT_CONFIG_VERSION,
326
+ scheduledTasks: [],
327
+ });
328
+
329
+ export const createProjectConfigRuntime = (deps) => {
330
+ const {
331
+ fsPromises,
332
+ path,
333
+ projectsDirPath,
334
+ createTaskID,
335
+ } = deps;
336
+
337
+ const taskIDFactory = typeof createTaskID === 'function'
338
+ ? createTaskID
339
+ : (() => {
340
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
341
+ return crypto.randomUUID();
342
+ }
343
+ return `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
344
+ });
345
+
346
+ const writeLocks = new Map();
347
+
348
+ const sanitizeProjectID = (projectID) => {
349
+ const value = asNonEmptyString(projectID);
350
+ if (!value) {
351
+ throw new Error('projectId is required');
352
+ }
353
+ if (!/^[a-zA-Z0-9._:-]+$/.test(value)) {
354
+ throw new Error('projectId contains unsupported characters');
355
+ }
356
+ return value;
357
+ };
358
+
359
+ const resolveProjectConfigPath = (projectID) => {
360
+ const safeProjectID = sanitizeProjectID(projectID);
361
+ return path.join(projectsDirPath, `${safeProjectID}.json`);
362
+ };
363
+
364
+ const readRawProjectConfigFromDisk = async (projectID) => {
365
+ const filePath = resolveProjectConfigPath(projectID);
366
+ try {
367
+ const raw = await fsPromises.readFile(filePath, 'utf8');
368
+ const parsed = JSON.parse(raw);
369
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
370
+ } catch (error) {
371
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
372
+ return {};
373
+ }
374
+ throw error;
375
+ }
376
+ };
377
+
378
+ const readProjectConfigFromDisk = async (projectID) => {
379
+ const parsed = await readRawProjectConfigFromDisk(projectID);
380
+ const tasksRaw = Array.isArray(parsed.scheduledTasks) ? parsed.scheduledTasks : [];
381
+ const now = Date.now();
382
+ const scheduledTasks = [];
383
+ for (const task of tasksRaw) {
384
+ try {
385
+ const normalized = normalizeTaskForStorage(task, {
386
+ now,
387
+ createId: taskIDFactory,
388
+ existingTask: null,
389
+ allowCreate: true,
390
+ refreshUpdatedAt: false,
391
+ });
392
+ scheduledTasks.push(normalized);
393
+ } catch {
394
+ }
395
+ }
396
+ return {
397
+ version: PROJECT_CONFIG_VERSION,
398
+ scheduledTasks,
399
+ };
400
+ };
401
+
402
+ const writeProjectConfigToDisk = async (projectID, config) => {
403
+ const filePath = resolveProjectConfigPath(projectID);
404
+ const parentDirectory = path.dirname(filePath);
405
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
406
+
407
+ const existing = await readRawProjectConfigFromDisk(projectID);
408
+ const merged = {
409
+ ...existing,
410
+ version: PROJECT_CONFIG_VERSION,
411
+ scheduledTasks: Array.isArray(config?.scheduledTasks) ? config.scheduledTasks : [],
412
+ };
413
+
414
+ await fsPromises.mkdir(parentDirectory, { recursive: true });
415
+ await fsPromises.writeFile(temporaryPath, JSON.stringify(merged, null, 2), 'utf8');
416
+ await fsPromises.rename(temporaryPath, filePath);
417
+ };
418
+
419
+ const withProjectWriteLock = async (projectID, mutate) => {
420
+ const key = sanitizeProjectID(projectID);
421
+ const previous = writeLocks.get(key) || Promise.resolve();
422
+ let release;
423
+ const next = new Promise((resolve) => {
424
+ release = resolve;
425
+ });
426
+ const chained = previous.finally(() => next);
427
+ writeLocks.set(key, chained);
428
+
429
+ await previous;
430
+ try {
431
+ return await mutate();
432
+ } finally {
433
+ release();
434
+ const current = writeLocks.get(key);
435
+ if (current === chained) {
436
+ writeLocks.delete(key);
437
+ }
438
+ }
439
+ };
440
+
441
+ const listScheduledTasks = async (projectID) => {
442
+ const config = await readProjectConfigFromDisk(projectID);
443
+ return config.scheduledTasks;
444
+ };
445
+
446
+ const upsertScheduledTask = async (projectID, taskInput) => {
447
+ return withProjectWriteLock(projectID, async () => {
448
+ const now = Date.now();
449
+ const current = await readProjectConfigFromDisk(projectID);
450
+ const incomingID = asNonEmptyString(taskInput?.id);
451
+ const existingIndex = incomingID
452
+ ? current.scheduledTasks.findIndex((task) => task.id === incomingID)
453
+ : -1;
454
+ const existingTask = existingIndex >= 0 ? current.scheduledTasks[existingIndex] : null;
455
+
456
+ const normalizedTask = normalizeTaskForStorage(taskInput, {
457
+ now,
458
+ createId: taskIDFactory,
459
+ existingTask,
460
+ allowCreate: true,
461
+ });
462
+
463
+ const nextTasks = current.scheduledTasks.slice();
464
+ const created = !existingTask;
465
+ if (existingIndex >= 0) {
466
+ nextTasks[existingIndex] = normalizedTask;
467
+ } else {
468
+ nextTasks.push(normalizedTask);
469
+ }
470
+
471
+ const nextConfig = {
472
+ version: PROJECT_CONFIG_VERSION,
473
+ scheduledTasks: nextTasks,
474
+ };
475
+ await writeProjectConfigToDisk(projectID, nextConfig);
476
+
477
+ return {
478
+ task: normalizedTask,
479
+ tasks: nextTasks,
480
+ created,
481
+ };
482
+ });
483
+ };
484
+
485
+ const deleteScheduledTask = async (projectID, taskID) => {
486
+ return withProjectWriteLock(projectID, async () => {
487
+ const normalizedTaskID = asNonEmptyString(taskID);
488
+ if (!normalizedTaskID) {
489
+ throw new Error('taskId is required');
490
+ }
491
+
492
+ const current = await readProjectConfigFromDisk(projectID);
493
+ const nextTasks = current.scheduledTasks.filter((task) => task.id !== normalizedTaskID);
494
+ const deleted = nextTasks.length !== current.scheduledTasks.length;
495
+
496
+ if (deleted) {
497
+ await writeProjectConfigToDisk(projectID, {
498
+ version: PROJECT_CONFIG_VERSION,
499
+ scheduledTasks: nextTasks,
500
+ });
501
+ }
502
+
503
+ return {
504
+ deleted,
505
+ tasks: nextTasks,
506
+ };
507
+ });
508
+ };
509
+
510
+ const updateScheduledTaskState = async (projectID, taskID, statePatch) => {
511
+ return withProjectWriteLock(projectID, async () => {
512
+ const normalizedTaskID = asNonEmptyString(taskID);
513
+ if (!normalizedTaskID) {
514
+ throw new Error('taskId is required');
515
+ }
516
+
517
+ const current = await readProjectConfigFromDisk(projectID);
518
+ const taskIndex = current.scheduledTasks.findIndex((task) => task.id === normalizedTaskID);
519
+ if (taskIndex === -1) {
520
+ return { task: null, tasks: current.scheduledTasks };
521
+ }
522
+
523
+ const currentTask = current.scheduledTasks[taskIndex];
524
+ const patchObject = statePatch && typeof statePatch === 'object' ? statePatch : {};
525
+ const nextTask = {
526
+ ...currentTask,
527
+ state: normalizeState(
528
+ {
529
+ ...currentTask.state,
530
+ ...patchObject,
531
+ updatedAt: Date.now(),
532
+ },
533
+ currentTask.state,
534
+ ),
535
+ };
536
+
537
+ const nextTasks = current.scheduledTasks.slice();
538
+ nextTasks[taskIndex] = nextTask;
539
+
540
+ await writeProjectConfigToDisk(projectID, {
541
+ version: PROJECT_CONFIG_VERSION,
542
+ scheduledTasks: nextTasks,
543
+ });
544
+
545
+ return {
546
+ task: nextTask,
547
+ tasks: nextTasks,
548
+ };
549
+ });
550
+ };
551
+
552
+ return {
553
+ listScheduledTasks,
554
+ upsertScheduledTask,
555
+ deleteScheduledTask,
556
+ updateScheduledTaskState,
557
+ resolveProjectConfigPath,
558
+ };
559
+ };
560
+
561
+ export {
562
+ MAX_TASK_NAME_LENGTH,
563
+ MAX_TASK_PROMPT_LENGTH,
564
+ MAX_CRON_LENGTH,
565
+ MAX_LAST_ERROR_LENGTH,
566
+ normalizeTaskForStorage,
567
+ };