@desplega.ai/agent-swarm 2.4.4 → 2.5.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.
- package/README.md +4 -2
- package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
|
@@ -246,23 +246,83 @@ export function parseCookieHeader(
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
* The single shared cookie name used before sessions became per-page. Every
|
|
250
|
+
* launch overwrote it, so only the newest page in a browser had a session.
|
|
251
|
+
* Still read for back-compat, but only for the page its payload names.
|
|
252
|
+
*/
|
|
253
|
+
export const LEGACY_PAGE_SESSION_COOKIE = "page_session";
|
|
254
|
+
|
|
255
|
+
/** Page ids are 32 hex chars today; anything outside the cookie-name-safe set is refused. */
|
|
256
|
+
const COOKIE_SAFE_PAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
257
|
+
|
|
258
|
+
export function isCookieSafePageId(pageId: string): boolean {
|
|
259
|
+
return COOKIE_SAFE_PAGE_ID.test(pageId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Per-page cookie name, so sessions for different pages coexist in one browser. */
|
|
263
|
+
export function pageSessionCookieName(pageId: string): string {
|
|
264
|
+
return `${LEGACY_PAGE_SESSION_COOKIE}_${pageId}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface VerifiedPageSession {
|
|
268
|
+
/** The raw signed token, forwarded upstream by the page proxy. */
|
|
269
|
+
token: string;
|
|
270
|
+
payload: PageSessionPayload;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type CookieRequest = { headers: { cookie?: string | string[] | undefined } };
|
|
274
|
+
|
|
275
|
+
async function verifyNamedCookie(
|
|
276
|
+
req: CookieRequest,
|
|
277
|
+
name: string,
|
|
278
|
+
): Promise<VerifiedPageSession | null> {
|
|
279
|
+
const token = parseCookieHeader(req.headers.cookie, name);
|
|
280
|
+
const payload = await verifyPageSession(token);
|
|
281
|
+
return payload && token ? { token, payload } : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find and verify the session for `pageId`: `page_session_<pageId>` first,
|
|
286
|
+
* then the legacy `page_session`. A cookie whose signed payload names a
|
|
287
|
+
* different page is ignored, so the result always satisfies
|
|
288
|
+
* `payload.pageId === pageId`. Returns `null` on any failure (no cookie,
|
|
289
|
+
* malformed, bad signature, expired, wrong page).
|
|
290
|
+
*/
|
|
291
|
+
export async function extractPageSession(
|
|
292
|
+
req: CookieRequest,
|
|
293
|
+
pageId: string,
|
|
294
|
+
): Promise<VerifiedPageSession | null> {
|
|
295
|
+
if (!isCookieSafePageId(pageId)) return null;
|
|
296
|
+
for (const name of [pageSessionCookieName(pageId), LEGACY_PAGE_SESSION_COOKIE]) {
|
|
297
|
+
const session = await verifyNamedCookie(req, name);
|
|
298
|
+
if (session?.payload.pageId === pageId) return session;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verify the legacy `page_session` cookie for whatever page it names. Only for
|
|
305
|
+
* callers that send no page id (clients predating per-page cookies).
|
|
306
|
+
*/
|
|
307
|
+
export async function extractLegacyPageSession(
|
|
308
|
+
req: CookieRequest,
|
|
309
|
+
): Promise<VerifiedPageSession | null> {
|
|
310
|
+
return verifyNamedCookie(req, LEGACY_PAGE_SESSION_COOKIE);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Payload-only form of `extractPageSession`. Shared by the `/p/:id` handler
|
|
315
|
+
* (`src/http/pages-public.ts`) and the realtime upgrade
|
|
316
|
+
* (`src/realtime/transport.ts`) so they converge on the same semantics.
|
|
256
317
|
*
|
|
257
318
|
* Accepts an object with a `headers.cookie` field — duck-typed to keep this
|
|
258
319
|
* helper test-friendly without dragging `node:http` types into the utility.
|
|
259
320
|
*/
|
|
260
|
-
export async function extractAndVerifyCookie(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return verifyPageSession(token);
|
|
321
|
+
export async function extractAndVerifyCookie(
|
|
322
|
+
req: CookieRequest,
|
|
323
|
+
pageId: string,
|
|
324
|
+
): Promise<PageSessionPayload | null> {
|
|
325
|
+
return (await extractPageSession(req, pageId))?.payload ?? null;
|
|
266
326
|
}
|
|
267
327
|
|
|
268
328
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -284,6 +344,10 @@ const PAGE_SESSION_TTL_SECONDS = 3600;
|
|
|
284
344
|
* - `dev=true` → `SameSite=Lax` without `Secure` (works on http://localhost).
|
|
285
345
|
* - `dev=false` → `SameSite=None; Secure` (cross-site iframe embedding in prod).
|
|
286
346
|
*
|
|
347
|
+
* The cookie is named `page_session_<pageId>`, so launching page B leaves
|
|
348
|
+
* page A's session intact. Responses should go through `pageSessionSetCookies`,
|
|
349
|
+
* which also bounds how many of these a browser keeps.
|
|
350
|
+
*
|
|
287
351
|
* The caller is responsible for setting `Set-Cookie` on the response — this
|
|
288
352
|
* helper only builds the string. TTL is 1 hour; renewed on every issuance.
|
|
289
353
|
*/
|
|
@@ -299,13 +363,12 @@ export async function issuePageSessionCookie(
|
|
|
299
363
|
...(opts.name ? { name: opts.name } : {}),
|
|
300
364
|
...(opts.op ? { op: true as const } : {}),
|
|
301
365
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (opts.dev) {
|
|
366
|
+
return buildCookie(pageSessionCookieName(pageId), token, PAGE_SESSION_TTL_SECONDS, opts.dev);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCookie(name: string, value: string, maxAge: number, dev: boolean): string {
|
|
370
|
+
const attrs = [`${name}=${value}`, "HttpOnly", "Path=/", `Max-Age=${maxAge}`];
|
|
371
|
+
if (dev) {
|
|
309
372
|
attrs.push("SameSite=Lax");
|
|
310
373
|
} else {
|
|
311
374
|
attrs.push("SameSite=None");
|
|
@@ -313,3 +376,53 @@ export async function issuePageSessionCookie(
|
|
|
313
376
|
}
|
|
314
377
|
return attrs.join("; ");
|
|
315
378
|
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Most per-page session cookies one browser keeps, counting the one being
|
|
382
|
+
* issued. Each is ~220 bytes on the wire, and `Path=/` sends all of them on
|
|
383
|
+
* every request to this host, so an unbounded jar grows past the server's
|
|
384
|
+
* 16 KB header limit (431 on every route, `/health` included) after ~75
|
|
385
|
+
* launches within the hour. 20 keeps the jar near 4.5 KB.
|
|
386
|
+
*/
|
|
387
|
+
export const MAX_PAGE_SESSION_COOKIES = 20;
|
|
388
|
+
|
|
389
|
+
const PAGE_SESSION_COOKIE_RE = new RegExp(
|
|
390
|
+
`(?:^|;\\s*)(${LEGACY_PAGE_SESSION_COOKIE}_[A-Za-z0-9_-]{1,64})=([^;]*)`,
|
|
391
|
+
"g",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* `Set-Cookie` values for a launch of `pageId`: the new session, plus an
|
|
396
|
+
* expiring cookie for every other per-page session in the request's jar that
|
|
397
|
+
* is invalid or expired, or falls outside the `MAX_PAGE_SESSION_COOKIES - 1`
|
|
398
|
+
* newest. The browser then holds at most `MAX_PAGE_SESSION_COOKIES` of them.
|
|
399
|
+
* An evicted page's open tab loses its session, the same as after expiry.
|
|
400
|
+
*/
|
|
401
|
+
export async function pageSessionSetCookies(
|
|
402
|
+
req: CookieRequest,
|
|
403
|
+
pageId: string,
|
|
404
|
+
opts: { dev: boolean; uid?: string; name?: string; op?: true },
|
|
405
|
+
): Promise<string[]> {
|
|
406
|
+
const issued = await issuePageSessionCookie(pageId, opts);
|
|
407
|
+
const header = Array.isArray(req.headers.cookie)
|
|
408
|
+
? req.headers.cookie.join("; ")
|
|
409
|
+
: (req.headers.cookie ?? "");
|
|
410
|
+
const keep: { name: string; exp: number; order: number }[] = [];
|
|
411
|
+
const evict = new Set<string>();
|
|
412
|
+
const seen = new Set([pageSessionCookieName(pageId)]);
|
|
413
|
+
for (const [, name, token] of header.matchAll(PAGE_SESSION_COOKIE_RE)) {
|
|
414
|
+
if (!name || seen.has(name)) continue;
|
|
415
|
+
seen.add(name);
|
|
416
|
+
const payload = await verifyPageSession(token);
|
|
417
|
+
if (payload && name === pageSessionCookieName(payload.pageId)) {
|
|
418
|
+
keep.push({ name, exp: payload.exp, order: keep.length });
|
|
419
|
+
} else {
|
|
420
|
+
evict.add(name);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// `exp` has one-second granularity. Browsers list older cookies first, so a
|
|
424
|
+
// later position breaks ties toward the more recent launch.
|
|
425
|
+
keep.sort((a, b) => b.exp - a.exp || b.order - a.order);
|
|
426
|
+
for (const { name } of keep.slice(MAX_PAGE_SESSION_COOKIES - 1)) evict.add(name);
|
|
427
|
+
return [issued, ...[...evict].map((name) => buildCookie(name, "", 0, opts.dev))];
|
|
428
|
+
}
|
|
@@ -53,6 +53,10 @@ const SENSITIVE_KEY_EXACT = new Set<string>([
|
|
|
53
53
|
"AZURE_DEVOPS_TOKEN",
|
|
54
54
|
// Worker-side copy of AZURE_DEVOPS_TOKEN read by the az azure-devops extension
|
|
55
55
|
"AZURE_DEVOPS_EXT_PAT",
|
|
56
|
+
// The `_TOKEN` / `_SECRET` suffixes already match these, so listing them
|
|
57
|
+
// does not change redaction and does not bump SCRUBBER_RULES_VERSION.
|
|
58
|
+
"BITBUCKET_TOKEN",
|
|
59
|
+
"BITBUCKET_WEBHOOK_SECRET",
|
|
56
60
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
57
61
|
"ANTHROPIC_API_KEY",
|
|
58
62
|
"OPENAI_API_KEY",
|
|
@@ -77,26 +77,56 @@ export async function refreshSkillsIfChanged(
|
|
|
77
77
|
isEnabled: boolean;
|
|
78
78
|
isActive: boolean;
|
|
79
79
|
};
|
|
80
|
+
type BundledFile = { path: string; content: string; isBinary: boolean };
|
|
80
81
|
let skillRows: SkillRow[] = [];
|
|
82
|
+
// Present when the bundle call succeeded: every complex skill's files arrive
|
|
83
|
+
// in the same response, so Step 3 never fans out per file.
|
|
84
|
+
let bundledFiles: Map<string, BundledFile[]> | null = null;
|
|
81
85
|
let newHash: string | null = null;
|
|
82
86
|
let listFetchOk = false;
|
|
87
|
+
|
|
88
|
+
// Preferred path: one request for the skills and all of their bundled files.
|
|
89
|
+
// A 4xx from an older server (no bundle endpoint yet) falls through to the
|
|
90
|
+
// legacy list + per-file path below.
|
|
83
91
|
try {
|
|
84
|
-
const
|
|
92
|
+
const bundleResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills/bundle`, {
|
|
85
93
|
headers: authHeaders,
|
|
86
94
|
});
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
skills: SkillRow[];
|
|
95
|
+
if (bundleResp.ok) {
|
|
96
|
+
const bundle = (await bundleResp.json()) as {
|
|
97
|
+
skills: (SkillRow & { files?: BundledFile[] })[];
|
|
90
98
|
signature?: string;
|
|
91
99
|
};
|
|
92
|
-
skillRows =
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
skillRows = bundle.skills;
|
|
101
|
+
bundledFiles = new Map(bundle.skills.map((skill) => [skill.id, skill.files ?? []]));
|
|
102
|
+
if (typeof bundle.signature === "string") {
|
|
103
|
+
newHash = bundle.signature;
|
|
95
104
|
}
|
|
96
105
|
listFetchOk = true;
|
|
97
106
|
}
|
|
98
107
|
} catch {
|
|
99
|
-
// Transient network / parse error —
|
|
108
|
+
// Transient network / parse error — the legacy path below gets a chance.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!listFetchOk) {
|
|
112
|
+
try {
|
|
113
|
+
const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
|
|
114
|
+
headers: authHeaders,
|
|
115
|
+
});
|
|
116
|
+
if (skillsResp.ok) {
|
|
117
|
+
const skillsData = (await skillsResp.json()) as {
|
|
118
|
+
skills: SkillRow[];
|
|
119
|
+
signature?: string;
|
|
120
|
+
};
|
|
121
|
+
skillRows = skillsData.skills;
|
|
122
|
+
if (typeof skillsData.signature === "string") {
|
|
123
|
+
newHash = skillsData.signature;
|
|
124
|
+
}
|
|
125
|
+
listFetchOk = true;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Transient network / parse error — bail out without touching the local FS
|
|
129
|
+
}
|
|
100
130
|
}
|
|
101
131
|
|
|
102
132
|
// Guard: a failed list fetch must not proceed to writeSkillsToFilesystem.
|
|
@@ -125,45 +155,55 @@ export async function refreshSkillsIfChanged(
|
|
|
125
155
|
const files: { path: string; content: string; isBinary: boolean }[] = [];
|
|
126
156
|
|
|
127
157
|
if (skill.isComplex) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
files
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const bundled = bundledFiles?.get(skill.id);
|
|
159
|
+
if (bundled) {
|
|
160
|
+
// Bundle path: every file (including binaries) arrived with the
|
|
161
|
+
// skill list, so there is nothing left to fetch.
|
|
162
|
+
files.push(...bundled);
|
|
163
|
+
} else {
|
|
164
|
+
// Legacy server: fetch manifest to know which files exist + which are binary
|
|
165
|
+
try {
|
|
166
|
+
const manifestResp = await fetch(`${apiUrl}/api/skills/${skill.id}/files`, {
|
|
167
|
+
headers: authHeaders,
|
|
168
|
+
});
|
|
169
|
+
if (manifestResp.ok) {
|
|
170
|
+
const manifestData = (await manifestResp.json()) as {
|
|
171
|
+
files: { path: string; isBinary: boolean }[];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Fetch content for each non-binary file (N+1 — acceptable for v1)
|
|
175
|
+
for (const manifestEntry of manifestData.files) {
|
|
176
|
+
if (manifestEntry.isBinary) {
|
|
177
|
+
files.push({ path: manifestEntry.path, content: "", isBinary: true });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const encodedPath = manifestEntry.path
|
|
182
|
+
.split("/")
|
|
183
|
+
.map(encodeURIComponent)
|
|
184
|
+
.join("/");
|
|
185
|
+
const fileResp = await fetch(
|
|
186
|
+
`${apiUrl}/api/skills/${skill.id}/files/${encodedPath}`,
|
|
187
|
+
{ headers: authHeaders },
|
|
188
|
+
);
|
|
189
|
+
if (fileResp.ok) {
|
|
190
|
+
const fileData = (await fileResp.json()) as {
|
|
191
|
+
file: { path: string; content: string; isBinary: boolean };
|
|
192
|
+
};
|
|
193
|
+
files.push({
|
|
194
|
+
path: fileData.file.path,
|
|
195
|
+
content: fileData.file.content,
|
|
196
|
+
isBinary: fileData.file.isBinary,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Non-fatal — skip this file
|
|
159
201
|
}
|
|
160
|
-
} catch {
|
|
161
|
-
// Non-fatal — skip this file
|
|
162
202
|
}
|
|
163
203
|
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Non-fatal — treat as no files (will skip complex skill per writer logic)
|
|
164
206
|
}
|
|
165
|
-
} catch {
|
|
166
|
-
// Non-fatal — treat as no files (will skip complex skill per writer logic)
|
|
167
207
|
}
|
|
168
208
|
}
|
|
169
209
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech-to-text for inbound audio. `TRANSCRIPTION_PROVIDER` picks the request
|
|
3
|
+
* shape:
|
|
4
|
+
*
|
|
5
|
+
* - `openai`: `POST <base>/audio/transcriptions`, `Authorization: Bearer`, form
|
|
6
|
+
* field `model`. OpenAI Whisper and self-hosted whisper servers speak this
|
|
7
|
+
* shape.
|
|
8
|
+
* - `groq`: the `openai` shape against Groq's OpenAI-compatible endpoint.
|
|
9
|
+
* - `elevenlabs`: `POST <base>/speech-to-text`, `xi-api-key` header, form
|
|
10
|
+
* field `model_id`.
|
|
11
|
+
*
|
|
12
|
+
* The key: an explicit `TRANSCRIPTION_API_KEY` wins, with `TRANSCRIPTION_PROVIDER`
|
|
13
|
+
* (default `openai`). Without it, transcription reuses a provider key the
|
|
14
|
+
* swarm already has: the pinned provider's key when `TRANSCRIPTION_PROVIDER` is
|
|
15
|
+
* set, else the first of `ELEVENLABS_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`.
|
|
16
|
+
* Off when none is set.
|
|
17
|
+
*
|
|
18
|
+
* `TRANSCRIPTION_MODEL` overrides the provider's default model.
|
|
19
|
+
* `TRANSCRIPTION_BASE_URL` overrides its base URL for an explicit key only: an
|
|
20
|
+
* auto-detected key always goes to its provider's own endpoint. Deepgram and AssemblyAI use other request shapes and are not
|
|
21
|
+
* supported. Read at call time, so a config reload applies without a restart.
|
|
22
|
+
*/
|
|
23
|
+
import { createValueScrubber, scrubSecrets } from "./secret-scrubber";
|
|
24
|
+
|
|
25
|
+
const TRANSCRIPTION_TIMEOUT_MS = 60_000;
|
|
26
|
+
|
|
27
|
+
/** The config key that switches transcription on explicitly. */
|
|
28
|
+
export const TRANSCRIPTION_API_KEY_ENV = "TRANSCRIPTION_API_KEY";
|
|
29
|
+
|
|
30
|
+
type ProviderSpec = {
|
|
31
|
+
/** The provider's own key, reused when `TRANSCRIPTION_API_KEY` is unset. */
|
|
32
|
+
keyEnv: string;
|
|
33
|
+
defaultBaseUrl: string;
|
|
34
|
+
defaultModel: string;
|
|
35
|
+
path: string;
|
|
36
|
+
modelField: string;
|
|
37
|
+
authHeaders: (apiKey: string) => Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const openAIShape = {
|
|
41
|
+
path: "/audio/transcriptions",
|
|
42
|
+
modelField: "model",
|
|
43
|
+
authHeaders: (apiKey: string) => ({ authorization: `Bearer ${apiKey}` }),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const PROVIDERS = {
|
|
47
|
+
openai: {
|
|
48
|
+
keyEnv: "OPENAI_API_KEY",
|
|
49
|
+
defaultBaseUrl: "https://api.openai.com/v1",
|
|
50
|
+
defaultModel: "whisper-1",
|
|
51
|
+
...openAIShape,
|
|
52
|
+
},
|
|
53
|
+
groq: {
|
|
54
|
+
keyEnv: "GROQ_API_KEY",
|
|
55
|
+
defaultBaseUrl: "https://api.groq.com/openai/v1",
|
|
56
|
+
defaultModel: "whisper-large-v3-turbo",
|
|
57
|
+
...openAIShape,
|
|
58
|
+
},
|
|
59
|
+
elevenlabs: {
|
|
60
|
+
keyEnv: "ELEVENLABS_API_KEY",
|
|
61
|
+
defaultBaseUrl: "https://api.elevenlabs.io/v1",
|
|
62
|
+
defaultModel: "scribe_v2",
|
|
63
|
+
path: "/speech-to-text",
|
|
64
|
+
modelField: "model_id",
|
|
65
|
+
authHeaders: (apiKey) => ({ "xi-api-key": apiKey }),
|
|
66
|
+
},
|
|
67
|
+
} satisfies Record<string, ProviderSpec>;
|
|
68
|
+
|
|
69
|
+
export type TranscriptionProvider = keyof typeof PROVIDERS;
|
|
70
|
+
|
|
71
|
+
/** Order in which an existing provider key is picked when nothing is pinned. */
|
|
72
|
+
const AUTO_DETECT_ORDER: TranscriptionProvider[] = ["elevenlabs", "openai", "groq"];
|
|
73
|
+
|
|
74
|
+
/** Every env key the provider and key resolution reads. */
|
|
75
|
+
export const TRANSCRIPTION_ENV_KEYS = [
|
|
76
|
+
TRANSCRIPTION_API_KEY_ENV,
|
|
77
|
+
"TRANSCRIPTION_PROVIDER",
|
|
78
|
+
"TRANSCRIPTION_BASE_URL",
|
|
79
|
+
"TRANSCRIPTION_MODEL",
|
|
80
|
+
...AUTO_DETECT_ORDER.map((name) => PROVIDERS[name].keyEnv),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export class TranscriptionError extends Error {
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "TranscriptionError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The provider and key a transcription call uses. */
|
|
91
|
+
export type TranscriptionConfig = {
|
|
92
|
+
provider: string;
|
|
93
|
+
apiKey: string;
|
|
94
|
+
/** The env key the API key came from. */
|
|
95
|
+
keyEnv: string;
|
|
96
|
+
/** `explicit`: `TRANSCRIPTION_API_KEY`. `auto`: a provider key the swarm already had. */
|
|
97
|
+
source: "explicit" | "auto";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function envValue(key: string): string | undefined {
|
|
101
|
+
return process.env[key]?.trim() || undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve provider and key, or null when no key is set. Unsupported provider names pass through. */
|
|
105
|
+
export function resolveTranscriptionConfig(): TranscriptionConfig | null {
|
|
106
|
+
const pinned = envValue("TRANSCRIPTION_PROVIDER")?.toLowerCase();
|
|
107
|
+
const explicitKey = envValue(TRANSCRIPTION_API_KEY_ENV);
|
|
108
|
+
if (explicitKey) {
|
|
109
|
+
return {
|
|
110
|
+
provider: pinned ?? "openai",
|
|
111
|
+
apiKey: explicitKey,
|
|
112
|
+
keyEnv: TRANSCRIPTION_API_KEY_ENV,
|
|
113
|
+
source: "explicit",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// A pinned provider only uses its own key, never another provider's.
|
|
117
|
+
const candidates = pinned
|
|
118
|
+
? AUTO_DETECT_ORDER.filter((name) => name === pinned)
|
|
119
|
+
: AUTO_DETECT_ORDER;
|
|
120
|
+
for (const name of candidates) {
|
|
121
|
+
const { keyEnv } = PROVIDERS[name];
|
|
122
|
+
const apiKey = envValue(keyEnv);
|
|
123
|
+
if (apiKey) return { provider: name, apiKey, keyEnv, source: "auto" };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isTranscriptionConfigured(): boolean {
|
|
129
|
+
return resolveTranscriptionConfig() !== null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function providerSpec(name: string): ProviderSpec {
|
|
133
|
+
if (!Object.hasOwn(PROVIDERS, name)) {
|
|
134
|
+
throw new TranscriptionError(
|
|
135
|
+
`TRANSCRIPTION_PROVIDER "${name}" is not supported (use ${Object.keys(PROVIDERS).join(", ")})`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return PROVIDERS[name as TranscriptionProvider];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** OpenAI puts the reason in `error.message`; ElevenLabs in `detail.message` or `detail`. */
|
|
142
|
+
function errorDetail(payload: Record<string, unknown> | null, status: number): string {
|
|
143
|
+
const error = payload?.error as { message?: unknown } | undefined;
|
|
144
|
+
if (typeof error?.message === "string") return error.message;
|
|
145
|
+
const detail = payload?.detail as { message?: unknown } | string | undefined;
|
|
146
|
+
if (typeof detail === "string") return detail;
|
|
147
|
+
if (typeof detail?.message === "string") return detail.message;
|
|
148
|
+
return `HTTP ${status}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Transcribe one audio file. Throws `TranscriptionError` when unconfigured or when the call fails. */
|
|
152
|
+
export async function transcribeAudio(input: {
|
|
153
|
+
data: Blob;
|
|
154
|
+
filename: string;
|
|
155
|
+
mimeType?: string;
|
|
156
|
+
}): Promise<string> {
|
|
157
|
+
const config = resolveTranscriptionConfig();
|
|
158
|
+
if (!config) throw new TranscriptionError("no transcription provider key is set");
|
|
159
|
+
const { apiKey } = config;
|
|
160
|
+
const provider = providerSpec(config.provider);
|
|
161
|
+
// An auto-detected key only goes to its own provider: a base URL set in
|
|
162
|
+
// config must not receive a key that was stored for another purpose.
|
|
163
|
+
const baseUrlOverride =
|
|
164
|
+
config.source === "explicit" ? process.env.TRANSCRIPTION_BASE_URL?.trim() : undefined;
|
|
165
|
+
const baseUrl = (baseUrlOverride || provider.defaultBaseUrl).replace(/\/+$/, "");
|
|
166
|
+
// scrubSecrets skips known values under 12 chars, and a self-hosted key can
|
|
167
|
+
// be shorter, so the submitted key is scrubbed by value from every error.
|
|
168
|
+
const keyScrubber = createValueScrubber([{ value: apiKey, name: config.keyEnv }]);
|
|
169
|
+
const scrub = (text: string) => scrubSecrets(keyScrubber.text(text));
|
|
170
|
+
const form = new FormData();
|
|
171
|
+
const type = input.mimeType ?? input.data.type;
|
|
172
|
+
form.append("file", type ? new Blob([input.data], { type }) : input.data, input.filename);
|
|
173
|
+
form.append(
|
|
174
|
+
provider.modelField,
|
|
175
|
+
process.env.TRANSCRIPTION_MODEL?.trim() || provider.defaultModel,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let response: Response;
|
|
179
|
+
try {
|
|
180
|
+
response = await fetch(`${baseUrl}${provider.path}`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: provider.authHeaders(apiKey),
|
|
183
|
+
body: form,
|
|
184
|
+
signal: AbortSignal.timeout(TRANSCRIPTION_TIMEOUT_MS),
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
188
|
+
throw new TranscriptionError(`no answer within ${TRANSCRIPTION_TIMEOUT_MS / 1000} s`);
|
|
189
|
+
}
|
|
190
|
+
throw new TranscriptionError(scrub(error instanceof Error ? error.message : String(error)));
|
|
191
|
+
}
|
|
192
|
+
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
193
|
+
if (!response.ok || typeof payload?.text !== "string") {
|
|
194
|
+
throw new TranscriptionError(scrub(errorDetail(payload, response.status)));
|
|
195
|
+
}
|
|
196
|
+
return payload.text.trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The task-text note for an audio file when no provider is configured. */
|
|
200
|
+
export const NOT_TRANSCRIBED_NOTE = `(not transcribed: no transcription provider configured, set ${TRANSCRIPTION_API_KEY_ENV} or one of ${AUTO_DETECT_ORDER.map((name) => PROVIDERS[name].keyEnv).join(", ")})`;
|
|
201
|
+
|
|
202
|
+
const AUDIO_EXTENSIONS: Record<string, string> = {
|
|
203
|
+
"audio/ogg": "ogg",
|
|
204
|
+
"audio/opus": "ogg",
|
|
205
|
+
"audio/mpeg": "mp3",
|
|
206
|
+
"audio/mp4": "m4a",
|
|
207
|
+
"audio/x-m4a": "m4a",
|
|
208
|
+
"audio/wav": "wav",
|
|
209
|
+
"audio/x-wav": "wav",
|
|
210
|
+
"audio/webm": "webm",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** One audio file to transcribe. `key` names its note in the returned map. */
|
|
214
|
+
export type AudioFile = { key: string; name: string; data: Blob; mimeType?: string };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transcribe audio files into task-text notes, one per file: `[Transcript of
|
|
218
|
+
* <name>]` and the text, `(transcription failed: <reason>)`, or (when no
|
|
219
|
+
* provider is configured) `NOT_TRANSCRIBED_NOTE`. Never throws, so a failed
|
|
220
|
+
* call never blocks the task it belongs to. `logPrefix` tags the warning.
|
|
221
|
+
*/
|
|
222
|
+
export async function transcribeAudioNotes(
|
|
223
|
+
files: AudioFile[],
|
|
224
|
+
logPrefix: string,
|
|
225
|
+
): Promise<Map<string, string>> {
|
|
226
|
+
const notes = new Map<string, string>();
|
|
227
|
+
if (files.length === 0) return notes;
|
|
228
|
+
if (!isTranscriptionConfigured()) {
|
|
229
|
+
for (const file of files) notes.set(file.key, NOT_TRANSCRIBED_NOTE);
|
|
230
|
+
return notes;
|
|
231
|
+
}
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
// Voice notes can carry no file name; the endpoint reads the format from the extension.
|
|
234
|
+
const extension = AUDIO_EXTENSIONS[file.mimeType ?? ""] ?? "ogg";
|
|
235
|
+
const filename = /\.\w+$/.test(file.name) ? file.name : `${file.name}.${extension}`;
|
|
236
|
+
try {
|
|
237
|
+
const text = await transcribeAudio({ data: file.data, filename, mimeType: file.mimeType });
|
|
238
|
+
notes.set(file.key, `[Transcript of ${file.name}]\n${text || "(no speech recognized)"}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const reason = scrubSecrets(error instanceof Error ? error.message : String(error));
|
|
241
|
+
console.warn(`${logPrefix} could not transcribe file ${file.key}: ${reason}`);
|
|
242
|
+
notes.set(file.key, `(transcription failed: ${reason})`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return notes;
|
|
246
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud URL helpers. Pure functions, safe on both the API server and
|
|
3
|
+
* the worker (no DB imports). Bitbucket Data Center is out of scope.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* True when the URL's host is bitbucket.org. Only the host counts, so a
|
|
8
|
+
* `bitbucket.org` path segment on another host (`gitlab.com/bitbucket.org/x`)
|
|
9
|
+
* is not Bitbucket.
|
|
10
|
+
*/
|
|
11
|
+
export function isBitbucketUrl(url: string): boolean {
|
|
12
|
+
const trimmed = url.trim();
|
|
13
|
+
// SCP-style SSH: user@host:path
|
|
14
|
+
const scp = trimmed.match(/^[^@/\s:]+@([^:/\s]+):/);
|
|
15
|
+
if (scp) return (scp[1] as string).toLowerCase() === "bitbucket.org";
|
|
16
|
+
try {
|
|
17
|
+
return new URL(trimmed).hostname.toLowerCase() === "bitbucket.org";
|
|
18
|
+
} catch {
|
|
19
|
+
// Scheme-less `bitbucket.org/ws/repo`
|
|
20
|
+
return /^bitbucket\.org(\/|$)/i.test(trimmed);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Split a Bitbucket Cloud remote into its workspace and repository slug.
|
|
26
|
+
* Accepts the HTTPS forms (with or without a `user@` prefix, trailing `/` or
|
|
27
|
+
* `.git`) and the SSH forms (`git@bitbucket.org:ws/repo.git`,
|
|
28
|
+
* `ssh://git@bitbucket.org/ws/repo.git`). Returns null otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function parseBitbucketRepoUrl(url: string): { workspace: string; repoSlug: string } | null {
|
|
31
|
+
const trimmed = url.trim();
|
|
32
|
+
if (!isBitbucketUrl(trimmed)) return null;
|
|
33
|
+
|
|
34
|
+
// SCP-style SSH: git@bitbucket.org:{workspace}/{repo}.git
|
|
35
|
+
const scp = trimmed.match(/^[^@/\s]+@bitbucket\.org:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
36
|
+
if (scp) return { workspace: scp[1] as string, repoSlug: scp[2] as string };
|
|
37
|
+
|
|
38
|
+
let parsed: URL;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(trimmed);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.hostname.toLowerCase() !== "bitbucket.org") return null;
|
|
45
|
+
const parts = parsed.pathname
|
|
46
|
+
.replace(/\.git\/?$/, "")
|
|
47
|
+
.split("/")
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const [workspace, repoSlug] = parts;
|
|
50
|
+
if (!workspace || !repoSlug) return null;
|
|
51
|
+
return { workspace: decodeURIComponent(workspace), repoSlug: decodeURIComponent(repoSlug) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `workspace/repo_slug` for a Bitbucket Cloud remote, or null. */
|
|
55
|
+
export function bitbucketFullName(url: string): string | null {
|
|
56
|
+
const parsed = parseBitbucketRepoUrl(url);
|
|
57
|
+
return parsed ? `${parsed.workspace}/${parsed.repoSlug}` : null;
|
|
58
|
+
}
|
package/src/vcs/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isAzureDevOpsUrl } from "./azure-devops";
|
|
6
|
+
import { isBitbucketUrl } from "./bitbucket";
|
|
6
7
|
import type { VcsProvider } from "./types";
|
|
7
8
|
|
|
8
9
|
export type { VcsProvider } from "./types";
|
|
@@ -13,6 +14,7 @@ export type { VcsProvider } from "./types";
|
|
|
13
14
|
*/
|
|
14
15
|
export function detectVcsProvider(url: string): VcsProvider | null {
|
|
15
16
|
if (isAzureDevOpsUrl(url)) return "azure-devops";
|
|
17
|
+
if (isBitbucketUrl(url)) return "bitbucket";
|
|
16
18
|
if (url.includes("gitlab.com") || url.includes("gitlab.")) return "gitlab";
|
|
17
19
|
if (url.includes("github.com") || /^[\w.-]+\/[\w.-]+$/.test(url)) return "github";
|
|
18
20
|
return null;
|