@desplega.ai/agent-swarm 1.172.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 (525) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
  3. package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
  4. package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
  6. package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
  7. package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
  8. package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
  9. package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
  10. package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
  11. package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
  12. package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
  13. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  14. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  15. package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
  16. package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
  17. package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
  18. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  19. package/dist/cli-2m1ktfzz.js +17795 -0
  20. package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
  21. package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
  22. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  23. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  24. package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
  25. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  26. package/dist/cli-6cvfxs8r.js +47 -0
  27. package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
  28. package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
  29. package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
  30. package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
  31. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  32. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  33. package/dist/cli-9traj3dn.js +638 -0
  34. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  35. package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
  36. package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
  37. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  38. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  39. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  40. package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
  41. package/dist/cli-e3g5mbkz.js +776 -0
  42. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  43. package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
  44. package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
  45. package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
  46. package/dist/cli-f527ce74.js +781 -0
  47. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  48. package/dist/cli-hjwmhcg1.js +10622 -0
  49. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  50. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  51. package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
  52. package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
  53. package/dist/cli-jta2eyzc.js +619 -0
  54. package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
  55. package/dist/cli-k3dp5tpf.js +263 -0
  56. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  57. package/dist/cli-kceah1eh.js +92 -0
  58. package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
  59. package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
  60. package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
  61. package/dist/cli-n2wq4j7m.js +6985 -0
  62. package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
  63. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  64. package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
  65. package/dist/cli-p0sg24zp.js +16 -0
  66. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  67. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  68. package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
  69. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  70. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  71. package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
  72. package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
  73. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  74. package/dist/cli-rhs9pz33.js +294 -0
  75. package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
  76. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  77. package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
  78. package/dist/cli-skxvvpap.js +10140 -0
  79. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  80. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  81. package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
  82. package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
  83. package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
  84. package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
  85. package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
  86. package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
  87. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  88. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  89. package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
  90. package/dist/cli-wmgrkscj.js +23254 -0
  91. package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
  92. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  93. package/dist/cli.js +32 -48
  94. package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
  95. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  96. package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
  97. package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
  98. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  99. package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
  100. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  101. package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
  102. package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
  103. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  104. package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
  105. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  106. package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
  107. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  108. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  109. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  110. package/dist/external-channel-h3359cwn.js +52 -0
  111. package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
  112. package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
  113. package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
  114. package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
  115. package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
  116. package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
  117. package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
  118. package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
  119. package/dist/index-52j1aja4.js +3465 -0
  120. package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
  121. package/dist/index-9gw58n4k.js +193 -0
  122. package/dist/index-9j87mcm8.js +56305 -0
  123. package/dist/index-bsmcresk.js +79424 -0
  124. package/dist/index-mh85qhec.js +475 -0
  125. package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
  126. package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
  127. package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
  128. package/dist/index-we27kd31.js +12810 -0
  129. package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
  130. package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
  131. package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
  132. package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
  133. package/dist/node-c9we95xf.js +1 -1
  134. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  135. package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
  136. package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
  137. package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
  138. package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
  139. package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
  140. package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
  141. package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
  142. package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
  143. package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
  144. package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
  145. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  146. package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
  147. package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
  148. package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
  149. package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
  150. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  151. package/openapi.json +951 -356
  152. package/package.json +12 -13
  153. package/src/apps/catalog.generated.json +1309 -53
  154. package/src/apps/page-validator.ts +93 -31
  155. package/src/azure-devops/api.ts +94 -18
  156. package/src/azure-devops/auth.ts +32 -0
  157. package/src/azure-devops/boards-sync.ts +708 -0
  158. package/src/azure-devops/handlers.ts +52 -8
  159. package/src/azure-devops/html.ts +26 -0
  160. package/src/azure-devops/index.ts +15 -0
  161. package/src/azure-devops/templates.ts +86 -0
  162. package/src/azure-devops/types.ts +65 -1
  163. package/src/be/config-refs.ts +60 -0
  164. package/src/be/db/page-versions.ts +15 -0
  165. package/src/be/db/runtime.ts +35 -8
  166. package/src/be/db/skill-name-resolution.ts +36 -0
  167. package/src/be/db/tasks/read.ts +230 -0
  168. package/src/be/db/tasks/write.ts +7 -0
  169. package/src/be/db.ts +74 -13
  170. package/src/be/migrations/213_script_connections_sql.sql +79 -0
  171. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  172. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  173. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  174. package/src/be/model-catalog-store.ts +5 -1
  175. package/src/be/model-tier-resolution.ts +12 -1
  176. package/src/be/model-validation.ts +5 -2
  177. package/src/be/models-catalog.ts +26 -1
  178. package/src/be/modelsdev-cache.json +198 -63
  179. package/src/be/modelsdev-cache.ts +9 -1
  180. package/src/be/script-connections.ts +47 -8
  181. package/src/be/script-credential-broker.ts +39 -2
  182. package/src/be/scripts/run-saved.ts +1 -0
  183. package/src/be/scripts/typecheck.ts +10 -8
  184. package/src/be/seed/runner.ts +13 -1
  185. package/src/be/seed/state-db.ts +5 -0
  186. package/src/be/seed/types.ts +6 -0
  187. package/src/be/seed-pricing.ts +19 -2
  188. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  189. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  190. package/src/be/seed-skills/bundled-files.generated.json +10 -18
  191. package/src/be/seed-skills/index.ts +107 -3
  192. package/src/be/sql-connections.ts +118 -0
  193. package/src/be/swarm-config-guard.ts +53 -0
  194. package/src/cli.tsx +0 -16
  195. package/src/commands/onboard/non-interactive.ts +4 -2
  196. package/src/commands/profile-sync.ts +5 -3
  197. package/src/commands/provider-credentials.ts +52 -6
  198. package/src/commands/runner.ts +196 -56
  199. package/src/e2b/env.ts +5 -0
  200. package/src/extensions/contract-types.generated.ts +1 -1
  201. package/src/extensions/contract.ts +45 -0
  202. package/src/extensions/dispatcher.ts +7 -0
  203. package/src/extensions/post-bridge.ts +40 -0
  204. package/src/github/handlers.ts +11 -4
  205. package/src/heartbeat/templates.ts +8 -5
  206. package/src/hooks/hook.ts +0 -26
  207. package/src/http/all-routes.ts +0 -1
  208. package/src/http/config.ts +36 -17
  209. package/src/http/core.ts +4 -0
  210. package/src/http/index.ts +14 -2
  211. package/src/http/integrations.ts +88 -1
  212. package/src/http/mcp-oauth.ts +6 -13
  213. package/src/http/mcp.ts +15 -5
  214. package/src/http/page-proxy.ts +2 -4
  215. package/src/http/pages-public.ts +6 -5
  216. package/src/http/pages.ts +5 -14
  217. package/src/http/poll.ts +28 -68
  218. package/src/http/requested-by.ts +54 -0
  219. package/src/http/route-def.ts +7 -6
  220. package/src/http/script-connection-proxy.ts +51 -4
  221. package/src/http/script-connections.ts +5 -2
  222. package/src/http/scripts.ts +1 -0
  223. package/src/http/session-cost-recompute.ts +12 -2
  224. package/src/http/session-data.ts +4 -0
  225. package/src/http/stats.ts +1 -30
  226. package/src/http/tasks.ts +61 -5
  227. package/src/http/utils.ts +14 -0
  228. package/src/http/webhooks.ts +183 -2
  229. package/src/http/x.ts +1 -0
  230. package/src/integrations/chat-sdk/adapters.ts +129 -0
  231. package/src/integrations/chat-sdk/attachments.ts +232 -0
  232. package/src/integrations/chat-sdk/background.ts +36 -0
  233. package/src/integrations/chat-sdk/commands.ts +194 -0
  234. package/src/integrations/chat-sdk/config.ts +198 -0
  235. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  236. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  237. package/src/integrations/chat-sdk/handlers.ts +28 -0
  238. package/src/integrations/chat-sdk/inbound.ts +426 -0
  239. package/src/integrations/chat-sdk/limits.ts +48 -0
  240. package/src/integrations/chat-sdk/logger.ts +81 -0
  241. package/src/integrations/chat-sdk/outbound.ts +123 -0
  242. package/src/integrations/chat-sdk/relay.ts +172 -0
  243. package/src/integrations/chat-sdk/router.ts +169 -0
  244. package/src/integrations/chat-sdk/runtime.ts +351 -0
  245. package/src/integrations/chat-sdk/schema.ts +245 -0
  246. package/src/integrations/chat-sdk/state.ts +243 -0
  247. package/src/integrations/chat-sdk/status.ts +244 -0
  248. package/src/integrations/chat-sdk/steering.ts +58 -0
  249. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  250. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  251. package/src/integrations/chat-sdk/templates.ts +16 -0
  252. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  253. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  254. package/src/integrations/chat-sdk/typing.ts +65 -0
  255. package/src/jira/adf.ts +3 -1
  256. package/src/kv-reserved-namespaces.ts +2 -0
  257. package/src/oauth/mcp-wrapper.ts +10 -6
  258. package/src/otel-impl.ts +2 -1
  259. package/src/pages/version.ts +4 -5
  260. package/src/prompts/base-prompt.ts +111 -45
  261. package/src/providers/acp-target-catalog.ts +6 -0
  262. package/src/providers/acp-targets.ts +56 -3
  263. package/src/providers/amp-adapter.ts +1 -1
  264. package/src/providers/claude-adapter.ts +27 -2
  265. package/src/providers/claude-flag-settings.ts +133 -0
  266. package/src/providers/claude-managed-adapter.ts +1 -1
  267. package/src/providers/claude-sdk-session.ts +7 -0
  268. package/src/providers/claude-session-events.ts +6 -1
  269. package/src/providers/dsh-adapter.ts +1 -1
  270. package/src/providers/opencode-adapter.ts +268 -37
  271. package/src/providers/opencode-billing.ts +86 -0
  272. package/src/providers/opencode-swarm-plugin.ts +51 -0
  273. package/src/providers/pi-mono-adapter.ts +398 -153
  274. package/src/providers/pricing-sources.md +37 -3
  275. package/src/providers/reasoning-effort.ts +27 -0
  276. package/src/providers/types.ts +11 -2
  277. package/src/rbac/legacy-policy.ts +8 -2
  278. package/src/rbac/permissions.ts +21 -0
  279. package/src/scheduler/scheduler.ts +4 -2
  280. package/src/scripts-runtime/api-client.ts +15 -1
  281. package/src/scripts-runtime/api-types.ts +9 -1
  282. package/src/scripts-runtime/ctx.ts +1 -1
  283. package/src/scripts-runtime/loader.ts +19 -7
  284. package/src/scripts-runtime/mcp-client.ts +30 -19
  285. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  286. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  287. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  288. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  289. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  290. package/src/server.ts +52 -18
  291. package/src/slack/actions.ts +29 -1
  292. package/src/slack/app.ts +14 -2
  293. package/src/slack/assistant.ts +36 -2
  294. package/src/slack/channel-join.ts +3 -16
  295. package/src/slack/commands.ts +9 -1
  296. package/src/slack/event-dedup.ts +1 -1
  297. package/src/slack/external-channel-egress.ts +113 -0
  298. package/src/slack/external-channel.ts +309 -0
  299. package/src/slack/handlers.ts +61 -0
  300. package/src/slack/render-v2.ts +40 -47
  301. package/src/slack/work-objects.ts +5 -0
  302. package/src/tasks/additive-buffer.ts +4 -2
  303. package/src/tasks/context-key.ts +94 -2
  304. package/src/tasks/model-pin.ts +67 -0
  305. package/src/tasks/worker-follow-up.ts +4 -31
  306. package/src/telemetry.ts +2 -1
  307. package/src/tests/acp-adapter.test.ts +42 -0
  308. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  309. package/src/tests/apps-catalog-feedback.test.ts +310 -0
  310. package/src/tests/apps-catalog-inputs.test.ts +70 -0
  311. package/src/tests/apps-catalog-table.test.ts +113 -0
  312. package/src/tests/azure-devops-boards-sync.test.ts +584 -0
  313. package/src/tests/azure-devops-html.test.ts +13 -0
  314. package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
  315. package/src/tests/base-prompt.test.ts +58 -54
  316. package/src/tests/chat-sdk-commands.test.ts +191 -0
  317. package/src/tests/chat-sdk-config.test.ts +334 -0
  318. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  319. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  320. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  321. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  322. package/src/tests/chat-sdk-harness.ts +146 -0
  323. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  324. package/src/tests/chat-sdk-relay.test.ts +470 -0
  325. package/src/tests/chat-sdk-reload.test.ts +128 -0
  326. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  327. package/src/tests/chat-sdk-slack.test.ts +268 -0
  328. package/src/tests/chat-sdk-state.test.ts +163 -0
  329. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  330. package/src/tests/chat-sdk-teams.test.ts +277 -0
  331. package/src/tests/chat-sdk-tools.test.ts +520 -0
  332. package/src/tests/chat-sdk-typing.test.ts +120 -0
  333. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  334. package/src/tests/claude-adapter-binary.test.ts +144 -0
  335. package/src/tests/claude-flag-settings.test.ts +114 -0
  336. package/src/tests/claude-managed-adapter.test.ts +1 -1
  337. package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
  338. package/src/tests/claude-sdk-process.fixture.ts +7 -0
  339. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  340. package/src/tests/claude-worker-parity.test.ts +2 -7
  341. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  342. package/src/tests/config-refs.test.ts +66 -0
  343. package/src/tests/context-key.test.ts +51 -0
  344. package/src/tests/cors-allowlist.test.ts +25 -0
  345. package/src/tests/create-page-tool.test.ts +18 -0
  346. package/src/tests/credential-check.test.ts +139 -0
  347. package/src/tests/credentials.test.ts +21 -0
  348. package/src/tests/db-runtime.test.ts +61 -0
  349. package/src/tests/db-task-read-contract.test.ts +20 -0
  350. package/src/tests/db-task-write-contract.test.ts +10 -0
  351. package/src/tests/deferred-task-wake.test.ts +52 -4
  352. package/src/tests/docs-index-footprint.test.js +3 -3
  353. package/src/tests/dsh-adapter.test.ts +1 -1
  354. package/src/tests/enabled-capabilities.test.ts +20 -0
  355. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  356. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  357. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  358. package/src/tests/explicit-model-validation.test.ts +105 -1
  359. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  360. package/src/tests/extensions-post-bridge.test.ts +51 -0
  361. package/src/tests/fetch-resolved-env.test.ts +128 -0
  362. package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
  363. package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
  364. package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
  365. package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
  366. package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
  367. package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
  368. package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
  369. package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
  370. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  371. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  372. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  373. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  374. package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
  375. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  376. package/src/tests/github-handlers.test.ts +35 -0
  377. package/src/tests/harness-provider-resolution.test.ts +12 -0
  378. package/src/tests/heartbeat-checklist.test.ts +3 -2
  379. package/src/tests/hook-runtime-close.test.ts +0 -2
  380. package/src/tests/http-api-integration.test.ts +0 -25
  381. package/src/tests/migration-046-budgets.test.ts +17 -16
  382. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  383. package/src/tests/model-tier-resolution.test.ts +93 -0
  384. package/src/tests/models-catalog.test.ts +42 -1
  385. package/src/tests/node-env-runtime.test.ts +98 -0
  386. package/src/tests/openai-compatible-providers.test.ts +134 -0
  387. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  388. package/src/tests/opencode-adapter.test.ts +348 -3
  389. package/src/tests/opencode-billing.test.ts +28 -0
  390. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  391. package/src/tests/opencode-shell-env.test.ts +145 -0
  392. package/src/tests/openrouter-base-url.test.ts +101 -151
  393. package/src/tests/page-proxy.test.ts +1 -1
  394. package/src/tests/pages-browser-sdk.test.ts +76 -0
  395. package/src/tests/pages-feedback.test.ts +1 -1
  396. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  397. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  398. package/src/tests/pi-mono-adapter.test.ts +385 -5
  399. package/src/tests/pi-shell-env.test.ts +170 -0
  400. package/src/tests/prompt-template-remaining.test.ts +35 -0
  401. package/src/tests/rbac-engine.test.ts +11 -3
  402. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  403. package/src/tests/runner-identity-refresh.test.ts +3 -0
  404. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  405. package/src/tests/script-connections-sql.test.ts +675 -0
  406. package/src/tests/script-connections.test.ts +7 -1
  407. package/src/tests/scripts-http.test.ts +201 -1
  408. package/src/tests/scripts-only-gating.test.ts +2 -3
  409. package/src/tests/scripts-runtime.test.ts +58 -40
  410. package/src/tests/secret-scrubber.test.ts +86 -0
  411. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  412. package/src/tests/seed-skills-retire.test.ts +182 -0
  413. package/src/tests/send-task-requester-gate.test.ts +58 -0
  414. package/src/tests/session-cost-recompute-unit.test.ts +94 -0
  415. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  416. package/src/tests/slack-channel-join.test.ts +4 -1
  417. package/src/tests/slack-external-channel-config.test.ts +65 -0
  418. package/src/tests/slack-external-channel-egress.test.ts +237 -0
  419. package/src/tests/slack-external-channel-guard.test.ts +280 -0
  420. package/src/tests/slack-external-channel-tools.test.ts +173 -0
  421. package/src/tests/slack-external-channel.test.ts +370 -0
  422. package/src/tests/slack-render-v2.test.ts +86 -10
  423. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  424. package/src/tests/spec-base-url.test.ts +16 -1
  425. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  426. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  427. package/src/tests/swarm-diff.test.ts +1 -1
  428. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  429. package/src/tests/task-session-prompts.test.ts +132 -0
  430. package/src/tests/task-tool-manifest.test.ts +14 -0
  431. package/src/tests/task-tools-ctx.test.ts +2 -0
  432. package/src/tests/tool-annotations.test.ts +6 -6
  433. package/src/tests/ui-logs-parser.test.ts +43 -0
  434. package/src/tests/used-by-route.test.ts +6 -0
  435. package/src/tests/vendored-openapi.test.ts +16 -1
  436. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  437. package/src/tests/workflow-swarm-script.test.ts +38 -0
  438. package/src/tests/workflow-wait-filter.test.ts +5 -3
  439. package/src/tools/chat-access.ts +41 -0
  440. package/src/tools/chat-open-dm.ts +59 -0
  441. package/src/tools/chat-post.ts +74 -0
  442. package/src/tools/chat-read-thread.ts +100 -0
  443. package/src/tools/chat-reply.ts +75 -0
  444. package/src/tools/create-page.ts +3 -13
  445. package/src/tools/defer-task.ts +17 -0
  446. package/src/tools/model-catalog/index.ts +2 -1
  447. package/src/tools/register-chat-channel.ts +139 -0
  448. package/src/tools/send-task.ts +24 -0
  449. package/src/tools/skills/skill-get.ts +17 -9
  450. package/src/tools/slack-post.ts +6 -0
  451. package/src/tools/slack-read.ts +8 -0
  452. package/src/tools/slack-reply.ts +6 -0
  453. package/src/tools/slack-start-thread.ts +6 -0
  454. package/src/tools/slack-update.ts +6 -0
  455. package/src/tools/slack-upload-file.ts +6 -0
  456. package/src/tools/swarm-config/get-config.ts +2 -1
  457. package/src/tools/swarm-config/list-config.ts +2 -1
  458. package/src/tools/swarm-config/set-config.ts +16 -3
  459. package/src/tools/tool-config.ts +8 -6
  460. package/src/types.ts +12 -2
  461. package/src/utils/config-env-export.ts +74 -0
  462. package/src/utils/credentials.test.ts +92 -0
  463. package/src/utils/credentials.ts +52 -4
  464. package/src/utils/harness-provider.ts +15 -6
  465. package/src/utils/node-env.ts +24 -0
  466. package/src/utils/runtime-model-catalog.ts +11 -1
  467. package/src/utils/secret-scrubber.ts +71 -2
  468. package/src/utils/task-tool-manifest.ts +19 -2
  469. package/src/workflows/executors/swarm-script.ts +1 -0
  470. package/templates/skills/agent-browser/SKILL.md +1 -1
  471. package/templates/skills/agent-browser/content.md +1 -1
  472. package/templates/skills/apps/SKILL.md +749 -0
  473. package/templates/skills/apps/content.md +60 -22
  474. package/templates/skills/chat-sdk/config.json +14 -0
  475. package/templates/skills/chat-sdk/content.md +84 -0
  476. package/templates/skills/composio-gmail/content.md +7 -0
  477. package/templates/skills/composio-google-docs/content.md +1 -1
  478. package/templates/skills/email-domain-health/config.json +14 -0
  479. package/templates/skills/email-domain-health/content.md +131 -0
  480. package/templates/skills/kv-storage/content.md +2 -2
  481. package/templates/skills/pages/content.md +8 -10
  482. package/templates/skills/qa-loom/config.json +14 -0
  483. package/templates/skills/qa-loom/content.md +187 -0
  484. package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
  485. package/templates/skills/qa-loom/files/run-qa.sh +57 -0
  486. package/templates/skills/slack-interaction/content.md +4 -0
  487. package/templates/skills/swarm-extensions/content.md +2 -0
  488. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  489. package/templates/skills/swarm-scripts/config.json +1 -1
  490. package/templates/skills/swarm-scripts/content.md +4 -2
  491. package/templates/skills/workflow-iterate/content.md +38 -0
  492. package/dist/artifact-eczsknc3.js +0 -3843
  493. package/dist/cli-9xsmdpdt.js +0 -56377
  494. package/dist/cli-bd957dgb.js +0 -153
  495. package/dist/cli-q51yprfq.js +0 -1104
  496. package/dist/cli-t1ze64h3.js +0 -811
  497. package/dist/index-9ch1n761.js +0 -1102
  498. package/src/artifact-sdk/index.ts +0 -2
  499. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  500. package/src/artifact-sdk/port.ts +0 -12
  501. package/src/artifact-sdk/server.ts +0 -183
  502. package/src/artifact-sdk/tunnel.ts +0 -19
  503. package/src/commands/artifact.ts +0 -249
  504. package/src/http/ecosystem.ts +0 -77
  505. package/src/tests/artifact-commands.test.ts +0 -92
  506. package/src/tests/artifact-sdk.test.ts +0 -793
  507. package/src/tests/register-service-security.test.ts +0 -194
  508. package/src/tools/list-services.ts +0 -107
  509. package/src/tools/register-service.ts +0 -177
  510. package/src/tools/unregister-service.ts +0 -73
  511. package/src/tools/update-service-status.ts +0 -96
  512. package/templates/skills/artifacts/config.json +0 -14
  513. package/templates/skills/artifacts/content.md +0 -344
  514. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  515. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  516. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  517. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  518. package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
  519. package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
  520. package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
  521. package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
  522. package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
  523. package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
  524. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  525. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -7,18 +7,20 @@
7
7
  * config wiring, steering, abort, and stream-death handling.
8
8
  */
9
9
 
10
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
10
+ import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
11
11
  import type { OpenCodeEvent } from "@opencode/sdk";
12
12
  import { normalizeSessionLogs, type SessionLogRecord } from "../../apps/ui/src/logs-parser";
13
13
  import {
14
14
  _setOpencodeSdkLoaderForTests,
15
15
  _setOpencodeSessionHooksForTests,
16
16
  applyOpenRouterBaseUrlOverride,
17
+ applyProviderKeySplice,
17
18
  OpencodeAdapter,
18
19
  type OpencodeSession,
19
20
  resolveOpencodeSdkSpecifier,
20
21
  } from "../providers/opencode-adapter";
21
22
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
23
+ import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
22
24
 
23
25
  const SESSION_ID = "ses_test";
24
26
 
@@ -63,6 +65,8 @@ type FakeHostOptions = {
63
65
  mcpStatuses?: Array<McpStatus | null>;
64
66
  /** Registered tool ids the plugin's tool lister returns. */
65
67
  tools?: string[];
68
+ /** Model responses handed to the plugin's `http.response` hook at startup. */
69
+ responses?: Response[];
66
70
  };
67
71
 
68
72
  /**
@@ -83,6 +87,11 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
83
87
  interrupts: [] as unknown[],
84
88
  closeCalls: 0,
85
89
  mcpListCalls: 0,
90
+ /** Session hooks the plugins registered, with their options. */
91
+ sessionHooks: {} as Record<
92
+ string,
93
+ { fn: (event: any) => unknown; options?: { providerID?: string } }
94
+ >,
86
95
  mcp: {
87
96
  list: async () => {
88
97
  const status = statuses[Math.min(host.mcpListCalls, statuses.length - 1)];
@@ -91,6 +100,7 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
91
100
  },
92
101
  },
93
102
  tools: opts.tools ?? ["read", "swarm_store-progress"],
103
+ responses: opts.responses ?? [],
94
104
  sessions: {
95
105
  create: async (input: unknown) => {
96
106
  host.created.push(input);
@@ -120,13 +130,14 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
120
130
  return host;
121
131
  }
122
132
 
123
- function installHost(host: FakeHost) {
133
+ function installHost(host: FakeHost, onCreate?: (options: Record<string, unknown>) => void) {
124
134
  _setOpencodeSdkLoaderForTests(async () => ({
125
135
  version: "2.0.28",
126
136
  sdk: {
127
137
  OpenCode: {
128
138
  create: async (options: Record<string, unknown>) => {
129
139
  host.createOptions = options;
140
+ onCreate?.(options);
130
141
  // Set the plugins up the way OpenCode does, with a stub context.
131
142
  const plugins = options.plugins as Array<{ setup: (ctx: unknown) => Promise<void> }>;
132
143
  for (const plugin of plugins) {
@@ -135,9 +146,22 @@ function installHost(host: FakeHost) {
135
146
  hook: async () => {},
136
147
  list: async () => host.tools.map((id) => ({ id })),
137
148
  },
138
- session: { hook: async () => {} },
149
+ // Registered when the session config carries a per-task env.
150
+ shell: { hook: async () => {} },
151
+ session: {
152
+ hook: async (
153
+ name: string,
154
+ fn: (event: unknown) => unknown,
155
+ options?: { providerID?: string },
156
+ ) => {
157
+ host.sessionHooks[name] = { fn, options };
158
+ },
159
+ },
139
160
  });
140
161
  }
162
+ for (const response of host.responses) {
163
+ await host.sessionHooks["http.response"]?.fn({ response });
164
+ }
141
165
  return host;
142
166
  },
143
167
  },
@@ -445,6 +469,118 @@ describe("OpencodeSession — event mapping", () => {
445
469
  });
446
470
  });
447
471
 
472
+ describe("OpencodeSession — cost of an OpenRouter session", () => {
473
+ // Task 06c031dd (opencode 2.0.28, openrouter/deepseek/deepseek-v4.1-flash),
474
+ // verbatim from session_logs: a title call, then three agent steps.
475
+ // `session.usage.updated` is cumulative; `output` excludes reasoning.
476
+ const deepseek = testConfig({ model: "openrouter/deepseek/deepseek-v4.1-flash" });
477
+ const usage = (cost: number, input: number, output: number, reasoning: number, read: number) =>
478
+ ev("session.usage.updated", {
479
+ sessionID: SESSION_ID,
480
+ cost,
481
+ tokens: { input, output, reasoning, cache: { read, write: 0 } },
482
+ });
483
+ const step = (cost: number, input: number, output: number, reasoning: number, read: number) =>
484
+ ev("session.step.ended", {
485
+ sessionID: SESSION_ID,
486
+ finish: "tool-calls",
487
+ cost,
488
+ tokens: { input, output, reasoning, cache: { read, write: 0 } },
489
+ });
490
+ const v2Events = [
491
+ usage(0.00010640000000000001, 1019, 9, 0, 0),
492
+ step(0.00023894900000000002, 51770, 53, 26, 0),
493
+ usage(0.000345349, 52789, 62, 26, 0),
494
+ step(0.0005280831000000001, 12963, 102, 7, 51840),
495
+ usage(0.0008734321000000001, 65752, 164, 33, 51840),
496
+ step(0.0005287785, 165, 15, 0, 64896),
497
+ usage(0.0014022106, 65917, 179, 33, 116736),
498
+ succeeded,
499
+ ];
500
+
501
+ // OpenRouter's final SSE chunk for deepseek/deepseek-v4.1-flash, served by
502
+ // Together (live probe, 2026-10-14): `usage.cost` is the billed USD.
503
+ const openRouterSse = (cost: number, cached: number) =>
504
+ new Response(
505
+ [
506
+ ": OPENROUTER PROCESSING",
507
+ "",
508
+ `data: ${JSON.stringify({ id: "gen-1", choices: [{ index: 0, delta: { content: "pong" } }] })}`,
509
+ "",
510
+ `data: ${JSON.stringify({
511
+ id: "gen-1",
512
+ provider: "Together",
513
+ model: "deepseek/deepseek-v4.1-flash",
514
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
515
+ usage: {
516
+ prompt_tokens: 26545,
517
+ completion_tokens: 15,
518
+ total_tokens: 26560,
519
+ cost,
520
+ is_byok: false,
521
+ prompt_tokens_details: { cached_tokens: cached, cache_write_tokens: 0 },
522
+ completion_tokens_details: { reasoning_tokens: 11 },
523
+ },
524
+ })}`,
525
+ "",
526
+ "data: [DONE]",
527
+ "",
528
+ ].join("\n"),
529
+ { headers: { "content-type": "text/event-stream" } },
530
+ );
531
+
532
+ test("reports v2 tokens with reasoning billed as output", async () => {
533
+ const { result } = await runSession(v2Events, deepseek);
534
+ expect(result.cost).toMatchObject({
535
+ totalCostUsd: 0.0014022106,
536
+ inputTokens: 65917,
537
+ outputTokens: 212,
538
+ reasoningOutputTokens: 33,
539
+ cacheReadTokens: 116736,
540
+ cacheWriteTokens: 0,
541
+ numTurns: 3,
542
+ });
543
+ expect(result.cost?.harnessCostBilled).toBeUndefined();
544
+ });
545
+
546
+ test("reports OpenRouter's billed cost when every response carried one", async () => {
547
+ const { result } = await runSession(v2Events, deepseek, {
548
+ responses: [
549
+ openRouterSse(0.0079815, 0),
550
+ openRouterSse(0.000191676, 26496),
551
+ new Response('{"error":{"message":"rate limited"}}', { status: 429 }),
552
+ ],
553
+ });
554
+ expect(result.cost?.harnessCostBilled).toBe(true);
555
+ expect(result.cost?.totalCostUsd).toBeCloseTo(0.0079815 + 0.000191676, 12);
556
+ expect(result.cost?.inputTokens).toBe(65917);
557
+ });
558
+
559
+ test("falls back to OpenCode's estimate when a response has no billed cost", async () => {
560
+ const { result } = await runSession(v2Events, deepseek, {
561
+ responses: [openRouterSse(0.0079815, 0), new Response("data: [DONE]\n\n")],
562
+ });
563
+ expect(result.cost?.harnessCostBilled).toBeUndefined();
564
+ expect(result.cost?.totalCostUsd).toBe(0.0014022106);
565
+ });
566
+
567
+ test("hooks only OpenRouter responses, and only for an OpenRouter model", async () => {
568
+ const openrouter = await runSession([succeeded], deepseek);
569
+ expect(openrouter.host.sessionHooks["http.response"]?.options).toEqual({
570
+ providerID: "openrouter",
571
+ });
572
+ const direct = await runSession([succeeded], testConfig({ model: "anthropic/claude-haiku" }));
573
+ expect(direct.host.sessionHooks["http.response"]).toBeUndefined();
574
+ });
575
+
576
+ test("skips title generation", async () => {
577
+ const { host } = await runSession([succeeded]);
578
+ const event: { result?: string } = {};
579
+ await host.sessionHooks.title?.fn(event);
580
+ expect(event.result).toBe("swarm task task-1");
581
+ });
582
+ });
583
+
448
584
  describe("OpencodeSession — persisted rows render in the dashboard", () => {
449
585
  test("text, tools and errors render once each, in order", async () => {
450
586
  const text = (ordinal: number, value: string, messageID = "msg_1") => [
@@ -710,3 +846,212 @@ describe("OpenRouter attribution config", () => {
710
846
  expect(config).toEqual({});
711
847
  });
712
848
  });
849
+
850
+ describe("provider key splice (one picked key per task)", () => {
851
+ const PICKED = "sk-picked-member-0123456789";
852
+ const POOL = "sk-pool-member-aaaaaaaaaa,sk-pool-member-bbbbbbbbbb";
853
+ const SPLICE_VARS = [
854
+ "NAN_API_KEY",
855
+ "HELMCODE_API_NAN",
856
+ "HELMCODE_API_KEY",
857
+ "OPENROUTER_API_KEY",
858
+ "OPENROUTER_BASE_URL",
859
+ ];
860
+ const saved: Record<string, string | undefined> = {};
861
+ beforeAll(() => {
862
+ for (const key of SPLICE_VARS) saved[key] = process.env[key];
863
+ });
864
+ afterEach(() => {
865
+ for (const key of SPLICE_VARS) {
866
+ if (saved[key] === undefined) delete process.env[key];
867
+ else process.env[key] = saved[key];
868
+ }
869
+ clearVolatileSecretsForTesting();
870
+ });
871
+
872
+ test("nan: env [] and the per-task key, even with a pool in process.env", async () => {
873
+ process.env.NAN_API_KEY = POOL;
874
+ const { host } = await runSession(
875
+ [succeeded],
876
+ testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: PICKED } }),
877
+ );
878
+ const nan = configOf(host).provider.nan;
879
+ expect(nan.env).toEqual([]);
880
+ expect(nan.options.apiKey).toBe(PICKED);
881
+ });
882
+
883
+ test("nan: the HELMCODE_API_NAN alias is read when NAN_API_KEY is empty", () => {
884
+ const config = {};
885
+ const picked = applyProviderKeySplice(config, "nan/qwen3.6", { HELMCODE_API_NAN: PICKED });
886
+ expect(picked).toEqual({ key: PICKED, name: "HELMCODE_API_NAN" });
887
+ expect(config).toEqual({ provider: { nan: { env: [], options: { apiKey: PICKED } } } });
888
+ });
889
+
890
+ test("a pool that reaches the splice unpicked yields its first member", () => {
891
+ const config = {};
892
+ applyProviderKeySplice(config, "nan/qwen3.6", {
893
+ NAN_API_KEY: ` ${PICKED} , sk-other-0000000000`,
894
+ });
895
+ expect(config).toEqual({ provider: { nan: { env: [], options: { apiKey: PICKED } } } });
896
+ });
897
+
898
+ test("helmcode: full custom block with limits, reasoning options merged in", async () => {
899
+ const { host } = await runSession(
900
+ [succeeded],
901
+ testConfig({
902
+ model: "helmcode/glm5.3",
903
+ reasoningEffort: "high",
904
+ env: { HELMCODE_API_KEY: PICKED },
905
+ }),
906
+ );
907
+ const helmcode = configOf(host).provider.helmcode;
908
+ expect(helmcode).toMatchObject({
909
+ npm: "@ai-sdk/openai-compatible",
910
+ name: "Helmcode",
911
+ env: [],
912
+ options: { baseURL: "https://api.helmcode.com/v1", apiKey: PICKED },
913
+ });
914
+ expect(helmcode.models["glm5.3"]).toEqual({
915
+ name: "GLM-5.3",
916
+ tool_call: true,
917
+ reasoning: true,
918
+ limit: { context: 1048576, output: 32768 },
919
+ options: { reasoningEffort: "high" },
920
+ });
921
+ expect(Object.keys(helmcode.models)).toEqual([
922
+ "deepseek-v4-flash",
923
+ "glm5.3",
924
+ "glm5.3-flash",
925
+ "qwen3.6",
926
+ "gemma4",
927
+ ]);
928
+ });
929
+
930
+ test("helmcode: a model missing from the table still gets an entry", () => {
931
+ const config: Record<string, any> = {};
932
+ applyProviderKeySplice(config, "helmcode/brand-new", { HELMCODE_API_KEY: PICKED });
933
+ expect(config.provider.helmcode.models["brand-new"]).toEqual({
934
+ name: "brand-new",
935
+ tool_call: true,
936
+ });
937
+ });
938
+
939
+ test("openrouter gateway: baseURL kept next to env [] and the picked key", async () => {
940
+ process.env.OPENROUTER_API_KEY = POOL;
941
+ const { host } = await runSession(
942
+ [succeeded],
943
+ testConfig({
944
+ model: "openrouter/deepseek/deepseek-v4-flash",
945
+ env: { OPENROUTER_API_KEY: PICKED, OPENROUTER_BASE_URL: "http://127.0.0.1:1234/v1" },
946
+ }),
947
+ );
948
+ expect(configOf(host).provider.openrouter).toEqual({
949
+ env: [],
950
+ options: { apiKey: PICKED, baseURL: "http://127.0.0.1:1234/v1" },
951
+ });
952
+ });
953
+
954
+ test("openrouter host: attribution headers kept next to env [] and the picked key", async () => {
955
+ const { host } = await runSession(
956
+ [succeeded],
957
+ testConfig({
958
+ model: "openrouter/deepseek/deepseek-v4-flash",
959
+ env: { OPENROUTER_API_KEY: PICKED },
960
+ }),
961
+ );
962
+ const openrouter = configOf(host).provider.openrouter;
963
+ expect(openrouter.env).toEqual([]);
964
+ expect(openrouter.options.apiKey).toBe(PICKED);
965
+ expect(openrouter.options.headers).toMatchObject({ "X-Title": "Agent Swarm" });
966
+ });
967
+
968
+ test("no splice without a key, or for providers outside the splice", () => {
969
+ for (const model of ["nan/qwen3.6", "helmcode/glm5.3", "openrouter/x/y"]) {
970
+ const config = {};
971
+ expect(applyProviderKeySplice(config, model, {})).toBeUndefined();
972
+ expect(config).toEqual({});
973
+ }
974
+ for (const model of ["anthropic/claude-sonnet-4-5", "openai/gpt-5", "glm5.3", "", undefined]) {
975
+ const config = {};
976
+ applyProviderKeySplice(config, model, {
977
+ ANTHROPIC_API_KEY: PICKED,
978
+ OPENAI_API_KEY: PICKED,
979
+ NAN_API_KEY: PICKED,
980
+ });
981
+ expect(config).toEqual({});
982
+ }
983
+ });
984
+ });
985
+
986
+ describe("spliced key never reaches logs, events, or errors", () => {
987
+ // Not in process.env: the key a dashboard save delivers to the per-task env only.
988
+ const SENTINEL = "sk-nan-sentinel-do-not-log-4f9c2e";
989
+ let captured: string[] = [];
990
+ let spies: Array<ReturnType<typeof spyOn>> = [];
991
+ beforeAll(() => {
992
+ delete process.env.NAN_API_KEY;
993
+ });
994
+ afterEach(() => {
995
+ for (const spy of spies) spy.mockRestore();
996
+ spies = [];
997
+ captured = [];
998
+ clearVolatileSecretsForTesting();
999
+ });
1000
+ function captureConsole() {
1001
+ for (const method of ["log", "info", "warn", "error", "debug"] as const) {
1002
+ spies.push(
1003
+ spyOn(console, method).mockImplementation((...args: unknown[]) => {
1004
+ captured.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
1005
+ }),
1006
+ );
1007
+ }
1008
+ }
1009
+
1010
+ test("SDK log lines and a provider failure that echo the config are scrubbed", async () => {
1011
+ captureConsole();
1012
+ const host = makeFakeHost([
1013
+ ev("session.execution.failed", {
1014
+ sessionID: SESSION_ID,
1015
+ error: { message: `401 Invalid API key ${SENTINEL}` },
1016
+ }),
1017
+ ]);
1018
+ installHost(host, (options) => {
1019
+ const content = (options.config as { content: string }).content;
1020
+ const log = options.log as { emit: (entry: { message: string }) => void };
1021
+ log.emit({ message: `config: ${content}` });
1022
+ });
1023
+ const session = (await new OpencodeAdapter().createSession(
1024
+ testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: SENTINEL } }),
1025
+ )) as OpencodeSession;
1026
+ const emitted: ProviderEvent[] = [];
1027
+ session.onEvent((e) => emitted.push(e));
1028
+ const result = await session.waitForCompletion();
1029
+
1030
+ // Positive control: the key did reach the SDK config.
1031
+ expect(configOf(host).provider.nan.options.apiKey).toBe(SENTINEL);
1032
+ expect(captured.some((line) => line.includes("[opencode] config:"))).toBe(true);
1033
+ expect(result.isError).toBe(true);
1034
+
1035
+ for (const line of captured) expect(line).not.toContain(SENTINEL);
1036
+ expect(JSON.stringify(emitted)).not.toContain(SENTINEL);
1037
+ expect(result.failureReason ?? "").not.toContain(SENTINEL);
1038
+ });
1039
+
1040
+ test("a host start failure that echoes the config is scrubbed before it propagates", async () => {
1041
+ captureConsole();
1042
+ const host = makeFakeHost([]);
1043
+ installHost(host, (options) => {
1044
+ const content = (options.config as { content: string }).content;
1045
+ throw new Error(`invalid config ${content}`);
1046
+ });
1047
+ const error = await new OpencodeAdapter()
1048
+ .createSession(testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: SENTINEL } }))
1049
+ .then(
1050
+ () => undefined,
1051
+ (err: unknown) => err as Error,
1052
+ );
1053
+ expect(error?.message).toStartWith("opencode host failed to start: invalid config");
1054
+ expect(error?.message).not.toContain(SENTINEL);
1055
+ for (const line of captured) expect(line).not.toContain(SENTINEL);
1056
+ });
1057
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { OpenRouterBilling, parseOpenRouterBilledCost } from "../providers/opencode-billing";
3
+
4
+ describe("parseOpenRouterBilledCost", () => {
5
+ test("reads usage.cost from a non-streamed JSON body", () => {
6
+ expect(parseOpenRouterBilledCost('{"id":"gen-1","usage":{"cost":0.0042}}')).toBe(0.0042);
7
+ });
8
+
9
+ test("a BYOK cost is only OpenRouter's fee, so it is not the bill", () => {
10
+ expect(parseOpenRouterBilledCost('{"usage":{"cost":0.0001,"is_byok":true}}')).toBeNull();
11
+ });
12
+
13
+ test("a stream without usage has no billed cost", () => {
14
+ expect(parseOpenRouterBilledCost('data: {"choices":[]}\n\ndata: [DONE]\n\n')).toBeNull();
15
+ });
16
+ });
17
+
18
+ describe("OpenRouterBilling", () => {
19
+ test("no observed request reports nothing", async () => {
20
+ expect(await new OpenRouterBilling().settle(10)).toBeNull();
21
+ });
22
+
23
+ test("a body still streaming at the deadline leaves the bill incomplete", async () => {
24
+ const billing = new OpenRouterBilling();
25
+ billing.observe(new Response(new ReadableStream({ start() {} })));
26
+ expect(await billing.settle(10)).toEqual({ costUsd: 0, complete: false });
27
+ });
28
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The provider key splice and the `helmcode` block, run through the real
3
+ * `@opencode/sdk` embedded host against a fake OpenAI-compatible server.
4
+ *
5
+ * `applyProviderKeySplice` builds the provider entry; the test only repoints
6
+ * `options.baseURL` at a local `Bun.serve({ port: 0 })` that answers
7
+ * `/v1/chat/completions`, then checks the request OpenCode sent: the picked
8
+ * pool member as the bearer (not the comma pool sitting in `process.env`) and
9
+ * the bare model id. The model catalog is bundled in the SDK, and no request
10
+ * leaves the machine. The control test pins why `env: []` is needed: without
11
+ * it, the pool in `process.env` beats `options.apiKey`.
12
+ */
13
+
14
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
15
+ import { mkdtempSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { OpenCode, type OpenCodeEvent } from "@opencode/sdk";
19
+ import { applyProviderKeySplice, type OpencodeConfig } from "../providers/opencode-adapter";
20
+
21
+ const TEST_TIMEOUT_MS = 60_000;
22
+
23
+ const PICKED = "sk-helmcode-picked-live-0123456789";
24
+ const POOL_IN_PROCESS_ENV = "sk-helmcode-wrong-aaaaaaaaaa,sk-helmcode-wrong-bbbbbbbbbb";
25
+
26
+ type SeenRequest = { path: string; authorization: string | null; model?: string };
27
+
28
+ let server: ReturnType<typeof Bun.serve>;
29
+ let directory: string;
30
+ const seen: SeenRequest[] = [];
31
+ let prevHelmcodeKey: string | undefined;
32
+
33
+ /** One streamed (or plain) assistant turn that says "pong". */
34
+ function completion(model: string, stream: boolean): Response {
35
+ const base = { id: "chatcmpl-test", created: 0, model };
36
+ const usage = { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 };
37
+ if (!stream) {
38
+ return Response.json({
39
+ ...base,
40
+ object: "chat.completion",
41
+ choices: [
42
+ {
43
+ index: 0,
44
+ message: { role: "assistant", content: "pong" },
45
+ finish_reason: "stop",
46
+ },
47
+ ],
48
+ usage,
49
+ });
50
+ }
51
+ const chunk = (body: Record<string, unknown>) =>
52
+ `data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", ...body })}\n\n`;
53
+ const sse =
54
+ chunk({
55
+ choices: [{ index: 0, delta: { role: "assistant", content: "pong" }, finish_reason: null }],
56
+ }) +
57
+ chunk({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage }) +
58
+ "data: [DONE]\n\n";
59
+ return new Response(sse, { headers: { "content-type": "text/event-stream" } });
60
+ }
61
+
62
+ beforeAll(() => {
63
+ directory = mkdtempSync(join(tmpdir(), "opencode-helmcode-sdk-"));
64
+ server = Bun.serve({
65
+ port: 0,
66
+ async fetch(req) {
67
+ const url = new URL(req.url);
68
+ const body = req.method === "POST" ? ((await req.json().catch(() => ({}))) as any) : {};
69
+ seen.push({
70
+ path: url.pathname,
71
+ authorization: req.headers.get("authorization"),
72
+ model: body?.model,
73
+ });
74
+ if (url.pathname.endsWith("/chat/completions")) {
75
+ return completion(String(body?.model ?? ""), body?.stream === true);
76
+ }
77
+ return new Response("not found", { status: 404 });
78
+ },
79
+ });
80
+ // The pool a production entrypoint exports raw; the splice must beat it.
81
+ prevHelmcodeKey = process.env.HELMCODE_API_KEY;
82
+ process.env.HELMCODE_API_KEY = POOL_IN_PROCESS_ENV;
83
+ });
84
+
85
+ afterAll(() => {
86
+ server.stop(true);
87
+ rmSync(directory, { recursive: true, force: true });
88
+ if (prevHelmcodeKey === undefined) delete process.env.HELMCODE_API_KEY;
89
+ else process.env.HELMCODE_API_KEY = prevHelmcodeKey;
90
+ });
91
+
92
+ async function runOnePrompt(config: OpencodeConfig): Promise<string> {
93
+ const host = await OpenCode.create({
94
+ config: { content: JSON.stringify(config) },
95
+ plugins: [],
96
+ log: { level: "error", emit: () => {} },
97
+ });
98
+ try {
99
+ const session = await host.sessions.create({ location: { directory } });
100
+ const done = (async () => {
101
+ for await (const event of host.events.subscribe() as AsyncIterable<OpenCodeEvent>) {
102
+ const data = (event as { data?: { sessionID?: string; error?: unknown } }).data;
103
+ if (data?.sessionID !== session.id) continue;
104
+ if (event.type === "session.execution.succeeded") return "succeeded";
105
+ if (event.type === "session.execution.failed") {
106
+ return `failed: ${JSON.stringify(data.error)}`;
107
+ }
108
+ }
109
+ return "stream ended";
110
+ })();
111
+ await host.sessions.prompt({ sessionID: session.id, text: "say pong", delivery: "queue" });
112
+ return await done;
113
+ } finally {
114
+ await host.close();
115
+ }
116
+ }
117
+
118
+ describe("provider key splice through the real OpenCode host", () => {
119
+ test(
120
+ "sends the picked key and the bare model id to the provider's baseURL",
121
+ async () => {
122
+ seen.length = 0;
123
+ const model = "helmcode/glm5.3";
124
+ const config: OpencodeConfig = { model };
125
+ const spliced = applyProviderKeySplice(config, model, { HELMCODE_API_KEY: PICKED });
126
+ expect(spliced?.name).toBe("HELMCODE_API_KEY");
127
+ const helmcode = config.provider?.helmcode;
128
+ if (!helmcode?.options) throw new Error("helmcode block missing");
129
+ helmcode.options.baseURL = `http://127.0.0.1:${server.port}/v1`;
130
+
131
+ const outcome = await runOnePrompt(config);
132
+
133
+ const chats = seen.filter((r) => r.path === "/v1/chat/completions");
134
+ expect(chats.length).toBeGreaterThan(0);
135
+ for (const chat of chats) {
136
+ expect(chat.authorization).toBe(`Bearer ${PICKED}`);
137
+ expect(chat.model).toBe("glm5.3");
138
+ }
139
+ expect(outcome).toBe("succeeded");
140
+ },
141
+ TEST_TIMEOUT_MS,
142
+ );
143
+
144
+ test(
145
+ "nan (built-in provider): env [] plus the picked key beats a pool in process.env",
146
+ async () => {
147
+ const prevNan = process.env.NAN_API_KEY;
148
+ process.env.NAN_API_KEY = POOL_IN_PROCESS_ENV;
149
+ seen.length = 0;
150
+ try {
151
+ const model = "nan/glm5.3-flash";
152
+ const config: OpencodeConfig = { model };
153
+ const spliced = applyProviderKeySplice(config, model, { NAN_API_KEY: PICKED });
154
+ expect(spliced?.name).toBe("NAN_API_KEY");
155
+ const nan = config.provider?.nan;
156
+ if (!nan?.options) throw new Error("nan block missing");
157
+ nan.options.baseURL = `http://127.0.0.1:${server.port}/v1`;
158
+
159
+ const outcome = await runOnePrompt(config);
160
+
161
+ const chats = seen.filter((r) => r.path === "/v1/chat/completions");
162
+ expect(chats.length).toBeGreaterThan(0);
163
+ for (const chat of chats) {
164
+ expect(chat.authorization).toBe(`Bearer ${PICKED}`);
165
+ expect(chat.model).toBe("glm5.3-flash");
166
+ }
167
+ expect(outcome).toBe("succeeded");
168
+ } finally {
169
+ if (prevNan === undefined) delete process.env.NAN_API_KEY;
170
+ else process.env.NAN_API_KEY = prevNan;
171
+ }
172
+ },
173
+ TEST_TIMEOUT_MS,
174
+ );
175
+
176
+ test(
177
+ "nan control: without the splice the pool in process.env reaches the provider",
178
+ async () => {
179
+ const prevNan = process.env.NAN_API_KEY;
180
+ process.env.NAN_API_KEY = POOL_IN_PROCESS_ENV;
181
+ seen.length = 0;
182
+ try {
183
+ const model = "nan/glm5.3-flash";
184
+ const config: OpencodeConfig = {
185
+ model,
186
+ provider: {
187
+ nan: { options: { apiKey: PICKED, baseURL: `http://127.0.0.1:${server.port}/v1` } },
188
+ },
189
+ };
190
+ await runOnePrompt(config);
191
+ const chats = seen.filter((r) => r.path === "/v1/chat/completions");
192
+ expect(chats.length).toBeGreaterThan(0);
193
+ expect(chats[0]?.authorization).not.toBe(`Bearer ${PICKED}`);
194
+ } finally {
195
+ if (prevNan === undefined) delete process.env.NAN_API_KEY;
196
+ else process.env.NAN_API_KEY = prevNan;
197
+ }
198
+ },
199
+ TEST_TIMEOUT_MS,
200
+ );
201
+ });