zidane 7.2.5 → 8.0.4
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/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27243
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-BKVIIOqH.js +0 -331
- package/dist/ipc-BKVIIOqH.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-DfdXrGg9.js +0 -1549
- package/dist/shell-path-DfdXrGg9.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2b-CEVOdRib.js","names":[],"sources":["../src/contexts/e2b-sdk.ts","../src/contexts/e2b-stream.ts","../src/contexts/e2b.ts"],"sourcesContent":["/**\n * Structural `e2b` SDK views + env merge shared by the E2B provider modules.\n * Internal module.\n */\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `e2b` SDK surface we touch. Declared locally\n// (rather than imported) so the package typechecks without the optional peer\n// dependency installed — mirroring how `docker.ts` keeps `dockerode` at arm's\n// length. The real SDK satisfies these shapes. The peer dependency floor is\n// `e2b@^2`: every member used here exists across 2.x, but the pause surface\n// shifted within it — `betaPause()` from 2.0.0, the stable `pause()` from\n// 2.27.1 — so `onDestroy: 'pause'` prefers `pause()` and falls back to\n// `betaPause()` (both optional below, resolved at runtime in `destroy`).\n// ---------------------------------------------------------------------------\n\n/**\n * Options that flip `commands.run` into background mode. Split out so the\n * overload above can discriminate on it: `background: true` changes the\n * return type from a finished result to a live handle.\n *\n * `stdin: true` and a huge `timeoutMs` are not incidental — the SDK\n * defaults are `false` and 60s, which would respectively hand the child an\n * immediate EOF and kill a session-lifetime process after a minute. See\n * {@link E2B_STREAM_TIMEOUT_MS} for why the timeout is a large number and\n * not `0`.\n */\nexport interface E2BBackgroundOptions {\n background: true\n stdin: true\n timeoutMs: number\n cwd?: string\n envs?: Record<string, string>\n onStdout?: (data: string) => void\n onStderr?: (data: string) => void\n}\n\n/**\n * \"Effectively unlimited\" command timeout for `execStream`. 2^31-1 ms\n * (~24.8 days — far beyond any sandbox lifetime) because both edges are\n * cliffs: `0` is treated by `@connectrpc/connect`'s deadline signal as\n * already expired (instant DeadlineExceeded), and anything above 2^31-1\n * overflows `setTimeout` and fires immediately.\n */\nexport const E2B_STREAM_TIMEOUT_MS = 2 ** 31 - 1\n\nexport interface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\nexport interface E2BEntry {\n name: string\n /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */\n type?: string\n}\n\n/**\n * Handle returned by a `background: true` command. Structural subset of the\n * SDK's `CommandHandle` — only what `execStream` needs.\n */\nexport interface E2BCommandHandle {\n pid: number\n wait: () => Promise<E2BCommandResult>\n}\n\n/** The SDK's PTY `CommandHandle`, as far as terminals use it. */\nexport interface E2BPtyHandle {\n pid: number\n wait: () => Promise<{ exitCode?: number }>\n disconnect: () => Promise<void>\n}\n\nexport interface E2BPtyOutput {\n onData: (data: Uint8Array) => void\n timeoutMs?: number\n}\n\nexport interface E2BSandbox {\n sandboxId: string\n /** Interactive terminals (`sandbox.pty`); used only by `openTerminal`. */\n pty?: {\n create: (opts: E2BPtyOutput & { cols: number, rows: number, cwd?: string, envs?: Record<string, string> }) => Promise<E2BPtyHandle>\n connect: (pid: number, opts?: E2BPtyOutput) => Promise<E2BPtyHandle>\n sendInput: (pid: number, data: Uint8Array) => Promise<void>\n resize: (pid: number, size: { cols: number, rows: number }) => Promise<void>\n kill: (pid: number) => Promise<boolean>\n }\n commands: {\n run: ((cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>)\n & ((cmd: string, opts: E2BBackgroundOptions) => Promise<E2BCommandHandle>)\n /** Present from the SDK's Commands module; used only by `execStream`. */\n sendStdin?: (pid: number, data: string | Uint8Array) => Promise<void>\n kill?: (pid: number) => Promise<boolean>\n }\n files: {\n read: (path: string) => Promise<string>\n /**\n * Deliberately WIDER than the SDK's declared input (string | ArrayBuffer |\n * Blob | ReadableStream): every upload path there ends at `toBlob`, whose\n * fallback is `new Blob([data])` — a BlobPart, so a Uint8Array view\n * (byteOffset-aware) uploads verbatim. Strings still travel as strings, so\n * only a byte write depends on this widening.\n */\n write: (path: string, data: string | Uint8Array) => Promise<unknown>\n list: (path: string) => Promise<E2BEntry[]>\n }\n kill: () => Promise<unknown>\n // The pause surface shifted within 2.x: `betaPause` from 2.0.0, the stable\n // `pause` from 2.27.1. Both are optional so `destroy` can prefer `pause`,\n // fall back to `betaPause`, and stay defensive if a mismatched install\n // exposes neither. Both resolve to whether the sandbox was newly paused.\n pause?: () => Promise<boolean>\n betaPause?: () => Promise<boolean>\n /** Extend (or shorten) the sandbox's timeout from now — used by `keepAliveSeconds`. */\n setTimeout?: (timeoutMs: number) => Promise<void>\n}\n\n/**\n * Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn\n * `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`\n * when the merge is empty so callers can skip setting `envs` on the create\n * options entirely (rather than handing the SDK an empty object).\n */\nexport function resolveE2BEnv(\n optionEnv: Record<string, string> | undefined,\n perSpawnEnv: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const merged = { ...optionEnv, ...perSpawnEnv }\n return Object.keys(merged).length > 0 ? merged : undefined\n}\n","/**\n * Live bidirectional `execStream` of the E2B sandbox provider. Internal module.\n */\n\nimport type { Logger } from '../logger'\nimport type { E2BCommandHandle, E2BPtyHandle, E2BSandbox } from './e2b-sdk'\nimport type { SandboxProvider } from './sandbox'\nimport type { StreamHandle, TerminalProcess } from './types'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { E2B_STREAM_TIMEOUT_MS, resolveE2BEnv } from './e2b-sdk'\nimport { TERMINAL_ENV } from './local-terminal'\nimport { StreamQueue } from './stream-queue'\n\n/** Registry lookup of the provider (`get` in `createE2BProvider`). */\nexport type E2BLiveGet = (sandboxId: string) => { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean }\n\nexport function e2bStreamMethods(get: E2BLiveGet, logger: Logger): Required<Pick<SandboxProvider, 'execStream'>> {\n return {\n /**\n * Live bidirectional stream — see `ExecutionContext.execStream`.\n *\n * Backed by E2B's pipe-based command API rather than `sandbox.pty`.\n * Both can carry stdin, but a PTY would force us to defeat terminal\n * semantics we never wanted (input echo, `\\n`→`\\r\\n` rewriting, the\n * ~4096-byte canonical line cap, and stdout/stderr merged into one\n * stream). Pipes give split streams and byte-faithful framing for free.\n *\n * KNOWN RESIDUAL: `onStdout` / `onStderr` deliver `string`, already\n * decoded by the SDK. A chunk boundary that splits a multibyte\n * codepoint is therefore corrupted before it reaches us — we re-encode\n * to bytes to satisfy the seam, but cannot recover what the decoder\n * dropped. Newline framing and ASCII structure are unaffected, so\n * JSON-RPC still parses; the damage would be confined to non-ASCII\n * inside string values. If that ever bites, `sandbox.pty.create`\n * hands back raw `Uint8Array` and the Daytona PTY path in\n * `./daytona.ts` is the template for adopting it here.\n */\n async execStream(sandboxId, command, opts): Promise<StreamHandle> {\n const { sandbox, env } = get(sandboxId)\n\n opts?.signal?.throwIfAborted()\n\n // A stalled consumer cannot be signalled back to the E2B SDK (its\n // output callbacks are sync `void`), so surface the backlog instead of\n // letting it read as an unexplained host OOM.\n const warnBacklog = (which: string) => (bytes: number) =>\n logger.warn('execStream consumer is not draining output', { sandboxId, stream: which, bufferedBytes: bytes })\n const stdout = new StreamQueue(warnBacklog('stdout'))\n const stderr = new StreamQueue(warnBacklog('stderr'))\n\n // `background: true` returns a handle instead of awaiting completion;\n // `stdin: true` is required (it defaults to FALSE) or the child gets\n // EOF immediately and a JSON-RPC server exits before its first read.\n // `timeoutMs` overrides E2B's 60s default command timeout, which\n // would otherwise kill a session-lifetime server mid-run. NOT `0`:\n // the SDK forwards the value into `@connectrpc/connect`, whose\n // deadline signal treats `timeoutMs <= 0` as ALREADY EXPIRED and\n // aborts the call instantly (the PTY docs' \"0 = indefinite\" does not\n // hold for `commands.run` on this SDK). So: the largest value a JS\n // timer accepts (~24.8 days) — anything above 2^31-1 overflows\n // setTimeout and fires immediately, which is just `0` with extra\n // steps.\n // Kill semantics need a process GROUP, and E2B does not give us one:\n // the reported pid runs in envd's own group (observed live: pid 2003,\n // pgid 1235 = envd). Signalling that group would kill the sandbox\n // agent itself, and signalling the pid alone leaves grandchildren\n // running — a wrapper launcher (npx, uv, a shell one-liner) keeps the\n // real server alive to accumulate across reconnects. So put the child\n // in its own session with `setsid -w` and record ITS pid, which is\n // then the group id. `-w` makes setsid wait and propagate the exit\n // code; bare setsid's parent exits immediately and the stream tears\n // down. The probe falls back to a plain exec on images without `-w`,\n // where the pid-only kill still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${Math.random().toString(36).slice(2, 10)}.pid`\n // `sh -c <quoted>` rather than a bare `exec ${command}`: with a\n // compound command (`a; b >&2; c`) a bare exec binds to the FIRST\n // command only and silently drops the rest.\n const leader = `echo ${'$'}${'$'} > ${pidFile}; exec sh -c ${alwaysQuote(command)}`\n const wrapped = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${alwaysQuote(leader)}; else exec sh -c ${alwaysQuote(leader)}; fi`\n\n // Typed via the `background: true` overload on `E2BSandbox['commands']`\n // — that is what the overload exists for, so no cast is needed here.\n const handle: E2BCommandHandle = await sandbox.commands.run(wrapped, {\n background: true,\n stdin: true,\n timeoutMs: E2B_STREAM_TIMEOUT_MS,\n ...(opts?.cwd ? { cwd: opts.cwd } : {}),\n envs: resolveE2BEnv(env, opts?.env),\n onStdout: (data: string) => stdout.push(new TextEncoder().encode(data)),\n onStderr: (data: string) => stderr.push(new TextEncoder().encode(data)),\n })\n\n const pid: number = handle.pid\n\n const exited = handle.wait()\n .then(result => result?.exitCode ?? 0)\n // E2B throws CommandExitError on a non-zero exit; the seam promises\n // a never-rejecting `exited`, so translate rather than propagate.\n .catch((err: any) => typeof err?.exitCode === 'number' ? err.exitCode : 137)\n .finally(() => { stdout.close(); stderr.close() })\n\n // Read back the session leader's pid, which `setsid -w` made the\n // group id. Separate one-shot command: the API exposes no group verb.\n let commandExited = false\n void exited.then(() => { commandExited = true })\n const readLeaderPid = async (): Promise<number> => {\n for (let attempt = 0; attempt < 20; attempt++) {\n const probe = await sandbox.commands\n .run(`cat ${pidFile} 2>/dev/null || true`)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout?.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0)\n return parsed\n // A dead command will never write the pidfile — a missing binary\n // in the image (the most likely user-facing failure) exits\n // immediately, and burning the remaining attempts would cost up\n // to 20 sequential sandbox round-trips before bootstrap can\n // surface the real error. One more probe already ran above, so\n // nothing written before death is missed.\n if (commandExited)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n return -1\n }\n const leaderPid = await readLeaderPid()\n\n const kill = async (): Promise<void> => {\n if (leaderPid > 0) {\n // Group first (the leader IS the pgid), pid-only as the fallback\n // for images that took the no-setsid branch. NEVER a bare group\n // kill on the reported pid: that group belongs to envd.\n await sandbox.commands\n .run(`kill -TERM -${leaderPid} 2>/dev/null || kill -TERM ${leaderPid} 2>/dev/null || true`)\n .catch(() => {})\n }\n await sandbox.commands.run(`rm -f ${pidFile}`).catch(() => {})\n await sandbox.commands.kill?.(pid).catch(() => {})\n }\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit — the (typically run-long) signal otherwise\n // retains the `kill` closure and the sandbox handle for the rest\n // of the run, one dead closure per exec.\n const onAbort = () => { void kill() }\n const detach = () => opts.signal!.removeEventListener('abort', onAbort)\n opts.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid,\n stdout,\n stderr,\n write: async (data: Uint8Array): Promise<void> => {\n if (!sandbox.commands.sendStdin)\n throw new Error('E2B SDK is too old: `commands.sendStdin` is required for execStream')\n await sandbox.commands.sendStdin(pid, data)\n },\n kill,\n exited,\n }\n },\n }\n}\n\n/**\n * A `TerminalProcess` over an E2B PTY handle. Input is chained so keystrokes\n * sent as separate RPCs can't overtake each other.\n */\nfunction e2bTerminal(sandbox: E2BSandbox, handle: E2BPtyHandle): TerminalProcess {\n const pty = sandbox.pty!\n const pid = handle.pid\n const encoder = new TextEncoder()\n let input: Promise<unknown> = Promise.resolve()\n let killed: Promise<void> | null = null\n return {\n id: String(pid),\n reconnectable: true,\n write: (data) => {\n const bytes = typeof data === 'string' ? encoder.encode(data) : data\n input = input.then(() => pty.sendInput(pid, bytes)).catch(() => {})\n },\n resize: (cols, rows) => {\n void pty.resize(pid, { cols, rows }).catch(() => {})\n },\n kill: () => (killed ??= pty.kill(pid).then(() => {}, () => {})),\n detach: () => handle.disconnect().catch(() => {}),\n // A kill or a normal exit settles with a code; a dropped connection with -1.\n exited: handle.wait().then(\n result => result?.exitCode ?? 0,\n (err: any) => typeof err?.exitCode === 'number' ? err.exitCode : -1,\n ),\n }\n}\n\nexport function e2bTerminalMethods(get: E2BLiveGet): Required<Pick<SandboxProvider, 'openTerminal' | 'reconnectTerminal'>> {\n return {\n /**\n * A login shell in the sandbox through `sandbox.pty`. The PTY is a\n * sandbox process, not a connection: it outlives the host and\n * `reconnectTerminal` finds it again by pid. `timeoutMs` is maxed for\n * the same reason as `execStream`'s (the 60 s default would cut the\n * shell; `0` would expire it at once).\n */\n async openTerminal(sandboxId, options): Promise<TerminalProcess> {\n const { sandbox, env } = get(sandboxId)\n if (!sandbox.pty)\n throw new Error('E2B SDK is too old: `sandbox.pty` is required for terminals.')\n const handle = await sandbox.pty.create({\n cols: options.cols,\n rows: options.rows,\n ...(options.cwd ? { cwd: options.cwd } : {}),\n envs: resolveE2BEnv(env, { ...options.env, ...TERMINAL_ENV }),\n timeoutMs: E2B_STREAM_TIMEOUT_MS,\n onData: options.onData,\n })\n return e2bTerminal(sandbox, handle)\n },\n\n async reconnectTerminal(sandboxId, id, options): Promise<TerminalProcess | null> {\n const { sandbox } = get(sandboxId)\n const pid = Number.parseInt(id, 10)\n if (!sandbox.pty || !Number.isFinite(pid))\n return null\n const handle = await sandbox.pty.connect(pid, { onData: options.onData, timeoutMs: E2B_STREAM_TIMEOUT_MS }).catch(() => null)\n if (!handle)\n return null\n const terminal = e2bTerminal(sandbox, handle)\n terminal.resize(options.cols, options.rows)\n return terminal\n },\n }\n}\n","/**\n * E2B sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of E2B's firecracker-backed\n * micro-VMs. Pair it with {@link createSandboxContext} to run an agent's\n * shell + filesystem tools inside an ephemeral E2B sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createE2BProvider } from 'zidane/contexts/e2b'\n *\n * const execution = createSandboxContext(createE2BProvider())\n * ```\n *\n * ## Pointing at your own E2B\n *\n * The two knobs that select *which* E2B you talk to are `apiKey` and\n * `domain`. Self-hosted / on-prem E2B clusters expose a custom domain; pass\n * it here (or via the `E2B_DOMAIN` env var) and the SDK routes every call to\n * your cluster instead of E2B's hosted API. Precedence, highest first:\n *\n * 1. Per-spawn `SpawnConfig.sandbox` fields (`apiKey`, `domain`, `template`)\n * 2. {@link E2BProviderOptions} passed to this factory\n * 3. `E2B_API_KEY` / `E2B_DOMAIN` env vars (read by the SDK itself), and\n * `E2B_TEMPLATE` (read by this provider — the SDK does not read it)\n *\n * The default `base` template does not exist on self-hosted clusters, so\n * picking the right template matters: an invalid template still returns a\n * sandbox id from the control plane, but its envd never boots and every\n * `exec`/file op then hangs until it times out. Set `E2B_TEMPLATE` (or pass\n * `template`) to a template that actually exists on your cluster.\n *\n * Requires `e2b@^2` as an optional peer dependency:\n * `bun add e2b`.\n */\n\nimport type { Logger } from '../logger'\nimport type { E2BCommandResult, E2BSandbox } from './e2b-sdk'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig } from './types'\nimport { Buffer } from 'node:buffer'\nimport { consoleSink, createLogger } from '../logger'\nimport { resolveE2BEnv } from './e2b-sdk'\nimport { e2bStreamMethods, e2bTerminalMethods } from './e2b-stream'\nimport { capBufferedExecOutput } from './output-cap'\n\nexport { resolveE2BEnv } from './e2b-sdk'\n\n/** What E2B does when a sandbox's timeout elapses (e2b SDK `SandboxLifecycle`). */\nexport interface E2BLifecycle {\n onTimeout: 'pause' | 'kill'\n /** Resume a paused sandbox on the next connect / request (needs `onTimeout: 'pause'`). */\n autoResume?: boolean\n}\n\ninterface E2BCreateOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n envs?: Record<string, string>\n metadata?: Record<string, string>\n lifecycle?: E2BLifecycle\n}\n\ninterface E2BConnectOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n}\n\ninterface E2BSandboxStatic {\n create: (templateOrOpts?: string | E2BCreateOptions, opts?: E2BCreateOptions) => Promise<E2BSandbox>\n connect: (sandboxId: string, opts?: E2BConnectOptions) => Promise<E2BSandbox>\n}\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport interface E2BProviderOptions {\n /** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */\n apiKey?: string\n /**\n * E2B API domain. Set this to point at a self-hosted / on-prem cluster\n * (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,\n * then to E2B's hosted default.\n */\n domain?: string\n /**\n * Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`\n * env var, then to E2B's hosted `base` template. Note that `base` does not\n * exist on self-hosted clusters — supply a template that does.\n */\n template?: string\n /**\n * Connect to a pre-existing sandbox by id instead of creating a fresh one\n * (E2B's `Sandbox.connect`). When set, `template` is ignored — you're\n * attaching to a sandbox that already exists. The provider treats such a\n * sandbox as externally owned: it is NOT killed on `destroy`, nor torn down\n * if the readiness probe / pregame fails, since the caller manages its\n * lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.\n */\n sandboxId?: string\n /**\n * Default working directory, applied when a spawn doesn't set its own\n * `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.\n */\n cwd?: string\n /**\n * Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the\n * SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.\n */\n timeoutSeconds?: number\n /**\n * Environment variables baked into every sandbox at create time (passed to\n * E2B's `Sandbox.create({ envs })`), so every command run in the sandbox\n * sees them. Per-spawn `SpawnConfig.env` is merged over these.\n */\n env?: Record<string, string>\n /**\n * Logger for provider-level lifecycle lines (notably the readiness wait).\n * Defaults to a {@link consoleSink}-backed logger so the messages reach\n * stderr without the caller wiring one up.\n */\n logger?: Logger\n /**\n * Total deadline, in seconds, for the post-create readiness probe (see\n * {@link waitForE2BReady}). A freshly-created sandbox can return its id\n * before `envd` accepts commands; `spawn` blocks until a trivial probe\n * succeeds so the first real command never absorbs the cold start. Defaults\n * to `60`. Set to `0` to disable the gate.\n */\n readinessTimeoutSeconds?: number\n /**\n * A setup script to upload and run once the sandbox is ready, before any\n * prompting begins (see {@link runE2BPregame}). `name` is the basename used\n * for the uploaded file; `content` is the script body. The script is run\n * directly so its shebang selects the interpreter. A non-zero exit aborts\n * `spawn` (and tears the sandbox down) — a broken setup must not silently\n * hand back a half-provisioned sandbox.\n */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox when `destroy` is called:\n *\n * - `'kill'` (default) — call `sandbox.kill()`. The sandbox is deleted and\n * its state is gone.\n * - `'pause'` — call `sandbox.pause()` (falling back to the older\n * `betaPause()` on `e2b` 2.0–2.26). The sandbox's filesystem and process\n * state are preserved; reconnect later with the `sandboxId` option (E2B's\n * `Sandbox.connect` auto-resumes a paused sandbox).\n * - `'leave'` — drop the local handle without making any API call. The\n * sandbox keeps running until E2B's own timeout fires.\n *\n * Has no effect on sandboxes the provider only connected to (the `sandboxId`\n * option) — those are always left untouched on destroy since the caller owns\n * their lifecycle.\n */\n onDestroy?: 'kill' | 'pause' | 'leave'\n /**\n * What happens when a created sandbox's timeout elapses. `{ onTimeout:\n * 'pause', autoResume: true }` makes a timeout a pause instead of a kill —\n * what long-lived sessions want. Default: E2B's (kill).\n */\n lifecycle?: E2BLifecycle\n /**\n * Keep the sandbox alive while it's used: at most once a minute, any\n * command or file call pushes its timeout out to this many seconds from\n * now (`sandbox.setTimeout`). An idle sandbox still times out. Off when\n * unset.\n */\n keepAliveSeconds?: number\n}\n\n/** Minimum gap between two keep-alive `setTimeout` calls on one sandbox. */\nconst KEEP_ALIVE_THROTTLE_MS = 60_000\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_MS = 5000\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/**\n * Command timeout for the pregame setup script. E2B's `commands.run` defaults\n * to 60s and offers no true \"unlimited\" — a `timeoutMs` of 0 is a 0ms deadline\n * (instant timeout), not \"off\". Setup scripts (`apt`/`pip` installs, builds)\n * routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.\n * The sandbox's own lifetime independently bounds anything longer.\n */\nconst PREGAME_TIMEOUT_MS = 30 * 60 * 1000\n\n/**\n * Upload a setup (\"pregame\") script into a ready sandbox and execute it.\n *\n * Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly\n * (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the\n * interpreter) in `cwd` with `envs` applied — the same env the sandbox was\n * created with, so the script sees the run's `--env` / `--pass-env`. A\n * generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`\n * installs aren't killed by E2B's 60s command default. A non-zero exit\n * (whether E2B returns it or raises on it) throws, so the caller can tear the\n * sandbox down rather than prompt against a broken environment.\n */\nexport async function runE2BPregame(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envs?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running E2B pregame script', { sandboxId: sandbox.sandboxId, path })\n await sandbox.files.write(path, opts.content)\n\n const quoted = JSON.stringify(path)\n let result: E2BCommandResult\n try {\n result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {\n cwd: opts.cwd,\n envs: opts.envs,\n timeoutMs: PREGAME_TIMEOUT_MS,\n })\n }\n catch (err: any) {\n // E2B raises CommandExitError (carrying the captured streams) on a\n // non-zero exit. Surface its output, then fail the spawn.\n const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 124\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode, stdout: err?.stdout ?? '', stderr: err?.stderr ?? err?.message ?? '' })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`)\n }\n\n if (result.exitCode !== 0) {\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('E2B pregame ready', { sandboxId: sandbox.sandboxId, stdout: result.stdout, stderr: result.stderr })\n}\n\nexport interface E2BReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /**\n * Whether to kill the sandbox if it never becomes ready. Defaults to `true`\n * (we created it, so a dead sandbox is ours to clean up). Pass `false` when\n * attaching to an externally-owned sandbox — failing the probe shouldn't tear\n * down a sandbox the caller is managing.\n */\n killOnTimeout?: boolean\n /** Clock source — injectable so tests can drive the deadline deterministically. */\n now?: () => number\n /** Sleep between probes — injectable so tests can advance the clock without waiting. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Block until an E2B sandbox is ready to run commands, or throw if it never\n * becomes ready within the deadline.\n *\n * The E2B control plane can hand back a sandbox id before the sandbox's `envd`\n * is accepting commands; the first real command then absorbs that cold start\n * and may time out. This polls a trivial `true` command on a short backoff,\n * logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on\n * success. On deadline exhaustion it best-effort kills the sandbox and throws.\n *\n * `now`/`sleep` are injectable purely for deterministic tests; in production\n * they default to `Date.now` and a real timer.\n */\nexport async function waitForE2BReady(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: E2BReadinessOptions,\n): Promise<void> {\n if (opts.timeoutSeconds <= 0)\n return\n\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const start = now()\n const deadline = start + opts.timeoutSeconds * 1000\n\n logger.info('waiting for E2B sandbox to initialize', { sandboxId: sandbox.sandboxId })\n\n while (true) {\n try {\n await sandbox.commands.run('true', { timeoutMs: READINESS_PROBE_TIMEOUT_MS })\n logger.info('E2B sandbox ready', { sandboxId: sandbox.sandboxId, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n if (opts.killOnTimeout !== false)\n await sandbox.kill().catch(() => {})\n throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\n/**\n * Resolve the effective E2B template, highest precedence first: the per-spawn\n * `SpawnConfig.sandbox.template`, then the factory `template` option, then the\n * `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when\n * none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but\n * NOT `E2B_TEMPLATE`, so this provider has to honor it itself.\n */\nexport function resolveE2BTemplate(\n perSpawnTemplate: string | undefined,\n optionTemplate: string | undefined,\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || undefined\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link SandboxProvider} backed by E2B.\n *\n * The provider owns a registry of live {@link E2BSandbox} instances keyed by\n * sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm\n * connection rather than reconnecting each time.\n */\nexport function createE2BProvider(options: E2BProviderOptions = {}): SandboxProvider {\n // Track the live sandbox, the env it was created with, and whether WE own it.\n // We re-send that env on every `commands.run` rather than relying on\n // `Sandbox.create`'s `envs` alone — a warm / pre-provisioned sandbox handed\n // back by the control plane may never have run our create-time env injection.\n // `owned` is false for sandboxes we attached to via `Sandbox.connect`: those\n // are never killed on teardown/failure because the caller manages them.\n const live = new Map<string, { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean, keptAliveAt?: number }>()\n\n // Resolve the readiness logger once: caller-supplied, else a console-backed\n // default so the \"waiting for sandbox\" line still reaches stderr.\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<E2BSandboxStatic> {\n try {\n const mod = await import('e2b') as { Sandbox: E2BSandboxStatic }\n return mod.Sandbox\n }\n catch {\n throw new Error('e2b is required for the E2B sandbox provider. Install it with: bun add e2b')\n }\n }\n\n function get(sandboxId: string): { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`)\n keepAlive(entry)\n return entry\n }\n\n // Fire-and-forget: a failed extension only means the sandbox may time out\n // (and, with a pause lifecycle, pause) sooner.\n function keepAlive(entry: { sandbox: E2BSandbox, keptAliveAt?: number }): void {\n const seconds = options.keepAliveSeconds\n if (!seconds || seconds <= 0 || !entry.sandbox.setTimeout)\n return\n const now = Date.now()\n if (entry.keptAliveAt !== undefined && now - entry.keptAliveAt < KEEP_ALIVE_THROTTLE_MS)\n return\n entry.keptAliveAt = now\n entry.sandbox.setTimeout(seconds * 1000).catch((err: unknown) => {\n logger.warn('failed to extend E2B sandbox timeout', { sandboxId: entry.sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n })\n }\n\n // Best-effort kill of an owned sandbox. Failures are swallowed: the sandbox\n // may already be gone (timed out, or killed out-of-band).\n async function killOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n await sandbox.kill()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n // Pause an owned sandbox so its filesystem/process state survives for a later\n // reconnect. Prefers the stable `pause()` (e2b@2.27.1+), falling back to the\n // deprecated `betaPause()` (e2b@2.0.0+) across the rest of the supported 2.x\n // range. If a mismatched install exposes neither, surface it loudly rather\n // than let a bare `catch` swallow the \"not a function\" error — which would\n // leave the caller believing state was preserved while the sandbox is really\n // about to time out. A genuine pause failure is logged the same way; we\n // deliberately do NOT fall back to kill, since that would destroy the very\n // state the caller asked to keep.\n async function pauseOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n if (sandbox.pause) {\n await sandbox.pause()\n }\n else if (sandbox.betaPause) {\n await sandbox.betaPause()\n }\n else {\n logger.error('cannot pause E2B sandbox: this e2b SDK exposes neither pause() nor betaPause() (expected e2b@^2); leaving it to time out', { sandboxId: sandbox.sandboxId })\n return\n }\n logger.info('paused E2B sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.sandboxId })\n }\n catch (err) {\n logger.error('failed to pause E2B sandbox; it will run until its timeout', { sandboxId: sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n const streamM = e2bStreamMethods(get, logger)\n const termM = e2bTerminalMethods(get)\n\n return {\n name: 'e2b',\n\n async spawn(config: SpawnConfig): Promise<{ id: string, cwd: string }> {\n const Sandbox = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n // Per-spawn overrides win over factory options win over SDK env defaults.\n const apiKey = (sb.apiKey as string | undefined) ?? options.apiKey\n const domain = (sb.domain as string | undefined) ?? options.domain\n const template = resolveE2BTemplate(sb.template as string | undefined, options.template)\n const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds\n const connectId = (sb.sandboxId as string | undefined) ?? options.sandboxId\n\n const envs = resolveE2BEnv(options.env, config.env)\n\n // Attach to an existing sandbox when a sandbox id was supplied; otherwise\n // create a fresh one. A connected sandbox is externally owned (`owned`\n // false) so we never kill it on failure or teardown. The create-time\n // `envs`/`template` don't apply to an attach — env is still re-sent on\n // every command, so a connected sandbox isn't left without it.\n let sandbox: E2BSandbox\n const owned = connectId === undefined\n if (connectId !== undefined) {\n const connectOpts: E2BConnectOptions = {}\n if (apiKey !== undefined)\n connectOpts.apiKey = apiKey\n if (domain !== undefined)\n connectOpts.domain = domain\n if (timeoutSeconds !== undefined)\n connectOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = await Sandbox.connect(connectId, connectOpts)\n }\n else {\n const createOpts: E2BCreateOptions = {}\n if (apiKey !== undefined)\n createOpts.apiKey = apiKey\n if (domain !== undefined)\n createOpts.domain = domain\n if (envs)\n createOpts.envs = envs\n if (timeoutSeconds !== undefined)\n createOpts.timeoutMs = timeoutSeconds * 1000\n if (options.lifecycle)\n createOpts.lifecycle = options.lifecycle\n sandbox = template !== undefined\n ? await Sandbox.create(template, createOpts)\n : await Sandbox.create(createOpts)\n }\n\n live.set(sandbox.sandboxId, { sandbox, env: envs, owned })\n\n // Wait for envd to actually accept commands before handing back the\n // handle — otherwise the cwd mkdir below (and the agent's first turn)\n // races the sandbox's cold start. On failure a sandbox we created is\n // already killed; a connected one is left alone (`killOnTimeout`). Either\n // way, stop tracking it and propagate.\n try {\n await waitForE2BReady(sandbox, logger, { timeoutSeconds: readinessTimeoutSeconds, killOnTimeout: owned })\n }\n catch (err) {\n live.delete(sandbox.sandboxId)\n throw err\n }\n\n // Resolve the working directory. An explicit cwd (per-spawn, else the\n // provider default) is the caller's responsibility — create it up front\n // so relative ops have somewhere to land. With no explicit cwd, DON'T\n // assume E2B's hosted `/home/user`: self-hosted templates may run as a\n // different user whose home lives elsewhere and which we lack permission\n // to create under `/home`. Handing envd a non-existent cwd makes it fail\n // process spawn with ENOENT (\"No such file or directory\"), breaking the\n // pregame script and the agent's first shell turn alike. Instead, ask the\n // sandbox where a login shell actually lands (a cwd-less `pwd`, which\n // can't itself hit the ENOENT) and use that.\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n // Best-effort: the cwd usually already exists (or is creatable), and a\n // genuine failure surfaces as ENOENT on the very next command anyway.\n await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else {\n const discovered = await sandbox.commands.run('pwd')\n .then(r => r.stdout.trim())\n .catch(() => '')\n cwd = discovered || '/home/user'\n }\n\n // Pre-provision the environment before any prompting: upload + run the\n // setup script in the now-ready sandbox. A failure tears the sandbox\n // down (mirroring the readiness gate) rather than handing back a\n // half-provisioned environment.\n if (options.pregame) {\n try {\n await runE2BPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envs })\n }\n catch (err) {\n // Only tear down a sandbox we created; a connected one is the\n // caller's to manage even when our setup script fails.\n if (owned)\n await sandbox.kill().catch(() => {})\n live.delete(sandbox.sandboxId)\n throw err\n }\n }\n\n return { id: sandbox.sandboxId, cwd }\n },\n\n execStream: streamM.execStream,\n\n openTerminal: termM.openTerminal,\n\n reconnectTerminal: termM.reconnectTerminal,\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n // Match the process/docker contexts: `timeout` is in seconds, default 30.\n const timeoutMs = (opts?.timeout ?? 30) * 1000\n try {\n const result = await sandbox.commands.run(command, {\n cwd: opts?.cwd,\n // Re-send the sandbox's configured env on every command (warm\n // sandboxes may lack the create-time injection), with the per-call\n // env layered on top.\n envs: resolveE2BEnv(env, opts?.env),\n timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,\n })\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n }\n catch (err: any) {\n // E2B throws on non-zero exit (CommandExitError carries the captured\n // streams) and on timeout. Normalize both into an ExecResult so the\n // shell tool sees a clean exit code rather than a thrown error.\n if (typeof err?.exitCode === 'number') {\n return {\n ...capBufferedExecOutput(err.stdout ?? '', err.stderr ?? err.message ?? ''),\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: err?.message ?? String(err), exitCode: 124 }\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return get(sandboxId).sandbox.files.read(path)\n },\n\n async writeFile(sandboxId, path, content, encoding: BufferEncoding = 'utf-8'): Promise<void> {\n // A plain utf-8 string rides the SDK's own string path, byte-identical to\n // what it did before byte writes existed: `new Blob(['…'])` encodes utf-8.\n // Only a Buffer or an explicit non-utf8 encoding needs host-side bytes,\n // so the common write keeps hitting the code the SDK actually documents.\n const data = typeof content === 'string'\n ? (encoding === 'utf-8' || encoding === 'utf8' ? content : Buffer.from(content, encoding))\n : content\n await get(sandboxId).sandbox.files.write(path, data)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.files.list(path)\n // Trailing `/` on directories — parity with process/docker/daytona.\n return entries.map(e => (e.type === 'dir' ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n // Never kill/pause a sandbox we only connected to — the caller owns its\n // lifecycle. Just drop our handle to it.\n if (entry.owned) {\n const action = options.onDestroy ?? 'kill'\n if (action === 'pause')\n await pauseOwnedSandbox(entry.sandbox)\n else if (action === 'kill')\n await killOwnedSandbox(entry.sandbox)\n // 'leave' makes no API call — the sandbox runs until E2B's own timeout.\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
//#region src/tools/edit-utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Internal helpers shared between the `edit` and `multi_edit` tools.
|
|
4
|
+
*
|
|
5
|
+
* Not part of the public API — intentionally not re-exported from `tools/index.ts`
|
|
6
|
+
* or the package barrel.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Count exact (non-overlapping) occurrences of `needle` in `haystack`.
|
|
10
|
+
* Returns 0 for an empty needle — both edit tools reject empty `old_string`
|
|
11
|
+
* up front, so this branch is defensive rather than semantic.
|
|
12
|
+
*/
|
|
13
|
+
function countExactMatches(haystack, needle) {
|
|
14
|
+
if (needle.length === 0) return 0;
|
|
15
|
+
let count = 0;
|
|
16
|
+
let idx = 0;
|
|
17
|
+
while (true) {
|
|
18
|
+
const next = haystack.indexOf(needle, idx);
|
|
19
|
+
if (next === -1) break;
|
|
20
|
+
count++;
|
|
21
|
+
idx = next + needle.length;
|
|
22
|
+
}
|
|
23
|
+
return count;
|
|
24
|
+
}
|
|
25
|
+
/** Map curly quotes (any of the four) to their straight ASCII equivalents. */
|
|
26
|
+
function normalizeQuotes(str) {
|
|
27
|
+
return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Substitutions Anthropic's API applies to assistant output before the model
|
|
31
|
+
* sees it. The model emits the sanitized form; the file on disk contains the
|
|
32
|
+
* unsanitized form. We undo the substitutions on `old_string` so the search
|
|
33
|
+
* lands on the actual file contents.
|
|
34
|
+
*
|
|
35
|
+
* Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
|
|
36
|
+
*/
|
|
37
|
+
const DESANITIZATIONS = [
|
|
38
|
+
["<fnr>", "<function_results>"],
|
|
39
|
+
["<n>", "<name>"],
|
|
40
|
+
["</n>", "</name>"],
|
|
41
|
+
["<o>", "<output>"],
|
|
42
|
+
["</o>", "</output>"],
|
|
43
|
+
["<e>", "<error>"],
|
|
44
|
+
["</e>", "</error>"],
|
|
45
|
+
["<s>", "<system>"],
|
|
46
|
+
["</s>", "</system>"],
|
|
47
|
+
["<r>", "<result>"],
|
|
48
|
+
["</r>", "</result>"],
|
|
49
|
+
["< META_START >", "<META_START>"],
|
|
50
|
+
["< META_END >", "<META_END>"],
|
|
51
|
+
["< EOT >", "<EOT>"],
|
|
52
|
+
["< META >", "<META>"],
|
|
53
|
+
["< SOS >", "<SOS>"],
|
|
54
|
+
["\n\nH:", "\n\nHuman:"],
|
|
55
|
+
["\n\nA:", "\n\nAssistant:"]
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Apply the SDK desanitization table to a string. Exported so the edit tools
|
|
59
|
+
* can apply it to `new_string` whenever `old_string` matched via a
|
|
60
|
+
* desanitize-class fallback — keeps the file's unsanitized form on disk
|
|
61
|
+
* instead of writing the model's abbreviated form back.
|
|
62
|
+
*/
|
|
63
|
+
function desanitize(s) {
|
|
64
|
+
let out = s;
|
|
65
|
+
for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strip line-number prefixes from each line of a needle, used as a recovery
|
|
70
|
+
* fallback when the model pastes a `read_file` chunk verbatim into
|
|
71
|
+
* `old_string` — the on-disk file doesn't carry the metadata prefix.
|
|
72
|
+
*
|
|
73
|
+
* Accepts three separator characters so a model that learned on a different
|
|
74
|
+
* agent stack still works here: `\t` (Claude Code compact, our default),
|
|
75
|
+
* `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
|
|
76
|
+
* of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
|
|
77
|
+
* overshooting into legitimate `\d{N}<sep>` content like Markdown table
|
|
78
|
+
* cells with long numeric IDs.
|
|
79
|
+
*/
|
|
80
|
+
const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
|
|
81
|
+
function stripLineNumberPrefixes(s) {
|
|
82
|
+
return s.replace(LINE_NUMBER_PREFIX_RE, "");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Search `target` in `normFile` and slice the matching span out of the
|
|
86
|
+
* original `haystack`, counting all non-overlapping occurrences. `normFile`
|
|
87
|
+
* is the haystack with whatever transform (quotes / desanitize / combined)
|
|
88
|
+
* was applied to make the indices align — slicing the original haystack
|
|
89
|
+
* preserves the file's actual typography so `replace_all` writes back the
|
|
90
|
+
* file's form, not the model's.
|
|
91
|
+
*
|
|
92
|
+
* Pre-condition: `normFile.length === haystack.length` (every transform
|
|
93
|
+
* we use is one-to-one). Returns null on miss.
|
|
94
|
+
*/
|
|
95
|
+
function locateAndCount(haystack, normFile, target, via) {
|
|
96
|
+
const idx = normFile.indexOf(target);
|
|
97
|
+
if (idx === -1) return null;
|
|
98
|
+
const actual = haystack.slice(idx, idx + target.length);
|
|
99
|
+
let occ = 0;
|
|
100
|
+
let cursor = 0;
|
|
101
|
+
while (true) {
|
|
102
|
+
const next = normFile.indexOf(target, cursor);
|
|
103
|
+
if (next === -1) break;
|
|
104
|
+
occ++;
|
|
105
|
+
cursor = next + target.length;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
actual,
|
|
109
|
+
occurrences: occ,
|
|
110
|
+
via
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function resolveOldString(haystack, needle) {
|
|
114
|
+
const exact = countExactMatches(haystack, needle);
|
|
115
|
+
if (exact > 0) return {
|
|
116
|
+
actual: needle,
|
|
117
|
+
occurrences: exact,
|
|
118
|
+
via: "exact"
|
|
119
|
+
};
|
|
120
|
+
const normNeedle = normalizeQuotes(needle);
|
|
121
|
+
const normFile = normalizeQuotes(haystack);
|
|
122
|
+
if (normNeedle !== needle || normFile !== haystack) {
|
|
123
|
+
const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
|
|
124
|
+
if (m) return m;
|
|
125
|
+
}
|
|
126
|
+
const desan = desanitize(needle);
|
|
127
|
+
if (desan !== needle) {
|
|
128
|
+
const desanCount = countExactMatches(haystack, desan);
|
|
129
|
+
if (desanCount === 1) return {
|
|
130
|
+
actual: desan,
|
|
131
|
+
occurrences: desanCount,
|
|
132
|
+
via: "desanitize"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const combo = desanitize(normNeedle);
|
|
136
|
+
if (combo !== needle) {
|
|
137
|
+
const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
|
|
138
|
+
if (m && m.occurrences === 1) return m;
|
|
139
|
+
}
|
|
140
|
+
const stripped = stripLineNumberPrefixes(needle);
|
|
141
|
+
if (stripped !== needle && stripped.trim().length > 0) {
|
|
142
|
+
const count = countExactMatches(haystack, stripped);
|
|
143
|
+
if (count > 0) return {
|
|
144
|
+
actual: stripped,
|
|
145
|
+
occurrences: count,
|
|
146
|
+
via: "line-numbers"
|
|
147
|
+
};
|
|
148
|
+
const strippedNorm = normalizeQuotes(stripped);
|
|
149
|
+
if (strippedNorm !== stripped || normFile !== haystack) {
|
|
150
|
+
const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
|
|
151
|
+
if (m) return m;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Short human-readable label for a recovery path, used by `edit` /
|
|
158
|
+
* `multi_edit` to flag non-exact matches in their result messages — when a
|
|
159
|
+
* fallback mistargets, the model (and the user reading the transcript) can
|
|
160
|
+
* see WHICH transform fired instead of silently trusting the edit.
|
|
161
|
+
*/
|
|
162
|
+
function describeVia(via) {
|
|
163
|
+
switch (via) {
|
|
164
|
+
case "exact": return "exact match";
|
|
165
|
+
case "quotes": return "curly-quote normalization";
|
|
166
|
+
case "desanitize": return "sanitization recovery (e.g. <n> → <name>)";
|
|
167
|
+
case "quotes+desanitize": return "sanitization recovery + curly-quote normalization";
|
|
168
|
+
case "line-numbers": return "line-number-prefix stripping";
|
|
169
|
+
case "quotes+line-numbers": return "line-number-prefix stripping + curly-quote normalization";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Apply the same recovery transforms used to find `old_string` to
|
|
174
|
+
* `new_string`, so the file gets back its native form: desanitize when
|
|
175
|
+
* the model emitted `<n>` for `<name>`, strip line-number prefixes when
|
|
176
|
+
* the match required them, then re-curlify when the match required
|
|
177
|
+
* quote normalization. Shared between `edit` and `multi_edit`.
|
|
178
|
+
*/
|
|
179
|
+
function styleReplacementForVia(replacement, via, actual) {
|
|
180
|
+
let out = replacement;
|
|
181
|
+
if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
|
|
182
|
+
if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
|
|
183
|
+
if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* When `old_string` matched via curly-quote normalization, re-style
|
|
188
|
+
* `new_string` so the file's typography is preserved across the edit.
|
|
189
|
+
* Detects whether the matched file region had curly singles, doubles, or
|
|
190
|
+
* both, and applies the matching curlification to the replacement.
|
|
191
|
+
*
|
|
192
|
+
* Apostrophes in contractions (`don't`, `it's`) get the right-single curly
|
|
193
|
+
* quote regardless of opening context — that's the canonical typographer's
|
|
194
|
+
* convention for English. Other quotes use a simple
|
|
195
|
+
* preceded-by-whitespace-or-opening-punctuation heuristic.
|
|
196
|
+
*/
|
|
197
|
+
function preserveQuoteStyle(actual, replacement) {
|
|
198
|
+
const hasDouble = actual.includes("“") || actual.includes("”");
|
|
199
|
+
const hasSingle = actual.includes("‘") || actual.includes("’");
|
|
200
|
+
if (!hasDouble && !hasSingle) return replacement;
|
|
201
|
+
let out = replacement;
|
|
202
|
+
if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
|
|
203
|
+
if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function applyCurly(s, straight, left, right, contractionAware) {
|
|
207
|
+
const chars = [...s];
|
|
208
|
+
const result = [];
|
|
209
|
+
for (let i = 0; i < chars.length; i++) {
|
|
210
|
+
if (chars[i] !== straight) {
|
|
211
|
+
result.push(chars[i]);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (contractionAware) {
|
|
215
|
+
const prev = i > 0 ? chars[i - 1] : "";
|
|
216
|
+
const next = i < chars.length - 1 ? chars[i + 1] : "";
|
|
217
|
+
if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
218
|
+
result.push(right);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
result.push(isOpeningContext(chars, i) ? left : right);
|
|
223
|
+
}
|
|
224
|
+
return result.join("");
|
|
225
|
+
}
|
|
226
|
+
function isOpeningContext(chars, i) {
|
|
227
|
+
if (i === 0) return true;
|
|
228
|
+
const prev = chars[i - 1];
|
|
229
|
+
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { styleReplacementForVia as i, resolveOldString as n, stripLineNumberPrefixes as r, describeVia as t };
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=edit-utils-EGosADZq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-utils-EGosADZq.js","names":[],"sources":["../src/tools/edit-utils.ts"],"sourcesContent":["/**\n * Internal helpers shared between the `edit` and `multi_edit` tools.\n *\n * Not part of the public API — intentionally not re-exported from `tools/index.ts`\n * or the package barrel.\n */\n\n/**\n * Count exact (non-overlapping) occurrences of `needle` in `haystack`.\n * Returns 0 for an empty needle — both edit tools reject empty `old_string`\n * up front, so this branch is defensive rather than semantic.\n */\nexport function countExactMatches(haystack: string, needle: string): number {\n if (needle.length === 0)\n return 0\n let count = 0\n let idx = 0\n while (true) {\n const next = haystack.indexOf(needle, idx)\n if (next === -1)\n break\n count++\n idx = next + needle.length\n }\n return count\n}\n\n// ---------------------------------------------------------------------------\n// Curly-quote normalization + SDK-sanitization fallbacks\n// ---------------------------------------------------------------------------\n//\n// Models can't reliably emit Unicode curly quotes, and the Anthropic API\n// silently sanitizes a handful of XML-like tags before they hit the model\n// (`<name>` → `<n>`, etc.). When `old_string` doesn't match exactly, we retry\n// against the same content with both transforms applied so an \"old_string not\n// found\" failure that's only typographical resolves on the second pass.\n//\n// Mirrors the Claude Code Edit tool's recovery strategy. Verbatim port of the\n// quote constants and the desanitization table; the resolve loop below is\n// reorganized to share state between exact / quote-normalized / desanitized.\n\nexport const LEFT_SINGLE_CURLY_QUOTE = '\\u2018'\nexport const RIGHT_SINGLE_CURLY_QUOTE = '\\u2019'\nexport const LEFT_DOUBLE_CURLY_QUOTE = '\\u201C'\nexport const RIGHT_DOUBLE_CURLY_QUOTE = '\\u201D'\n\n/** Map curly quotes (any of the four) to their straight ASCII equivalents. */\nexport function normalizeQuotes(str: string): string {\n return str\n .replaceAll(LEFT_SINGLE_CURLY_QUOTE, '\\'')\n .replaceAll(RIGHT_SINGLE_CURLY_QUOTE, '\\'')\n .replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '\"')\n .replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '\"')\n}\n\n/**\n * Substitutions Anthropic's API applies to assistant output before the model\n * sees it. The model emits the sanitized form; the file on disk contains the\n * unsanitized form. We undo the substitutions on `old_string` so the search\n * lands on the actual file contents.\n *\n * Verbatim from `claude-code/tools/FileEditTool/utils.ts`.\n */\nconst DESANITIZATIONS: ReadonlyArray<readonly [string, string]> = [\n ['<fnr>', '<function_results>'],\n ['<n>', '<name>'],\n ['</n>', '</name>'],\n ['<o>', '<output>'],\n ['</o>', '</output>'],\n ['<e>', '<error>'],\n ['</e>', '</error>'],\n ['<s>', '<system>'],\n ['</s>', '</system>'],\n ['<r>', '<result>'],\n ['</r>', '</result>'],\n ['< META_START >', '<META_START>'],\n ['< META_END >', '<META_END>'],\n ['< EOT >', '<EOT>'],\n ['< META >', '<META>'],\n ['< SOS >', '<SOS>'],\n ['\\n\\nH:', '\\n\\nHuman:'],\n ['\\n\\nA:', '\\n\\nAssistant:'],\n]\n\n/**\n * Apply the SDK desanitization table to a string. Exported so the edit tools\n * can apply it to `new_string` whenever `old_string` matched via a\n * desanitize-class fallback — keeps the file's unsanitized form on disk\n * instead of writing the model's abbreviated form back.\n */\nexport function desanitize(s: string): string {\n let out = s\n for (const [from, to] of DESANITIZATIONS)\n out = out.replaceAll(from, to)\n return out\n}\n\n/**\n * Strip line-number prefixes from each line of a needle, used as a recovery\n * fallback when the model pastes a `read_file` chunk verbatim into\n * `old_string` — the on-disk file doesn't carry the metadata prefix.\n *\n * Accepts three separator characters so a model that learned on a different\n * agent stack still works here: `\\t` (Claude Code compact, our default),\n * `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one\n * of `\\t | →`. The 9-digit ceiling covers files up to ~1B lines without\n * overshooting into legitimate `\\d{N}<sep>` content like Markdown table\n * cells with long numeric IDs.\n */\nconst LINE_NUMBER_PREFIX_RE = /^[ \\t]*\\d{1,9}[\\t|\\u2192]/gm\nexport function stripLineNumberPrefixes(s: string): string {\n return s.replace(LINE_NUMBER_PREFIX_RE, '')\n}\n\n/**\n * Locate the actual substring in `haystack` that corresponds to `needle`,\n * recovering from typographical mismatch on six escalating fallbacks:\n *\n * 1. Exact substring match (the happy path — no transformation).\n * 2. Curly-quote normalization on both sides — when the model emits straight\n * quotes but the file has curly ones (or vice versa), the slice of the\n * file at the matched position carries the file's actual typography.\n * 3. Anthropic-sanitization undo on the needle — the model's `<n>` becomes\n * the file's `<name>`, etc.\n * 4. Combined: desanitize + quote normalization on the needle.\n * 5. Line-number-prefix strip — when the model pasted a numbered `read_file`\n * chunk verbatim into `old_string` and the file carries no such prefix.\n * 6. Combined: line-number strip + quote normalization — paste-back of a\n * numbered chunk against a file whose typography differs from the model's.\n *\n * Returns `{ actual, occurrences }` — `actual` is the string that exists in\n * the file (use this to do the actual replace; preserves the file's\n * typography). `occurrences` is the count of those matches in the file.\n *\n * Fallbacks 3-4 (desanitize-class) only fire when the transformed needle is\n * UNIQUE in the file — they rewrite the needle's meaning rather than its\n * typography, so an ambiguous match risks silently retargeting the wrong\n * text (see the inline comment at step 3).\n *\n * Returns `null` when no recovery worked.\n */\nexport interface ResolvedMatch {\n actual: string\n occurrences: number\n /** Recovery path — `'exact'` when no transformation was applied. */\n via: 'exact' | 'quotes' | 'desanitize' | 'quotes+desanitize' | 'line-numbers' | 'quotes+line-numbers'\n}\n\n/**\n * Search `target` in `normFile` and slice the matching span out of the\n * original `haystack`, counting all non-overlapping occurrences. `normFile`\n * is the haystack with whatever transform (quotes / desanitize / combined)\n * was applied to make the indices align — slicing the original haystack\n * preserves the file's actual typography so `replace_all` writes back the\n * file's form, not the model's.\n *\n * Pre-condition: `normFile.length === haystack.length` (every transform\n * we use is one-to-one). Returns null on miss.\n */\nfunction locateAndCount(\n haystack: string,\n normFile: string,\n target: string,\n via: ResolvedMatch['via'],\n): ResolvedMatch | null {\n const idx = normFile.indexOf(target)\n if (idx === -1)\n return null\n const actual = haystack.slice(idx, idx + target.length)\n let occ = 0\n let cursor = 0\n while (true) {\n const next = normFile.indexOf(target, cursor)\n if (next === -1)\n break\n occ++\n cursor = next + target.length\n }\n return { actual, occurrences: occ, via }\n}\n\nexport function resolveOldString(haystack: string, needle: string): ResolvedMatch | null {\n // 1. Exact match — happy path.\n const exact = countExactMatches(haystack, needle)\n if (exact > 0)\n return { actual: needle, occurrences: exact, via: 'exact' }\n\n // 2. Curly-quote normalization. Either side can carry curly quotes:\n // model emitted straight + file has curly, OR model emitted curly +\n // file has straight. Normalize both and look there. Slice the original\n // haystack to recover the actual file-side typography.\n const normNeedle = normalizeQuotes(needle)\n const normFile = normalizeQuotes(haystack)\n if (normNeedle !== needle || normFile !== haystack) {\n const m = locateAndCount(haystack, normFile, normNeedle, 'quotes')\n if (m)\n return m\n }\n\n // 3. Desanitize the needle — model-side substitution undo (`<n>` → `<name>`,\n // etc.) The file always has the unsanitized form; only the model emits\n // the abbreviated version.\n //\n // Desanitize-class fallbacks additionally require a UNIQUE transformed\n // match. Unlike the quote / line-number transforms (typography-only /\n // verbatim paste-back), this one rewrites the needle's meaning — a model\n // that genuinely meant the literal `<n>` against a file full of `<name>`\n // would otherwise silently retarget every occurrence under `replace_all`.\n // A unique hit bounds the blast radius to one visible replacement.\n const desan = desanitize(needle)\n if (desan !== needle) {\n const desanCount = countExactMatches(haystack, desan)\n if (desanCount === 1)\n return { actual: desan, occurrences: desanCount, via: 'desanitize' }\n }\n\n // 4. Combined: desanitize + quote normalization. Hit when the model\n // emitted both sanitized tokens and straight quotes, but the file has\n // the unsanitized form with curly quotes. Same uniqueness requirement\n // as #3.\n const combo = desanitize(normNeedle)\n if (combo !== needle) {\n const m = locateAndCount(haystack, normFile, combo, 'quotes+desanitize')\n if (m && m.occurrences === 1)\n return m\n }\n\n // 5. Line-number-prefix strip. When `read_file` returns line-numbered\n // output and the model pasted the chunk verbatim into `old_string`,\n // the on-disk file doesn't carry those prefixes. Strip them and\n // retry; the file-side `actual` is the prefix-free needle so the\n // replacement lands on real bytes.\n //\n // Guard against a needle that strips down to whitespace-only (e.g.\n // `1\\t\\n2\\t\\n3\\t` → `\\n\\n`). Searching for `\\n\\n` would match arbitrary\n // blank-line patterns anywhere in the file — a high false-positive risk.\n // Require the stripped needle to carry at least one non-whitespace\n // character before we trust the result.\n const stripped = stripLineNumberPrefixes(needle)\n if (stripped !== needle && stripped.trim().length > 0) {\n const count = countExactMatches(haystack, stripped)\n if (count > 0)\n return { actual: stripped, occurrences: count, via: 'line-numbers' }\n\n // 6. Combined: line-number strip + curly-quote normalization. The\n // typography mismatch may live in the file, the needle, or both.\n const strippedNorm = normalizeQuotes(stripped)\n if (strippedNorm !== stripped || normFile !== haystack) {\n const m = locateAndCount(haystack, normFile, strippedNorm, 'quotes+line-numbers')\n if (m)\n return m\n }\n }\n\n return null\n}\n\n/**\n * Short human-readable label for a recovery path, used by `edit` /\n * `multi_edit` to flag non-exact matches in their result messages — when a\n * fallback mistargets, the model (and the user reading the transcript) can\n * see WHICH transform fired instead of silently trusting the edit.\n */\nexport function describeVia(via: ResolvedMatch['via']): string {\n switch (via) {\n case 'exact': return 'exact match'\n case 'quotes': return 'curly-quote normalization'\n case 'desanitize': return 'sanitization recovery (e.g. <n> → <name>)'\n case 'quotes+desanitize': return 'sanitization recovery + curly-quote normalization'\n case 'line-numbers': return 'line-number-prefix stripping'\n case 'quotes+line-numbers': return 'line-number-prefix stripping + curly-quote normalization'\n }\n}\n\n/**\n * Apply the same recovery transforms used to find `old_string` to\n * `new_string`, so the file gets back its native form: desanitize when\n * the model emitted `<n>` for `<name>`, strip line-number prefixes when\n * the match required them, then re-curlify when the match required\n * quote normalization. Shared between `edit` and `multi_edit`.\n */\nexport function styleReplacementForVia(\n replacement: string,\n via: ResolvedMatch['via'],\n actual: string,\n): string {\n let out = replacement\n if (via === 'desanitize' || via === 'quotes+desanitize')\n out = desanitize(out)\n if (via === 'line-numbers' || via === 'quotes+line-numbers')\n out = stripLineNumberPrefixes(out)\n if (via === 'quotes' || via === 'quotes+desanitize' || via === 'quotes+line-numbers')\n out = preserveQuoteStyle(actual, out)\n return out\n}\n\n/**\n * When `old_string` matched via curly-quote normalization, re-style\n * `new_string` so the file's typography is preserved across the edit.\n * Detects whether the matched file region had curly singles, doubles, or\n * both, and applies the matching curlification to the replacement.\n *\n * Apostrophes in contractions (`don't`, `it's`) get the right-single curly\n * quote regardless of opening context — that's the canonical typographer's\n * convention for English. Other quotes use a simple\n * preceded-by-whitespace-or-opening-punctuation heuristic.\n */\nexport function preserveQuoteStyle(actual: string, replacement: string): string {\n const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE)\n const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE)\n if (!hasDouble && !hasSingle)\n return replacement\n\n let out = replacement\n if (hasDouble)\n out = applyCurly(out, '\"', LEFT_DOUBLE_CURLY_QUOTE, RIGHT_DOUBLE_CURLY_QUOTE, false)\n if (hasSingle)\n out = applyCurly(out, '\\'', LEFT_SINGLE_CURLY_QUOTE, RIGHT_SINGLE_CURLY_QUOTE, true)\n return out\n}\n\nfunction applyCurly(\n s: string,\n straight: string,\n left: string,\n right: string,\n contractionAware: boolean,\n): string {\n const chars = [...s]\n const result: string[] = []\n for (let i = 0; i < chars.length; i++) {\n if (chars[i] !== straight) {\n result.push(chars[i])\n continue\n }\n if (contractionAware) {\n const prev = i > 0 ? chars[i - 1] : ''\n const next = i < chars.length - 1 ? chars[i + 1] : ''\n // Letter-quote-letter is a contraction, not a quote — always right.\n if (/\\p{L}/u.test(prev) && /\\p{L}/u.test(next)) {\n result.push(right)\n continue\n }\n }\n result.push(isOpeningContext(chars, i) ? left : right)\n }\n return result.join('')\n}\n\nfunction isOpeningContext(chars: string[], i: number): boolean {\n if (i === 0)\n return true\n const prev = chars[i - 1]\n return prev === ' ' || prev === '\\t' || prev === '\\n' || prev === '\\r'\n || prev === '(' || prev === '[' || prev === '{'\n || prev === '\\u2014' || prev === '\\u2013' // em / en dash\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//#region src/chat/environment.ts
|
|
2
|
+
const ENVIRONMENT_METADATA_KEY = "environment";
|
|
3
|
+
const LOCAL_ENVIRONMENT = {
|
|
4
|
+
kind: "local",
|
|
5
|
+
state: "ready"
|
|
6
|
+
};
|
|
7
|
+
const STATES = /* @__PURE__ */ new Set([
|
|
8
|
+
"provisioning",
|
|
9
|
+
"ready",
|
|
10
|
+
"failed"
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* The session's environment, from its metadata. Tolerant: absent or
|
|
14
|
+
* malformed reads as {@link LOCAL_ENVIRONMENT} — what every session was
|
|
15
|
+
* before environments existed.
|
|
16
|
+
*/
|
|
17
|
+
function readSessionEnvironment(metadata) {
|
|
18
|
+
const raw = metadata?.[ENVIRONMENT_METADATA_KEY];
|
|
19
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return LOCAL_ENVIRONMENT;
|
|
20
|
+
const record = raw;
|
|
21
|
+
if (typeof record.kind !== "string" || !record.kind || typeof record.state !== "string" || !STATES.has(record.state)) return LOCAL_ENVIRONMENT;
|
|
22
|
+
return {
|
|
23
|
+
kind: record.kind,
|
|
24
|
+
state: record.state,
|
|
25
|
+
...typeof record.execCwd === "string" && record.execCwd ? { execCwd: record.execCwd } : {},
|
|
26
|
+
...typeof record.branch === "string" && record.branch ? { branch: record.branch } : {},
|
|
27
|
+
...typeof record.base === "string" && record.base ? { base: record.base } : {},
|
|
28
|
+
...record.ref !== void 0 ? { ref: record.ref } : {},
|
|
29
|
+
...record.owned === false ? { owned: false } : {},
|
|
30
|
+
...typeof record.error === "string" && record.error ? { error: record.error } : {},
|
|
31
|
+
...record.suspended === true ? { suspended: true } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The metadata patch that persists `environment` (plain JSON, `undefined` keys dropped). */
|
|
35
|
+
function environmentMetadata(environment) {
|
|
36
|
+
return { [ENVIRONMENT_METADATA_KEY]: JSON.parse(JSON.stringify(environment)) };
|
|
37
|
+
}
|
|
38
|
+
/** A session whose environment can't run yet (provisioning) or can't run at all (failed). */
|
|
39
|
+
var EnvironmentNotReadyError = class extends Error {
|
|
40
|
+
state;
|
|
41
|
+
constructor(environment) {
|
|
42
|
+
super(environment.state === "failed" ? `The session's ${environment.kind} environment failed to provision${environment.error ? `: ${environment.error}` : "."} Retry it or delete the session.` : `The session's ${environment.kind} environment is still being set up. Try again once it is ready.`);
|
|
43
|
+
this.name = "EnvironmentNotReadyError";
|
|
44
|
+
this.state = environment.state;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** Throws {@link EnvironmentNotReadyError} unless the environment can run. */
|
|
48
|
+
function assertEnvironmentReady(environment) {
|
|
49
|
+
if (environment.state !== "ready") throw new EnvironmentNotReadyError(environment);
|
|
50
|
+
}
|
|
51
|
+
/** Whether `provider` takes a branch under `config`. */
|
|
52
|
+
function environmentTakesBranch(provider, config) {
|
|
53
|
+
return typeof provider.takesBranch === "function" ? provider.takesBranch(config) : provider.takesBranch === true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A registry of `providers`. Hosts pass the chat layer's built-ins
|
|
57
|
+
* (`builtinEnvironmentProviders`, which include `local`) plus any extras; a
|
|
58
|
+
* registry without `local` can still create local sessions (an absent
|
|
59
|
+
* environment IS local) but can't resolve their execution.
|
|
60
|
+
*/
|
|
61
|
+
function createEnvironmentRegistry(providers = []) {
|
|
62
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
63
|
+
const registry = {
|
|
64
|
+
get: (kind) => byKind.get(kind),
|
|
65
|
+
list: () => [...byKind.values()],
|
|
66
|
+
register: (provider) => {
|
|
67
|
+
if (!byKind.has(provider.kind)) byKind.set(provider.kind, provider);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
for (const provider of providers) registry.register(provider);
|
|
71
|
+
return registry;
|
|
72
|
+
}
|
|
73
|
+
/** The provider for `kind`, or a clear error — an unknown kind can't run anywhere. */
|
|
74
|
+
function requireEnvironmentProvider(registry, kind) {
|
|
75
|
+
const provider = registry.get(kind);
|
|
76
|
+
if (!provider) throw new Error(`Unknown session environment "${kind}". Is the extension that provides it enabled?`);
|
|
77
|
+
return provider;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* THE way a host gets a session's execution context: its environment's
|
|
81
|
+
* provider decides. The key is namespaced by kind so two providers can't
|
|
82
|
+
* collide on the same configuration string.
|
|
83
|
+
*/
|
|
84
|
+
function resolveExecution(registry, input) {
|
|
85
|
+
const execution = requireEnvironmentProvider(registry, input.environment.kind).execution(input);
|
|
86
|
+
return {
|
|
87
|
+
...execution,
|
|
88
|
+
key: `${input.environment.kind}:${execution.key}`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** The actions `environment`'s kind offers for it right now (a ready, owned-or-shared environment). */
|
|
92
|
+
function availableEnvironmentActions(registry, environment, config) {
|
|
93
|
+
if (environment.state !== "ready") return [];
|
|
94
|
+
return (registry.get(environment.kind)?.actions ?? []).filter((action) => action.available?.(environment, config) ?? true);
|
|
95
|
+
}
|
|
96
|
+
/** {@link availableEnvironmentActions}, wire-safe. */
|
|
97
|
+
function environmentActionSummaries(registry, environment, config) {
|
|
98
|
+
return availableEnvironmentActions(registry, environment, config).map(({ id, label, description }) => ({
|
|
99
|
+
id,
|
|
100
|
+
label,
|
|
101
|
+
...description ? { description } : {}
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run `actionId` for a session on `execution` — the session's own (hosts pass
|
|
106
|
+
* the cached one, so the action runs exactly where the agent does). Throws
|
|
107
|
+
* when the environment isn't ready or doesn't offer the action.
|
|
108
|
+
*/
|
|
109
|
+
async function runSessionEnvironmentAction(input) {
|
|
110
|
+
const { registry, sessionId, actionId, environment, config, cwd, execution } = input;
|
|
111
|
+
assertEnvironmentReady(environment);
|
|
112
|
+
const action = availableEnvironmentActions(registry, environment, config).find((candidate) => candidate.id === actionId);
|
|
113
|
+
if (!action) throw new Error(`"${actionId}" isn't available for this session's environment.`);
|
|
114
|
+
return action.run({
|
|
115
|
+
sessionId,
|
|
116
|
+
environment,
|
|
117
|
+
config,
|
|
118
|
+
cwd,
|
|
119
|
+
exec: async (command, options) => execution.context.exec(await execution.handle(), command, {
|
|
120
|
+
...options?.cwd ? { cwd: options.cwd } : {},
|
|
121
|
+
...options?.env ? { env: options.env } : {},
|
|
122
|
+
...options?.timeoutSeconds ? { timeout: options.timeoutSeconds } : {}
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** "Test connection" for `kind`; a throwing or missing `verify` reads as a failure with the reason. */
|
|
127
|
+
async function verifyEnvironment(registry, kind, config) {
|
|
128
|
+
const provider = registry.get(kind);
|
|
129
|
+
if (!provider?.verify) return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: provider ? `${provider.label} has no connection to test.` : `Unknown environment "${kind}".`
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
return await provider.verify({ config });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `path` in a session workspace rooted at `root` (POSIX resolve): relative
|
|
144
|
+
* paths against `root`, absolute paths as given. The same resolution the
|
|
145
|
+
* execution contexts apply against their handle's cwd, so a workspace call
|
|
146
|
+
* means what the agent's own tools mean; containment is the context's own
|
|
147
|
+
* (sandboxes confine to their workspace root, the local process context
|
|
148
|
+
* doesn't). Clients that must stay inside the workspace (the GUI's file
|
|
149
|
+
* panels) guard their paths themselves.
|
|
150
|
+
*/
|
|
151
|
+
function workspacePath(root, path) {
|
|
152
|
+
const resolved = [];
|
|
153
|
+
for (const segment of `${path.startsWith("/") ? "" : root}/${path}`.split("/")) {
|
|
154
|
+
if (!segment || segment === ".") continue;
|
|
155
|
+
if (segment === "..") resolved.pop();
|
|
156
|
+
else resolved.push(segment);
|
|
157
|
+
}
|
|
158
|
+
return `/${resolved.join("/")}`;
|
|
159
|
+
}
|
|
160
|
+
/** Whether `provider` can be offered for `input`; a throwing probe reads as unavailable. */
|
|
161
|
+
async function probeEnvironment(provider, input) {
|
|
162
|
+
try {
|
|
163
|
+
return await provider.available?.(input) ?? { ok: true };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The environment to stamp on a new session: `ready` for a provider without
|
|
173
|
+
* `prepare`, `provisioning` otherwise; a branch-taking kind gets the
|
|
174
|
+
* requested branch or the provider's suggestion. Throws for an unknown or
|
|
175
|
+
* unavailable kind — creation must fail loudly rather than produce a session
|
|
176
|
+
* that can never run.
|
|
177
|
+
*/
|
|
178
|
+
async function initialEnvironment(registry, request, context) {
|
|
179
|
+
const provider = requireEnvironmentProvider(registry, request.kind);
|
|
180
|
+
const availability = await probeEnvironment(provider, context);
|
|
181
|
+
if (!availability.ok) throw new Error(`The ${provider.label} environment is unavailable: ${availability.reason}`);
|
|
182
|
+
const branch = environmentTakesBranch(provider, context.config) ? request.branch?.trim() || provider.suggestBranch?.(context.config) : void 0;
|
|
183
|
+
return {
|
|
184
|
+
kind: request.kind,
|
|
185
|
+
state: provider.prepare ? "provisioning" : "ready",
|
|
186
|
+
...branch ? { branch } : {},
|
|
187
|
+
...request.base ? { base: request.base } : {}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Remove a deleted session's environment when it owns one. Never throws —
|
|
192
|
+
* a teardown failure must not block the delete; it comes back as a kept
|
|
193
|
+
* result with the reason.
|
|
194
|
+
*/
|
|
195
|
+
async function teardownSessionEnvironment(registry, metadata, context) {
|
|
196
|
+
const environment = readSessionEnvironment(metadata);
|
|
197
|
+
const provider = registry.get(environment.kind);
|
|
198
|
+
if (environment.owned === false || !provider?.teardown) return { removed: true };
|
|
199
|
+
try {
|
|
200
|
+
return await provider.teardown(environment, {
|
|
201
|
+
cwd: context.cwd,
|
|
202
|
+
config: context.config(environment.kind)
|
|
203
|
+
});
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
removed: false,
|
|
207
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { workspacePath as _, availableEnvironmentActions as a, environmentMetadata as c, probeEnvironment as d, readSessionEnvironment as f, verifyEnvironment as g, teardownSessionEnvironment as h, assertEnvironmentReady as i, environmentTakesBranch as l, runSessionEnvironmentAction as m, EnvironmentNotReadyError as n, createEnvironmentRegistry as o, resolveExecution as p, LOCAL_ENVIRONMENT as r, environmentActionSummaries as s, ENVIRONMENT_METADATA_KEY as t, initialEnvironment as u };
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=environment-8wsKE2Ln.js.map
|