@desplega.ai/agent-swarm 2.7.4 → 2.7.5

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 (253) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-g3ads55s.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
  16. package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
  17. package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
  18. package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
  19. package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
  20. package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
  21. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  22. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  23. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  24. package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
  25. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  26. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  27. package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
  28. package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
  29. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  30. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  31. package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
  32. package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
  33. package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
  34. package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
  35. package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
  39. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  40. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  41. package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
  42. package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
  43. package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
  44. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  45. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
  47. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  48. package/dist/cli-fw2w0kva.js +47 -0
  49. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  50. package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
  51. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  52. package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
  53. package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
  54. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  55. package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
  56. package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
  57. package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
  58. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  59. package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
  60. package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
  61. package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
  62. package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
  63. package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
  64. package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
  65. package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
  66. package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
  67. package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
  68. package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
  69. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  70. package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
  71. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  72. package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
  73. package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
  74. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  75. package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
  76. package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
  77. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  78. package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
  79. package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
  80. package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
  81. package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
  82. package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
  83. package/dist/cli-zh3fp8v2.js +151 -0
  84. package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
  85. package/dist/cli.js +18 -18
  86. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
  87. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  88. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  89. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
  90. package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
  91. package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
  92. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
  93. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
  94. package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
  95. package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
  96. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  97. package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
  98. package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
  99. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
  100. package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
  101. package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
  102. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
  103. package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
  104. package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
  105. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  106. package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
  107. package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
  108. package/dist/fx-adapter-gt6pncj2.js +18 -0
  109. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
  110. package/dist/handlers-vwmba1m2.js +31 -0
  111. package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
  112. package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
  113. package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
  114. package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
  115. package/dist/index-5r71rf98.js +32 -0
  116. package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
  117. package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
  118. package/dist/index-anmwxs87.js +29 -0
  119. package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
  120. package/dist/index-spp0ra5c.js +36 -0
  121. package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
  122. package/dist/lead-fjy418cw.js +69 -0
  123. package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
  125. package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
  126. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
  127. package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
  128. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
  129. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
  130. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
  131. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
  132. package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
  133. package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
  134. package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
  135. package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
  136. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
  137. package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
  138. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
  139. package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
  140. package/dist/worker-0c5tyzqe.js +69 -0
  141. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
  142. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  143. package/openapi.json +97 -32
  144. package/package.json +1 -1
  145. package/src/be/db/session-summaries.ts +8 -2
  146. package/src/be/db/session-tree.ts +174 -61
  147. package/src/be/db/session-visibility.ts +5 -4
  148. package/src/be/db.ts +8 -4
  149. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  150. package/src/be/model-validation.ts +5 -1
  151. package/src/be/pricing-normalize.ts +7 -2
  152. package/src/be/scripts/typecheck.ts +1 -0
  153. package/src/be/seed/schedules-seeder.ts +135 -9
  154. package/src/be/seed/workflows-seeder.ts +65 -1
  155. package/src/be/seed-pricing.ts +23 -0
  156. package/src/be/steering.ts +50 -3
  157. package/src/be/swarm-config-guard.ts +1 -0
  158. package/src/commands/provider-credentials.ts +29 -2
  159. package/src/commands/runner.ts +86 -5
  160. package/src/extensions/contract-types.generated.ts +1 -1
  161. package/src/extensions/contract.ts +27 -1
  162. package/src/extensions/dispatcher.ts +4 -0
  163. package/src/http/agents.ts +1 -0
  164. package/src/http/mcp-bridge.ts +4 -0
  165. package/src/integrations/chat-sdk/inbound.ts +15 -5
  166. package/src/integrations/chat-sdk/steering.ts +3 -1
  167. package/src/kv-reserved-namespaces.ts +2 -0
  168. package/src/prompts/base-prompt.ts +4 -1
  169. package/src/prompts/session-templates.ts +26 -0
  170. package/src/providers/acp-adapter.ts +190 -26
  171. package/src/providers/acp-targets.ts +306 -6
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +9 -44
  174. package/src/providers/codex-oauth/flow.ts +24 -3
  175. package/src/providers/codex-oauth/storage.ts +56 -17
  176. package/src/providers/codex-oauth/subscription.ts +67 -0
  177. package/src/providers/fx-adapter.ts +211 -0
  178. package/src/providers/index.ts +5 -1
  179. package/src/providers/mcp-loopback-proxy.ts +94 -0
  180. package/src/providers/pi-mono-adapter.ts +98 -13
  181. package/src/providers/pricing-sources.md +8 -0
  182. package/src/providers/reasoning-effort.ts +17 -1
  183. package/src/providers/types.ts +2 -1
  184. package/src/scheduler/scheduler.ts +36 -3
  185. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  186. package/src/scripts-runtime/types/stdlib.d.ts +5 -0
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
  189. package/src/server.ts +7 -0
  190. package/src/slack/assistant.ts +17 -1
  191. package/src/slack/handlers.ts +18 -5
  192. package/src/slack/left-threads.ts +65 -0
  193. package/src/slack/steering.ts +3 -0
  194. package/src/slack/thread-buffer.ts +40 -16
  195. package/src/tests/acp-adapter.test.ts +79 -0
  196. package/src/tests/base-prompt.test.ts +12 -0
  197. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  198. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  199. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  200. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  201. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  202. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  203. package/src/tests/context-window.test.ts +7 -0
  204. package/src/tests/explicit-model-validation.test.ts +99 -0
  205. package/src/tests/extensions-run-context.test.ts +11 -0
  206. package/src/tests/extensions-steering.test.ts +353 -0
  207. package/src/tests/extensions-validate.test.ts +5 -0
  208. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  209. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  210. package/src/tests/fixtures/fake-fx.ts +167 -0
  211. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  212. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  213. package/src/tests/fx-adapter.test.ts +771 -0
  214. package/src/tests/grok-adapter.test.ts +36 -0
  215. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  216. package/src/tests/model-tier-resolution.test.ts +3 -3
  217. package/src/tests/pricing-refresh.test.ts +5 -5
  218. package/src/tests/prompt-template-session.test.ts +5 -3
  219. package/src/tests/provider-registration.test.ts +2 -1
  220. package/src/tests/query-audit-plans.test.ts +3 -2
  221. package/src/tests/script-connections.test.ts +2 -0
  222. package/src/tests/seed-model-profiles.test.ts +2 -2
  223. package/src/tests/seed-schedules.test.ts +138 -0
  224. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  225. package/src/tests/session-costs-golden.test.ts +26 -0
  226. package/src/tests/session-summaries-plans.test.ts +120 -0
  227. package/src/tests/session-summaries.test.ts +72 -12
  228. package/src/tests/sessions-tree.test.ts +11 -3
  229. package/src/tests/slack-leave-thread.test.ts +431 -0
  230. package/src/tests/tool-annotations.test.ts +2 -1
  231. package/src/tests/workflow-engine-v2.test.ts +42 -0
  232. package/src/tests/workflow-script-principal.test.ts +207 -7
  233. package/src/tools/slack-leave-thread.ts +104 -0
  234. package/src/tools/steer-task.ts +3 -2
  235. package/src/tools/tool-config.ts +2 -1
  236. package/src/types.ts +19 -0
  237. package/src/utils/codex-auth-failure.ts +30 -0
  238. package/src/utils/context-window.ts +4 -1
  239. package/src/utils/credentials.ts +7 -0
  240. package/src/utils/fx-models.ts +43 -0
  241. package/src/workflows/cooldown.ts +3 -2
  242. package/src/workflows/engine.ts +24 -9
  243. package/templates/schedules/dreaming-daily/config.json +1 -1
  244. package/templates/schedules/dreaming-daily/content.md +6 -14
  245. package/templates/skills/swarm-docs/content.md +1 -0
  246. package/templates/workflows/dreaming/content.md +5 -1
  247. package/dist/acp-adapter-96dc64m2.js +0 -15
  248. package/dist/handlers-jg7v74ty.js +0 -31
  249. package/dist/index-8c1zbz59.js +0 -29
  250. package/dist/index-8k8tmfxd.js +0 -32
  251. package/dist/index-hq617xdj.js +0 -36
  252. package/dist/lead-567kw7x3.js +0 -68
  253. package/dist/worker-fjdqra6q.js +0 -68
@@ -40,8 +40,9 @@ const ROUTING_PREFIXES_BY_PROVIDER: Record<PricingProvider, readonly string[]> =
40
40
  // openai, … — strip whichever proxy prefix the user picked.
41
41
  opencode: ["openrouter/", "github-copilot/"],
42
42
  // pi-mono can hit openrouter mirrors, the github-copilot proxy, or native
43
- // anthropic/openai/google providers.
44
- pi: ["openrouter/", "github-copilot/"],
43
+ // anthropic/openai/google providers. `openai-codex/<id>` is an OpenAI id
44
+ // served by the ChatGPT subscription pool, priced like codex on that pool.
45
+ pi: ["openrouter/", "github-copilot/", "openai-codex/"],
45
46
  // codex normally reports a bare id, but a user may set MODEL_OVERRIDE to a
46
47
  // prefixed form. Be forgiving on the lookup side.
47
48
  codex: ["openai/", "github-copilot/"],
@@ -69,6 +70,10 @@ const ROUTING_PREFIXES_BY_PROVIDER: Record<PricingProvider, readonly string[]> =
69
70
  // The Grok CLI reports a bare xAI id (`grok-4.6`) or `openrouter/<vendor>/<id>`
70
71
  // for an OpenRouter model; stripping the prefix lands it on the openrouter rows.
71
72
  grok: ["openrouter/", "xai/"],
73
+ // fx reports the swarm model string: `openrouter/<vendor>/<id>` (OpenRouter
74
+ // connection), `vercel/<vendor>/<id>` (AI Gateway) or a bare OpenAI id.
75
+ // Stripping the route prefix lands the first two on `<vendor>/<id>` rows.
76
+ fx: ["openrouter/", "vercel/"],
72
77
  };
73
78
 
74
79
  /** `gpt-5-nano-2025-08-07` -> `gpt-5-nano`. Anthropic's 8-digit dates (`-20251001`) are real catalog ids and are left alone. */
@@ -315,6 +315,7 @@ export interface SwarmSdk {
315
315
  slack_downloadFile(args: { url: string }): Promise<unknown>;
316
316
  slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
317
317
  slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
318
+ slack_leaveThread(args: { taskId?: string; channelId?: string; threadTs?: string }): Promise<unknown>;
318
319
 
319
320
  // --- write: chat sdk ---
320
321
  chat_reply(args: { text: string }): Promise<unknown>;
@@ -76,8 +76,11 @@ import {
76
76
  computeContentHash,
77
77
  createScheduledTask,
78
78
  getScheduledTaskByName,
79
+ getWorkflow,
80
+ listWorkflows,
79
81
  updateScheduledTask,
80
82
  } from "../db";
83
+ import { getScript } from "../scripts/db";
81
84
  import { resolveSeededEnabled } from "./automation-toggle";
82
85
  import type { Seeder, SeedItem } from "./types";
83
86
  import { canonicalJson } from "./workflows-seeder";
@@ -98,8 +101,27 @@ type ScheduleBlock = {
98
101
  cron: string;
99
102
  timezone?: string;
100
103
  enabled?: boolean;
104
+ /** What a fire runs. Default `agent-task`: the `## Scheduled Task` prompt becomes a task. */
105
+ targetType?: ScheduleTargetType;
106
+ /** `targetType: "workflow"`: the workflow's name, resolved to its id at apply time. */
107
+ workflow?: string;
108
+ /** `targetType: "script"`: a global script, run directly with `scriptArgs`. */
109
+ scriptName?: string;
110
+ scriptArgs?: Record<string, unknown>;
101
111
  };
102
112
 
113
+ type ScheduleTargetType = "agent-task" | "workflow" | "script";
114
+ const TARGET_TYPES: readonly ScheduleTargetType[] = ["agent-task", "workflow", "script"];
115
+
116
+ /**
117
+ * The non-default target of a seeded schedule. Agent-task schedules carry none
118
+ * of these keys, so their seed hash is unchanged from before targets existed
119
+ * and recorded seed state on existing installs still matches.
120
+ */
121
+ type SeedScheduleTarget =
122
+ | { targetType: "workflow"; workflow: string }
123
+ | { targetType: "script"; scriptName: string; scriptArgs: Record<string, unknown> };
124
+
103
125
  export type SeedSchedule = {
104
126
  name: string;
105
127
  description: string;
@@ -112,7 +134,7 @@ export type SeedSchedule = {
112
134
  params: Record<string, unknown>;
113
135
  requiredParams: string[];
114
136
  requires: AutomationIntegrationId[];
115
- };
137
+ } & (SeedScheduleTarget | { targetType?: undefined });
116
138
 
117
139
  export type ScheduleTemplateSource = { config: string; content: string };
118
140
  type ScheduleSeedItem = SeedItem & { schedule: SeedSchedule };
@@ -167,10 +189,21 @@ function parseScheduleSource(source: ScheduleTemplateSource): SeedSchedule | nul
167
189
  const block = JSON.parse(blockMatch[1]) as ScheduleBlock;
168
190
  const afterBlock = scheduleStart + blockMatch.index! + blockMatch[0].length;
169
191
  const taskHeading = source.content.indexOf("## Scheduled Task", afterBlock);
170
- const taskTemplate = source.content
171
- .slice(taskHeading >= 0 ? taskHeading + "## Scheduled Task".length : afterBlock)
172
- .trim();
173
- if (!taskTemplate) throw new Error(`Schedule template ${config.name} has no task prompt`);
192
+ const target = parseScheduleTarget(config.name, block);
193
+ let taskTemplate = "";
194
+ if (target) {
195
+ // A workflow or script fire never reads a prompt: shipping one is a mistake.
196
+ if (taskHeading >= 0) {
197
+ throw new Error(
198
+ `Schedule template ${config.name} targets a ${target.targetType} but ships a ## Scheduled Task prompt it would ignore`,
199
+ );
200
+ }
201
+ } else {
202
+ taskTemplate = source.content
203
+ .slice(taskHeading >= 0 ? taskHeading + "## Scheduled Task".length : afterBlock)
204
+ .trim();
205
+ if (!taskTemplate) throw new Error(`Schedule template ${config.name} has no task prompt`);
206
+ }
174
207
 
175
208
  const requires = config.requires ?? [];
176
209
  const requiredParams = config.placeholders ?? [];
@@ -196,7 +229,37 @@ function parseScheduleSource(source: ScheduleTemplateSource): SeedSchedule | nul
196
229
  params: {},
197
230
  requiredParams,
198
231
  requires,
232
+ ...target,
233
+ };
234
+ }
235
+
236
+ /** Validate the block's target fields. Returns null for the default agent-task target. */
237
+ function parseScheduleTarget(name: string, block: ScheduleBlock): SeedScheduleTarget | null {
238
+ const targetType = block.targetType ?? "agent-task";
239
+ if (!TARGET_TYPES.includes(targetType)) {
240
+ throw new Error(`Schedule template ${name} has unknown targetType "${targetType}"`);
241
+ }
242
+ const stray = (fields: (keyof ScheduleBlock)[]) => {
243
+ const set = fields.filter((field) => block[field] !== undefined);
244
+ if (set.length > 0) {
245
+ throw new Error(
246
+ `Schedule template ${name} (targetType ${targetType}) sets ${set.join(", ")}, which it would ignore`,
247
+ );
248
+ }
199
249
  };
250
+ switch (targetType) {
251
+ case "agent-task":
252
+ stray(["workflow", "scriptName", "scriptArgs"]);
253
+ return null;
254
+ case "workflow":
255
+ stray(["scriptName", "scriptArgs"]);
256
+ if (!block.workflow) throw new Error(`Schedule template ${name} has no workflow name`);
257
+ return { targetType, workflow: block.workflow };
258
+ case "script":
259
+ stray(["workflow"]);
260
+ if (!block.scriptName) throw new Error(`Schedule template ${name} has no scriptName`);
261
+ return { targetType, scriptName: block.scriptName, scriptArgs: block.scriptArgs ?? {} };
262
+ }
200
263
  }
201
264
 
202
265
  export function loadSeedSchedules(
@@ -212,7 +275,27 @@ function scheduleSeedHash(schedule: SeedSchedule): string {
212
275
  return computeContentHash(canonicalJson(schedule));
213
276
  }
214
277
 
215
- function scheduleFromRow(schedule: ScheduledTask): SeedSchedule {
278
+ async function scheduleTargetFromRow(
279
+ schedule: ScheduledTask,
280
+ ): Promise<SeedScheduleTarget | undefined> {
281
+ switch (schedule.targetType) {
282
+ case "workflow": {
283
+ const workflow = schedule.workflowId ? await getWorkflow(schedule.workflowId) : null;
284
+ // A dangling id can never equal a seeded name, so the row reads as edited.
285
+ return { targetType: "workflow", workflow: workflow?.name ?? `id:${schedule.workflowId}` };
286
+ }
287
+ case "script":
288
+ return {
289
+ targetType: "script",
290
+ scriptName: schedule.scriptName ?? "",
291
+ scriptArgs: schedule.scriptArgs ?? {},
292
+ };
293
+ default:
294
+ return undefined;
295
+ }
296
+ }
297
+
298
+ async function scheduleFromRow(schedule: ScheduledTask): Promise<SeedSchedule> {
216
299
  return {
217
300
  name: schedule.name,
218
301
  description: schedule.description ?? "",
@@ -225,9 +308,48 @@ function scheduleFromRow(schedule: ScheduledTask): SeedSchedule {
225
308
  params: schedule.params ?? {},
226
309
  requiredParams: schedule.requiredParams ?? [],
227
310
  requires: schedule.requires ?? [],
311
+ ...(await scheduleTargetFromRow(schedule)),
228
312
  };
229
313
  }
230
314
 
315
+ /** Resolve a seeded target to row fields, failing the item when its target is missing. */
316
+ async function resolveTargetFields(schedule: SeedSchedule) {
317
+ switch (schedule.targetType) {
318
+ case "workflow": {
319
+ const name = schedule.workflow;
320
+ const workflow = (await listWorkflows()).find((candidate) => candidate.name === name);
321
+ if (!workflow) throw new Error(`Workflow "${name}" not found`);
322
+ return {
323
+ targetType: "workflow" as const,
324
+ workflowId: workflow.id,
325
+ scriptName: null,
326
+ scriptArgs: null,
327
+ taskTemplate: null,
328
+ };
329
+ }
330
+ case "script": {
331
+ if (!(await getScript({ name: schedule.scriptName, scope: "global" }))) {
332
+ throw new Error(`Global script "${schedule.scriptName}" not found`);
333
+ }
334
+ return {
335
+ targetType: "script" as const,
336
+ workflowId: null,
337
+ scriptName: schedule.scriptName,
338
+ scriptArgs: schedule.scriptArgs,
339
+ taskTemplate: null,
340
+ };
341
+ }
342
+ default:
343
+ return {
344
+ targetType: "agent-task" as const,
345
+ workflowId: null,
346
+ scriptName: null,
347
+ scriptArgs: null,
348
+ taskTemplate: schedule.taskTemplate,
349
+ };
350
+ }
351
+ }
352
+
231
353
  function nextRunAt(schedule: SeedSchedule): string | null {
232
354
  try {
233
355
  // Disabled templates can await a required timezone parameter. Validate the
@@ -263,7 +385,7 @@ export function createSchedulesSeeder(
263
385
 
264
386
  async upstreamHash(item): Promise<string | null> {
265
387
  const existing = await getScheduledTaskByName(item.key);
266
- return existing ? scheduleSeedHash(scheduleFromRow(existing)) : null;
388
+ return existing ? scheduleSeedHash(await scheduleFromRow(existing)) : null;
267
389
  },
268
390
 
269
391
  async apply(item): Promise<void> {
@@ -274,8 +396,7 @@ export function createSchedulesSeeder(
274
396
  cronExpression: schedule.cronExpression,
275
397
  timezone: schedule.timezone,
276
398
  enabled: schedule.enabled,
277
- targetType: "agent-task" as const,
278
- taskTemplate: schedule.taskTemplate,
399
+ ...(await resolveTargetFields(schedule)),
279
400
  taskType: schedule.taskType,
280
401
  tags: schedule.tags,
281
402
  params: schedule.params,
@@ -284,11 +405,16 @@ export function createSchedulesSeeder(
284
405
  };
285
406
  const existing = await getScheduledTaskByName(schedule.name);
286
407
  if (existing) {
408
+ // Nulls clear the previous target's fields when a template changes target.
287
409
  await updateScheduledTask(existing.id, { ...data, nextRunAt: resolvedNextRunAt });
288
410
  } else {
289
411
  await createScheduledTask({
290
412
  name: schedule.name,
291
413
  ...data,
414
+ workflowId: data.workflowId ?? undefined,
415
+ scriptName: data.scriptName ?? undefined,
416
+ scriptArgs: data.scriptArgs ?? undefined,
417
+ taskTemplate: data.taskTemplate ?? undefined,
292
418
  nextRunAt: resolvedNextRunAt ?? undefined,
293
419
  });
294
420
  }
@@ -71,10 +71,18 @@ import type {
71
71
  TriggerConfig,
72
72
  Workflow,
73
73
  WorkflowDefinition,
74
+ WorkflowSnapshot,
74
75
  } from "../../types";
75
76
  import { validateDefinition } from "../../workflows/definition";
76
- import { computeContentHash, createWorkflow, listWorkflows, updateWorkflow } from "../db";
77
+ import {
78
+ computeContentHash,
79
+ createWorkflow,
80
+ getWorkflowVersions,
81
+ listWorkflows,
82
+ updateWorkflow,
83
+ } from "../db";
77
84
  import { resolveSeededEnabled } from "./automation-toggle";
85
+ import { getSeedState } from "./state-db";
78
86
  import type { Seeder, SeedItem } from "./types";
79
87
 
80
88
  type AutomationTemplateConfig = {
@@ -220,6 +228,62 @@ function workflowFromRow(workflow: Workflow): SeedWorkflow {
220
228
  };
221
229
  }
222
230
 
231
+ function workflowFromSnapshot(snapshot: WorkflowSnapshot): SeedWorkflow {
232
+ return {
233
+ name: snapshot.name,
234
+ description: snapshot.description ?? "",
235
+ enabled: snapshot.enabled,
236
+ definition: snapshot.definition,
237
+ triggers: snapshot.triggers,
238
+ cooldown: snapshot.cooldown,
239
+ input: snapshot.input,
240
+ triggerSchema: snapshot.triggerSchema,
241
+ params: snapshot.params ?? {},
242
+ requiredParams: snapshot.requiredParams ?? [],
243
+ requires: snapshot.requires ?? [],
244
+ };
245
+ }
246
+
247
+ /**
248
+ * What a run executes, without the settings an operator is told to change:
249
+ * inputs (plain data, resolved the same for every caller), on/off, cooldown,
250
+ * description and event triggers.
251
+ */
252
+ function executableSurface(workflow: SeedWorkflow): string {
253
+ const {
254
+ input: _input,
255
+ enabled: _enabled,
256
+ cooldown: _cooldown,
257
+ description: _description,
258
+ triggers: _triggers,
259
+ ...surface
260
+ } = workflow;
261
+ return canonicalJson(surface);
262
+ }
263
+
264
+ /**
265
+ * Whether a live workflow still runs seed source: `pristine` while what it
266
+ * executes matches what the seeder last wrote for its name, `edited` once
267
+ * anyone changed that, `unseeded` for a workflow the seeder never wrote.
268
+ * Operator settings (see `executableSurface`) keep it `pristine`. The seeded
269
+ * state is the live row itself or, once edited, the version snapshot taken
270
+ * before the first edit; an edit with no such snapshot counts as `edited`.
271
+ */
272
+ export async function seededWorkflowState(
273
+ workflow: Workflow,
274
+ ): Promise<"pristine" | "edited" | "unseeded"> {
275
+ const state = await getSeedState("workflow", workflow.name);
276
+ if (!state) return "unseeded";
277
+ const live = workflowFromRow(workflow);
278
+ if (workflowSeedHash(live) === state.seededHash) return "pristine";
279
+ for (const version of await getWorkflowVersions(workflow.id)) {
280
+ const seeded = workflowFromSnapshot(version.snapshot);
281
+ if (workflowSeedHash(seeded) !== state.seededHash) continue;
282
+ return executableSurface(seeded) === executableSurface(live) ? "pristine" : "edited";
283
+ }
284
+ return "edited";
285
+ }
286
+
223
287
  export function createWorkflowsSeeder(
224
288
  sources: readonly WorkflowTemplateSource[] = BUILT_IN_WORKFLOW_SOURCES,
225
289
  ): Seeder<WorkflowSeedItem> {
@@ -299,6 +299,8 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
299
299
  // strips the prefix, so we seed the bare id under `pi` too. Without this
300
300
  // every gh-copilot-backed pi run fell through to `costSource='unpriced'`.
301
301
  add(id, projectCostBlock("pi", id, model.cost));
302
+ // fx runs a bare OpenAI id on its `openai` custom connection.
303
+ add(id, projectCostBlock("fx", id, model.cost));
302
304
  }
303
305
 
304
306
  // ---- Cursor (bare vendor ids through Cursor's hosted inference) --------
@@ -358,6 +360,13 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
358
360
  anthropicBilled: id.startsWith("anthropic/"),
359
361
  }),
360
362
  );
363
+ // fx runs `openrouter/<id>` on its OpenRouter custom connection.
364
+ add(
365
+ id,
366
+ projectCostBlock("fx", id, model.cost, {
367
+ anthropicBilled: id.startsWith("anthropic/"),
368
+ }),
369
+ );
361
370
  // Gemini specifically: also project under the 'gemini' provider so
362
371
  // internal-ai callers that tag with provider='gemini' find a hit.
363
372
  if (id.startsWith("google/")) {
@@ -369,6 +378,20 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
369
378
  }
370
379
  }
371
380
 
381
+ // ---- fx on Vercel AI Gateway (`vercel/<vendor>/<id>`) -------------------
382
+ // The prefix is stripped at lookup, so a gateway id lands on the same key as
383
+ // its OpenRouter twin. OpenRouter's row wins where both list the model (one
384
+ // source per key); the gateway section fills the ids OpenRouter lacks.
385
+ for (const [id, model] of Object.entries(cache.vercel?.models ?? {})) {
386
+ if (!model?.cost || openrouter[id]?.cost) continue;
387
+ add(
388
+ id,
389
+ projectCostBlock("fx", id, model.cost, {
390
+ anthropicBilled: id.startsWith("anthropic/"),
391
+ }),
392
+ );
393
+ }
394
+
372
395
  // ---- OpenAI-compatible registry providers ($0 flat-rate subscriptions) --
373
396
  // pi and opencode run these as `<provider>/<id>`. The models.dev `nan`
374
397
  // section and the registry-synthesized `helmcode` section
@@ -1,3 +1,5 @@
1
+ import { z } from "zod";
2
+ import type { TaskSteerEvent } from "../extensions/contract";
1
3
  import {
2
4
  type Agent,
3
5
  type AgentTask,
@@ -37,6 +39,20 @@ export class SteeringRequestError extends Error {
37
39
  }
38
40
  }
39
41
 
42
+ export class SteeringBlockedError extends SteeringRequestError {
43
+ constructor(
44
+ public readonly reason: string,
45
+ public readonly extension: { id: string; name: string },
46
+ ) {
47
+ super(`Steering blocked by extension ${extension.name}: ${reason}`, 403);
48
+ this.name = "SteeringBlockedError";
49
+ }
50
+ }
51
+
52
+ const steeringModifySchema = z.strictObject({
53
+ message: z.string().refine((message) => message.trim().length > 0, "Message must not be empty"),
54
+ });
55
+
40
56
  /** Server-side entry point for the global steering kill switch. */
41
57
  export function isSteeringEnabled(): boolean {
42
58
  return readSteeringEnabled();
@@ -51,6 +67,8 @@ export interface RequestSteeringArgs {
51
67
  createdByKind?: SteeringMessage["createdByKind"];
52
68
  createdByUserId?: string;
53
69
  createdByAgentId?: string;
70
+ messageTimestamps?: string[];
71
+ slack?: TaskSteerEvent["slack"];
54
72
  }
55
73
 
56
74
  async function providerForTask(task: AgentTask): Promise<ProviderName> {
@@ -291,7 +309,36 @@ export async function requestSteering(args: RequestSteeringArgs): Promise<SteerR
291
309
 
292
310
  const requestedMode = args.mode ?? "queue";
293
311
  const onUnsupported = args.onUnsupported ?? "degrade";
294
- const provider = await providerForTask(task);
312
+ // The dispatcher loads task/workflow assets; keep it off the HTTP boot import cycle.
313
+ const { dispatchPre, extensionIdForAgent } = await import("../extensions/dispatcher");
314
+ const preSteer = await dispatchPre(
315
+ "pre.task.steer",
316
+ {
317
+ task,
318
+ message: args.message,
319
+ mode: requestedMode,
320
+ source: args.source ?? "api",
321
+ sender: {
322
+ kind: args.createdByKind ?? "system",
323
+ userId: args.createdByUserId,
324
+ agentId: args.createdByAgentId,
325
+ },
326
+ messageTimestamps: args.messageTimestamps,
327
+ slack: args.slack,
328
+ },
329
+ {
330
+ skipExtensionId: extensionIdForAgent(args.createdByAgentId),
331
+ validateModify: (data) => steeringModifySchema.safeParse(data),
332
+ },
333
+ );
334
+ if (preSteer.action === "block") {
335
+ throw new SteeringBlockedError(preSteer.reason, preSteer.extension);
336
+ }
337
+
338
+ // A hook can outlive the running session. Decide delivery from the current task.
339
+ const currentTask = await getTaskById(args.taskId);
340
+ if (!currentTask) throw new SteeringRequestError("Task not found", 404);
341
+ const provider = await providerForTask(currentTask);
295
342
  const supportedModes = PROVIDER_STEER_CAPABILITIES[provider];
296
343
 
297
344
  if (onUnsupported === "fail" && !supportedModes.includes(requestedMode)) {
@@ -302,12 +349,12 @@ export async function requestSteering(args: RequestSteeringArgs): Promise<SteerR
302
349
  );
303
350
  }
304
351
 
305
- const activeTask = task.status === "paused" ? await resumeTask(task.id) : task;
352
+ const activeTask = currentTask.status === "paused" ? await resumeTask(task.id) : currentTask;
306
353
  if (!activeTask) {
307
354
  throw new SteeringRequestError("Failed to resume paused task", 500);
308
355
  }
309
356
 
310
- const body = scrubSecrets(args.message);
357
+ const body = scrubSecrets(preSteer.action === "modify" ? preSteer.data.message : args.message);
311
358
  // A task whose session hasn't started yet can still accept queued steering:
312
359
  // the row stays `pending` and the worker delivers it once the session is
313
360
  // live (the dispatch poll covers every active task). Interrupting a session
@@ -508,6 +508,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
508
508
  "ADDITIVE_SLACK",
509
509
  "SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION",
510
510
  "SLACK_THREAD_WAKE_ON_ANY_MESSAGE",
511
+ "SLACK_LEAVE_THREAD_DISABLE",
511
512
  "RBAC_ENABLED",
512
513
  "SEED_AUTOMATIONS_ENABLED",
513
514
  "RBAC_AUDIT_DISABLED",
@@ -20,6 +20,7 @@
20
20
 
21
21
  import {
22
22
  openAICompatibleKeyVars,
23
+ openAICompatibleProvider,
23
24
  openAICompatibleProviders,
24
25
  providerBaseUrl,
25
26
  providerKeyFromEnv,
@@ -34,6 +35,7 @@ import { hasCodexOAuthPoolSlots } from "../providers/codex-oauth/env-keys";
34
35
  import { checkCursorCredentials } from "../providers/cursor-adapter";
35
36
  import { checkDevinCredentials } from "../providers/devin-adapter";
36
37
  import { checkDshCredentials } from "../providers/dsh-adapter";
38
+ import { checkFxCredentials } from "../providers/fx-adapter";
37
39
  import { checkGrokCredentials } from "../providers/grok-adapter";
38
40
  import { checkOpencodeCredentials } from "../providers/opencode-adapter";
39
41
  import type { CredCheckOptions, CredStatus } from "../providers/types";
@@ -85,7 +87,8 @@ export type SupportedProvider =
85
87
  | "dsh"
86
88
  | "cursor"
87
89
  | "amp"
88
- | "grok";
90
+ | "grok"
91
+ | "fx";
89
92
 
90
93
  /**
91
94
  * True when the pi harness authenticates against Bedrock rather than a provider
@@ -158,6 +161,7 @@ export const REQUIRED_CRED_VARS_BY_PROVIDER: Record<SupportedProvider, readonly
158
161
  amp: ["AMP_API_KEY"],
159
162
  cursor: ["CURSOR_API_KEY"],
160
163
  grok: ["XAI_API_KEY", "OPENROUTER_API_KEY"],
164
+ fx: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "AI_GATEWAY_API_KEY"],
161
165
  };
162
166
 
163
167
  type CredentialChecker = (
@@ -171,6 +175,7 @@ export const CREDENTIAL_PROVIDER_CHECKERS: Record<SupportedProvider, CredentialC
171
175
  amp: (env) => checkAmpCredentials(env),
172
176
  cursor: (env) => checkCursorCredentials(env),
173
177
  grok: (env) => checkGrokCredentials(env),
178
+ fx: (env) => checkFxCredentials(env),
174
179
  claude: (env) => checkClaudeCredentials(env),
175
180
  "claude-managed": (env) => checkClaudeManagedCredentials(env),
176
181
  codex: (env, opts) => checkCodexCredentials(env, opts),
@@ -383,6 +388,7 @@ function parseCodexOAuthAccess(blob: string | undefined): string | null {
383
388
  * | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v3/self` |
384
389
  * | `dsh` | `DEEPSEEK_API_KEY` → `OPENROUTER_API_KEY` | DeepSeek `/models`, or OpenRouter `/key` |
385
390
  * | `amp` | `AMP_API_KEY` | `amp usage` (credit balance, no inference; 5s timeout) |
391
+ * | `fx` | `OPENROUTER_API_KEY` → `OPENAI_API_KEY` → `AI_GATEWAY_API_KEY` | OpenRouter `/key`, OpenAI `/v1/models`, or AI Gateway `/v1/models` |
386
392
  *
387
393
  * Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
388
394
  * otherwise. Errors are scrubbed via `scrubSecrets` before being returned.
@@ -639,12 +645,33 @@ export async function validateProviderCredentials(
639
645
  latency_ms: Date.now() - startedAt,
640
646
  };
641
647
  }
648
+ case "fx": {
649
+ // Same keys as `checkFxCredentials`, probed in route-table order.
650
+ const openRouterKey = env.OPENROUTER_API_KEY?.trim();
651
+ if (openRouterKey) {
652
+ return checkEachKey("OPENROUTER_API_KEY", openRouterKey, checkOpenRouter);
653
+ }
654
+ const openAiKey = env.OPENAI_API_KEY?.trim();
655
+ if (openAiKey) return checkEachKey("OPENAI_API_KEY", openAiKey, checkOpenAiApiKey);
656
+ const gatewayKey = env.AI_GATEWAY_API_KEY?.trim();
657
+ const gateway = openAICompatibleProvider("vercel");
658
+ if (gatewayKey && gateway) {
659
+ return checkEachKey("AI_GATEWAY_API_KEY", gatewayKey, (member) =>
660
+ checkOpenAICompatibleProvider(gateway, member),
661
+ );
662
+ }
663
+ return {
664
+ ok: false,
665
+ error: "Set OPENROUTER_API_KEY, OPENAI_API_KEY or AI_GATEWAY_API_KEY for fx.",
666
+ latency_ms: Date.now() - startedAt,
667
+ };
668
+ }
642
669
  case "acp":
643
670
  return presenceCheckOk();
644
671
  default:
645
672
  return {
646
673
  ok: false,
647
- error: `Unknown provider "${provider}". Supported: claude, claude-managed, codex, devin, opencode, pi, acp, dsh, cursor, amp, grok.`,
674
+ error: `Unknown provider "${provider}". Supported: claude, claude-managed, codex, devin, opencode, pi, acp, dsh, cursor, amp, grok, fx.`,
648
675
  latency_ms: Date.now() - startedAt,
649
676
  };
650
677
  }