@desplega.ai/agent-swarm 2.7.5 → 2.7.7

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 (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -25,15 +25,26 @@ import type { IncomingMessage, ServerResponse } from "node:http";
25
25
  import { z } from "zod";
26
26
  import { getPage, incrementPageViewCount } from "../be/db";
27
27
  import { BROWSER_SDK_JS, SWARM_UI_JS } from "../pages/browser-sdk";
28
+ import { COLOR_LITERAL_THRESHOLD, countColorLiterals } from "../pages/color-literals";
28
29
  import {
29
30
  feedbackKeyGuardScript,
30
31
  feedbackOverlayScripts,
31
32
  isPageFeedbackRequested,
32
33
  } from "../pages/feedback-overlay";
34
+ import {
35
+ buildPageHeadV2,
36
+ PAGE_HEAD_V1,
37
+ PAGE_MODE_PARAM,
38
+ PAGE_THEME_PARAM,
39
+ } from "../pages/page-head";
40
+ import { resolvePageTheme } from "../themes/page-theme";
33
41
  import { type Page, PageSchema } from "../types";
34
- import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
35
- import { getNodeEnv } from "../utils/node-env";
36
- import { extractAndVerifyCookie, pageSessionSetCookies } from "../utils/page-session";
42
+ import { getAppUrl, getConfiguredAppUrls, getLocalDevAppOrigins } from "../utils/constants";
43
+ import {
44
+ extractAndVerifyCookie,
45
+ isLocalDevPageRequest,
46
+ pageSessionSetCookies,
47
+ } from "../utils/page-session";
37
48
  import { scrubSecrets } from "../utils/secret-scrubber";
38
49
  import { route } from "./route-def";
39
50
 
@@ -95,96 +106,76 @@ const publicPageJsonRoute = route({
95
106
  // ─── Helpers ────────────────────────────────────────────────────────────────
96
107
 
97
108
  /**
98
- * Inject the BROWSER_SDK script tag into an HTML body. Insert immediately
99
- * after `<head>` if present; otherwise prepend so partial fragments still get
100
- * the SDK. The script is wrapped in `<script>...</script>` with no token
101
- * injection (the SDK relies on server-side header injection at the
102
- * `/@swarm/api/*` proxy boundary).
103
- *
104
- * Also injects `<base target="_blank">` so links inside the iframed page
105
- * open in the parent window — avoids the user being trapped inside an
106
- * iframe by a misbehaving page.
109
+ * The `<head>` defaults for a page's design version (`src/pages/page-head.ts`).
110
+ * Version 1 is the frozen original head and ignores the viewer params. Version
111
+ * 2 resolves the theme: the page `theme`, else the viewer's `__swarm-theme`
112
+ * preset, else Hive, plus the page `themeTokens` on top. It is dark only for
113
+ * `__swarm-mode=dark`, and a `?print=1` export is always light. A body with
114
+ * `COLOR_LITERAL_THRESHOLD` or more hardcoded colors also stays light: its
115
+ * hardcoded light surfaces would get the dark-mode text color.
107
116
  */
117
+ function pageHead(page: Page, queryParams: URLSearchParams, wantsPrint: boolean): string {
118
+ if (page.designVersion === 1) return PAGE_HEAD_V1;
119
+ const theme = resolvePageTheme({
120
+ pageTheme: page.theme,
121
+ viewerTheme: queryParams.get(PAGE_THEME_PARAM),
122
+ overrides: page.themeTokens,
123
+ });
124
+ const dark =
125
+ !wantsPrint &&
126
+ queryParams.get(PAGE_MODE_PARAM) === "dark" &&
127
+ countColorLiterals(page.body) < COLOR_LITERAL_THRESHOLD;
128
+ return buildPageHeadV2({ theme, mode: dark ? "dark" : "light" });
129
+ }
130
+
131
+ /** A doctype at the start of a body (`\s` also matches a BOM). */
132
+ const LEADING_DOCTYPE_RE = /^\s*<!doctype\b[^>]*>/i;
133
+ /** The `<html ...>` start tag right after a leading doctype. */
134
+ const LEADING_HTML_TAG_RE = /^\s*<!doctype\b[^>]*>\s*<html\b[^>]*>/i;
135
+
108
136
  /**
109
- * Default `<head>` injection: `<base>` so links escape the iframe, Tailwind
110
- * Play CDN so agent pages can use utility classes out of the box, Space
111
- * Grotesk / Space Mono fonts to match the swarm SPA, a small reset that
112
- * makes pages theme-aware (light by default) so an agent who writes zero CSS
113
- * still gets a presentable page, and finally the Browser SDK so
114
- * `window.swarmSdk` works.
137
+ * Inject the head defaults and the BROWSER_SDK script tag into an HTML body,
138
+ * right after the first `<head ...>` tag. The script is wrapped in
139
+ * `<script>...</script>` with no token injection (the SDK relies on
140
+ * server-side header injection at the `/@swarm/api/*` proxy boundary).
141
+ *
142
+ * A body without `<head>` (a raw fragment) still gets the SDK:
143
+ * - Design version 1 prepends it, as it always did.
144
+ * - Design version 2 (`standardsMode`) keeps the browser in standards mode,
145
+ * because its defaults are written for it. It prepends `<!doctype html>`
146
+ * when the body has no doctype, then injects after `<html ...>`, else after
147
+ * the doctype. Content before the doctype puts the page in quirks mode.
115
148
  *
116
- * Agent-provided styles can override these defaults. Tailwind utilities stay
117
- * available, but its Preflight reset is disabled to preserve browser heading,
118
- * margin, and list styles.
149
+ * Both head versions start with `<base target="_blank">`, so links inside the
150
+ * iframed page open in a new tab: a misbehaving page cannot trap the user
151
+ * inside the iframe.
119
152
  */
120
- const PAGE_HEAD_DEFAULTS = `<base target="_blank">
121
- <link rel="preconnect" href="https://fonts.googleapis.com">
122
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
123
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
124
- <script src="https://cdn.tailwindcss.com"></script>
125
- <script>
126
- if (window.tailwind) {
127
- window.tailwind.config = { corePlugins: { preflight: false } };
128
- }
129
- </script>
130
- <style>
131
- *, ::before, ::after { box-sizing: border-box; border: 0 solid #e5e7eb; }
132
- hr { border-top-width: 1px; }
133
- img, video { display: block; max-width: 100%; height: auto; }
134
- button, input, optgroup, select, textarea, ::file-selector-button { font: inherit; }
135
- :root {
136
- color-scheme: light;
137
- --swarm-bg: #fbfbfa;
138
- --swarm-card: #ffffff;
139
- --swarm-border: #e5e7eb;
140
- --swarm-text: #111827;
141
- --swarm-muted: #6b7280;
142
- --swarm-primary: #3b82f6;
143
- }
144
- html, body { background: var(--swarm-bg); color: var(--swarm-text); }
145
- body {
146
- font-family: "Space Grotesk", system-ui, sans-serif;
147
- margin: 0;
148
- padding: 24px;
149
- line-height: 1.5;
150
- }
151
- code, pre, kbd, samp { font-family: "Space Mono", ui-monospace, monospace; }
152
- a { color: var(--swarm-primary); }
153
- ::selection { background: var(--swarm-primary); color: #fff; }
154
- @media print {
155
- /* Override theme variables so built-in primitives (swarm-diff, swarm-card)
156
- that read var(--swarm-card) / var(--swarm-border) etc. inline-styled
157
- backgrounds also flip to light. Without this, diff cards stay dark navy
158
- on print. */
159
- :root {
160
- --swarm-bg: #ffffff;
161
- --swarm-card: #ffffff;
162
- --swarm-border: #cccccc;
163
- --swarm-text: #000000;
164
- --swarm-muted: #555555;
165
- }
166
- html, body { background: white !important; color: black !important; }
167
- a { color: black !important; text-decoration: underline; }
168
- /* Hide any swarm chrome the agent (or built-in primitives) tagged with
169
- .no-print. Use this class on annotation badges, jump-list nav, anything
170
- that shouldn't appear in the PDF export. */
171
- .no-print { display: none !important; }
172
- /* Avoid page-break inside cards / diff blocks. */
173
- .swarm-card, swarm-diff { break-inside: avoid; }
174
- }
175
- </style>`;
176
-
177
- function injectBrowserSdk(html: string, headFirst = ""): string {
178
- const injection = `${headFirst}${PAGE_HEAD_DEFAULTS}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
179
- // Use the first occurrence of `<head>` (case-insensitive). A page that
180
- // doesn't have a `<head>` element (raw fragment) still gets the SDK at the
181
- // front of the document.
182
- const headOpenMatch = html.match(/<head\b[^>]*>/i);
183
- if (headOpenMatch) {
184
- const idx = headOpenMatch.index! + headOpenMatch[0].length;
185
- return html.slice(0, idx) + injection + html.slice(idx);
186
- }
187
- return injection + html;
153
+ function injectBrowserSdk(
154
+ html: string,
155
+ { head, headFirst, standardsMode }: { head: string; headFirst: string; standardsMode: boolean },
156
+ ): string {
157
+ const injection = `${headFirst}${head}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
158
+ const doc = standardsMode && !LEADING_DOCTYPE_RE.test(html) ? `<!doctype html>${html}` : html;
159
+ // The first `<head>` (case-insensitive) wins.
160
+ const leading = standardsMode
161
+ ? (LEADING_HTML_TAG_RE.exec(doc) ?? LEADING_DOCTYPE_RE.exec(doc))
162
+ : null;
163
+ const idx = headTagEnd(doc) ?? (leading ? leading.index + leading[0].length : null);
164
+ if (idx === null) return injection + doc;
165
+ return doc.slice(0, idx) + injection + doc.slice(idx);
166
+ }
167
+
168
+ /**
169
+ * End offset of the first `<head ...>` start tag, or null. It finds the same
170
+ * tag as `/<head\b[^>]*>/i` in linear time. That regex rescans the rest of the
171
+ * body for every `<head` that has no `>` after it, so a planted body could
172
+ * block the API on each view.
173
+ */
174
+ function headTagEnd(doc: string): number | null {
175
+ const start = doc.search(/<head\b/i);
176
+ if (start === -1) return null;
177
+ const close = doc.indexOf(">", start);
178
+ return close === -1 ? null : close + 1;
188
179
  }
189
180
 
190
181
  /**
@@ -350,24 +341,16 @@ function buildCsp(page: Page): string {
350
341
  // `frame-ancestors` lists every origin allowed to iframe `/p/:id`. We must
351
342
  // include the SPA origin(s). Configured app URLs may be comma-separated so
352
343
  // portless dev (`https://ui.swarm.localhost`), a Vite port (`http://localhost:5274`),
353
- // and a tunnel/staging origin can all coexist. Additionally, in non-production
354
- // we always allow `http://localhost:*` and `https://*.localhost` so swapping
355
- // between Vite ports / portless dev doesn't require restarting the API.
344
+ // and a tunnel/staging origin can all coexist. Under an explicit
345
+ // development/test NODE_ENV we also allow the local Vite ports and
346
+ // `*.localhost` so swapping between them doesn't require restarting the API.
347
+ // Unset NODE_ENV (every deployed image) gets no localhost ancestors.
356
348
  const configured = getConfiguredAppUrls();
357
- const devFallbacks =
358
- getNodeEnv() === "production"
359
- ? []
360
- : [
361
- "http://localhost:5274",
362
- "http://localhost:5175",
363
- "http://127.0.0.1:5274",
364
- "http://127.0.0.1:5175",
365
- "https://*.localhost",
366
- "http://*.localhost",
367
- ];
349
+ const devFallbacks = getLocalDevAppOrigins();
368
350
  const embedders = page.authMode === "public" ? getPageFrameAncestors() : [];
369
351
  const ancestors = Array.from(new Set([...configured, ...devFallbacks, ...embedders]));
370
- // Allow Tailwind Play CDN (`cdn.tailwindcss.com`) for scripts, Google
352
+ // Allow Tailwind Play CDN (`cdn.tailwindcss.com`, design version 1; the
353
+ // pinned v4 build of design version 2 comes from jsdelivr) for scripts, Google
371
354
  // Fonts (`fonts.googleapis.com` stylesheets + `fonts.gstatic.com` font
372
355
  // files) for the swarm default typography, and same-origin /@swarm/api/*
373
356
  // for the Browser SDK. Inline scripts/styles remain allowed so
@@ -471,29 +454,6 @@ function extractPasswordCandidate(
471
454
  return decoded.slice(colonIdx + 1);
472
455
  }
473
456
 
474
- /**
475
- * Detect whether the originating request is from a "dev" / localhost context,
476
- * mirroring the same logic used by the bearer-launch endpoint in
477
- * `src/http/pages.ts`. Used to decide whether to issue cookies with
478
- * `SameSite=Lax` (dev) vs `SameSite=None; Secure` (prod).
479
- */
480
- function isLocalhostRequest(req: IncomingMessage): boolean {
481
- if (getNodeEnv() === "production") return false;
482
- // Only emit `SameSite=Lax` (no Secure) when the request comes from the
483
- // SAME http://localhost origin as the API — Lax cookies don't travel on
484
- // cross-site fetches, so portless `*.localhost` setups (SPA on https
485
- // talking to the API on http) must use `SameSite=None; Secure`. Chrome
486
- // treats localhost as a secure origin so Secure is honored on HTTP.
487
- const origin = (req.headers.origin as string | undefined) ?? "";
488
- if (origin === "") {
489
- const rawHost = req.headers.host;
490
- const host = Array.isArray(rawHost) ? rawHost[0] : rawHost;
491
- if (!host) return true; // best-effort dev default
492
- return host.startsWith("localhost") || host.startsWith("127.0.0.1");
493
- }
494
- return origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1");
495
- }
496
-
497
457
  // ─── Handler ────────────────────────────────────────────────────────────────
498
458
 
499
459
  export async function handlePagesPublic(
@@ -562,7 +522,7 @@ export async function handlePagesPublic(
562
522
  }
563
523
  if (matched) {
564
524
  inlineSetCookie = await pageSessionSetCookies(req, page.id, {
565
- dev: isLocalhostRequest(req),
525
+ dev: isLocalDevPageRequest(req),
566
526
  });
567
527
  // fall through to the regular 200 path below.
568
528
  } else {
@@ -669,7 +629,11 @@ export async function handlePagesPublic(
669
629
  // The feedback key guard goes first in <head> so it runs before page scripts.
670
630
  const feedback = isPageFeedbackRequested(queryParams);
671
631
  const html =
672
- injectBrowserSdk(page.body, feedback ? feedbackKeyGuardScript() : "") +
632
+ injectBrowserSdk(page.body, {
633
+ head: pageHead(page, queryParams, wantsPrint),
634
+ headFirst: feedback ? feedbackKeyGuardScript() : "",
635
+ standardsMode: page.designVersion !== 1,
636
+ }) +
673
637
  (wantsPrint ? PRINT_AUTOTRIGGER_SCRIPT : "") +
674
638
  (feedback ? feedbackOverlayScripts(page.id) : "");
675
639
  const headers: Record<string, string | string[]> = {
package/src/http/pages.ts CHANGED
@@ -25,6 +25,8 @@ import { resolveTemplate } from "../prompts/resolver";
25
25
  // Side-effect import: registers `task.page.feedback`.
26
26
  import "../prompts/session-templates";
27
27
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
28
+ import { readThemeFields } from "../themes/page-fields";
29
+ import { THEME_PRESET_IDS } from "../themes/presets";
28
30
  import {
29
31
  AssetKeySchema,
30
32
  type Page,
@@ -32,11 +34,11 @@ import {
32
34
  PageContentTypeSchema,
33
35
  PageSchema,
34
36
  type PageSummary,
37
+ PageThemeTokensInputSchema,
35
38
  PageVersionSchema,
36
39
  } from "../types";
37
40
  import { getAppUrl, getPublicMcpBaseUrl } from "../utils/constants";
38
- import { getNodeEnv } from "../utils/node-env";
39
- import { pageSessionSetCookies } from "../utils/page-session";
41
+ import { isLocalDevPageRequest, pageSessionSetCookies } from "../utils/page-session";
40
42
  import { authUser, getRequestAuth, isOperatorAuth } from "../utils/request-auth-context";
41
43
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
42
44
  import { route } from "./route-def";
@@ -94,12 +96,43 @@ const PageWithShareUrlsSchema = PageSchema.extend({
94
96
  * used, which omits them entirely) — modeled as optional rather than a union
95
97
  * since both shapes share every other field verbatim.
96
98
  */
97
- const PageListItemSchema = PageSchema.partial({ body: true, passwordHash: true }).extend({
99
+ const PageListItemSchema = PageSchema.partial({
100
+ body: true,
101
+ passwordHash: true,
102
+ theme: true,
103
+ themeTokens: true,
104
+ designVersion: true,
105
+ }).extend({
98
106
  favorite: z.boolean(),
99
107
  api_url: z.string(),
100
108
  app_url: z.string(),
101
109
  });
102
110
 
111
+ /** Design fields shared by the create and update bodies. */
112
+ const pageDesignBodyFields = {
113
+ theme: z
114
+ .string()
115
+ .nullable()
116
+ .optional()
117
+ .describe(
118
+ `Theme preset id that restyles the whole page. One of: ${THEME_PRESET_IDS.join(", ")}. Omit to follow the viewer's theme. Pass null on update to clear it.`,
119
+ ),
120
+ themeTokens: PageThemeTokensInputSchema.nullable()
121
+ .optional()
122
+ .describe(
123
+ 'Per-page token overrides, applied on top of the preset. Send { light, dark } maps of token name to CSS value, or { format: "css", css } with a CSS variable block (:root, .dark, @theme). Stored normalized. Names outside the token contract, and values that reference a variable outside it, are ignored and listed in ignoredTokens. Replaces the stored overrides. Pass null on update to clear them.',
124
+ ),
125
+ designVersion: z
126
+ .number()
127
+ .int()
128
+ .min(1)
129
+ .max(2)
130
+ .optional()
131
+ .describe(
132
+ "Design system version. 2 is the swarm design system and the default for new pages. 1 is the original look that older pages keep. Omit on update to keep the stored value.",
133
+ ),
134
+ };
135
+
103
136
  // ─── Route Definitions ──────────────────────────────────────────────────────
104
137
 
105
138
  const createPageRoute = route({
@@ -118,6 +151,7 @@ const createPageRoute = route({
118
151
  password: z.string().min(1).optional(),
119
152
  body: z.string(),
120
153
  needsCredentials: z.array(z.string()).optional(),
154
+ ...pageDesignBodyFields,
121
155
  }),
122
156
  responses: {
123
157
  201: {
@@ -128,9 +162,20 @@ const createPageRoute = route({
128
162
  version: z.literal(1),
129
163
  api_url: z.string(),
130
164
  app_url: z.string(),
165
+ ignoredTokens: z
166
+ .array(z.string())
167
+ .optional()
168
+ .describe(
169
+ "Theme tokens the importer ignored. A bare name is outside the token contract. Other entries give the reason, for example --font-sans (references --font-geist-sans) or --ring (in .card). Absent when none were ignored.",
170
+ ),
171
+ designVersion: z
172
+ .number()
173
+ .int()
174
+ .min(1)
175
+ .describe("The design version of the new page: 2 unless the body set designVersion: 1."),
131
176
  }),
132
177
  },
133
- 400: { description: "Invalid body" },
178
+ 400: { description: "Invalid body, unknown theme, or invalid themeTokens" },
134
179
  409: { description: "Slug already exists for this agent" },
135
180
  },
136
181
  });
@@ -214,12 +259,31 @@ const updatePageRoute = route({
214
259
  password: z.string().min(1).nullable().optional(),
215
260
  body: z.string().optional(),
216
261
  needsCredentials: z.array(z.string()).nullable().optional(),
262
+ ...pageDesignBodyFields,
217
263
  }),
218
264
  responses: {
219
265
  200: {
220
266
  description: "Page updated",
221
- schema: z.object({ id: z.string(), key: AssetKeySchema, version: z.number().int().min(1) }),
267
+ schema: z.object({
268
+ id: z.string(),
269
+ key: AssetKeySchema,
270
+ version: z.number().int().min(1),
271
+ ignoredTokens: z
272
+ .array(z.string())
273
+ .optional()
274
+ .describe(
275
+ "Theme tokens the importer ignored. A bare name is outside the token contract. Other entries give the reason, for example --font-sans (references --font-geist-sans) or --ring (in .card). Absent when none were ignored.",
276
+ ),
277
+ designVersion: z
278
+ .number()
279
+ .int()
280
+ .min(1)
281
+ .describe(
282
+ "The design version the page has after the update. An omitted designVersion keeps the stored one, so an older page stays on 1.",
283
+ ),
284
+ }),
222
285
  },
286
+ 400: { description: "Unknown theme or invalid themeTokens" },
223
287
  404: { description: "Page not found" },
224
288
  413: { description: "Payload too large" },
225
289
  },
@@ -429,7 +493,7 @@ const getPageVersionRoute = route({
429
493
  * Cookie issuance moved to `src/utils/page-session.ts::issuePageSessionCookie`
430
494
  * so the password-flow on `/p/:id` (step-5) can mint cookies via the same
431
495
  * helper. `dev=true` softens the cookie for `http://localhost` (no Secure
432
- * required; SameSite=Lax). Detected by `isDevRequest()` below.
496
+ * required; SameSite=Lax). Detected by `isLocalDevPageRequest()` in `src/utils/page-session.ts`.
433
497
  */
434
498
 
435
499
  /**
@@ -528,20 +592,6 @@ function formatFeedbackComments(comments: z.infer<typeof PageFeedbackCommentSche
528
592
  .join("\n\n");
529
593
  }
530
594
 
531
- function isDevRequest(req: IncomingMessage): boolean {
532
- if (getNodeEnv() === "production") return false;
533
- // We want `SameSite=Lax` only when the request itself comes from the same
534
- // local-`http://localhost` origin as the API — same-site loads tolerate
535
- // Lax without Secure. Anything else (including portless `*.localhost`
536
- // setups talking from HTTPS to the HTTP API) must use `SameSite=None;
537
- // Secure` so the cookie travels on cross-site fetches; Chrome treats
538
- // localhost as a secure origin so the Secure flag is fine on HTTP.
539
- const origin = (req.headers.origin as string | undefined) ?? "";
540
- return (
541
- origin === "" || origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1")
542
- );
543
- }
544
-
545
595
  // ─── Handler ────────────────────────────────────────────────────────────────
546
596
 
547
597
  export async function handlePages(
@@ -562,6 +612,12 @@ export async function handlePages(
562
612
  return true;
563
613
  }
564
614
 
615
+ const themeFields = readThemeFields(parsed.body);
616
+ if ("error" in themeFields) {
617
+ jsonError(res, themeFields.error, 400);
618
+ return true;
619
+ }
620
+
565
621
  const slug = parsed.body.slug ?? slugify(parsed.body.title);
566
622
 
567
623
  // Hash password if provided. Bun.password.hash is async (Argon2 by default;
@@ -589,6 +645,9 @@ export async function handlePages(
589
645
  passwordHash,
590
646
  body: parsed.body.body,
591
647
  needsCredentials: parsed.body.needsCredentials,
648
+ theme: themeFields.theme,
649
+ themeTokens: themeFields.themeTokens,
650
+ designVersion: parsed.body.designVersion,
592
651
  });
593
652
  // First write has no prior snapshot — version 1 is implicit (the parent
594
653
  // IS v1). Subsequent edits land via PUT and bump the counter.
@@ -598,6 +657,8 @@ export async function handlePages(
598
657
  version: 1,
599
658
  api_url: `${getApiBaseUrl()}/p/${page.id}`,
600
659
  app_url: `${getAppBaseUrl()}/pages/${page.slug}`,
660
+ ...(themeFields.ignored.length > 0 ? { ignoredTokens: themeFields.ignored } : {}),
661
+ designVersion: page.designVersion,
601
662
  });
602
663
  } catch (err) {
603
664
  if (err instanceof AssetKeyAuthorizationError) {
@@ -790,6 +851,13 @@ export async function handlePages(
790
851
  return true;
791
852
  }
792
853
 
854
+ // Before the snapshot: a rejected theme must not leave a version row behind.
855
+ const themeFields = readThemeFields(parsed.body);
856
+ if ("error" in themeFields) {
857
+ jsonError(res, themeFields.error, 400);
858
+ return true;
859
+ }
860
+
793
861
  // Hash password if a new one was provided. `null` → clear the hash.
794
862
  let passwordHashUpdate: string | null | undefined;
795
863
  if (parsed.body.password === null) {
@@ -830,6 +898,9 @@ export async function handlePages(
830
898
  passwordHash: passwordHashUpdate,
831
899
  body: parsed.body.body,
832
900
  needsCredentials: parsed.body.needsCredentials ?? undefined,
901
+ theme: themeFields.theme,
902
+ themeTokens: themeFields.themeTokens,
903
+ designVersion: parsed.body.designVersion,
833
904
  });
834
905
  if (!updated) {
835
906
  res.writeHead(404);
@@ -840,6 +911,8 @@ export async function handlePages(
840
911
  id: updated.id,
841
912
  key: updated.key,
842
913
  version: await getPageEditCounter(updated.id),
914
+ ...(themeFields.ignored.length > 0 ? { ignoredTokens: themeFields.ignored } : {}),
915
+ designVersion: updated.designVersion,
843
916
  });
844
917
  return true;
845
918
  }
@@ -988,7 +1061,7 @@ export async function handlePages(
988
1061
  ? { name: `guest-${randomUUID().slice(0, 8)}`, op: true as const }
989
1062
  : undefined;
990
1063
  const cookies = await pageSessionSetCookies(req, page.id, {
991
- dev: isDevRequest(req),
1064
+ dev: isLocalDevPageRequest(req),
992
1065
  ...identity,
993
1066
  });
994
1067