@cloudflare/sandbox 0.13.0-next.649.1 → 0.13.0-next.681.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Dockerfile +6 -11
  2. package/README.md +2 -2
  3. package/dist/bridge/index.d.ts.map +1 -1
  4. package/dist/bridge/index.js +68 -49
  5. package/dist/bridge/index.js.map +1 -1
  6. package/dist/codes-y-U76vnQ.d.ts +79 -0
  7. package/dist/codes-y-U76vnQ.d.ts.map +1 -0
  8. package/dist/contexts-DY1LHU1v.d.ts +327 -0
  9. package/dist/contexts-DY1LHU1v.d.ts.map +1 -0
  10. package/dist/{dist-mAH_7Ui7.js → dist-BStBkGIC.js} +50 -93
  11. package/dist/dist-BStBkGIC.js.map +1 -0
  12. package/dist/errors/index.d.ts +4 -0
  13. package/dist/errors/index.js +4 -0
  14. package/dist/errors-ewgSNicb.js +741 -0
  15. package/dist/errors-ewgSNicb.js.map +1 -0
  16. package/dist/{errors-CpoDEfUZ.js → errors-k3B8orjH.js} +41 -4
  17. package/dist/errors-k3B8orjH.js.map +1 -0
  18. package/dist/extensions/index.d.ts +2 -0
  19. package/dist/extensions/index.js +4 -0
  20. package/dist/extensions-CepYdzro.js +191 -0
  21. package/dist/extensions-CepYdzro.js.map +1 -0
  22. package/dist/git/index.d.ts +74 -0
  23. package/dist/git/index.d.ts.map +1 -0
  24. package/dist/git/index.js +345 -0
  25. package/dist/git/index.js.map +1 -0
  26. package/dist/index-B7QgIs0N.d.ts +1841 -0
  27. package/dist/index-B7QgIs0N.d.ts.map +1 -0
  28. package/dist/index-Dy6u9r60.d.ts +390 -0
  29. package/dist/index-Dy6u9r60.d.ts.map +1 -0
  30. package/dist/index.d.ts +27 -189
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -43
  33. package/dist/index.js.map +1 -1
  34. package/dist/interpreter/index.d.ts +309 -0
  35. package/dist/interpreter/index.d.ts.map +1 -0
  36. package/dist/interpreter/index.js +264 -0
  37. package/dist/interpreter/index.js.map +1 -0
  38. package/dist/openai/index.d.ts +3 -2
  39. package/dist/openai/index.d.ts.map +1 -1
  40. package/dist/openai/index.js +2 -2
  41. package/dist/openai/index.js.map +1 -1
  42. package/dist/opencode/index.d.ts +137 -160
  43. package/dist/opencode/index.d.ts.map +1 -1
  44. package/dist/opencode/index.js +269 -159
  45. package/dist/opencode/index.js.map +1 -1
  46. package/dist/sandbox-DHNO89IF.d.ts +828 -0
  47. package/dist/sandbox-DHNO89IF.d.ts.map +1 -0
  48. package/dist/{sandbox-B-FLGbkP.js → sandbox-boKWPIcd.js} +8214 -7267
  49. package/dist/sandbox-boKWPIcd.js.map +1 -0
  50. package/dist/sidecar/index.d.ts +77 -0
  51. package/dist/sidecar/index.d.ts.map +1 -0
  52. package/dist/sidecar/index.js +201 -0
  53. package/dist/sidecar/index.js.map +1 -0
  54. package/dist/xterm/index.d.ts +6 -6
  55. package/dist/xterm/index.d.ts.map +1 -1
  56. package/dist/xterm/index.js +6 -6
  57. package/dist/xterm/index.js.map +1 -1
  58. package/package.json +36 -3
  59. package/dist/contexts-DPFhc2nR.d.ts +0 -238
  60. package/dist/contexts-DPFhc2nR.d.ts.map +0 -1
  61. package/dist/dist-mAH_7Ui7.js.map +0 -1
  62. package/dist/errors-CpoDEfUZ.js.map +0 -1
  63. package/dist/sandbox-44kEJjAc.d.ts +0 -2652
  64. package/dist/sandbox-44kEJjAc.d.ts.map +0 -1
  65. package/dist/sandbox-B-FLGbkP.js.map +0 -1
package/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # Bun version — override via --build-arg BUN_VERSION=$(cat .bun-version)
2
- ARG BUN_VERSION=1
2
+ ARG BUN_VERSION=1.3.14
3
3
  # Node version — override via --build-arg NODE_VERSION=24
4
4
  ARG NODE_VERSION=24
5
5
  # cloudflared release — shared across glibc and musl image variants.
@@ -186,12 +186,9 @@ COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
186
186
  # Copy standalone binary
187
187
  COPY --from=builder /app/packages/sandbox-container/dist/sandbox /container-server/sandbox
188
188
 
189
- # Set up container server directory for executors
189
+ # Set up container server directory
190
190
  WORKDIR /container-server
191
191
 
192
- # Copy bundled JavaScript executor (runs on Node or Bun for code interpreter)
193
- COPY --from=builder /app/packages/sandbox-container/dist/runtime/executors/javascript/node_executor.js ./dist/runtime/executors/javascript/
194
-
195
192
  # Copy legacy JS bundle for backwards compatibility
196
193
  # Users with custom startup scripts that call `bun /container-server/dist/index.js` need this
197
194
  COPY --from=builder /app/packages/sandbox-container/dist/index.js ./dist/
@@ -245,14 +242,13 @@ RUN ln -s /usr/local/python/bin/python3.11 /usr/local/bin/python3.11 && \
245
242
  # Set Python 3.11 as default python3
246
243
  RUN update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.11 1
247
244
 
248
- # Install Python packages for data science and code interpreter
245
+ # Install Python packages for data science and the code interpreter extension.
246
+ # The interpreter runtime itself is provisioned by the @cloudflare/sandbox/interpreter
247
+ # extension; this image only provides the Python toolchain it relies on.
249
248
  RUN --mount=type=cache,target=/root/.cache/pip \
250
249
  pip3 install --no-cache-dir matplotlib numpy pandas ipython
251
250
 
252
- # Copy Python executor
253
- COPY --from=builder /app/packages/sandbox-container/src/runtime/executors/python/ipython_executor.py ./dist/runtime/executors/python/
254
-
255
- # Enable all interpreter pools
251
+ # Pre-warm pool sizes consumed by the interpreter extension sidecar
256
252
  ENV PYTHON_POOL_MIN_SIZE=3
257
253
  ENV JAVASCRIPT_POOL_MIN_SIZE=3
258
254
  ENV TYPESCRIPT_POOL_MIN_SIZE=3
@@ -318,7 +314,6 @@ COPY --from=builder /app/packages/sandbox-container/dist/sandbox-musl /container
318
314
 
319
315
  WORKDIR /container-server
320
316
 
321
- COPY --from=builder /app/packages/sandbox-container/dist/runtime/executors/javascript/node_executor.js ./dist/runtime/executors/javascript/
322
317
  COPY --from=builder /app/packages/sandbox-container/dist/index.js ./dist/
323
318
 
324
319
  RUN mkdir -p /workspace
package/README.md CHANGED
@@ -82,7 +82,7 @@ export default {
82
82
 
83
83
  // Execute Python code
84
84
  if (url.pathname === '/run') {
85
- const result = await sandbox.exec('python3 -c "print(2 + 2)"');
85
+ const result = await sandbox.exec('python3 -c "print(2 + 2)"').output();
86
86
  return Response.json({ output: result.stdout, success: result.success });
87
87
  }
88
88
 
@@ -154,7 +154,7 @@ Notes:
154
154
  - **Edge-Native** - Runs on Cloudflare's global network
155
155
  - **Code Interpreter** - Execute Python and JavaScript with rich outputs
156
156
  - **File System Access** - Read, write, and manage files
157
- - **Command Execution** - Run any command with streaming support
157
+ - **Command Execution** - Run commands with a Containers-style process handle, streaming output, stdin, and background process control
158
158
  - **Preview URLs** - Expose services with public URLs
159
159
  - **Quick tunnels** - Zero-config `*.trycloudflare.com` URLs via `sandbox.tunnels.get(port)`
160
160
  - **Git Integration** - Clone repositories directly
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6DgB,UAvCC,YAAA,CAuCD;EAEP;;;AAaT;;;;ICJgB,OAAA,CAAA,EAAU,MAAA;IAyBV;;;;AC7EhB;AAOA;AAWC;AA6CD;EAA4C,cAAA,CAAA,EAAA,MAAA;EA2BH;;;;;EAiFf,WAAA,CAAA,EAAA,MAAA;;;;;;;;UFzIT,cAAA;EGgCD,KAAA,EAAA,OAAM,EH9BT,OG8BS,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EH5Bb,gBG4Ba,CAAA,EH3BjB,QG2BiB,GH3BN,OG2BM,CH3BE,QG2BF,CAAA;EACZ,SAAA,EAAA,UAAA,EH1BM,mBG0BN,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EHxBD,gBGwBC,CAAA,EAAA,IAAA,GHvBE,OGuBF,CAAA,IAAA,CAAA;EACC,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;UHZM,SAAA;;;;;;;;;;;AAAjB;;;;ACJA;AAyBgB,iBAzBA,UAAA,CAyBoB,GAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;AC7EpC;AAOA;AAmDU,iBDmBM,oBAAA,CClBL,QAAsB,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;AFHhB,UExDA,cAAA,CFwDS;;;;ECJV,eAAU,CAAA,EAAA,MAAA;AAyB1B;UCtEiB,SAAA;;;EAPA;EAOA,QAAA,EAAA,MAAS;EAmDhB;EAKG,KAAA,EAAA,MAAS;EAAsB;EA2BH,MAAA,EA3E/B,QA2E+B,CA3EtB,cA2EsB,CAAA;EA6CG;EAYE,YAAA,EAAA,MAAA,GAAA,IAAA;;;;;;;UAzFpC,WAAA,CAKoB;EAAa,OAAA,EAJhC,sBAIgC;;;cAA9B,QAAA,SAAiB,cAAc;ECG5B,QAAA,MAAM;EACZ;EACC,QAAA,cAAA;EACQ;EAAhB,QAAA,WAAA;EAAe;;;;;;;;;;;mCDqBuB;;;;;;sCA6CG;;;;wCAYE;;;;cAS1B,QAAQ;;;;;oBAeF,iBAAiB;;;;;uBAUd;WAyBZ;;;;;;;;;;;;;;;;;;;;;;;;AAvMjB;AAWC;AA6CD;;;;;;;;;;AA+IiB,iBC5ID,MAAA,CD4IC,MAAA,EC3IP,cD2IO,EAAA,MAAA,CAAA,EC1IN,YD0IM,CAAA,ECzId,eDyIc,CCzIE,SDyIF,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6DgB,UAvCC,YAAA,CAuCD;EAEP;;;AAaT;;;;ICJgB,OAAA,CAAA,EAAU,MAAA;IAyBV;;;;AC7EhB;AAOA;AAWC;AA6CD;EAA4C,cAAA,CAAA,EAAA,MAAA;EA2BH;;;;;EAiFf,WAAA,CAAA,EAAA,MAAA;;;;;;;;UFzIT,cAAA;EGgCD,KAAA,EAAA,OAAM,EH9BT,OG8BS,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EH5Bb,gBG4Ba,CAAA,EH3BjB,QG2BiB,GH3BN,OG2BM,CH3BE,QG2BF,CAAA;EACZ,SAAA,EAAA,UAAA,EH1BM,mBG0BN,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EHxBD,gBGwBC,CAAA,EAAA,IAAA,GHvBE,OGuBF,CAAA,IAAA,CAAA;EACC,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;UHZM,SAAA;;;;;;;;;;;AAAjB;;;;ACJA;AAyBgB,iBAzBA,UAAA,CAyBoB,GAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;AC7EpC;AAOA;AAmDU,iBDmBM,oBAAA,CClBL,QAAA,EAAsB,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;AFHhB,UExDA,cAAA,CFwDS;;;;ECJV,eAAU,CAAA,EAAA,MAAA;AAyB1B;UCtEiB,SAAA;;;EAPA;EAOA,QAAA,EAAA,MAAS;EAmDhB;EAKG,KAAA,EAAA,MAAS;EAAsB;EA2BH,MAAA,EA3E/B,QA2E+B,CA3EtB,cA2EsB,CAAA;EA6CG;EAYE,YAAA,EAAA,MAAA,GAAA,IAAA;;;;;;;UAzFpC,WAAA,CAKoB;EAAa,OAAA,EAJhC,sBAIgC;;;cAA9B,QAAA,SAAiB,cAAc;ECG5B,QAAA,MAAM;EACZ;EACC,QAAA,cAAA;EACQ;EAAhB,QAAA,WAAA;EAAe;;;;;;;;;;;mCDqBuB;;;;;;sCA6CG;;;;wCAYE;;;;cAS1B,QAAQ;;;;;oBAeF,iBAAiB;;;;;uBAUd;WAyBZ;;;;;;;;;;;;;;;;;;;;;;;;AAvMjB;AAWC;AA6CD;;;;;;;;;;AA+IiB,iBC5ID,MAAA,CD4IC,MAAA,EC3IP,cD2IO,EAAA,MAAA,CAAA,EC1IN,YD0IM,CAAA,ECzId,eDyIc,CCzIE,SDyIF,CAAA"}
@@ -1,6 +1,8 @@
1
- import "../dist-mAH_7Ui7.js";
2
- import "../errors-CpoDEfUZ.js";
3
- import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-B-FLGbkP.js";
1
+ import "../dist-BStBkGIC.js";
2
+ import "../errors-k3B8orjH.js";
3
+ import "../errors-ewgSNicb.js";
4
+ import { S as streamFile, g as validateTunnelName, h as validatePort, m as SandboxSecurityError, n as getSandbox } from "../sandbox-boKWPIcd.js";
5
+ import "../extensions-CepYdzro.js";
4
6
  import { DurableObject, env } from "cloudflare:workers";
5
7
  import { Hono } from "hono";
6
8
 
@@ -454,7 +456,7 @@ const OPENAPI_SCHEMA = {
454
456
  type: "string",
455
457
  pattern: "^[a-zA-Z0-9._-]{1,128}$"
456
458
  },
457
- description: "Scope this operation to a specific session. Uses the default session if omitted."
459
+ description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
458
460
  }],
459
461
  requestBody: {
460
462
  required: true,
@@ -618,7 +620,7 @@ const OPENAPI_SCHEMA = {
618
620
  type: "string",
619
621
  pattern: "^[a-zA-Z0-9._-]{1,128}$"
620
622
  },
621
- description: "Scope this operation to a specific session. Uses the default session if omitted."
623
+ description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
622
624
  }
623
625
  ],
624
626
  responses: {
@@ -694,7 +696,7 @@ const OPENAPI_SCHEMA = {
694
696
  type: "string",
695
697
  pattern: "^[a-zA-Z0-9._-]{1,128}$"
696
698
  },
697
- description: "Scope this operation to a specific session. Uses the default session if omitted."
699
+ description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
698
700
  }
699
701
  ],
700
702
  requestBody: {
@@ -737,12 +739,12 @@ const OPENAPI_SCHEMA = {
737
739
  },
738
740
  "/v1/sandbox/{id}/pty": { get: {
739
741
  operationId: "ptyTerminal",
740
- summary: "Open a PTY terminal session via WebSocket",
741
- description: "Upgrades the HTTP connection to a WebSocket and proxies it to a PTY shell inside the sandbox.\n\n**WebSocket frame protocol:**\n\n| Direction | Frame type | Content |\n|-----------|------------|--------------------------------------------------|\n| Client → Server | Binary | UTF-8 encoded keystrokes / input |\n| Server → Client | Binary | Terminal output (including ANSI escape sequences) |\n| Client → Server | Text (JSON) | Control messages, e.g. `{\"type\":\"resize\",\"cols\":120,\"rows\":30}` |\n| Server → Client | Text (JSON) | Status messages: `ready`, `exit`, `error` |\n\n**Status messages (server → client):**\n- `{\"type\":\"ready\"}` — PTY is accepting input\n- `{\"type\":\"exit\",\"code\":0,\"signal\":\"SIGTERM\"}` — PTY exited\n- `{\"type\":\"error\",\"message\":\"...\"}` — error occurred\n\nIf the client disconnects, the PTY stays alive; reconnecting replays buffered output.",
742
+ summary: "Open a terminal via WebSocket",
743
+ description: "Upgrades the HTTP connection to a WebSocket and proxies it to a PTY shell inside the sandbox.\n\n**A terminal ID is required**, supplied via either the `terminalId` query parameter or the `Terminal-Id` header (header takes precedence). The terminal ID is a stable resource name: reconnecting with the same ID reattaches to the same live PTY while the sandbox container is alive. If the sandbox container restarts (for example after it sleeps), connecting with the same ID creates a fresh shell — prior PTY state and process history are not preserved.\n\n**WebSocket frame protocol:**\n\n| Direction | Frame type | Content |\n|-----------|------------|--------------------------------------------------|\n| Client → Server | Binary | UTF-8 encoded keystrokes / input |\n| Server → Client | Binary | Terminal output (including ANSI escape sequences) |\n| Client → Server | Text (JSON) | Control messages, e.g. `{\"type\":\"resize\",\"cols\":120,\"rows\":30}` |\n| Server → Client | Text (JSON) | Status messages: `ready`, `exit`, `error` |\n\n**Status messages (server → client):**\n- `{\"type\":\"ready\"}` — PTY is accepting input\n- `{\"type\":\"exit\",\"code\":0,\"signal\":\"SIGTERM\"}` — PTY exited\n- `{\"type\":\"error\",\"message\":\"...\"}` — error occurred\n\nIf the client disconnects, the terminal stays alive while the sandbox container is alive; reconnect with the same terminal ID to reattach.",
742
744
  "x-codeSamples": [{
743
745
  lang: "JavaScript",
744
746
  label: "WebSocket",
745
- source: "const ws = new WebSocket(\"wss://$HOST/v1/sandbox/my-sandbox/pty?cols=120&rows=30\");\nws.binaryType = \"arraybuffer\";\nws.onmessage = (e) => { /* handle binary output or JSON status */ };"
747
+ source: "const ws = new WebSocket(\"wss://$HOST/v1/sandbox/my-sandbox/pty?terminalId=term-1&cols=120&rows=30\");\nws.binaryType = \"arraybuffer\";\nws.onmessage = (e) => { /* handle binary output or JSON status */ };"
746
748
  }],
747
749
  parameters: [
748
750
  {
@@ -780,21 +782,31 @@ const OPENAPI_SCHEMA = {
780
782
  description: "Shell binary to run (e.g. `/bin/bash`). Uses the container default if omitted."
781
783
  },
782
784
  {
783
- name: "session",
785
+ name: "cwd",
784
786
  in: "query",
785
787
  required: false,
786
788
  schema: { type: "string" },
787
- description: "SDK session ID. If provided, the PTY is scoped to this session."
789
+ description: "Initial working directory for newly created terminal resources."
788
790
  },
789
791
  {
790
- name: "Session-Id",
792
+ name: "terminalId",
793
+ in: "query",
794
+ required: false,
795
+ schema: {
796
+ type: "string",
797
+ pattern: "^[a-zA-Z0-9._-]{1,128}$"
798
+ },
799
+ description: "Terminal resource ID. Required unless the `Terminal-Id` request header is supplied. Stable name for this terminal resource: reuse the same ID across reconnects to reattach to the same live PTY. After the sandbox container restarts, the same ID creates a new shell. Ignored when `Terminal-Id` header is also present (header takes precedence)."
800
+ },
801
+ {
802
+ name: "Terminal-Id",
791
803
  in: "header",
792
804
  required: false,
793
805
  schema: {
794
806
  type: "string",
795
807
  pattern: "^[a-zA-Z0-9._-]{1,128}$"
796
808
  },
797
- description: "Scope this operation to a specific session. Uses the default session if omitted."
809
+ description: "Terminal resource ID. Required unless the `terminalId` query parameter is supplied. Takes precedence over the `terminalId` query parameter when both are present. Same stable-ID semantics apply: same ID reattaches to the live PTY; after the sandbox container restarts a new shell is created."
798
810
  }
799
811
  ],
800
812
  responses: {
@@ -1045,7 +1057,7 @@ const OPENAPI_SCHEMA = {
1045
1057
  "/v1/sandbox/{id}/session": { post: {
1046
1058
  operationId: "createSession",
1047
1059
  summary: "Create an execution session",
1048
- description: "Sessions isolate working directory and environment variables across commands. The returned session ID is used with the `Session-Id` header on exec, file, and PTY endpoints.",
1060
+ description: "Sessions isolate working directory and environment variables across commands. The returned session ID is used with the `Session-Id` header on exec and file endpoints.",
1049
1061
  parameters: [{
1050
1062
  name: "id",
1051
1063
  in: "path",
@@ -1102,7 +1114,7 @@ const OPENAPI_SCHEMA = {
1102
1114
  "/v1/sandbox/{id}/session/{sid}": { delete: {
1103
1115
  operationId: "deleteSession",
1104
1116
  summary: "Delete an execution session",
1105
- description: "Removes a named session. The default session cannot be deleted.",
1117
+ description: "Removes an explicit execution session.",
1106
1118
  parameters: [{
1107
1119
  name: "id",
1108
1120
  in: "path",
@@ -1136,11 +1148,11 @@ const OPENAPI_SCHEMA = {
1136
1148
  },
1137
1149
  "401": { $ref: "#/components/responses/Unauthorized" },
1138
1150
  "502": {
1139
- description: "Session deletion failed (e.g. cannot delete default session).",
1151
+ description: "Session deletion failed.",
1140
1152
  content: { "application/json": {
1141
1153
  schema: { $ref: "#/components/schemas/ErrorResponse" },
1142
1154
  example: {
1143
- error: "cannot delete the default session",
1155
+ error: "session deletion failed",
1144
1156
  code: "session_error"
1145
1157
  }
1146
1158
  } }
@@ -1942,31 +1954,36 @@ function createBridgeApp(config) {
1942
1954
  closed = true;
1943
1955
  lastWrite.then(() => writer.close()).catch(() => {});
1944
1956
  }
1945
- executor.exec(command, {
1946
- ...opts,
1947
- stream: true,
1948
- onOutput(stream, data) {
1949
- writeSSE(stream, toBase64(data));
1950
- },
1951
- onComplete(result) {
1952
- writeSSE("exit", JSON.stringify({ exit_code: result.exitCode }));
1953
- closeStream();
1954
- },
1955
- onError(err) {
1957
+ (async () => {
1958
+ try {
1959
+ const proc = await executor.exec(command, opts);
1960
+ const decoder = new TextDecoder();
1961
+ const pump = async (stream, name) => {
1962
+ if (!stream) return;
1963
+ const reader = stream.getReader();
1964
+ try {
1965
+ for (;;) {
1966
+ const { value, done } = await reader.read();
1967
+ if (done) break;
1968
+ if (value && value.byteLength > 0) writeSSE(name, toBase64(decoder.decode(value)));
1969
+ }
1970
+ } finally {
1971
+ reader.releaseLock();
1972
+ }
1973
+ };
1974
+ await Promise.all([pump(proc.stdout, "stdout"), pump(proc.stderr, "stderr")]);
1975
+ const exitCode = await proc.exitCode;
1976
+ writeSSE("exit", JSON.stringify({ exit_code: exitCode }));
1977
+ } catch (err) {
1978
+ const msg = err instanceof Error ? err.message : String(err);
1956
1979
  writeSSE("error", JSON.stringify({
1957
- error: err.message,
1958
- code: "exec_error"
1980
+ error: `exec failed: ${msg}`,
1981
+ code: "exec_transport_error"
1959
1982
  }));
1983
+ } finally {
1960
1984
  closeStream();
1961
1985
  }
1962
- }).catch((err) => {
1963
- const msg = err instanceof Error ? err.message : String(err);
1964
- writeSSE("error", JSON.stringify({
1965
- error: `exec failed: ${msg}`,
1966
- code: "exec_transport_error"
1967
- }));
1968
- closeStream();
1969
- });
1986
+ })();
1970
1987
  return new Response(readable, {
1971
1988
  status: 200,
1972
1989
  headers: {
@@ -2072,22 +2089,24 @@ function createBridgeApp(config) {
2072
2089
  const colsParam = c.req.query("cols");
2073
2090
  const rowsParam = c.req.query("rows");
2074
2091
  const shell = c.req.query("shell");
2075
- const sessionId = c.req.header("Session-Id") || c.req.query("session");
2092
+ const cwd = c.req.query("cwd");
2093
+ const terminalId = c.req.header("Terminal-Id") ?? c.req.query("terminalId") ?? c.req.query("terminal");
2076
2094
  const cols = colsParam ? Number(colsParam) : 80;
2077
2095
  const rows = rowsParam ? Number(rowsParam) : 24;
2078
2096
  if (Number.isNaN(cols) || Number.isNaN(rows)) return errorJson("cols and rows must be valid numbers", "invalid_request", 400);
2079
- const opts = {
2097
+ const terminalOptions = {};
2098
+ const connectOptions = {
2080
2099
  cols,
2081
2100
  rows
2082
2101
  };
2083
- if (shell) opts.shell = shell;
2102
+ if (shell) terminalOptions.shell = shell;
2103
+ if (cwd) terminalOptions.cwd = cwd;
2104
+ if (!terminalId) return errorJson("terminalId query parameter or Terminal-Id header required", "invalid_request", 400);
2105
+ const validatedId = validateSessionId(terminalId);
2106
+ if (!validatedId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2107
+ terminalOptions.id = validatedId;
2084
2108
  try {
2085
- if (sessionId) {
2086
- const validatedId = validateSessionId(sessionId);
2087
- if (!validatedId) return errorJson("Invalid session ID format", "invalid_request", 400);
2088
- return await (await sandbox.getSession(validatedId)).terminal(c.req.raw, opts);
2089
- }
2090
- return await sandbox.terminal(c.req.raw, opts);
2109
+ return await sandbox.terminal(terminalOptions).connect(c.req.raw, connectOptions);
2091
2110
  } catch (err) {
2092
2111
  return errorJson(`terminal failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
2093
2112
  }
@@ -2102,7 +2121,7 @@ function createBridgeApp(config) {
2102
2121
  const excludeArgs = excludes.map((rel) => `--exclude ${shellQuote(`./${rel.replace(/^\.\//, "")}`)}`).join(" ");
2103
2122
  const tarCmd = excludeArgs ? `tar cf ${shellQuote(tmpPath)} ${excludeArgs} -C ${shellQuote(root)} .` : `tar cf ${shellQuote(tmpPath)} -C ${shellQuote(root)} .`;
2104
2123
  try {
2105
- const result = await sandbox.exec(tarCmd);
2124
+ const result = await sandbox.exec(tarCmd).output({ encoding: "utf8" });
2106
2125
  if (result.exitCode !== 0) return errorJson(`tar failed (exit ${result.exitCode}): ${result.stderr}`, "workspace_archive_read_error", 502);
2107
2126
  const stream = await sandbox.readFileStream(tmpPath);
2108
2127
  sandbox.exec(`rm -f ${shellQuote(tmpPath)}`).catch(() => {});
@@ -2134,7 +2153,7 @@ function createBridgeApp(config) {
2134
2153
  const CHUNK = 6144;
2135
2154
  for (let i = 0; i < tarBytes.length; i += CHUNK) b64 += btoa(String.fromCharCode(...tarBytes.subarray(i, i + CHUNK)));
2136
2155
  await sandbox.writeFile(tmpPath, b64, { encoding: "base64" });
2137
- const extractResult = await sandbox.exec(`tar xf ${shellQuote(tmpPath)} -C ${shellQuote(root)} && rm -f ${shellQuote(tmpPath)}`);
2156
+ const extractResult = await sandbox.exec(`tar xf ${shellQuote(tmpPath)} -C ${shellQuote(root)} && rm -f ${shellQuote(tmpPath)}`).output({ encoding: "utf8" });
2138
2157
  if (extractResult.exitCode !== 0) return errorJson(`tar extract failed (exit ${extractResult.exitCode}): ${extractResult.stderr}`, "workspace_archive_write_error", 502);
2139
2158
  return c.json({ ok: true });
2140
2159
  } catch (err) {