@cloudflare/sandbox 0.7.8 → 0.7.10

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as getEnvString, a as isExecResult, c as shellEscape, d as TraceContext, f as Execution, g as filterEnvVars, h as extractRepoName, i as isWSStreamChunk, l as createLogger, m as GitLogger, n as isWSError, o as isProcess, p as ResultImpl, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createNoOpLogger, v as partitionEnvVars } from "./dist-D9B_6gn_.js";
2
- import { t as ErrorCode } from "./errors-CAZT-Gtg.js";
1
+ import { _ as filterEnvVars, a as isExecResult, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as extractRepoName, h as GitLogger, i as isWSStreamChunk, l as shellEscape, m as ResultImpl, n as isWSError, o as isProcess, p as Execution, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createLogger, v as getEnvString, y as partitionEnvVars } from "./dist-CwUZf_TJ.js";
2
+ import { t as ErrorCode } from "./errors-8W0q5Gll.js";
3
3
  import { Container, getContainer, switchPort } from "@cloudflare/containers";
4
4
  import { AwsClient } from "aws4fetch";
5
5
 
@@ -593,6 +593,42 @@ var BackupRestoreError = class extends SandboxError {
593
593
  return this.context.backupId;
594
594
  }
595
595
  };
596
+ var DesktopNotStartedError = class extends SandboxError {
597
+ constructor(errorResponse) {
598
+ super(errorResponse);
599
+ this.name = "DesktopNotStartedError";
600
+ }
601
+ };
602
+ var DesktopStartFailedError = class extends SandboxError {
603
+ constructor(errorResponse) {
604
+ super(errorResponse);
605
+ this.name = "DesktopStartFailedError";
606
+ }
607
+ };
608
+ var DesktopUnavailableError = class extends SandboxError {
609
+ constructor(errorResponse) {
610
+ super(errorResponse);
611
+ this.name = "DesktopUnavailableError";
612
+ }
613
+ };
614
+ var DesktopProcessCrashedError = class extends SandboxError {
615
+ constructor(errorResponse) {
616
+ super(errorResponse);
617
+ this.name = "DesktopProcessCrashedError";
618
+ }
619
+ };
620
+ var DesktopInvalidOptionsError = class extends SandboxError {
621
+ constructor(errorResponse) {
622
+ super(errorResponse);
623
+ this.name = "DesktopInvalidOptionsError";
624
+ }
625
+ };
626
+ var DesktopInvalidCoordinatesError = class extends SandboxError {
627
+ constructor(errorResponse) {
628
+ super(errorResponse);
629
+ this.name = "DesktopInvalidCoordinatesError";
630
+ }
631
+ };
596
632
 
597
633
  //#endregion
598
634
  //#region src/errors/adapter.ts
@@ -648,6 +684,12 @@ function createErrorFromResponse(errorResponse) {
648
684
  case ErrorCode.INTERPRETER_NOT_READY: return new InterpreterNotReadyError(errorResponse);
649
685
  case ErrorCode.CONTEXT_NOT_FOUND: return new ContextNotFoundError(errorResponse);
650
686
  case ErrorCode.CODE_EXECUTION_ERROR: return new CodeExecutionError(errorResponse);
687
+ case ErrorCode.DESKTOP_NOT_STARTED: return new DesktopNotStartedError(errorResponse);
688
+ case ErrorCode.DESKTOP_START_FAILED: return new DesktopStartFailedError(errorResponse);
689
+ case ErrorCode.DESKTOP_UNAVAILABLE: return new DesktopUnavailableError(errorResponse);
690
+ case ErrorCode.DESKTOP_PROCESS_CRASHED: return new DesktopProcessCrashedError(errorResponse);
691
+ case ErrorCode.DESKTOP_INVALID_OPTIONS: return new DesktopInvalidOptionsError(errorResponse);
692
+ case ErrorCode.DESKTOP_INVALID_COORDINATES: return new DesktopInvalidCoordinatesError(errorResponse);
651
693
  case ErrorCode.VALIDATION_FAILED: return new ValidationFailedError(errorResponse);
652
694
  case ErrorCode.INVALID_JSON_RESPONSE:
653
695
  case ErrorCode.UNKNOWN_ERROR:
@@ -990,6 +1032,10 @@ var WebSocketTransport = class extends BaseTransport {
990
1032
  *
991
1033
  * The stream will receive data chunks as they arrive over the WebSocket.
992
1034
  * Format matches SSE for compatibility with existing streaming code.
1035
+ *
1036
+ * This method waits for the first message before returning. If the server
1037
+ * responds with an error (non-streaming response), it throws immediately
1038
+ * rather than returning a stream that will error later.
993
1039
  */
994
1040
  async requestStream(method, path, body) {
995
1041
  await this.connect();
@@ -1001,41 +1047,75 @@ var WebSocketTransport = class extends BaseTransport {
1001
1047
  path,
1002
1048
  body
1003
1049
  };
1004
- return new ReadableStream({
1005
- start: (controller) => {
1006
- const timeoutMs = this.config.requestTimeoutMs ?? 12e4;
1007
- const timeoutId = setTimeout(() => {
1050
+ return new Promise((resolveStream, rejectStream) => {
1051
+ let streamController;
1052
+ let firstMessageReceived = false;
1053
+ const timeoutMs = this.config.requestTimeoutMs ?? 12e4;
1054
+ const timeoutId = setTimeout(() => {
1055
+ this.pendingRequests.delete(id);
1056
+ const error = /* @__PURE__ */ new Error(`Stream timeout after ${timeoutMs}ms: ${method} ${path}`);
1057
+ if (firstMessageReceived) streamController?.error(error);
1058
+ else rejectStream(error);
1059
+ }, timeoutMs);
1060
+ const stream = new ReadableStream({
1061
+ start: (controller) => {
1062
+ streamController = controller;
1063
+ },
1064
+ cancel: () => {
1065
+ const pending = this.pendingRequests.get(id);
1066
+ if (pending?.timeoutId) clearTimeout(pending.timeoutId);
1067
+ try {
1068
+ this.send({
1069
+ type: "cancel",
1070
+ id
1071
+ });
1072
+ } catch (error) {
1073
+ this.logger.debug("Failed to send stream cancel message", {
1074
+ id,
1075
+ error: error instanceof Error ? error.message : String(error)
1076
+ });
1077
+ }
1008
1078
  this.pendingRequests.delete(id);
1009
- controller.error(/* @__PURE__ */ new Error(`Stream timeout after ${timeoutMs}ms: ${method} ${path}`));
1010
- }, timeoutMs);
1011
- this.pendingRequests.set(id, {
1012
- resolve: (response) => {
1013
- clearTimeout(timeoutId);
1014
- this.pendingRequests.delete(id);
1015
- if (response.status >= 400) controller.error(/* @__PURE__ */ new Error(`Stream error: ${response.status} - ${JSON.stringify(response.body)}`));
1016
- else controller.close();
1017
- },
1018
- reject: (error) => {
1019
- clearTimeout(timeoutId);
1020
- this.pendingRequests.delete(id);
1021
- controller.error(error);
1022
- },
1023
- streamController: controller,
1024
- isStreaming: true,
1025
- timeoutId
1026
- });
1027
- try {
1028
- this.send(request);
1029
- } catch (error) {
1079
+ }
1080
+ });
1081
+ this.pendingRequests.set(id, {
1082
+ resolve: (response) => {
1030
1083
  clearTimeout(timeoutId);
1031
1084
  this.pendingRequests.delete(id);
1032
- controller.error(error instanceof Error ? error : new Error(String(error)));
1085
+ if (!firstMessageReceived) {
1086
+ firstMessageReceived = true;
1087
+ if (response.status >= 400) rejectStream(/* @__PURE__ */ new Error(`Stream error: ${response.status} - ${JSON.stringify(response.body)}`));
1088
+ else {
1089
+ streamController?.close();
1090
+ resolveStream(stream);
1091
+ }
1092
+ } else if (response.status >= 400) streamController?.error(/* @__PURE__ */ new Error(`Stream error: ${response.status} - ${JSON.stringify(response.body)}`));
1093
+ else streamController?.close();
1094
+ },
1095
+ reject: (error) => {
1096
+ clearTimeout(timeoutId);
1097
+ this.pendingRequests.delete(id);
1098
+ if (firstMessageReceived) streamController?.error(error);
1099
+ else rejectStream(error);
1100
+ },
1101
+ streamController: void 0,
1102
+ isStreaming: true,
1103
+ timeoutId,
1104
+ onFirstChunk: () => {
1105
+ if (!firstMessageReceived) {
1106
+ firstMessageReceived = true;
1107
+ const pending = this.pendingRequests.get(id);
1108
+ if (pending) pending.streamController = streamController;
1109
+ resolveStream(stream);
1110
+ }
1033
1111
  }
1034
- },
1035
- cancel: () => {
1036
- const pending = this.pendingRequests.get(id);
1037
- if (pending?.timeoutId) clearTimeout(pending.timeoutId);
1112
+ });
1113
+ try {
1114
+ this.send(request);
1115
+ } catch (error) {
1116
+ clearTimeout(timeoutId);
1038
1117
  this.pendingRequests.delete(id);
1118
+ rejectStream(error instanceof Error ? error : new Error(String(error)));
1039
1119
  }
1040
1120
  });
1041
1121
  }
@@ -1047,8 +1127,9 @@ var WebSocketTransport = class extends BaseTransport {
1047
1127
  this.ws.send(JSON.stringify(message));
1048
1128
  this.logger.debug("WebSocket sent", {
1049
1129
  id: message.id,
1050
- method: message.method,
1051
- path: message.path
1130
+ type: message.type,
1131
+ method: message.type === "request" ? message.method : void 0,
1132
+ path: message.type === "request" ? message.path : void 0
1052
1133
  });
1053
1134
  }
1054
1135
  /**
@@ -1086,10 +1167,18 @@ var WebSocketTransport = class extends BaseTransport {
1086
1167
  */
1087
1168
  handleStreamChunk(chunk) {
1088
1169
  const pending = this.pendingRequests.get(chunk.id);
1089
- if (!pending || !pending.streamController) {
1170
+ if (!pending) {
1090
1171
  this.logger.warn("Received stream chunk for unknown request", { id: chunk.id });
1091
1172
  return;
1092
1173
  }
1174
+ if (pending.onFirstChunk) {
1175
+ pending.onFirstChunk();
1176
+ pending.onFirstChunk = void 0;
1177
+ }
1178
+ if (!pending.streamController) {
1179
+ this.logger.warn("Stream chunk received but controller not ready", { id: chunk.id });
1180
+ return;
1181
+ }
1093
1182
  const encoder = new TextEncoder();
1094
1183
  let sseData;
1095
1184
  if (chunk.event) sseData = `event: ${chunk.event}\ndata: ${chunk.data}\n\n`;
@@ -1455,6 +1544,369 @@ var CommandClient = class extends BaseHttpClient {
1455
1544
  }
1456
1545
  };
1457
1546
 
1547
+ //#endregion
1548
+ //#region src/clients/desktop-client.ts
1549
+ /**
1550
+ * Client for desktop environment lifecycle, input, and screen operations
1551
+ */
1552
+ var DesktopClient = class extends BaseHttpClient {
1553
+ /**
1554
+ * Start the desktop environment with optional resolution and DPI.
1555
+ */
1556
+ async start(options) {
1557
+ try {
1558
+ const data = {
1559
+ ...options?.resolution !== void 0 && { resolution: options.resolution },
1560
+ ...options?.dpi !== void 0 && { dpi: options.dpi }
1561
+ };
1562
+ const response = await this.post("/api/desktop/start", data);
1563
+ this.logSuccess("Desktop started", `${response.resolution[0]}x${response.resolution[1]}`);
1564
+ return response;
1565
+ } catch (error) {
1566
+ this.logError("desktop.start", error);
1567
+ this.options.onError?.(error instanceof Error ? error.message : String(error));
1568
+ throw error;
1569
+ }
1570
+ }
1571
+ /**
1572
+ * Stop the desktop environment and all related processes.
1573
+ */
1574
+ async stop() {
1575
+ try {
1576
+ const response = await this.post("/api/desktop/stop", {});
1577
+ this.logSuccess("Desktop stopped");
1578
+ return response;
1579
+ } catch (error) {
1580
+ this.logError("desktop.stop", error);
1581
+ this.options.onError?.(error instanceof Error ? error.message : String(error));
1582
+ throw error;
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Get desktop lifecycle and process health status.
1587
+ */
1588
+ async status() {
1589
+ try {
1590
+ const response = await this.get("/api/desktop/status");
1591
+ this.logSuccess("Desktop status retrieved", response.status);
1592
+ return response;
1593
+ } catch (error) {
1594
+ this.logError("desktop.status", error);
1595
+ throw error;
1596
+ }
1597
+ }
1598
+ async screenshot(options) {
1599
+ try {
1600
+ const wantsBytes = options?.format === "bytes";
1601
+ const data = {
1602
+ format: "base64",
1603
+ ...options?.imageFormat !== void 0 && { imageFormat: options.imageFormat },
1604
+ ...options?.quality !== void 0 && { quality: options.quality },
1605
+ ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1606
+ };
1607
+ const response = await this.post("/api/desktop/screenshot", data);
1608
+ this.logSuccess("Screenshot captured", `${response.width}x${response.height}`);
1609
+ if (wantsBytes) {
1610
+ const binaryString = atob(response.data);
1611
+ const bytes = new Uint8Array(binaryString.length);
1612
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
1613
+ return {
1614
+ ...response,
1615
+ data: bytes
1616
+ };
1617
+ }
1618
+ return response;
1619
+ } catch (error) {
1620
+ this.logError("desktop.screenshot", error);
1621
+ throw error;
1622
+ }
1623
+ }
1624
+ async screenshotRegion(region, options) {
1625
+ try {
1626
+ const wantsBytes = options?.format === "bytes";
1627
+ const data = {
1628
+ region,
1629
+ format: "base64",
1630
+ ...options?.imageFormat !== void 0 && { imageFormat: options.imageFormat },
1631
+ ...options?.quality !== void 0 && { quality: options.quality },
1632
+ ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1633
+ };
1634
+ const response = await this.post("/api/desktop/screenshot/region", data);
1635
+ this.logSuccess("Region screenshot captured", `${region.width}x${region.height}`);
1636
+ if (wantsBytes) {
1637
+ const binaryString = atob(response.data);
1638
+ const bytes = new Uint8Array(binaryString.length);
1639
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
1640
+ return {
1641
+ ...response,
1642
+ data: bytes
1643
+ };
1644
+ }
1645
+ return response;
1646
+ } catch (error) {
1647
+ this.logError("desktop.screenshotRegion", error);
1648
+ throw error;
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Single-click at the given coordinates.
1653
+ */
1654
+ async click(x, y, options) {
1655
+ try {
1656
+ await this.post("/api/desktop/mouse/click", {
1657
+ x,
1658
+ y,
1659
+ button: options?.button ?? "left",
1660
+ clickCount: 1
1661
+ });
1662
+ this.logSuccess("Mouse click", `(${x}, ${y})`);
1663
+ } catch (error) {
1664
+ this.logError("desktop.click", error);
1665
+ throw error;
1666
+ }
1667
+ }
1668
+ /**
1669
+ * Double-click at the given coordinates.
1670
+ */
1671
+ async doubleClick(x, y, options) {
1672
+ try {
1673
+ await this.post("/api/desktop/mouse/click", {
1674
+ x,
1675
+ y,
1676
+ button: options?.button ?? "left",
1677
+ clickCount: 2
1678
+ });
1679
+ this.logSuccess("Mouse double click", `(${x}, ${y})`);
1680
+ } catch (error) {
1681
+ this.logError("desktop.doubleClick", error);
1682
+ throw error;
1683
+ }
1684
+ }
1685
+ /**
1686
+ * Triple-click at the given coordinates.
1687
+ */
1688
+ async tripleClick(x, y, options) {
1689
+ try {
1690
+ await this.post("/api/desktop/mouse/click", {
1691
+ x,
1692
+ y,
1693
+ button: options?.button ?? "left",
1694
+ clickCount: 3
1695
+ });
1696
+ this.logSuccess("Mouse triple click", `(${x}, ${y})`);
1697
+ } catch (error) {
1698
+ this.logError("desktop.tripleClick", error);
1699
+ throw error;
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Right-click at the given coordinates.
1704
+ */
1705
+ async rightClick(x, y) {
1706
+ try {
1707
+ await this.post("/api/desktop/mouse/click", {
1708
+ x,
1709
+ y,
1710
+ button: "right",
1711
+ clickCount: 1
1712
+ });
1713
+ this.logSuccess("Mouse right click", `(${x}, ${y})`);
1714
+ } catch (error) {
1715
+ this.logError("desktop.rightClick", error);
1716
+ throw error;
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Middle-click at the given coordinates.
1721
+ */
1722
+ async middleClick(x, y) {
1723
+ try {
1724
+ await this.post("/api/desktop/mouse/click", {
1725
+ x,
1726
+ y,
1727
+ button: "middle",
1728
+ clickCount: 1
1729
+ });
1730
+ this.logSuccess("Mouse middle click", `(${x}, ${y})`);
1731
+ } catch (error) {
1732
+ this.logError("desktop.middleClick", error);
1733
+ throw error;
1734
+ }
1735
+ }
1736
+ /**
1737
+ * Press and hold a mouse button.
1738
+ */
1739
+ async mouseDown(x, y, options) {
1740
+ try {
1741
+ await this.post("/api/desktop/mouse/down", {
1742
+ ...x !== void 0 && { x },
1743
+ ...y !== void 0 && { y },
1744
+ button: options?.button ?? "left"
1745
+ });
1746
+ this.logSuccess("Mouse down", x !== void 0 ? `(${x}, ${y})` : "current position");
1747
+ } catch (error) {
1748
+ this.logError("desktop.mouseDown", error);
1749
+ throw error;
1750
+ }
1751
+ }
1752
+ /**
1753
+ * Release a held mouse button.
1754
+ */
1755
+ async mouseUp(x, y, options) {
1756
+ try {
1757
+ await this.post("/api/desktop/mouse/up", {
1758
+ ...x !== void 0 && { x },
1759
+ ...y !== void 0 && { y },
1760
+ button: options?.button ?? "left"
1761
+ });
1762
+ this.logSuccess("Mouse up", x !== void 0 ? `(${x}, ${y})` : "current position");
1763
+ } catch (error) {
1764
+ this.logError("desktop.mouseUp", error);
1765
+ throw error;
1766
+ }
1767
+ }
1768
+ /**
1769
+ * Move the mouse cursor to coordinates.
1770
+ */
1771
+ async moveMouse(x, y) {
1772
+ try {
1773
+ await this.post("/api/desktop/mouse/move", {
1774
+ x,
1775
+ y
1776
+ });
1777
+ this.logSuccess("Mouse move", `(${x}, ${y})`);
1778
+ } catch (error) {
1779
+ this.logError("desktop.moveMouse", error);
1780
+ throw error;
1781
+ }
1782
+ }
1783
+ /**
1784
+ * Drag from start coordinates to end coordinates.
1785
+ */
1786
+ async drag(startX, startY, endX, endY, options) {
1787
+ try {
1788
+ await this.post("/api/desktop/mouse/drag", {
1789
+ startX,
1790
+ startY,
1791
+ endX,
1792
+ endY,
1793
+ button: options?.button ?? "left"
1794
+ });
1795
+ this.logSuccess("Mouse drag", `(${startX},${startY}) -> (${endX},${endY})`);
1796
+ } catch (error) {
1797
+ this.logError("desktop.drag", error);
1798
+ throw error;
1799
+ }
1800
+ }
1801
+ /**
1802
+ * Scroll at coordinates in the specified direction.
1803
+ */
1804
+ async scroll(x, y, direction, amount = 3) {
1805
+ try {
1806
+ await this.post("/api/desktop/mouse/scroll", {
1807
+ x,
1808
+ y,
1809
+ direction,
1810
+ amount
1811
+ });
1812
+ this.logSuccess("Mouse scroll", `${direction} ${amount} at (${x}, ${y})`);
1813
+ } catch (error) {
1814
+ this.logError("desktop.scroll", error);
1815
+ throw error;
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Get the current cursor coordinates.
1820
+ */
1821
+ async getCursorPosition() {
1822
+ try {
1823
+ const response = await this.get("/api/desktop/mouse/position");
1824
+ this.logSuccess("Cursor position retrieved", `(${response.x}, ${response.y})`);
1825
+ return response;
1826
+ } catch (error) {
1827
+ this.logError("desktop.getCursorPosition", error);
1828
+ throw error;
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Type text into the focused element.
1833
+ */
1834
+ async type(text, options) {
1835
+ try {
1836
+ await this.post("/api/desktop/keyboard/type", {
1837
+ text,
1838
+ ...options?.delayMs !== void 0 && { delayMs: options.delayMs }
1839
+ });
1840
+ this.logSuccess("Keyboard type", `${text.length} chars`);
1841
+ } catch (error) {
1842
+ this.logError("desktop.type", error);
1843
+ throw error;
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Press and release a key or key combination.
1848
+ */
1849
+ async press(key) {
1850
+ try {
1851
+ await this.post("/api/desktop/keyboard/press", { key });
1852
+ this.logSuccess("Key press", key);
1853
+ } catch (error) {
1854
+ this.logError("desktop.press", error);
1855
+ throw error;
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Press and hold a key.
1860
+ */
1861
+ async keyDown(key) {
1862
+ try {
1863
+ await this.post("/api/desktop/keyboard/down", { key });
1864
+ this.logSuccess("Key down", key);
1865
+ } catch (error) {
1866
+ this.logError("desktop.keyDown", error);
1867
+ throw error;
1868
+ }
1869
+ }
1870
+ /**
1871
+ * Release a held key.
1872
+ */
1873
+ async keyUp(key) {
1874
+ try {
1875
+ await this.post("/api/desktop/keyboard/up", { key });
1876
+ this.logSuccess("Key up", key);
1877
+ } catch (error) {
1878
+ this.logError("desktop.keyUp", error);
1879
+ throw error;
1880
+ }
1881
+ }
1882
+ /**
1883
+ * Get the active desktop screen size.
1884
+ */
1885
+ async getScreenSize() {
1886
+ try {
1887
+ const response = await this.get("/api/desktop/screen/size");
1888
+ this.logSuccess("Screen size retrieved", `${response.width}x${response.height}`);
1889
+ return response;
1890
+ } catch (error) {
1891
+ this.logError("desktop.getScreenSize", error);
1892
+ throw error;
1893
+ }
1894
+ }
1895
+ /**
1896
+ * Get health status for a specific desktop process.
1897
+ */
1898
+ async getProcessStatus(name) {
1899
+ try {
1900
+ const response = await this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
1901
+ this.logSuccess("Desktop process status retrieved", name);
1902
+ return response;
1903
+ } catch (error) {
1904
+ this.logError("desktop.getProcessStatus", error);
1905
+ throw error;
1906
+ }
1907
+ }
1908
+ };
1909
+
1458
1910
  //#endregion
1459
1911
  //#region src/clients/file-client.ts
1460
1912
  /**
@@ -2139,6 +2591,106 @@ var UtilityClient = class extends BaseHttpClient {
2139
2591
  }
2140
2592
  };
2141
2593
 
2594
+ //#endregion
2595
+ //#region src/clients/watch-client.ts
2596
+ /**
2597
+ * Client for file watch operations
2598
+ * Uses inotify under the hood for native filesystem event notifications
2599
+ *
2600
+ * @internal This client is used internally by the SDK.
2601
+ * Users should use `sandbox.watch()` instead.
2602
+ */
2603
+ var WatchClient = class extends BaseHttpClient {
2604
+ /**
2605
+ * Start watching a directory for changes.
2606
+ * The returned promise resolves only after the watcher is established
2607
+ * on the filesystem (i.e. the `watching` SSE event has been received).
2608
+ * The returned stream still contains the `watching` event so consumers
2609
+ * using `parseSSEStream` will see the full event sequence.
2610
+ *
2611
+ * @param request - Watch request with path and options
2612
+ */
2613
+ async watch(request) {
2614
+ try {
2615
+ const stream = await this.doStreamFetch("/api/watch", request);
2616
+ const readyStream = await this.waitForReadiness(stream);
2617
+ this.logSuccess("File watch started", request.path);
2618
+ return readyStream;
2619
+ } catch (error) {
2620
+ this.logError("watch", error);
2621
+ throw error;
2622
+ }
2623
+ }
2624
+ /**
2625
+ * Read SSE chunks until the `watching` event appears, then return a
2626
+ * wrapper stream that replays the buffered chunks followed by the
2627
+ * remaining original stream data.
2628
+ */
2629
+ async waitForReadiness(stream) {
2630
+ const reader = stream.getReader();
2631
+ const bufferedChunks = [];
2632
+ const decoder = new TextDecoder();
2633
+ let buffer = "";
2634
+ let currentEvent = { data: [] };
2635
+ let watcherReady = false;
2636
+ const processEventData = (eventData) => {
2637
+ let event;
2638
+ try {
2639
+ event = JSON.parse(eventData);
2640
+ } catch {
2641
+ return;
2642
+ }
2643
+ if (event.type === "watching") watcherReady = true;
2644
+ if (event.type === "error") throw new Error(event.error || "Watch failed to establish");
2645
+ };
2646
+ try {
2647
+ while (!watcherReady) {
2648
+ const { done, value } = await reader.read();
2649
+ if (done) {
2650
+ const finalParsed = parseSSEFrames(`${buffer}\n\n`, currentEvent);
2651
+ for (const frame of finalParsed.events) {
2652
+ processEventData(frame.data);
2653
+ if (watcherReady) break;
2654
+ }
2655
+ if (watcherReady) break;
2656
+ throw new Error("Watch stream ended before watcher was established");
2657
+ }
2658
+ bufferedChunks.push(value);
2659
+ buffer += decoder.decode(value, { stream: true });
2660
+ const parsed = parseSSEFrames(buffer, currentEvent);
2661
+ buffer = parsed.remaining;
2662
+ currentEvent = parsed.currentEvent;
2663
+ for (const frame of parsed.events) {
2664
+ processEventData(frame.data);
2665
+ if (watcherReady) break;
2666
+ }
2667
+ }
2668
+ } catch (error) {
2669
+ reader.cancel().catch(() => {});
2670
+ throw error;
2671
+ }
2672
+ let replayIndex = 0;
2673
+ return new ReadableStream({
2674
+ pull(controller) {
2675
+ if (replayIndex < bufferedChunks.length) {
2676
+ controller.enqueue(bufferedChunks[replayIndex++]);
2677
+ return;
2678
+ }
2679
+ return reader.read().then(({ done: d, value: v }) => {
2680
+ if (d) {
2681
+ controller.close();
2682
+ return;
2683
+ }
2684
+ controller.enqueue(v);
2685
+ });
2686
+ },
2687
+ cancel() {
2688
+ return reader.cancel();
2689
+ }
2690
+ });
2691
+ }
2692
+ };
2693
+
2142
2694
  //#endregion
2143
2695
  //#region src/clients/sandbox-client.ts
2144
2696
  /**
@@ -2160,6 +2712,8 @@ var SandboxClient = class {
2160
2712
  git;
2161
2713
  interpreter;
2162
2714
  utils;
2715
+ desktop;
2716
+ watch;
2163
2717
  transport = null;
2164
2718
  constructor(options) {
2165
2719
  if (options.transportMode === "websocket" && options.wsUrl) this.transport = createTransport({
@@ -2183,6 +2737,8 @@ var SandboxClient = class {
2183
2737
  this.git = new GitClient(clientOptions);
2184
2738
  this.interpreter = new InterpreterClient(clientOptions);
2185
2739
  this.utils = new UtilityClient(clientOptions);
2740
+ this.desktop = new DesktopClient(clientOptions);
2741
+ this.watch = new WatchClient(clientOptions);
2186
2742
  }
2187
2743
  /**
2188
2744
  * Get the current transport mode
@@ -2490,32 +3046,44 @@ async function* parseSSEStream(stream, signal) {
2490
3046
  const reader = stream.getReader();
2491
3047
  const decoder = new TextDecoder();
2492
3048
  let buffer = "";
3049
+ let currentEvent = { data: [] };
3050
+ let isAborted = signal?.aborted ?? false;
3051
+ const emitEvent = (data) => {
3052
+ if (data === "[DONE]" || data.trim() === "") return;
3053
+ try {
3054
+ return JSON.parse(data);
3055
+ } catch {
3056
+ return;
3057
+ }
3058
+ };
3059
+ const onAbort = () => {
3060
+ isAborted = true;
3061
+ reader.cancel().catch(() => {});
3062
+ };
3063
+ if (signal && !signal.aborted) signal.addEventListener("abort", onAbort);
2493
3064
  try {
2494
3065
  while (true) {
2495
- if (signal?.aborted) throw new Error("Operation was aborted");
3066
+ if (isAborted) throw new Error("Operation was aborted");
2496
3067
  const { done, value } = await reader.read();
3068
+ if (isAborted) throw new Error("Operation was aborted");
2497
3069
  if (done) break;
2498
3070
  buffer += decoder.decode(value, { stream: true });
2499
- const lines = buffer.split("\n");
2500
- buffer = lines.pop() || "";
2501
- for (const line of lines) {
2502
- if (line.trim() === "") continue;
2503
- if (line.startsWith("data: ")) {
2504
- const data = line.substring(6);
2505
- if (data === "[DONE]" || data.trim() === "") continue;
2506
- try {
2507
- yield JSON.parse(data);
2508
- } catch {}
2509
- }
3071
+ const parsed = parseSSEFrames(buffer, currentEvent);
3072
+ buffer = parsed.remaining;
3073
+ currentEvent = parsed.currentEvent;
3074
+ for (const frame of parsed.events) {
3075
+ const event = emitEvent(frame.data);
3076
+ if (event !== void 0) yield event;
2510
3077
  }
2511
3078
  }
2512
- if (buffer.trim() && buffer.startsWith("data: ")) {
2513
- const data = buffer.substring(6);
2514
- if (data !== "[DONE]" && data.trim()) try {
2515
- yield JSON.parse(data);
2516
- } catch {}
3079
+ if (isAborted) throw new Error("Operation was aborted");
3080
+ const finalParsed = parseSSEFrames(`${buffer}\n\n`, currentEvent);
3081
+ for (const frame of finalParsed.events) {
3082
+ const event = emitEvent(frame.data);
3083
+ if (event !== void 0) yield event;
2517
3084
  }
2518
3085
  } finally {
3086
+ if (signal) signal.removeEventListener("abort", onAbort);
2519
3087
  try {
2520
3088
  await reader.cancel();
2521
3089
  } catch {}
@@ -2712,7 +3280,7 @@ function buildS3fsSource(bucket, prefix) {
2712
3280
  * This file is auto-updated by .github/changeset-version.ts during releases
2713
3281
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
2714
3282
  */
2715
- const SDK_VERSION = "0.7.8";
3283
+ const SDK_VERSION = "0.7.10";
2716
3284
 
2717
3285
  //#endregion
2718
3286
  //#region src/sandbox.ts
@@ -2737,7 +3305,11 @@ function getSandbox(ns, id, options) {
2737
3305
  return enhanceSession(stub, await stub.getSession(sessionId));
2738
3306
  },
2739
3307
  terminal: (request, opts) => proxyTerminal(stub, defaultSessionId, request, opts),
2740
- wsConnect: connect(stub)
3308
+ wsConnect: connect(stub),
3309
+ desktop: new Proxy({}, { get(_, method) {
3310
+ if (typeof method !== "string" || method === "then") return void 0;
3311
+ return (...args) => stub.callDesktop(method, args);
3312
+ } })
2741
3313
  };
2742
3314
  return new Proxy(stub, { get(target, prop) {
2743
3315
  if (typeof prop === "string" && prop in enhancedMethods) return enhancedMethods[prop];
@@ -2811,6 +3383,56 @@ var Sandbox = class Sandbox extends Container {
2811
3383
  */
2812
3384
  containerTimeouts = { ...this.DEFAULT_CONTAINER_TIMEOUTS };
2813
3385
  /**
3386
+ * Desktop environment operations.
3387
+ * Within the DO, this getter provides direct access to DesktopClient.
3388
+ * Over RPC, the getSandbox() proxy intercepts this property and routes
3389
+ * calls through callDesktop() instead.
3390
+ */
3391
+ get desktop() {
3392
+ return this.client.desktop;
3393
+ }
3394
+ /**
3395
+ * Allowed desktop methods — derived from the Desktop interface.
3396
+ * Restricts callDesktop() to a known set of operations.
3397
+ */
3398
+ static DESKTOP_METHODS = new Set([
3399
+ "start",
3400
+ "stop",
3401
+ "status",
3402
+ "screenshot",
3403
+ "screenshotRegion",
3404
+ "click",
3405
+ "doubleClick",
3406
+ "tripleClick",
3407
+ "rightClick",
3408
+ "middleClick",
3409
+ "mouseDown",
3410
+ "mouseUp",
3411
+ "moveMouse",
3412
+ "drag",
3413
+ "scroll",
3414
+ "getCursorPosition",
3415
+ "type",
3416
+ "press",
3417
+ "keyDown",
3418
+ "keyUp",
3419
+ "getScreenSize",
3420
+ "getProcessStatus"
3421
+ ]);
3422
+ /**
3423
+ * Dispatch method for desktop operations.
3424
+ * Called by the client-side proxy created in getSandbox() to provide
3425
+ * the `sandbox.desktop.status()` API without relying on RPC pipelining
3426
+ * through property getters.
3427
+ */
3428
+ async callDesktop(method, args) {
3429
+ if (!Sandbox.DESKTOP_METHODS.has(method)) throw new Error(`Unknown desktop method: ${method}`);
3430
+ const client = this.client.desktop;
3431
+ const fn = client[method];
3432
+ if (typeof fn !== "function") throw new Error(`Unknown desktop method: ${method}`);
3433
+ return fn.apply(client, args);
3434
+ }
3435
+ /**
2814
3436
  * Create a SandboxClient with current transport settings
2815
3437
  */
2816
3438
  createSandboxClient() {
@@ -3080,6 +3702,9 @@ var Sandbox = class Sandbox extends Container {
3080
3702
  */
3081
3703
  async destroy() {
3082
3704
  this.logger.info("Destroying sandbox container");
3705
+ if (this.ctx.container?.running) try {
3706
+ await this.client.desktop.stop();
3707
+ } catch {}
3083
3708
  this.client.disconnect();
3084
3709
  for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
3085
3710
  if (mountInfo.mounted) try {
@@ -3839,6 +4464,63 @@ var Sandbox = class Sandbox extends Container {
3839
4464
  return this.client.files.exists(path, session);
3840
4465
  }
3841
4466
  /**
4467
+ * Get the noVNC preview URL for browser-based desktop viewing.
4468
+ * Confirms desktop is active, then uses exposePort() to generate
4469
+ * a token-authenticated preview URL for the noVNC port (6080).
4470
+ *
4471
+ * @param hostname - The custom domain hostname for preview URLs
4472
+ * (e.g., 'preview.example.com'). Required because preview URLs
4473
+ * use subdomain patterns that .workers.dev doesn't support.
4474
+ * @param options - Optional settings
4475
+ * @param options.token - Reuse an existing token instead of generating a new one
4476
+ * @returns The authenticated noVNC preview URL
4477
+ */
4478
+ async getDesktopStreamUrl(hostname, options) {
4479
+ if ((await this.client.desktop.status()).status === "inactive") throw new Error("Desktop is not running. Call sandbox.desktop.start() first.");
4480
+ let url;
4481
+ try {
4482
+ url = (await this.exposePort(6080, {
4483
+ hostname,
4484
+ token: options?.token
4485
+ })).url;
4486
+ } catch {
4487
+ const existingToken = (await this.ctx.storage.get("portTokens") || {})["6080"];
4488
+ if (existingToken && this.sandboxName) url = this.constructPreviewUrl(6080, this.sandboxName, hostname, existingToken);
4489
+ else throw new Error("Failed to get desktop stream URL: port 6080 could not be exposed and no existing token found.");
4490
+ }
4491
+ try {
4492
+ await this.waitForPort({
4493
+ portToCheck: 6080,
4494
+ retries: 30,
4495
+ waitInterval: 500
4496
+ });
4497
+ } catch {}
4498
+ return { url };
4499
+ }
4500
+ /**
4501
+ * Watch a directory for file system changes using native inotify.
4502
+ *
4503
+ * The returned promise resolves only after the watcher is established on the
4504
+ * filesystem, so callers can immediately perform actions that depend on the
4505
+ * watch being active. The returned stream contains the full event sequence
4506
+ * starting with the `watching` event.
4507
+ *
4508
+ * Consume the stream with `parseSSEStream<FileWatchSSEEvent>(stream)`.
4509
+ *
4510
+ * @param path - Path to watch (absolute or relative to /workspace)
4511
+ * @param options - Watch options
4512
+ */
4513
+ async watch(path, options = {}) {
4514
+ const sessionId = options.sessionId ?? await this.ensureDefaultSession();
4515
+ return this.client.watch.watch({
4516
+ path,
4517
+ recursive: options.recursive,
4518
+ include: options.include,
4519
+ exclude: options.exclude,
4520
+ sessionId
4521
+ });
4522
+ }
4523
+ /**
3842
4524
  * Expose a port and get a preview URL for accessing services running in the sandbox
3843
4525
  *
3844
4526
  * @param port - Port number to expose (1024-65535)
@@ -4048,6 +4730,10 @@ var Sandbox = class Sandbox extends Container {
4048
4730
  sessionId
4049
4731
  }),
4050
4732
  readFileStream: (path) => this.readFileStream(path, { sessionId }),
4733
+ watch: (path, options) => this.watch(path, {
4734
+ ...options,
4735
+ sessionId
4736
+ }),
4051
4737
  mkdir: (path, options) => this.mkdir(path, {
4052
4738
  ...options,
4053
4739
  sessionId
@@ -4586,20 +5272,23 @@ async function* parseSSE(stream) {
4586
5272
  const reader = stream.getReader();
4587
5273
  const decoder = new TextDecoder();
4588
5274
  let buffer = "";
5275
+ let currentEvent = { data: [] };
4589
5276
  try {
4590
5277
  while (true) {
4591
5278
  const { done, value } = await reader.read();
4592
5279
  if (done) break;
4593
5280
  buffer += decoder.decode(value, { stream: true });
4594
- const lines = buffer.split("\n");
4595
- buffer = lines.pop() || "";
4596
- for (const line of lines) if (line.startsWith("data: ")) {
4597
- const data = line.slice(6);
4598
- try {
4599
- yield JSON.parse(data);
4600
- } catch {}
4601
- }
5281
+ const parsed = parseSSEFrames(buffer, currentEvent);
5282
+ buffer = parsed.remaining;
5283
+ currentEvent = parsed.currentEvent;
5284
+ for (const frame of parsed.events) try {
5285
+ yield JSON.parse(frame.data);
5286
+ } catch {}
4602
5287
  }
5288
+ const finalParsed = parseSSEFrames(`${buffer}\n\n`, currentEvent);
5289
+ for (const frame of finalParsed.events) try {
5290
+ yield JSON.parse(frame.data);
5291
+ } catch {}
4603
5292
  } finally {
4604
5293
  try {
4605
5294
  await reader.cancel();
@@ -4697,5 +5386,5 @@ async function collectFile(stream) {
4697
5386
  }
4698
5387
 
4699
5388
  //#endregion
4700
- export { BackupClient, BackupCreateError, BackupExpiredError, BackupNotFoundError, BackupRestoreError, BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidBackupConfigError, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyTerminal, proxyToSandbox, responseToAsyncIterable, streamFile };
5389
+ export { BackupClient, BackupCreateError, BackupExpiredError, BackupNotFoundError, BackupRestoreError, BucketMountError, CodeInterpreter, CommandClient, DesktopClient, DesktopInvalidCoordinatesError, DesktopInvalidOptionsError, DesktopNotStartedError, DesktopProcessCrashedError, DesktopStartFailedError, DesktopUnavailableError, FileClient, GitClient, InvalidBackupConfigError, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyTerminal, proxyToSandbox, responseToAsyncIterable, streamFile };
4701
5390
  //# sourceMappingURL=index.js.map