@desplega.ai/agent-swarm 1.173.0 → 2.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 (432) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
  7. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
  8. package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
  15. package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
  16. package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
  17. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  18. package/dist/cli-2m1ktfzz.js +17795 -0
  19. package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
  20. package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
  21. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  22. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  23. package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
  24. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  25. package/dist/cli-6cvfxs8r.js +47 -0
  26. package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
  27. package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
  28. package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
  29. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  30. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  31. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  32. package/dist/cli-9traj3dn.js +638 -0
  33. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  34. package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
  35. package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
  36. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  37. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  38. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  39. package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
  40. package/dist/cli-e3g5mbkz.js +776 -0
  41. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  42. package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
  43. package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
  44. package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
  45. package/dist/cli-f527ce74.js +781 -0
  46. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  47. package/dist/cli-hjwmhcg1.js +10622 -0
  48. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  49. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  50. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  51. package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
  52. package/dist/cli-jta2eyzc.js +619 -0
  53. package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
  54. package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
  55. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  56. package/dist/cli-kceah1eh.js +92 -0
  57. package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
  58. package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
  59. package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
  60. package/dist/cli-n2wq4j7m.js +6985 -0
  61. package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
  62. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  63. package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
  64. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  65. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  66. package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
  67. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  68. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  69. package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
  70. package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
  71. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  72. package/dist/cli-rhs9pz33.js +294 -0
  73. package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
  74. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  75. package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
  76. package/dist/cli-skxvvpap.js +10140 -0
  77. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  78. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  79. package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
  80. package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
  81. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  82. package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
  83. package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
  84. package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
  85. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  86. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  87. package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
  88. package/dist/cli-wmgrkscj.js +23254 -0
  89. package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
  90. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  91. package/dist/cli.js +29 -45
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  99. package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
  100. package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.js} +11 -10
  109. package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
  110. package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
  111. package/dist/{grok-adapter-e7mr287e.js → grok-adapter-0d4x7gkx.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
  117. package/dist/index-52j1aja4.js +3465 -0
  118. package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
  119. package/dist/index-9gw58n4k.js +193 -0
  120. package/dist/index-9j87mcm8.js +56305 -0
  121. package/dist/index-bsmcresk.js +79424 -0
  122. package/dist/index-mh85qhec.js +475 -0
  123. package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
  124. package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
  125. package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
  126. package/dist/index-we27kd31.js +12810 -0
  127. package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-as9ga0d6.js} +2 -2
  144. package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
  145. package/dist/{work-objects-wpj8ck95.js → work-objects-y5g1y7ev.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  148. package/openapi.json +523 -344
  149. package/package.json +10 -11
  150. package/src/be/config-refs.ts +60 -0
  151. package/src/be/db/page-versions.ts +15 -0
  152. package/src/be/db/runtime.ts +35 -8
  153. package/src/be/db/skill-name-resolution.ts +36 -0
  154. package/src/be/db/tasks/read.ts +230 -0
  155. package/src/be/db/tasks/write.ts +7 -0
  156. package/src/be/db.ts +52 -7
  157. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  158. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  159. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  160. package/src/be/model-catalog-store.ts +5 -1
  161. package/src/be/model-tier-resolution.ts +12 -1
  162. package/src/be/model-validation.ts +5 -2
  163. package/src/be/models-catalog.ts +26 -1
  164. package/src/be/modelsdev-cache.json +198 -63
  165. package/src/be/modelsdev-cache.ts +9 -1
  166. package/src/be/script-credential-broker.ts +39 -2
  167. package/src/be/scripts/run-saved.ts +1 -0
  168. package/src/be/scripts/typecheck.ts +10 -8
  169. package/src/be/seed/runner.ts +13 -1
  170. package/src/be/seed/state-db.ts +5 -0
  171. package/src/be/seed/types.ts +6 -0
  172. package/src/be/seed-pricing.ts +19 -2
  173. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  174. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  175. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  176. package/src/be/seed-skills/index.ts +104 -3
  177. package/src/be/swarm-config-guard.ts +52 -0
  178. package/src/cli.tsx +0 -16
  179. package/src/commands/onboard/non-interactive.ts +4 -2
  180. package/src/commands/provider-credentials.ts +52 -6
  181. package/src/commands/runner.ts +190 -54
  182. package/src/e2b/env.ts +5 -0
  183. package/src/extensions/contract-types.generated.ts +1 -1
  184. package/src/extensions/contract.ts +45 -0
  185. package/src/extensions/dispatcher.ts +7 -0
  186. package/src/extensions/post-bridge.ts +40 -0
  187. package/src/github/handlers.ts +11 -4
  188. package/src/heartbeat/templates.ts +8 -5
  189. package/src/hooks/hook.ts +0 -26
  190. package/src/http/all-routes.ts +0 -1
  191. package/src/http/config.ts +36 -17
  192. package/src/http/core.ts +4 -0
  193. package/src/http/index.ts +14 -2
  194. package/src/http/integrations.ts +88 -1
  195. package/src/http/mcp.ts +15 -5
  196. package/src/http/page-proxy.ts +2 -4
  197. package/src/http/pages-public.ts +3 -3
  198. package/src/http/pages.ts +3 -13
  199. package/src/http/poll.ts +28 -68
  200. package/src/http/requested-by.ts +54 -0
  201. package/src/http/scripts.ts +1 -0
  202. package/src/http/stats.ts +1 -30
  203. package/src/http/tasks.ts +61 -5
  204. package/src/http/utils.ts +14 -0
  205. package/src/http/webhooks.ts +159 -1
  206. package/src/http/x.ts +1 -0
  207. package/src/integrations/chat-sdk/adapters.ts +129 -0
  208. package/src/integrations/chat-sdk/attachments.ts +232 -0
  209. package/src/integrations/chat-sdk/background.ts +36 -0
  210. package/src/integrations/chat-sdk/commands.ts +194 -0
  211. package/src/integrations/chat-sdk/config.ts +198 -0
  212. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  213. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  214. package/src/integrations/chat-sdk/handlers.ts +28 -0
  215. package/src/integrations/chat-sdk/inbound.ts +426 -0
  216. package/src/integrations/chat-sdk/limits.ts +48 -0
  217. package/src/integrations/chat-sdk/logger.ts +81 -0
  218. package/src/integrations/chat-sdk/outbound.ts +123 -0
  219. package/src/integrations/chat-sdk/relay.ts +172 -0
  220. package/src/integrations/chat-sdk/router.ts +169 -0
  221. package/src/integrations/chat-sdk/runtime.ts +351 -0
  222. package/src/integrations/chat-sdk/schema.ts +245 -0
  223. package/src/integrations/chat-sdk/state.ts +243 -0
  224. package/src/integrations/chat-sdk/status.ts +244 -0
  225. package/src/integrations/chat-sdk/steering.ts +58 -0
  226. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  227. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  228. package/src/integrations/chat-sdk/templates.ts +16 -0
  229. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  230. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  231. package/src/integrations/chat-sdk/typing.ts +65 -0
  232. package/src/pages/version.ts +4 -5
  233. package/src/prompts/base-prompt.ts +111 -45
  234. package/src/providers/acp-target-catalog.ts +6 -0
  235. package/src/providers/acp-targets.ts +55 -2
  236. package/src/providers/amp-adapter.ts +1 -1
  237. package/src/providers/claude-adapter.ts +1 -1
  238. package/src/providers/claude-flag-settings.ts +17 -5
  239. package/src/providers/claude-managed-adapter.ts +1 -1
  240. package/src/providers/claude-session-events.ts +6 -1
  241. package/src/providers/dsh-adapter.ts +1 -1
  242. package/src/providers/opencode-adapter.ts +243 -26
  243. package/src/providers/opencode-swarm-plugin.ts +31 -0
  244. package/src/providers/pi-mono-adapter.ts +398 -153
  245. package/src/providers/pricing-sources.md +33 -2
  246. package/src/providers/reasoning-effort.ts +27 -0
  247. package/src/providers/types.ts +6 -2
  248. package/src/rbac/legacy-policy.ts +8 -2
  249. package/src/rbac/permissions.ts +21 -0
  250. package/src/scheduler/scheduler.ts +4 -2
  251. package/src/scripts-runtime/loader.ts +19 -7
  252. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  253. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  254. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  255. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  256. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  257. package/src/server.ts +52 -18
  258. package/src/slack/event-dedup.ts +1 -1
  259. package/src/tasks/additive-buffer.ts +4 -2
  260. package/src/tasks/context-key.ts +60 -1
  261. package/src/tasks/model-pin.ts +67 -0
  262. package/src/tasks/worker-follow-up.ts +4 -31
  263. package/src/tests/acp-adapter.test.ts +42 -0
  264. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  265. package/src/tests/base-prompt.test.ts +58 -54
  266. package/src/tests/chat-sdk-commands.test.ts +191 -0
  267. package/src/tests/chat-sdk-config.test.ts +334 -0
  268. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  269. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  270. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  271. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  272. package/src/tests/chat-sdk-harness.ts +146 -0
  273. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  274. package/src/tests/chat-sdk-relay.test.ts +470 -0
  275. package/src/tests/chat-sdk-reload.test.ts +128 -0
  276. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  277. package/src/tests/chat-sdk-slack.test.ts +268 -0
  278. package/src/tests/chat-sdk-state.test.ts +163 -0
  279. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  280. package/src/tests/chat-sdk-teams.test.ts +277 -0
  281. package/src/tests/chat-sdk-tools.test.ts +520 -0
  282. package/src/tests/chat-sdk-typing.test.ts +120 -0
  283. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  284. package/src/tests/claude-adapter-binary.test.ts +9 -4
  285. package/src/tests/claude-flag-settings.test.ts +11 -3
  286. package/src/tests/claude-managed-adapter.test.ts +1 -1
  287. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  288. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  289. package/src/tests/claude-worker-parity.test.ts +2 -7
  290. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  291. package/src/tests/config-refs.test.ts +66 -0
  292. package/src/tests/context-key.test.ts +19 -0
  293. package/src/tests/cors-allowlist.test.ts +25 -0
  294. package/src/tests/create-page-tool.test.ts +18 -0
  295. package/src/tests/credential-check.test.ts +139 -0
  296. package/src/tests/credentials.test.ts +21 -0
  297. package/src/tests/db-runtime.test.ts +61 -0
  298. package/src/tests/db-task-read-contract.test.ts +20 -0
  299. package/src/tests/db-task-write-contract.test.ts +10 -0
  300. package/src/tests/docs-index-footprint.test.js +3 -3
  301. package/src/tests/dsh-adapter.test.ts +1 -1
  302. package/src/tests/enabled-capabilities.test.ts +20 -0
  303. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  304. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  305. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  306. package/src/tests/explicit-model-validation.test.ts +105 -1
  307. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  308. package/src/tests/extensions-post-bridge.test.ts +51 -0
  309. package/src/tests/fetch-resolved-env.test.ts +128 -0
  310. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  311. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  312. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  313. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  314. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  315. package/src/tests/github-handlers.test.ts +35 -0
  316. package/src/tests/harness-provider-resolution.test.ts +12 -0
  317. package/src/tests/heartbeat-checklist.test.ts +3 -2
  318. package/src/tests/hook-runtime-close.test.ts +0 -2
  319. package/src/tests/http-api-integration.test.ts +0 -25
  320. package/src/tests/migration-046-budgets.test.ts +17 -16
  321. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  322. package/src/tests/model-tier-resolution.test.ts +93 -0
  323. package/src/tests/models-catalog.test.ts +42 -1
  324. package/src/tests/openai-compatible-providers.test.ts +134 -0
  325. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  326. package/src/tests/opencode-adapter.test.ts +216 -2
  327. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  328. package/src/tests/opencode-shell-env.test.ts +145 -0
  329. package/src/tests/openrouter-base-url.test.ts +101 -151
  330. package/src/tests/page-proxy.test.ts +1 -1
  331. package/src/tests/pages-browser-sdk.test.ts +76 -0
  332. package/src/tests/pages-feedback.test.ts +1 -1
  333. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  334. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  335. package/src/tests/pi-mono-adapter.test.ts +385 -5
  336. package/src/tests/pi-shell-env.test.ts +170 -0
  337. package/src/tests/prompt-template-remaining.test.ts +35 -0
  338. package/src/tests/rbac-engine.test.ts +11 -3
  339. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  340. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  341. package/src/tests/script-connections.test.ts +2 -0
  342. package/src/tests/scripts-http.test.ts +201 -1
  343. package/src/tests/scripts-only-gating.test.ts +2 -3
  344. package/src/tests/scripts-runtime.test.ts +58 -40
  345. package/src/tests/secret-scrubber.test.ts +86 -0
  346. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  347. package/src/tests/seed-skills-retire.test.ts +182 -0
  348. package/src/tests/send-task-requester-gate.test.ts +58 -0
  349. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  350. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  351. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  352. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  353. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  354. package/src/tests/swarm-diff.test.ts +1 -1
  355. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  356. package/src/tests/task-session-prompts.test.ts +132 -0
  357. package/src/tests/task-tool-manifest.test.ts +14 -0
  358. package/src/tests/task-tools-ctx.test.ts +2 -0
  359. package/src/tests/tool-annotations.test.ts +6 -6
  360. package/src/tests/ui-logs-parser.test.ts +43 -0
  361. package/src/tests/used-by-route.test.ts +6 -0
  362. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  363. package/src/tests/workflow-swarm-script.test.ts +38 -0
  364. package/src/tests/workflow-wait-filter.test.ts +5 -3
  365. package/src/tools/chat-access.ts +41 -0
  366. package/src/tools/chat-open-dm.ts +59 -0
  367. package/src/tools/chat-post.ts +74 -0
  368. package/src/tools/chat-read-thread.ts +100 -0
  369. package/src/tools/chat-reply.ts +75 -0
  370. package/src/tools/create-page.ts +3 -13
  371. package/src/tools/model-catalog/index.ts +2 -1
  372. package/src/tools/register-chat-channel.ts +139 -0
  373. package/src/tools/send-task.ts +24 -0
  374. package/src/tools/skills/skill-get.ts +17 -9
  375. package/src/tools/swarm-config/get-config.ts +2 -1
  376. package/src/tools/swarm-config/list-config.ts +2 -1
  377. package/src/tools/swarm-config/set-config.ts +16 -3
  378. package/src/tools/tool-config.ts +8 -6
  379. package/src/types.ts +12 -2
  380. package/src/utils/config-env-export.ts +74 -0
  381. package/src/utils/credentials.test.ts +92 -0
  382. package/src/utils/credentials.ts +52 -4
  383. package/src/utils/harness-provider.ts +15 -6
  384. package/src/utils/runtime-model-catalog.ts +11 -1
  385. package/src/utils/secret-scrubber.ts +71 -2
  386. package/src/utils/task-tool-manifest.ts +19 -2
  387. package/src/workflows/executors/swarm-script.ts +1 -0
  388. package/templates/skills/agent-browser/SKILL.md +1 -1
  389. package/templates/skills/agent-browser/content.md +1 -1
  390. package/templates/skills/apps/SKILL.md +8 -1
  391. package/templates/skills/apps/content.md +8 -1
  392. package/templates/skills/chat-sdk/config.json +14 -0
  393. package/templates/skills/chat-sdk/content.md +84 -0
  394. package/templates/skills/composio-gmail/content.md +7 -0
  395. package/templates/skills/composio-google-docs/content.md +1 -1
  396. package/templates/skills/email-domain-health/config.json +14 -0
  397. package/templates/skills/email-domain-health/content.md +131 -0
  398. package/templates/skills/kv-storage/content.md +2 -2
  399. package/templates/skills/pages/content.md +8 -10
  400. package/templates/skills/swarm-extensions/content.md +2 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  402. package/templates/skills/swarm-scripts/config.json +1 -1
  403. package/templates/skills/swarm-scripts/content.md +4 -2
  404. package/templates/skills/workflow-iterate/content.md +38 -0
  405. package/dist/artifact-eczsknc3.js +0 -3843
  406. package/dist/cli-bd957dgb.js +0 -153
  407. package/dist/cli-q51yprfq.js +0 -1104
  408. package/dist/cli-t1ze64h3.js +0 -811
  409. package/dist/cli-tcc4wd5d.js +0 -56471
  410. package/dist/index-9ch1n761.js +0 -1102
  411. package/src/artifact-sdk/index.ts +0 -2
  412. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  413. package/src/artifact-sdk/port.ts +0 -12
  414. package/src/artifact-sdk/server.ts +0 -183
  415. package/src/artifact-sdk/tunnel.ts +0 -19
  416. package/src/commands/artifact.ts +0 -249
  417. package/src/http/ecosystem.ts +0 -77
  418. package/src/tests/artifact-commands.test.ts +0 -92
  419. package/src/tests/artifact-sdk.test.ts +0 -793
  420. package/src/tests/register-service-security.test.ts +0 -194
  421. package/src/tools/list-services.ts +0 -107
  422. package/src/tools/register-service.ts +0 -177
  423. package/src/tools/unregister-service.ts +0 -73
  424. package/src/tools/update-service-status.ts +0 -96
  425. package/templates/skills/artifacts/config.json +0 -14
  426. package/templates/skills/artifacts/content.md +0 -344
  427. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  428. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  429. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  430. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  431. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  432. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -0,0 +1,194 @@
1
+ import type { Chat, SlashCommandEvent } from "chat";
2
+ import {
3
+ cancelTask,
4
+ getAgentById,
5
+ getLatestActiveTaskByContextKey,
6
+ getLatestTaskByContextKey,
7
+ updateAgentStatusFromCapacity,
8
+ } from "@/be/db";
9
+ import { chatSdkContextKey } from "@/tasks/context-key";
10
+ import type { AgentTask } from "@/types";
11
+ import { scrubSecrets } from "@/utils/secret-scrubber";
12
+ import { resolveAdapterApiUrl } from "./adapters";
13
+ import { trackBackground } from "./background";
14
+ import type { ResolvedAdapterConfig } from "./config";
15
+ import { errorMessage } from "./logger";
16
+ import { SqliteChatState } from "./state";
17
+ import { callTelegramBotApi } from "./telegram-api";
18
+ import { stopTypingKeepalive } from "./typing";
19
+
20
+ /**
21
+ * Slash commands the swarm answers on every adapter that delivers them
22
+ * (Telegram, Discord, Slack). The platform learns the list in its own way:
23
+ * Telegram through `setMyCommands` at adapter start, Discord through a
24
+ * one-time application-command registration, Slack through the app manifest.
25
+ * Names follow the Telegram rules (lowercase, 1-32 chars) so one list fits all.
26
+ */
27
+ export interface ChatCommandSpec {
28
+ command: string;
29
+ description: string;
30
+ }
31
+
32
+ export const CHAT_COMMANDS: readonly ChatCommandSpec[] = [
33
+ { command: "help", description: "List the commands" },
34
+ { command: "status", description: "Show the latest task of this chat" },
35
+ { command: "cancel", description: "Cancel the running task of this chat" },
36
+ { command: "reset", description: "Start over: the next message gets no earlier context" },
37
+ ];
38
+
39
+ const RESET_KEY_PREFIX = "swarm:reset:";
40
+ const RESET_TTL_MS = 30 * 24 * 60 * 60 * 1000;
41
+ /** Upper bound on the active tasks `/cancel` walks in one chat. */
42
+ const CANCEL_LIMIT = 20;
43
+
44
+ /**
45
+ * Record that the user reset the conversation. The next flush in this thread
46
+ * creates a task with no parent and no thread history older than this mark.
47
+ */
48
+ export async function markChatThreadReset(threadId: string): Promise<Date> {
49
+ const at = new Date();
50
+ await new SqliteChatState().set(`${RESET_KEY_PREFIX}${threadId}`, at.toISOString(), RESET_TTL_MS);
51
+ return at;
52
+ }
53
+
54
+ /** The last `/reset` in this thread, or null. */
55
+ export async function getChatThreadResetAt(threadId: string): Promise<Date | null> {
56
+ const raw = await new SqliteChatState().get<string>(`${RESET_KEY_PREFIX}${threadId}`);
57
+ if (typeof raw !== "string") return null;
58
+ const at = new Date(raw);
59
+ return Number.isNaN(at.getTime()) ? null : at;
60
+ }
61
+
62
+ function helpText(): string {
63
+ return ["Commands:", ...CHAT_COMMANDS.map((c) => `/${c.command}: ${c.description}`)].join("\n");
64
+ }
65
+
66
+ function shortId(task: AgentTask): string {
67
+ return task.id.slice(0, 8);
68
+ }
69
+
70
+ async function assigneeLabel(task: AgentTask): Promise<string> {
71
+ if (!task.agentId) return "waiting for an agent";
72
+ const agent = await getAgentById(task.agentId);
73
+ return agent ? `assigned to ${agent.name}` : "assigned to a removed agent";
74
+ }
75
+
76
+ async function statusText(contextKey: string): Promise<string> {
77
+ const latest = await getLatestTaskByContextKey(contextKey);
78
+ if (!latest) return "No task yet for this chat. Send a message to create one.";
79
+ const lines = [
80
+ `Latest task ${shortId(latest)}: ${latest.status}, ${await assigneeLabel(latest)}.`,
81
+ ];
82
+ if (latest.status === "pending" || latest.status === "in_progress") {
83
+ lines.push("Send /cancel to stop it.");
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+
88
+ async function cancelActiveTasks(
89
+ contextKey: string,
90
+ threadId: string,
91
+ requestedBy: string,
92
+ ): Promise<string> {
93
+ const cancelled: AgentTask[] = [];
94
+ for (let i = 0; i < CANCEL_LIMIT; i++) {
95
+ const active = await getLatestActiveTaskByContextKey(contextKey, "chat");
96
+ if (!active) break;
97
+ const task = await cancelTask(active.id, `Cancelled from chat by ${requestedBy}`);
98
+ if (!task) break;
99
+ if (task.agentId) await updateAgentStatusFromCapacity(task.agentId);
100
+ cancelled.push(task);
101
+ }
102
+ stopTypingKeepalive(threadId);
103
+ if (cancelled.length === 0) return "No running task in this chat.";
104
+ const ids = cancelled.map(shortId).join(", ");
105
+ return cancelled.length === 1
106
+ ? `Cancelled task ${ids}.`
107
+ : `Cancelled ${cancelled.length} tasks: ${ids}.`;
108
+ }
109
+
110
+ /** The reply for one command. Exported for tests; the handler posts it. */
111
+ export async function runChatCommand(
112
+ command: string,
113
+ threadId: string,
114
+ requestedBy: string,
115
+ ): Promise<string> {
116
+ const name = command.replace(/^\//, "").toLowerCase();
117
+ if (name === "help") return helpText();
118
+ if (!CHAT_COMMANDS.some((c) => c.command === name)) {
119
+ return `Unknown command /${name}.\n${helpText()}`;
120
+ }
121
+ let contextKey: string;
122
+ try {
123
+ contextKey = chatSdkContextKey(threadId);
124
+ } catch {
125
+ return "This command works only in a chat the swarm can address.";
126
+ }
127
+ switch (name) {
128
+ case "status":
129
+ return statusText(contextKey);
130
+ case "cancel":
131
+ return cancelActiveTasks(contextKey, threadId, requestedBy);
132
+ case "reset": {
133
+ await markChatThreadReset(threadId);
134
+ const lines = ["Context reset. Your next message starts a new conversation."];
135
+ // A running task is state, not context: it keeps going, and the next task
136
+ // is still created next to it. Only /cancel stops it.
137
+ if (await getLatestActiveTaskByContextKey(contextKey, "chat")) {
138
+ lines.push("A task is still running in this chat. Send /cancel to stop it.");
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ default:
143
+ return helpText();
144
+ }
145
+ }
146
+
147
+ async function onSlashCommand(event: SlashCommandEvent): Promise<void> {
148
+ const adapter = event.adapter.name;
149
+ // Telegram passes the thread id as the channel id. Slack and Discord pass
150
+ // the channel, so a command there addresses the channel-level thread.
151
+ const threadId = event.channel.id;
152
+ const requestedBy = event.user.userName || event.user.fullName || event.user.userId;
153
+ console.log(`[chat-sdk] ${adapter} ${threadId}: command ${event.command} from ${requestedBy}`);
154
+ let reply: string;
155
+ try {
156
+ reply = await runChatCommand(event.command, threadId, requestedBy);
157
+ } catch (error) {
158
+ console.warn(
159
+ scrubSecrets(
160
+ `[chat-sdk] ${threadId}: command ${event.command} failed: ${errorMessage(error)}`,
161
+ ),
162
+ );
163
+ reply = "The command failed. Check the API log.";
164
+ }
165
+ try {
166
+ await event.channel.post(scrubSecrets(reply));
167
+ } catch (error) {
168
+ console.warn(
169
+ `[chat-sdk] ${threadId}: could not answer ${event.command}: ${errorMessage(error)}`,
170
+ );
171
+ }
172
+ }
173
+
174
+ /** Catch-all handler: known commands act, unknown ones get the list. */
175
+ export function registerChatCommands(chat: Chat): void {
176
+ chat.onSlashCommand((event) => {
177
+ const work = onSlashCommand(event);
178
+ trackBackground(work);
179
+ return work.catch(() => undefined);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Publish the command list to Telegram so the client shows the `/` menu.
185
+ * Called after the adapter initialized; a failure is logged, never fatal.
186
+ */
187
+ export async function registerTelegramCommands(config: ResolvedAdapterConfig): Promise<void> {
188
+ const raw = config.botToken;
189
+ const token = typeof raw === "function" ? await (raw as () => Promise<string>)() : raw;
190
+ if (typeof token !== "string" || token.length === 0) return;
191
+ await callTelegramBotApi(resolveAdapterApiUrl("telegram"), token, "setMyCommands", {
192
+ commands: CHAT_COMMANDS.map((c) => ({ command: c.command, description: c.description })),
193
+ });
194
+ }
@@ -0,0 +1,198 @@
1
+ import { ConfigRefError, resolveConfigRefs } from "@/be/config-refs";
2
+ import { getSwarmConfigs } from "@/be/db";
3
+ import { isReservedConfigKey } from "@/be/swarm-config-guard";
4
+ import { isEnvFlagEnabled } from "@/utils/env-flag";
5
+ import {
6
+ CHAT_SDK_ADAPTER_NAMES,
7
+ CHAT_SDK_ADAPTER_SCHEMAS,
8
+ type ChatSdkAdapterName,
9
+ type ChatSdkAdapters,
10
+ ChatSdkAdaptersDraftSchema,
11
+ firstIssueText,
12
+ isAdapterEntryEnabled,
13
+ } from "./schema";
14
+
15
+ export {
16
+ CHAT_SDK_ADAPTER_NAMES,
17
+ type ChatSdkAdapterName,
18
+ type ChatSdkAdapters,
19
+ ChatSdkAdaptersSchema,
20
+ } from "./schema";
21
+
22
+ class ChatSdkConfigError extends Error {
23
+ constructor(message: string) {
24
+ super(message);
25
+ this.name = "ChatSdkConfigError";
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Adapter config after `$config` resolution. Resolver-capable fields
31
+ * (`botToken`, `appId`, `applicationId`) are functions, every other field is
32
+ * the plain value. Passed to the adapter factory as-is.
33
+ */
34
+ export type ResolvedAdapterConfig = Record<string, unknown>;
35
+
36
+ export interface ChatSdkConfig {
37
+ enabled: boolean;
38
+ /** Adapters that are complete and resolved. Only these start. */
39
+ adapters: Partial<Record<ChatSdkAdapterName, ResolvedAdapterConfig>>;
40
+ /**
41
+ * Adapters present in `CHAT_SDK_ADAPTERS` that cannot start: a required
42
+ * field is missing (a partial dashboard save) or a `$config` reference does
43
+ * not resolve. The text names the field. The other adapters still start.
44
+ */
45
+ incomplete?: Partial<Record<ChatSdkAdapterName, string>>;
46
+ /** Adapters present with `enabled: false`: configured, kept, not started. */
47
+ disabled?: ChatSdkAdapterName[];
48
+ /** A problem with the whole value (bad JSON, unknown adapter, forbidden key): nothing starts. */
49
+ error?: string;
50
+ }
51
+
52
+ const RESOLVER_FIELDS = new Set(["botToken", "appId", "applicationId"]);
53
+
54
+ /**
55
+ * Raw `CHAT_SDK_ENABLED` / `CHAT_SDK_ADAPTERS` from `process.env`. Boot runs
56
+ * `loadGlobalConfigsIntoEnv(true)` first, so stored swarm_config rows win over
57
+ * the deployment env (same mechanism as `SLACK_DISABLE`).
58
+ */
59
+ function readChatSdkEnv(): { enabled: boolean; adaptersJson: string | undefined } {
60
+ const adaptersJson = process.env.CHAT_SDK_ADAPTERS?.trim();
61
+ return {
62
+ enabled: isEnvFlagEnabled("CHAT_SDK_ENABLED", false),
63
+ adaptersJson: adaptersJson ? adaptersJson : undefined,
64
+ };
65
+ }
66
+
67
+ /** True when `CHAT_SDK_ENABLED` resolves true. Drives the `chat-sdk` capability. */
68
+ export function isChatSdkEnabled(): boolean {
69
+ return readChatSdkEnv().enabled;
70
+ }
71
+
72
+ /**
73
+ * Build the `$config` lookup over decrypted global swarm_config rows. Never
74
+ * reads `process.env`: leads can write ordinary global config, and an
75
+ * env-backed lookup would let `{"$config":"API_KEY"}` leave the host.
76
+ */
77
+ async function buildSwarmConfigLookup(): Promise<(key: string) => string | undefined> {
78
+ const values = new Map<string, string>();
79
+ for (const row of await getSwarmConfigs({ scope: "global" })) {
80
+ if (typeof row.value === "string" && row.value.length > 0) values.set(row.key, row.value);
81
+ }
82
+ return (key) => {
83
+ if (isReservedConfigKey(key)) {
84
+ throw new ChatSdkConfigError(
85
+ `CHAT_SDK_ADAPTERS references reserved key '${key}', which is never resolvable`,
86
+ );
87
+ }
88
+ return values.get(key);
89
+ };
90
+ }
91
+
92
+ function toResolvedAdapterConfig(raw: Record<string, unknown>): ResolvedAdapterConfig {
93
+ const out: Record<string, unknown> = {};
94
+ for (const [key, value] of Object.entries(raw)) {
95
+ // The card's switch, not an adapter option.
96
+ if (key === "enabled") continue;
97
+ out[key] = RESOLVER_FIELDS.has(key) && typeof value === "string" ? async () => value : value;
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * Parse, validate and resolve the Chat SDK config. A problem with the whole
104
+ * value is returned in `error` with no adapters, so boot can log it and
105
+ * continue. A problem with one adapter (missing field, unresolvable `$config`)
106
+ * lands in `incomplete[name]` and the other adapters still resolve. Other
107
+ * errors (for example DB errors) propagate.
108
+ */
109
+ export async function getChatSdkConfig(): Promise<ChatSdkConfig> {
110
+ const { enabled, adaptersJson } = readChatSdkEnv();
111
+ if (!enabled) return { enabled, adapters: {} };
112
+ if (!adaptersJson) {
113
+ return {
114
+ enabled,
115
+ adapters: {},
116
+ error: "CHAT_SDK_ENABLED is true but CHAT_SDK_ADAPTERS is empty",
117
+ };
118
+ }
119
+ let parsed: unknown;
120
+ try {
121
+ parsed = JSON.parse(adaptersJson);
122
+ } catch {
123
+ // Fixed text: the parser message can quote the input, which may hold an inline token.
124
+ return { enabled, adapters: {}, error: "CHAT_SDK_ADAPTERS is not valid JSON" };
125
+ }
126
+ // The draft shape guards the whole value: unknown adapters and forbidden
127
+ // keys stop everything. Required fields are checked per adapter below.
128
+ const draft = ChatSdkAdaptersDraftSchema.safeParse(parsed);
129
+ if (!draft.success) return { enabled, adapters: {}, error: firstIssueText(draft.error) };
130
+
131
+ const lookup = await buildSwarmConfigLookup();
132
+ const adapters: ChatSdkConfig["adapters"] = {};
133
+ const incomplete: NonNullable<ChatSdkConfig["incomplete"]> = {};
134
+ const disabled: ChatSdkAdapterName[] = [];
135
+ for (const name of CHAT_SDK_ADAPTER_NAMES) {
136
+ const entry = draft.data[name];
137
+ if (entry === undefined) continue;
138
+ if (!isAdapterEntryEnabled(entry)) {
139
+ disabled.push(name);
140
+ continue;
141
+ }
142
+ const validated = CHAT_SDK_ADAPTER_SCHEMAS[name].safeParse(entry);
143
+ if (!validated.success) {
144
+ incomplete[name] = firstIssueText(validated.error, `CHAT_SDK_ADAPTERS.${name}`, entry);
145
+ continue;
146
+ }
147
+ try {
148
+ const resolved = resolveConfigRefs<ChatSdkAdapters[typeof name]>(
149
+ validated.data as ChatSdkAdapters[typeof name],
150
+ lookup,
151
+ );
152
+ adapters[name] = toResolvedAdapterConfig(resolved as Record<string, unknown>);
153
+ } catch (error) {
154
+ if (!(error instanceof ChatSdkConfigError || error instanceof ConfigRefError)) throw error;
155
+ incomplete[name] = error.message;
156
+ }
157
+ }
158
+ return {
159
+ enabled,
160
+ adapters,
161
+ ...(Object.keys(incomplete).length > 0 ? { incomplete } : {}),
162
+ ...(disabled.length > 0 ? { disabled } : {}),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * One Slack path per deployment: the Chat SDK Slack adapter requires the
168
+ * built-in Slack integration to be off. Evaluated on resolved config.
169
+ */
170
+ export function assertNoSlackClash(config: ChatSdkConfig): void {
171
+ if (config.adapters.slack && !isEnvFlagEnabled("SLACK_DISABLE", false)) {
172
+ throw new ChatSdkConfigError(
173
+ "CHAT_SDK_ADAPTERS.slack conflicts with the built-in Slack integration; set SLACK_DISABLE=true to use the Chat SDK Slack adapter",
174
+ );
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Env-only test/QA base URL overrides, keyed by adapter name
180
+ * (`CHAT_SDK_API_URL_OVERRIDES`, a RESERVED_KEYS entry, so never injected from
181
+ * a config row). Invalid JSON is ignored with an empty result.
182
+ */
183
+ export function readApiUrlOverrides(): Partial<Record<ChatSdkAdapterName, string>> {
184
+ const raw = process.env.CHAT_SDK_API_URL_OVERRIDES?.trim();
185
+ if (!raw) return {};
186
+ try {
187
+ const parsed = JSON.parse(raw) as unknown;
188
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
189
+ const out: Partial<Record<ChatSdkAdapterName, string>> = {};
190
+ for (const name of CHAT_SDK_ADAPTER_NAMES) {
191
+ const value = (parsed as Record<string, unknown>)[name];
192
+ if (typeof value === "string" && value.length > 0) out[name] = value;
193
+ }
194
+ return out;
195
+ } catch {
196
+ return {};
197
+ }
198
+ }
@@ -0,0 +1,63 @@
1
+ import { scrubSecrets } from "@/utils/secret-scrubber";
2
+ import { resolveAdapterApiUrl } from "./adapters";
3
+ import { errorMessage } from "./logger";
4
+
5
+ const DISCORD_TIMEOUT_MS = 5_000;
6
+ /** The status card polls every 5 s; one Discord call per minute is enough. */
7
+ const GUILDS_CACHE_MS = 60_000;
8
+ /** Bound on the server names the status carries. */
9
+ const MAX_GUILD_NAMES = 10;
10
+
11
+ export interface DiscordGuilds {
12
+ count: number;
13
+ /** Up to `MAX_GUILD_NAMES` server names, for the card. */
14
+ names: string[];
15
+ }
16
+
17
+ let cache: { token: string; at: number; guilds: DiscordGuilds | undefined } | undefined;
18
+
19
+ /**
20
+ * The servers the bot was invited to (`GET /users/@me/guilds`). A connected
21
+ * Gateway with zero servers means the invite step was skipped, which the card
22
+ * can say outright. Undefined when Discord does not answer; the error is
23
+ * logged without the token.
24
+ */
25
+ export async function fetchDiscordGuilds(
26
+ token: string,
27
+ now = Date.now(),
28
+ ): Promise<DiscordGuilds | undefined> {
29
+ if (cache && cache.token === token && now - cache.at < GUILDS_CACHE_MS) return cache.guilds;
30
+ let guilds: DiscordGuilds | undefined;
31
+ try {
32
+ const response = await fetch(`${resolveAdapterApiUrl("discord")}/users/@me/guilds`, {
33
+ headers: { authorization: `Bot ${token}` },
34
+ signal: AbortSignal.timeout(DISCORD_TIMEOUT_MS),
35
+ });
36
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
37
+ const payload = (await response.json()) as unknown;
38
+ if (!Array.isArray(payload)) throw new Error("unexpected response shape");
39
+ const names = payload
40
+ .map((guild: unknown) =>
41
+ guild !== null &&
42
+ typeof guild === "object" &&
43
+ typeof (guild as { name?: unknown }).name === "string"
44
+ ? (guild as { name: string }).name
45
+ : undefined,
46
+ )
47
+ .filter((name): name is string => name !== undefined);
48
+ guilds = { count: payload.length, names: names.slice(0, MAX_GUILD_NAMES) };
49
+ } catch (error) {
50
+ console.warn(
51
+ scrubSecrets(
52
+ `[chat-sdk] discord: could not list the bot's servers: ${errorMessage(error).replaceAll(token, "[REDACTED]")}`,
53
+ ),
54
+ );
55
+ }
56
+ cache = { token, at: now, guilds };
57
+ return guilds;
58
+ }
59
+
60
+ /** Test seam. */
61
+ export function resetDiscordGuildsCache(): void {
62
+ cache = undefined;
63
+ }
@@ -0,0 +1,185 @@
1
+ import type { DiscordAdapter } from "@chat-adapter/discord";
2
+ import type { Adapter } from "chat";
3
+ import { scrubSecrets } from "@/utils/secret-scrubber";
4
+ import { trackBackground } from "./background";
5
+ import { errorMessage } from "./logger";
6
+
7
+ /**
8
+ * Discord sends channel messages and mentions only over the Gateway WebSocket.
9
+ * The adapter's `startGatewayListener` listens for one window and then
10
+ * destroys its client (it is built for serverless re-invocation), so a
11
+ * long-lived API process re-arms it in a loop. The adapter has no
12
+ * `disconnect()`, so `chat.shutdown()` does not stop the Gateway; aborting
13
+ * this loop is the only stop path.
14
+ *
15
+ * The Gateway does not replay `MESSAGE_CREATE`: messages sent during a re-arm
16
+ * gap or a backoff are lost. A long window keeps gaps rare. 6 h uses 4
17
+ * identifies per day of Discord's 1000.
18
+ */
19
+ const DISCORD_GATEWAY_WINDOW_MS = 6 * 60 * 60 * 1000;
20
+
21
+ /** A window that ends sooner than this failed (bad token, network down). */
22
+ const MIN_HEALTHY_WINDOW_MS = 10_000;
23
+ const BACKOFF_MS = 30_000;
24
+ /** Bound on the response body text logged when the listener refuses to start. */
25
+ const MAX_LOGGED_BODY_CHARS = 200;
26
+
27
+ export interface DiscordGatewayHealth {
28
+ state: "running" | "backoff" | "stopped";
29
+ since: string;
30
+ /** Re-arms after the first listener window. */
31
+ restarts: number;
32
+ }
33
+
34
+ /** The part of the Discord adapter the loop uses. */
35
+ export type DiscordGatewayListener = Pick<DiscordAdapter, "startGatewayListener">;
36
+
37
+ export function isDiscordGatewayListener(
38
+ adapter: Adapter,
39
+ ): adapter is Adapter & DiscordGatewayListener {
40
+ return "startGatewayListener" in adapter && typeof adapter.startGatewayListener === "function";
41
+ }
42
+
43
+ /** Test seams. Production uses the defaults. */
44
+ export interface DiscordGatewayOptions {
45
+ windowMs?: number;
46
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
47
+ now?: () => number;
48
+ }
49
+
50
+ /** Resolves after `ms`, or at once when `signal` aborts. */
51
+ function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
52
+ return new Promise((resolve) => {
53
+ if (signal.aborted) {
54
+ resolve();
55
+ return;
56
+ }
57
+ const done = (): void => {
58
+ clearTimeout(timer);
59
+ signal.removeEventListener("abort", done);
60
+ resolve();
61
+ };
62
+ const timer = setTimeout(done, ms);
63
+ signal.addEventListener("abort", done, { once: true });
64
+ });
65
+ }
66
+
67
+ let current: { controller: AbortController; loop: Promise<void>; now: () => number } | undefined;
68
+ let health: DiscordGatewayHealth | undefined;
69
+
70
+ function setState(state: DiscordGatewayHealth["state"], restarts: number, nowMs: number): void {
71
+ if (health?.state === state) {
72
+ health = { ...health, restarts };
73
+ return;
74
+ }
75
+ health = { state, since: new Date(nowMs).toISOString(), restarts };
76
+ }
77
+
78
+ async function runGatewayLoop(
79
+ adapter: DiscordGatewayListener,
80
+ signal: AbortSignal,
81
+ options: DiscordGatewayOptions,
82
+ ): Promise<void> {
83
+ const windowMs = options.windowMs ?? DISCORD_GATEWAY_WINDOW_MS;
84
+ const sleep = options.sleep ?? abortableSleep;
85
+ const now = options.now ?? Date.now;
86
+ let restarts = 0;
87
+ let first = true;
88
+ while (!signal.aborted) {
89
+ if (!first) restarts += 1;
90
+ first = false;
91
+ setState("running", restarts, now());
92
+ const started = now();
93
+ let inner: Promise<unknown> | undefined;
94
+ // One controller per window. The adapter adds an `abort` listener to the
95
+ // signal it gets and never removes it, so passing the outer signal would
96
+ // leak one listener (and the destroyed client) per re-arm.
97
+ const windowController = new AbortController();
98
+ const abortWindow = (): void => windowController.abort();
99
+ signal.addEventListener("abort", abortWindow, { once: true });
100
+ try {
101
+ const response = await adapter.startGatewayListener(
102
+ {
103
+ waitUntil: (task) => {
104
+ inner = task;
105
+ trackBackground(task);
106
+ },
107
+ },
108
+ windowMs,
109
+ windowController.signal,
110
+ );
111
+ if (!response.ok) {
112
+ const body = await response.text().catch(() => "");
113
+ console.error(
114
+ scrubSecrets(
115
+ `[chat-sdk] discord gateway listener refused to start: HTTP ${response.status}: ${body.slice(0, MAX_LOGGED_BODY_CHARS)}`,
116
+ ),
117
+ );
118
+ }
119
+ // The inner promise resolves when the window ends or on abort. It never
120
+ // rejects: login errors are logged by the adapter and end the window.
121
+ await inner;
122
+ } catch (error) {
123
+ console.error(
124
+ scrubSecrets(`[chat-sdk] discord gateway listener failed: ${errorMessage(error)}`),
125
+ );
126
+ } finally {
127
+ signal.removeEventListener("abort", abortWindow);
128
+ }
129
+ const elapsed = now() - started;
130
+ if (signal.aborted) break;
131
+ if (elapsed < MIN_HEALTHY_WINDOW_MS) {
132
+ setState("backoff", restarts, now());
133
+ console.warn(
134
+ scrubSecrets(
135
+ `[chat-sdk] discord gateway backoff: window ended after ${elapsed}ms, retrying in ${BACKOFF_MS / 1000}s`,
136
+ ),
137
+ );
138
+ await sleep(BACKOFF_MS, signal);
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Start the supervised Gateway loop. Call only after `chat.initialize()`
145
+ * succeeded: the listener refuses to start before that.
146
+ */
147
+ export function startDiscordGateway(
148
+ adapter: DiscordGatewayListener,
149
+ options: DiscordGatewayOptions = {},
150
+ ): void {
151
+ if (current) return;
152
+ const now = options.now ?? Date.now;
153
+ health = { state: "running", since: new Date(now()).toISOString(), restarts: 0 };
154
+ const controller = new AbortController();
155
+ const loop = runGatewayLoop(adapter, controller.signal, options).catch((error: unknown) => {
156
+ console.error(`[chat-sdk] discord gateway loop crashed: ${errorMessage(error)}`);
157
+ });
158
+ current = { controller, loop, now };
159
+ }
160
+
161
+ /**
162
+ * Abort the loop and return at once. The open window is tracked by
163
+ * `trackBackground`, so the shutdown drain bounds the wait for it to close.
164
+ * The `stopped` state and log line follow when the loop exits.
165
+ */
166
+ export function stopDiscordGateway(): void {
167
+ if (!current) return;
168
+ const { controller, loop, now } = current;
169
+ current = undefined;
170
+ controller.abort();
171
+ void loop.then(() => {
172
+ // Skip when a new loop started, or when shutdown already reset health.
173
+ if (!current && health) setState("stopped", health.restarts, now());
174
+ console.log("[chat-sdk] discord gateway stopped");
175
+ });
176
+ }
177
+
178
+ export function getDiscordGatewayHealth(): DiscordGatewayHealth | undefined {
179
+ return health;
180
+ }
181
+
182
+ /** Forget the gateway record. Runtime shutdown clears adapter health the same way. */
183
+ export function resetDiscordGatewayHealth(): void {
184
+ health = undefined;
185
+ }
@@ -0,0 +1,28 @@
1
+ import type { Chat, Message, Thread } from "chat";
2
+ import { trackBackground } from "./background";
3
+ import { registerChatCommands } from "./commands";
4
+ import { type ChatInboundKind, handleChatInbound } from "./inbound";
5
+
6
+ async function onHumanMessage(
7
+ thread: Thread,
8
+ message: Message,
9
+ kind: ChatInboundKind,
10
+ ): Promise<void> {
11
+ // The SDK drops the bot's own messages; other bots never become tasks.
12
+ if (message.author.isBot === true) return;
13
+ // Socket-mode events (Slack) reach the SDK without a request, so it has no
14
+ // `waitUntil` to hand this work to. Track it here so the shutdown drain
15
+ // waits for it before the DB closes. The tracker logs a rejection.
16
+ const work = handleChatInbound(thread, message, kind);
17
+ trackBackground(work);
18
+ await work.catch(() => undefined);
19
+ }
20
+
21
+ export function registerChatHandlers(chat: Chat): void {
22
+ chat.onNewMention((thread, message) => onHumanMessage(thread, message, "mention"));
23
+ chat.onDirectMessage((thread, message) => onHumanMessage(thread, message, "dm"));
24
+ chat.onSubscribedMessage((thread, message) => onHumanMessage(thread, message, "subscribed"));
25
+ // A `/command` message never reaches the handlers above: the adapters route
26
+ // it here. Without a handler Telegram would drop it silently.
27
+ registerChatCommands(chat);
28
+ }