@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
@@ -9,13 +9,28 @@
9
9
 
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
+ import {
13
+ baseUrlOverride,
14
+ ignoredBaseUrlVar,
15
+ type OpenAICompatibleProvider,
16
+ openAICompatibleKeyVars,
17
+ openAICompatibleProvider,
18
+ openAICompatibleProviders,
19
+ providerBaseUrl,
20
+ providerForKeyVar,
21
+ providerKeyVars,
22
+ } from "@desplega/model-catalog";
12
23
  import type { OpenCode, OpenCodeEvent } from "@opencode/sdk";
13
24
  import {
14
25
  CONTEXT_FORMULA,
15
26
  clampContextPercent,
16
27
  getContextWindowSize,
17
28
  } from "../utils/context-window";
18
- import { validateOpencodeCredentials } from "../utils/credentials";
29
+ import {
30
+ openAICompatibleKeyLabels,
31
+ splitCredentialPool,
32
+ validateOpencodeCredentials,
33
+ } from "../utils/credentials";
19
34
  import { isEnvFlagEnabled } from "../utils/env-flag";
20
35
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
21
36
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
@@ -24,9 +39,10 @@ import {
24
39
  getOpenRouterAttributionHeaders,
25
40
  getOpenRouterBaseUrl,
26
41
  } from "../utils/openrouter-base-url";
27
- import { scrubSecrets } from "../utils/secret-scrubber";
42
+ import { registerVolatileSecret, scrubSecrets } from "../utils/secret-scrubber";
28
43
  import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
29
44
  import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
45
+ import { OpenRouterBilling } from "./opencode-billing";
30
46
  import { createOpencodeSwarmPlugin, type OpencodeToolLister } from "./opencode-swarm-plugin";
31
47
  import {
32
48
  apiHeaders,
@@ -60,8 +76,12 @@ export type OpencodeConfig = {
60
76
  provider?: Record<
61
77
  string,
62
78
  {
79
+ /** Env var names OpenCode reads the key from; `[]` stops it reading `process.env`. */
80
+ env?: string[];
81
+ npm?: string;
82
+ name?: string;
63
83
  options?: Record<string, unknown>;
64
- models?: Record<string, { options?: Record<string, unknown> }>;
84
+ models?: Record<string, { options?: Record<string, unknown>; [key: string]: unknown }>;
65
85
  }
66
86
  >;
67
87
  [key: string]: unknown;
@@ -79,18 +99,41 @@ function opencodeModelToCredKey(modelStr: string | undefined): string | null {
79
99
  if (provider === "anthropic") return "ANTHROPIC_API_KEY";
80
100
  if (provider === "openrouter") return "OPENROUTER_API_KEY";
81
101
  if (provider === "openai") return "OPENAI_API_KEY";
102
+ // OpenAI-compatible registry providers (`nan`, `helmcode`).
103
+ const direct = openAICompatibleProvider(provider);
104
+ if (direct) return direct.envVar;
82
105
  }
83
106
  return null;
84
107
  }
85
108
 
109
+ /**
110
+ * Env var names that satisfy `requiredKey`. The runner copies a registry
111
+ * key alias (`HELMCODE_API_NAN`) into its canonical var per task, but a
112
+ * check that reads `process.env` directly sees only the alias.
113
+ */
114
+ function credKeyNames(requiredKey: string): readonly string[] {
115
+ const direct = providerForKeyVar(requiredKey);
116
+ return direct ? providerKeyVars(direct) : [requiredKey];
117
+ }
118
+
119
+ /** Keys that satisfy opencode when no model pins a provider. */
120
+ const OPENCODE_ANY_KEY_VARS = [
121
+ "OPENROUTER_API_KEY",
122
+ "ANTHROPIC_API_KEY",
123
+ "OPENAI_API_KEY",
124
+ ...openAICompatibleKeyVars(),
125
+ ];
126
+
86
127
  /**
87
128
  * Opencode is satisfied by ANY of:
88
129
  * 1. `~/.local/share/opencode/auth.json` exists (the file `opencode auth login`
89
130
  * writes; OpenCode 2 imports it into its credential store on start).
90
131
  * 2. `MODEL_OVERRIDE` resolves to a provider-prefixed model — only that
91
- * provider's key is required.
132
+ * provider's key is required (a registry provider also accepts its key
133
+ * aliases: `nan/*` takes `HELMCODE_API_NAN`).
92
134
  * 3. Otherwise any one of OPENROUTER_API_KEY / ANTHROPIC_API_KEY /
93
- * OPENAI_API_KEY suffices.
135
+ * OPENAI_API_KEY / a registry provider key (canonical or alias)
136
+ * suffices.
94
137
  */
95
138
  export function checkOpencodeCredentials(
96
139
  env: Record<string, string | undefined>,
@@ -105,7 +148,7 @@ export function checkOpencodeCredentials(
105
148
 
106
149
  const requiredKey = opencodeModelToCredKey(env.MODEL_OVERRIDE);
107
150
  if (requiredKey) {
108
- if (env[requiredKey]) {
151
+ if (credKeyNames(requiredKey).some((name) => env[name])) {
109
152
  return { ready: true, missing: [], satisfiedBy: "env" };
110
153
  }
111
154
  return {
@@ -115,13 +158,26 @@ export function checkOpencodeCredentials(
115
158
  };
116
159
  }
117
160
 
118
- if (env.OPENROUTER_API_KEY || env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY) {
161
+ if (OPENCODE_ANY_KEY_VARS.some((name) => env[name])) {
119
162
  return { ready: true, missing: [], satisfiedBy: "env" };
120
163
  }
121
164
  return {
122
165
  ready: false,
123
- missing: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", authFile],
124
- hint: "Set one of OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY (any one suffices), or run `opencode auth login` to create ~/.local/share/opencode/auth.json.",
166
+ missing: [
167
+ "OPENROUTER_API_KEY",
168
+ "ANTHROPIC_API_KEY",
169
+ "OPENAI_API_KEY",
170
+ ...openAICompatibleProviders().map((p) => p.envVar),
171
+ authFile,
172
+ ],
173
+ hint: `Set one of ${[
174
+ "OPENROUTER_API_KEY",
175
+ "ANTHROPIC_API_KEY",
176
+ "OPENAI_API_KEY",
177
+ ...openAICompatibleKeyLabels(),
178
+ ].join(
179
+ " / ",
180
+ )} (any one suffices), or run \`opencode auth login\` to create ~/.local/share/opencode/auth.json.`,
125
181
  };
126
182
  }
127
183
 
@@ -216,9 +272,156 @@ export function applyOpenRouterBaseUrlOverride(
216
272
  };
217
273
  }
218
274
 
275
+ /**
276
+ * OpenCode provider block for an OpenAI-compatible registry provider that
277
+ * OpenCode does not ship (`catalog: "registry"`, e.g. `helmcode`). `apiKey` is
278
+ * a literal key (embedded host) or an `{env:NAME}` reference (ACP
279
+ * subprocess). `baseUrl` is the `<ID>_BASE_URL` override or the registry
280
+ * default. `env: []` keeps OpenCode from reading a key out of `process.env`.
281
+ * A `modelId` missing from the registry (accepted through `allowCustomModel`)
282
+ * gets a bare entry so OpenCode still resolves it instead of falling back to
283
+ * its default model.
284
+ */
285
+ export function buildOpenAICompatibleProviderBlock(
286
+ provider: OpenAICompatibleProvider,
287
+ apiKey: string,
288
+ modelId?: string,
289
+ baseUrl: string = provider.baseUrl,
290
+ ): NonNullable<OpencodeConfig["provider"]>[string] {
291
+ const models: Record<string, Record<string, unknown>> = {};
292
+ for (const m of provider.models) {
293
+ models[m.id] = {
294
+ name: m.name,
295
+ tool_call: true,
296
+ reasoning: m.reasoning,
297
+ limit: { context: m.contextWindow, output: m.maxOutputTokens },
298
+ };
299
+ }
300
+ if (modelId && !models[modelId]) models[modelId] = { name: modelId, tool_call: true };
301
+ return {
302
+ npm: provider.npm,
303
+ name: provider.displayName,
304
+ env: [],
305
+ options: { baseURL: baseUrl, apiKey },
306
+ models,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Built-in OpenCode providers whose key OpenCode reads from `process.env`:
312
+ * `openrouter` plus every registry provider models.dev lists
313
+ * (`catalog: "models.dev"`, e.g. `nan`), which OpenCode ships built in.
314
+ */
315
+ function envBoundKeyVars(providerId: string): readonly string[] | undefined {
316
+ if (providerId === "openrouter") return ["OPENROUTER_API_KEY"];
317
+ const direct = openAICompatibleProvider(providerId);
318
+ return direct?.catalog === "models.dev" ? providerKeyVars(direct) : undefined;
319
+ }
320
+
321
+ /** Registry providers OpenCode does not ship (`catalog: "registry"`); they get a full custom block. */
322
+ function customBlockProvider(providerId: string): OpenAICompatibleProvider | undefined {
323
+ const direct = openAICompatibleProvider(providerId);
324
+ return direct?.catalog === "registry" ? direct : undefined;
325
+ }
326
+
327
+ /**
328
+ * One key from the first non-empty var in `names`. The runner's per-task env
329
+ * already holds one picked pool member; a value that still carries a comma
330
+ * pool (no runner, or `process.env`) yields its first member.
331
+ */
332
+ function pickProviderKey(
333
+ env: Record<string, string | undefined>,
334
+ names: readonly string[],
335
+ ): { key: string; name: string } | undefined {
336
+ for (const name of names) {
337
+ const value = env[name]?.trim();
338
+ if (!value) continue;
339
+ const key = splitCredentialPool(value)[0];
340
+ if (key) return { key, name };
341
+ }
342
+ return undefined;
343
+ }
344
+
345
+ /**
346
+ * Hand the embedded host exactly one key for the active model's provider.
347
+ *
348
+ * Embedded OpenCode reads a built-in provider's key from the shared
349
+ * `process.env`, and that value beats `options.apiKey`. The entrypoint exports
350
+ * a credential pool there as a raw comma list, and a key saved from the
351
+ * dashboard after boot never reaches `process.env` at all, so both 401 without
352
+ * this splice. For `openrouter` and the built-in registry providers (`nan`,
353
+ * `vercel`) it sets `env: []` plus `options.apiKey` (and `options.baseURL`
354
+ * when `<ID>_BASE_URL` is set; a `baseUrlLocked` provider gets none); for registry providers OpenCode does not ship
355
+ * (`helmcode`) it writes the full custom provider block at
356
+ * `<ID>_BASE_URL` or the registry default. No splice when the key is absent
357
+ * (credStatus reports that).
358
+ *
359
+ * The key lives only in the in-memory config string handed to the SDK.
360
+ * Mutates `opencodeConfig` in place and returns the env var the key came from,
361
+ * or `undefined` when nothing was spliced.
362
+ */
363
+ export function applyProviderKeySplice(
364
+ opencodeConfig: OpencodeConfig,
365
+ model: string | undefined,
366
+ env: Record<string, string | undefined>,
367
+ ): { key: string; name: string } | undefined {
368
+ const slash = model?.indexOf("/") ?? -1;
369
+ if (!model || slash <= 0) return undefined;
370
+ const providerId = model.slice(0, slash);
371
+ const existing = opencodeConfig.provider?.[providerId];
372
+
373
+ const custom = customBlockProvider(providerId);
374
+ if (custom) {
375
+ const picked = pickProviderKey(env, providerKeyVars(custom));
376
+ if (!picked) return undefined;
377
+ const block = buildOpenAICompatibleProviderBlock(
378
+ custom,
379
+ picked.key,
380
+ model.slice(slash + 1),
381
+ providerBaseUrl(custom, env),
382
+ );
383
+ opencodeConfig.provider = {
384
+ ...opencodeConfig.provider,
385
+ [providerId]: {
386
+ ...existing,
387
+ ...block,
388
+ options: { ...existing?.options, ...block.options },
389
+ models: { ...existing?.models, ...block.models },
390
+ },
391
+ };
392
+ return picked;
393
+ }
394
+
395
+ const names = envBoundKeyVars(providerId);
396
+ if (!names) return undefined;
397
+ const picked = pickProviderKey(env, names);
398
+ if (!picked) return undefined;
399
+ const direct = openAICompatibleProvider(providerId);
400
+ const baseURL = direct ? baseUrlOverride(direct, env) : undefined;
401
+ const options = { ...existing?.options, ...(baseURL ? { baseURL } : {}), apiKey: picked.key };
402
+ if (direct?.baseUrlLocked) {
403
+ // OpenCode's built-in SDK (AI Gateway) knows its endpoint; any URL here is wrong.
404
+ const ignored = ignoredBaseUrlVar(direct, env);
405
+ if (ignored || options.baseURL !== undefined) {
406
+ console.warn(
407
+ `[opencode] ${ignored ?? `provider.${providerId}.options.baseURL`} is ignored: ${direct.displayName} uses its built-in endpoint`,
408
+ );
409
+ }
410
+ delete options.baseURL;
411
+ }
412
+ opencodeConfig.provider = {
413
+ ...opencodeConfig.provider,
414
+ [providerId]: { ...existing, env: [], options },
415
+ };
416
+ return picked;
417
+ }
418
+
219
419
  const SWARM_AGENT_NAME = "swarm";
220
420
  const TRANSCRIPT_TAIL_CHARS = 20_000;
221
421
  const TOOL_RESULT_PREVIEW_CHARS = 500;
422
+ // The last response body finishes streaming with the step; this only covers
423
+ // a read still in flight when the session ends.
424
+ const BILLING_SETTLE_MS = 2_000;
222
425
 
223
426
  /** The swarm MCP server's name in the OpenCode config. */
224
427
  const SWARM_MCP_SERVER = "swarm";
@@ -339,6 +542,7 @@ export class OpencodeSession implements ProviderSession {
339
542
  private readonly swarm: SwarmHooksConfig,
340
543
  private readonly cwd: string,
341
544
  private readonly appliedReasoningEffort: ReasoningEffort | null = null,
545
+ private readonly billing?: OpenRouterBilling,
342
546
  ) {
343
547
  this.completionPromise = new Promise<ProviderResult>((resolve) => {
344
548
  this.completionResolve = resolve;
@@ -494,12 +698,13 @@ export class OpencodeSession implements ProviderSession {
494
698
  this.usage = { cost: ev.data.cost, tokens: ev.data.tokens };
495
699
  break;
496
700
 
497
- case "session.execution.succeeded": {
498
- const cost = this.buildCostData(false);
499
- this.emit({ type: "result", cost, isError: false });
500
- void this.finish({ exitCode: 0, sessionId: this._sessionId, cost, isError: false });
701
+ case "session.execution.succeeded":
702
+ void this.finish(async () => {
703
+ const cost = await this.buildCostData(false);
704
+ this.emit({ type: "result", cost, isError: false });
705
+ return { exitCode: 0, sessionId: this._sessionId, cost, isError: false };
706
+ });
501
707
  break;
502
- }
503
708
 
504
709
  case "session.execution.failed":
505
710
  this.fail(errorMessage(ev.data.error, "opencode session error"));
@@ -541,27 +746,34 @@ export class OpencodeSession implements ProviderSession {
541
746
  }
542
747
 
543
748
  /** Settle the session as failed; also used when the event stream dies. */
544
- fail(message: string): void {
749
+ fail(rawMessage: string): void {
545
750
  if (this.aborted || this.completed) return;
751
+ // SDK and provider errors can echo request details; the message reaches
752
+ // the runner's logs and the task's failure reason.
753
+ const message = scrubSecrets(rawMessage);
546
754
  this.emit({ type: "error", message });
547
- void this.finish({
755
+ void this.finish(async () => ({
548
756
  exitCode: 1,
549
757
  sessionId: this._sessionId,
550
- cost: this.buildCostData(true),
758
+ cost: await this.buildCostData(true),
551
759
  isError: true,
552
760
  failureReason: message,
553
- });
761
+ }));
554
762
  }
555
763
 
556
- private buildCostData(isError: boolean): CostData {
764
+ private async buildCostData(isError: boolean): Promise<CostData> {
557
765
  const { cost, tokens } = this.usage;
766
+ const billed = await this.billing?.settle(BILLING_SETTLE_MS);
558
767
  return {
559
768
  sessionId: this._sessionId,
560
769
  taskId: this.swarm.taskId,
561
770
  agentId: this.swarm.agentId,
562
- totalCostUsd: cost,
771
+ ...(billed?.complete
772
+ ? { totalCostUsd: billed.costUsd, harnessCostBilled: true }
773
+ : { totalCostUsd: cost }),
563
774
  inputTokens: tokens.input,
564
- outputTokens: tokens.output,
775
+ // OpenCode 2's `output` excludes reasoning; both bill at the output rate.
776
+ outputTokens: tokens.output + tokens.reasoning,
565
777
  reasoningOutputTokens: tokens.reasoning,
566
778
  cacheReadTokens: tokens.cache.read,
567
779
  cacheWriteTokens: tokens.cache.write,
@@ -596,10 +808,11 @@ export class OpencodeSession implements ProviderSession {
596
808
  * Settle the session. Session-end work is bounded and cancellable, so the
597
809
  * host always closes and completion always resolves.
598
810
  */
599
- private async finish(result: ProviderResult): Promise<void> {
811
+ private async finish(settle: ProviderResult | (() => Promise<ProviderResult>)): Promise<void> {
600
812
  if (this.completed) return;
601
813
  this.completed = true;
602
814
  const { signal } = this.sessionEnd;
815
+ const result = typeof settle === "function" ? await settle() : settle;
603
816
  try {
604
817
  if (!this.aborted) {
605
818
  await settleWithin(this.runSessionEnd(signal), sessionHooks.budgetMs, signal);
@@ -722,6 +935,12 @@ export class OpencodeAdapter implements ProviderAdapter {
722
935
  },
723
936
  };
724
937
 
938
+ // One picked key for the model's provider (D3). It stays in this
939
+ // in-memory config; registering it lets every scrubbed egress redact it
940
+ // even when it never reached `process.env` (a key saved after boot).
941
+ const splicedKey = applyProviderKeySplice(opencodeConfig, config.model, env);
942
+ if (splicedKey) registerVolatileSecret(splicedKey.key, splicedKey.name);
943
+
725
944
  // Splice the provider-keyed reasoning options into
726
945
  // `provider[providerId].models[modelId].options`. `off` (and any
727
946
  // capability-rejected level) resolves to `noop`.
@@ -762,20 +981,31 @@ export class OpencodeAdapter implements ProviderAdapter {
762
981
  };
763
982
  const { sdk, version: opencodeVersion } = await loadOpencodeSdkImpl();
764
983
  let listTools: OpencodeToolLister | undefined;
765
- const host = await sdk.OpenCode.create({
766
- config: { content: JSON.stringify(opencodeConfig) },
767
- plugins: [
768
- createOpencodeSwarmPlugin(swarm, {
769
- onToolRegistry: (lister) => {
770
- listTools = lister;
771
- },
772
- }),
773
- ],
774
- log: {
775
- level: "warn",
776
- emit: (entry) => console.warn(scrubSecrets(`[opencode] ${entry.message}`)),
777
- },
778
- });
984
+ const billing = config.model.startsWith("openrouter/") ? new OpenRouterBilling() : undefined;
985
+ // The config string carries the spliced provider key and the swarm MCP
986
+ // bearer. It goes to the SDK only; nothing logs, writes, or uploads it,
987
+ // and errors raised while the SDK parses it are scrubbed before they
988
+ // propagate to the runner.
989
+ let host: OpenCode.Interface;
990
+ try {
991
+ host = await sdk.OpenCode.create({
992
+ config: { content: JSON.stringify(opencodeConfig) },
993
+ plugins: [
994
+ createOpencodeSwarmPlugin(swarm, {
995
+ onToolRegistry: (lister) => {
996
+ listTools = lister;
997
+ },
998
+ ...(billing ? { onOpenRouterResponse: (response) => billing.observe(response) } : {}),
999
+ }),
1000
+ ],
1001
+ log: {
1002
+ level: "warn",
1003
+ emit: (entry) => console.warn(scrubSecrets(`[opencode] ${entry.message}`)),
1004
+ },
1005
+ });
1006
+ } catch (err) {
1007
+ throw new Error(scrubSecrets(`opencode host failed to start: ${errorMessage(err, "")}`));
1008
+ }
779
1009
 
780
1010
  let sessionId: string;
781
1011
  try {
@@ -786,7 +1016,7 @@ export class OpencodeAdapter implements ProviderAdapter {
786
1016
  sessionId = created.id;
787
1017
  } catch (err) {
788
1018
  await host.close().catch(() => {});
789
- throw err;
1019
+ throw new Error(scrubSecrets(`opencode session create failed: ${errorMessage(err, "")}`));
790
1020
  }
791
1021
 
792
1022
  const session = new OpencodeSession(
@@ -796,6 +1026,7 @@ export class OpencodeAdapter implements ProviderAdapter {
796
1026
  swarm,
797
1027
  config.cwd,
798
1028
  appliedReasoningEffort,
1029
+ billing,
799
1030
  );
800
1031
  session.emitSessionInit(opencodeVersion ? { version: opencodeVersion } : undefined);
801
1032
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * OpenRouter's billed cost for an OpenCode 2 session.
3
+ *
4
+ * OpenCode prices tokens from its model catalog, and the swarm pricing table
5
+ * holds OpenRouter's model-level list price. Both are the cheapest endpoint's
6
+ * rate, while OpenRouter bills the endpoint that served the request, which
7
+ * can cost 100x more. OpenCode asks OpenRouter for `usage: { include: true }`,
8
+ * so every response carries `usage.cost`, the USD OpenRouter charged. The
9
+ * swarm plugin hands each OpenRouter response here, and the session reports
10
+ * the sum as billed when every response carried one.
11
+ */
12
+
13
+ /** The billed USD in an OpenRouter chat response (SSE or JSON), or null. */
14
+ export function parseOpenRouterBilledCost(body: string): number | null {
15
+ const payloads = body.trimStart().startsWith("{")
16
+ ? [body]
17
+ : body
18
+ .split("\n")
19
+ .filter((line) => line.startsWith("data:"))
20
+ .map((line) => line.slice(5).trim());
21
+ let cost: number | null = null;
22
+ for (const payload of payloads) {
23
+ if (!payload.includes('"cost"')) continue;
24
+ let usage: { cost?: unknown; is_byok?: unknown } | undefined;
25
+ try {
26
+ usage = (JSON.parse(payload) as { usage?: typeof usage }).usage;
27
+ } catch {
28
+ continue;
29
+ }
30
+ // A BYOK `cost` is OpenRouter's fee only; the upstream bills the rest.
31
+ if (usage?.is_byok === true) return null;
32
+ if (typeof usage?.cost === "number" && Number.isFinite(usage.cost)) cost = usage.cost;
33
+ }
34
+ return cost;
35
+ }
36
+
37
+ export interface BilledCost {
38
+ costUsd: number;
39
+ /** True when every observed response reported what OpenRouter charged. */
40
+ complete: boolean;
41
+ }
42
+
43
+ export class OpenRouterBilling {
44
+ private requests = 0;
45
+ private billed = 0;
46
+ private costUsd = 0;
47
+ private pending = new Set<Promise<void>>();
48
+
49
+ /** Reads a copy of `response`; the caller keeps streaming the original. */
50
+ observe(response: Response): void {
51
+ this.requests++;
52
+ // OpenRouter does not charge for a rejected request.
53
+ if (!response.ok) {
54
+ this.billed++;
55
+ return;
56
+ }
57
+ const read = response
58
+ .clone()
59
+ .text()
60
+ .then((body) => {
61
+ const cost = parseOpenRouterBilledCost(body);
62
+ if (cost === null) return;
63
+ this.billed++;
64
+ this.costUsd += cost;
65
+ })
66
+ .catch(() => {})
67
+ .finally(() => this.pending.delete(read));
68
+ this.pending.add(read);
69
+ }
70
+
71
+ /** Waits up to `timeoutMs` for in-flight responses, then reports. */
72
+ async settle(timeoutMs: number): Promise<BilledCost | null> {
73
+ if (this.pending.size > 0) {
74
+ let timer: ReturnType<typeof setTimeout> | undefined;
75
+ await Promise.race([
76
+ Promise.all(this.pending),
77
+ new Promise<void>((resolve) => {
78
+ timer = setTimeout(resolve, timeoutMs);
79
+ }),
80
+ ]);
81
+ clearTimeout(timer);
82
+ }
83
+ if (this.requests === 0) return null;
84
+ return { costUsd: this.costUsd, complete: this.billed === this.requests };
85
+ }
86
+ }
@@ -28,6 +28,29 @@ export type OpencodeToolLister = () => Promise<ReadonlyArray<{ readonly id: stri
28
28
  export interface OpencodeSwarmPluginOptions {
29
29
  /** Receives the tool lister once OpenCode sets the plugin up. */
30
30
  onToolRegistry?: (listTools: OpencodeToolLister) => void;
31
+ /** Receives every OpenRouter model response, for the billed cost. */
32
+ onOpenRouterResponse?: (response: Response) => void;
33
+ }
34
+
35
+ /** Keys OpenCode sets on each shell call, on top of its base env. */
36
+ const OPENCODE_SHELL_ENV_KEYS = ["TERM", "OPENCODE_TERMINAL"] as const;
37
+
38
+ /**
39
+ * Rewrites one OpenCode shell call's env, in place, to the session's own env
40
+ * plus the keys OpenCode set for the call. Nothing else from the process env
41
+ * gets through.
42
+ */
43
+ function applyOpencodeShellEnv(
44
+ callEnv: Record<string, string | undefined>,
45
+ sessionEnv: Record<string, string | undefined>,
46
+ ): void {
47
+ const keep = new Set<string>(OPENCODE_SHELL_ENV_KEYS);
48
+ for (const key of Object.keys(callEnv)) {
49
+ if (!keep.has(key)) delete callEnv[key];
50
+ }
51
+ for (const [key, value] of Object.entries(sessionEnv)) {
52
+ if (value !== undefined && !keep.has(key)) callEnv[key] = value;
53
+ }
31
54
  }
32
55
 
33
56
  export function createOpencodeSwarmPlugin(
@@ -39,6 +62,16 @@ export function createOpencodeSwarmPlugin(
39
62
  async setup(ctx) {
40
63
  options.onToolRegistry?.(() => ctx.tool.list());
41
64
 
65
+ // OpenCode runs in-process, so its shell tool spawns with process.env,
66
+ // which every concurrent session shares and which never sees this
67
+ // task's env (rows admitted or withheld by fetchResolvedEnv after boot).
68
+ const sessionEnv = config.env;
69
+ if (sessionEnv) {
70
+ await ctx.shell.hook("create.before", (event) => {
71
+ applyOpencodeShellEnv(event.env, sessionEnv);
72
+ });
73
+ }
74
+
42
75
  // A throw blocks the tool; OpenCode hands the message back to the model.
43
76
  await ctx.tool.hook("execute.before", async (event) => {
44
77
  if (!config.isLead && config.taskId) {
@@ -75,6 +108,24 @@ export function createOpencodeSwarmPlugin(
75
108
  if (concurrent) event.system.push({ type: "text", text: concurrent });
76
109
  });
77
110
 
111
+ // Nobody reads a headless session's title. Generating one is a second
112
+ // model call, on OpenCode's small model, that the session usage folds
113
+ // into the main model's tokens.
114
+ await ctx.session.hook("title", (event) => {
115
+ event.result = config.taskId ? `swarm task ${config.taskId}` : "swarm session";
116
+ });
117
+
118
+ const onOpenRouterResponse = options.onOpenRouterResponse;
119
+ if (onOpenRouterResponse) {
120
+ await ctx.session.hook(
121
+ "http.response",
122
+ (event) => {
123
+ onOpenRouterResponse(event.response);
124
+ },
125
+ { providerID: "openrouter" },
126
+ );
127
+ }
128
+
78
129
  // PreCompact parity: keep the task goal in the checkpoint summary.
79
130
  await ctx.session.hook("compaction", async (event) => {
80
131
  if (!config.taskId) return;