withruntime 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
@@ -12,6 +12,14 @@ export function connectionOrigins(env) {
12
12
  auth: apiOrigin(env.RUNTIME_AUTH_URL ?? "https://withruntime.com"),
13
13
  };
14
14
  }
15
+ /** Browser-issued production credentials must only reach the production API.
16
+ * CLI and MCP share this boundary, including resumed device requests. */
17
+ export function checkBrowserOrigins(origins) {
18
+ const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
19
+ if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
20
+ (local(origins.auth) && local(origins.api))))
21
+ throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
22
+ }
15
23
  function defaultDirectory(env) {
16
24
  const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
17
25
  if (!isAbsolute(root))
@@ -93,8 +101,75 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
93
101
  throw error;
94
102
  });
95
103
  }
104
+ const lockFile = join(directory, `${name}.lock`);
105
+ /** ARCHITECTURE.md section 3.9: concurrent CLI processes keep each account's
106
+ * connection. Serialize the entire read/modify/write, not only each rename.
107
+ * Never steal a lock: stale cleanup can otherwise delete a new live lock. */
108
+ async function updateAccounts(update) {
109
+ await privateDirectory(true);
110
+ const until = performance.now() + 10_000;
111
+ let handle;
112
+ for (;;) {
113
+ try {
114
+ handle = await open(lockFile, "wx", 0o600);
115
+ break;
116
+ }
117
+ catch (error) {
118
+ if (error.code !== "EEXIST")
119
+ throw error;
120
+ }
121
+ const text = await readPrivate(lockFile, 1024, "credential update lock");
122
+ let pid;
123
+ try {
124
+ pid = JSON.parse(text ?? "null")?.pid;
125
+ }
126
+ catch {
127
+ // The holder may not have finished writing its process id yet.
128
+ }
129
+ if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
130
+ try {
131
+ process.kill(pid, 0);
132
+ }
133
+ catch (error) {
134
+ if (error.code === "ESRCH")
135
+ throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
136
+ }
137
+ }
138
+ if (performance.now() >= until)
139
+ throw new Error(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`);
140
+ await new Promise((resolve) => setTimeout(resolve, 25));
141
+ }
142
+ try {
143
+ await handle.writeFile(JSON.stringify({ pid: process.pid }));
144
+ return await update();
145
+ }
146
+ finally {
147
+ try {
148
+ await handle.close();
149
+ }
150
+ finally {
151
+ await unlink(lockFile);
152
+ }
153
+ }
154
+ }
96
155
  const pendingFile = join(directory, `${name}.pending.json`);
97
156
  const accountsFile = join(directory, `${name}.accounts.json`);
157
+ /** Active and archived connections must satisfy the same contract: switching
158
+ * accounts cannot publish a credential the next command refuses to read. */
159
+ function validConnection(value) {
160
+ if (value === null || typeof value !== "object")
161
+ return false;
162
+ const saved = value;
163
+ return (saved.version === 1 &&
164
+ saved.apiOrigin === origins.api &&
165
+ saved.authOrigin === origins.auth &&
166
+ typeof saved.key === "string" &&
167
+ /^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) &&
168
+ typeof saved.connectionId === "string" &&
169
+ typeof saved.orgId === "string" &&
170
+ typeof saved.agentName === "string" &&
171
+ (saved.orgName === undefined || typeof saved.orgName === "string"));
172
+ }
98
173
  return {
99
174
  async read() {
100
175
  const text = await readPrivate(file, 4096, "saved connection");
@@ -107,15 +182,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
107
182
  catch {
108
183
  throw new Error("Runtime's saved connection is invalid. Connect again.");
109
184
  }
110
- if (saved.version !== 1 ||
111
- saved.apiOrigin !== origins.api ||
112
- saved.authOrigin !== origins.auth ||
113
- typeof saved.key !== "string" ||
114
- !/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) ||
115
- typeof saved.connectionId !== "string" ||
116
- typeof saved.orgId !== "string" ||
117
- typeof saved.agentName !== "string" ||
118
- (saved.orgName !== undefined && typeof saved.orgName !== "string"))
185
+ if (!validConnection(saved))
119
186
  throw new Error("Runtime's saved connection is invalid. Connect again.");
120
187
  return saved;
121
188
  },
@@ -125,12 +192,14 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
125
192
  async save(connection) {
126
193
  if (connection.apiOrigin !== origins.api || connection.authOrigin !== origins.auth)
127
194
  throw new Error("Connection origins do not match.");
128
- const previous = await this.read().catch(() => null);
129
- if (previous && previous.orgId !== connection.orgId) {
130
- const others = (await this.others()).filter((one) => one.orgId !== previous.orgId && one.orgId !== connection.orgId);
131
- await writePrivate(accountsFile, JSON.stringify([previous, ...others].slice(0, SAVED_ACCOUNTS)));
132
- }
133
- await writePrivate(file, JSON.stringify(connection));
195
+ return updateAccounts(async () => {
196
+ const previous = await this.read().catch(() => null);
197
+ if (previous && previous.orgId !== connection.orgId) {
198
+ const others = (await this.others()).filter((one) => one.orgId !== previous.orgId && one.orgId !== connection.orgId);
199
+ await writePrivate(accountsFile, JSON.stringify([previous, ...others].slice(0, SAVED_ACCOUNTS)));
200
+ }
201
+ await writePrivate(file, JSON.stringify(connection));
202
+ });
134
203
  },
135
204
  /** The other accounts this machine has connected, newest first. */
136
205
  async others() {
@@ -145,15 +214,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
145
214
  return [];
146
215
  try {
147
216
  const list = JSON.parse(text);
148
- return Array.isArray(list)
149
- ? list.filter((one) => one &&
150
- one.version === 1 &&
151
- one.apiOrigin === origins.api &&
152
- one.authOrigin === origins.auth &&
153
- typeof one.key === "string" &&
154
- /^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(one.key) &&
155
- typeof one.orgId === "string")
156
- : [];
217
+ return Array.isArray(list) ? list.filter(validConnection) : [];
157
218
  }
158
219
  catch {
159
220
  return [];
@@ -162,21 +223,29 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
162
223
  /** Use another saved account's connection, by account id or name. The one
163
224
  * in use moves to the saved accounts. Null when none matches. */
164
225
  async use(which) {
165
- const others = await this.others();
166
- const wanted = which.trim().toLowerCase();
167
- const chosen = others.find((one) => one.orgId === which) ??
168
- others.find((one) => one.orgName?.toLowerCase() === wanted) ??
169
- null;
170
- if (!chosen)
171
- return null;
172
- const current = await this.read().catch(() => null);
173
- const rest = others.filter((one) => one.orgId !== chosen.orgId);
174
- await writePrivate(accountsFile, JSON.stringify((current ? [current, ...rest] : rest).slice(0, SAVED_ACCOUNTS)));
175
- await writePrivate(file, JSON.stringify(chosen));
176
- return chosen;
226
+ return updateAccounts(async () => {
227
+ const others = await this.others();
228
+ const wanted = which.trim().toLowerCase();
229
+ const chosen = others.find((one) => one.orgId === which) ??
230
+ others.find((one) => one.orgName?.toLowerCase() === wanted) ??
231
+ null;
232
+ if (!chosen)
233
+ return null;
234
+ const current = await this.read().catch(() => null);
235
+ const rest = others.filter((one) => one.orgId !== chosen.orgId);
236
+ await writePrivate(accountsFile, JSON.stringify((current ? [current, ...rest] : rest).slice(0, SAVED_ACCOUNTS)));
237
+ await writePrivate(file, JSON.stringify(chosen));
238
+ return chosen;
239
+ });
177
240
  },
178
- async remove() {
179
- await removePrivate(file);
241
+ /** Forget the expected credential after a remote revoke or refusal. A
242
+ * concurrent login may have saved another connection during that request. */
243
+ async remove(expectedKey) {
244
+ await updateAccounts(async () => {
245
+ if (expectedKey !== undefined && (await this.read())?.key !== expectedKey)
246
+ return;
247
+ await removePrivate(file);
248
+ });
180
249
  },
181
250
  /** A browser approval that was started and not yet answered, so a command
182
251
  * that was stopped while it waited picks up the same link and code rather
@@ -0,0 +1,162 @@
1
+ import type { RuntimeSandbox } from "./client.js";
2
+ type Live = () => Promise<RuntimeSandbox>;
3
+ export interface ScreenshotRegion {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface ScreenshotOptions {
10
+ showCursor?: boolean;
11
+ format?: string;
12
+ quality?: number;
13
+ scale?: number;
14
+ }
15
+ export interface Position {
16
+ x?: number;
17
+ y?: number;
18
+ }
19
+ export interface ScreenshotResponse {
20
+ cursorPosition?: Position;
21
+ /** The image, base64. */
22
+ screenshot?: string;
23
+ sizeBytes?: number;
24
+ }
25
+ export interface DisplayInfo {
26
+ height?: number;
27
+ id?: number;
28
+ isActive?: boolean;
29
+ width?: number;
30
+ x?: number;
31
+ y?: number;
32
+ }
33
+ export interface WindowInfo {
34
+ height?: number;
35
+ id?: number;
36
+ isActive?: boolean;
37
+ title?: string;
38
+ width?: number;
39
+ x?: number;
40
+ y?: number;
41
+ }
42
+ export interface ProcessStatus {
43
+ autoRestart?: boolean;
44
+ pid?: number;
45
+ priority?: number;
46
+ running?: boolean;
47
+ }
48
+ export interface ComputerUseStartResponse {
49
+ message?: string;
50
+ status?: Record<string, ProcessStatus>;
51
+ }
52
+ export type ComputerUseStopResponse = ComputerUseStartResponse;
53
+ export interface ComputerUseStatusResponse {
54
+ status?: string;
55
+ }
56
+ export interface Recording {
57
+ durationSeconds?: number;
58
+ endTime?: string;
59
+ fileName: string;
60
+ filePath: string;
61
+ id: string;
62
+ sizeBytes?: number;
63
+ startTime: string;
64
+ status: string;
65
+ }
66
+ export interface ListRecordingsResponse {
67
+ recordings: Recording[];
68
+ }
69
+ /** `sandbox.computerUse.mouse`. */
70
+ export declare class Mouse {
71
+ #private;
72
+ /** Use sandbox.computerUse.mouse. */
73
+ constructor(live: Live);
74
+ getPosition(): Promise<Position>;
75
+ move(x: number, y: number): Promise<Position>;
76
+ click(x: number, y: number, button?: string, double?: boolean): Promise<Position>;
77
+ drag(startX: number, startY: number, endX: number, endY: number, button?: string): Promise<Position>;
78
+ /** `amount` wheel clicks up or down at (x, y). */
79
+ scroll(x: number, y: number, direction: "up" | "down", amount?: number): Promise<boolean>;
80
+ }
81
+ /** `sandbox.computerUse.keyboard`. Key names are Daytona's (enter, esc,
82
+ * ctrl, cmd, pageup, f5) or xdotool's (Return, Escape, Prior). */
83
+ export declare class Keyboard {
84
+ #private;
85
+ /** Use sandbox.computerUse.keyboard. */
86
+ constructor(live: Live);
87
+ /** Types `text`, `delay` milliseconds between keys. */
88
+ type(text: string, delay?: number): Promise<void>;
89
+ press(key: string, modifiers?: string[]): Promise<void>;
90
+ /** A combination such as "ctrl+c" or "ctrl+shift+t". */
91
+ hotkey(keys: string): Promise<void>;
92
+ }
93
+ /** `sandbox.computerUse.screenshot`: the whole screen, PNG or JPEG. */
94
+ export declare class Screenshot {
95
+ #private;
96
+ /** Use sandbox.computerUse.screenshot. */
97
+ constructor(live: Live);
98
+ takeFullScreen(showCursor?: boolean): Promise<ScreenshotResponse>;
99
+ takeRegion(_region: ScreenshotRegion, _showCursor?: boolean): Promise<ScreenshotResponse>;
100
+ takeCompressed(options?: ScreenshotOptions): Promise<ScreenshotResponse>;
101
+ takeCompressedRegion(_region: ScreenshotRegion, _options?: ScreenshotOptions): Promise<ScreenshotResponse>;
102
+ }
103
+ /** `sandbox.computerUse.display`. */
104
+ export declare class Display {
105
+ #private;
106
+ /** Use sandbox.computerUse.display. */
107
+ constructor(live: Live);
108
+ /** The one screen, its size read from a screenshot. */
109
+ getInfo(): Promise<{
110
+ displays?: DisplayInfo[];
111
+ }>;
112
+ getWindows(): Promise<{
113
+ windows?: WindowInfo[];
114
+ }>;
115
+ }
116
+ /** `sandbox.computerUse.recording`: screen recordings to MP4. */
117
+ export declare class RecordingService {
118
+ #private;
119
+ /** Use sandbox.computerUse.recording. */
120
+ constructor(live: Live);
121
+ /** Starts recording. Runtime names the file itself; `label` is not used. */
122
+ start(_label?: string): Promise<Recording>;
123
+ stop(id: string): Promise<Recording>;
124
+ list(): Promise<ListRecordingsResponse>;
125
+ get(id: string): Promise<Recording>;
126
+ delete(id: string): Promise<void>;
127
+ /** Saves the MP4 to `localPath`, making its directory. */
128
+ download(id: string, localPath: string): Promise<void>;
129
+ }
130
+ /** `sandbox.computerUse.accessibility`: Runtime's desktop has no
131
+ * accessibility tree; every method throws NotSupportedError. */
132
+ export declare class Accessibility {
133
+ #private;
134
+ getTree(_options?: unknown): Promise<never>;
135
+ findNodes(_options?: unknown): Promise<never>;
136
+ focusNode(_id: string): Promise<never>;
137
+ invokeNode(_id: string, _action?: string): Promise<never>;
138
+ setNodeValue(_id: string, _value: string): Promise<never>;
139
+ }
140
+ /** `sandbox.computerUse`: a Linux desktop driven like a person would. */
141
+ export declare class ComputerUse {
142
+ #private;
143
+ readonly mouse: Mouse;
144
+ readonly keyboard: Keyboard;
145
+ readonly screenshot: Screenshot;
146
+ readonly display: Display;
147
+ readonly recording: RecordingService;
148
+ readonly accessibility: Accessibility;
149
+ /** Use sandbox.computerUse. */
150
+ constructor(live: Live);
151
+ /** Starts the desktop. A sandbox's first start installs it, which takes a
152
+ * minute or two; this waits for that. */
153
+ start(): Promise<ComputerUseStartResponse>;
154
+ stop(): Promise<ComputerUseStopResponse>;
155
+ /** "active" while the desktop runs, else "inactive". */
156
+ getStatus(): Promise<ComputerUseStatusResponse>;
157
+ getProcessStatus(_processName: string): Promise<never>;
158
+ restartProcess(_processName: string): Promise<never>;
159
+ getProcessLogs(_processName: string): Promise<never>;
160
+ getProcessErrors(_processName: string): Promise<never>;
161
+ }
162
+ export {};
@@ -0,0 +1,324 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, dirname } from "node:path";
3
+ import { RuntimeError } from "../errors.js";
4
+ import { guard, NotSupportedError } from "./errors.js";
5
+ const ALTERNATIVE_STREAM = "Open the live desktop from `(await sandbox.withruntime.desktop.start()).streamUrl`.";
6
+ async function desktop(live) {
7
+ return (await live()).desktop;
8
+ }
9
+ /** `sandbox.computerUse.mouse`. */
10
+ export class Mouse {
11
+ #live;
12
+ /** Use sandbox.computerUse.mouse. */
13
+ constructor(live) {
14
+ this.#live = live;
15
+ }
16
+ async getPosition() {
17
+ const { x, y } = await guard("other", async () => (await desktop(this.#live)).cursor());
18
+ return { x, y };
19
+ }
20
+ async move(x, y) {
21
+ await guard("other", async () => (await desktop(this.#live)).move(x, y));
22
+ return { x, y };
23
+ }
24
+ async click(x, y, button = "left", double = false) {
25
+ await guard("other", async () => (await desktop(this.#live)).click(x, y, { button: buttonOf(button), double }));
26
+ return { x, y };
27
+ }
28
+ async drag(startX, startY, endX, endY, button = "left") {
29
+ const which = buttonOf(button);
30
+ await guard("other", async () => {
31
+ const screen = await desktop(this.#live);
32
+ if (which === "left")
33
+ return screen.drag([startX, startY], [endX, endY]);
34
+ await screen.move(startX, startY);
35
+ await screen.mouseDown(which);
36
+ await screen.move(endX, endY);
37
+ return screen.mouseUp(which);
38
+ });
39
+ return { x: endX, y: endY };
40
+ }
41
+ /** `amount` wheel clicks up or down at (x, y). */
42
+ async scroll(x, y, direction, amount = 1) {
43
+ if (direction !== "up" && direction !== "down")
44
+ throw new RangeError(`direction is "up" or "down", not ${JSON.stringify(direction)}.`);
45
+ await guard("other", async () => (await desktop(this.#live)).scroll(direction === "down" ? amount : -amount, { x, y }));
46
+ return true;
47
+ }
48
+ }
49
+ /** `sandbox.computerUse.keyboard`. Key names are Daytona's (enter, esc,
50
+ * ctrl, cmd, pageup, f5) or xdotool's (Return, Escape, Prior). */
51
+ export class Keyboard {
52
+ #live;
53
+ /** Use sandbox.computerUse.keyboard. */
54
+ constructor(live) {
55
+ this.#live = live;
56
+ }
57
+ /** Types `text`, `delay` milliseconds between keys. */
58
+ async type(text, delay) {
59
+ await guard("other", async () => (await desktop(this.#live)).type(text, delay === undefined ? {} : { delayMs: delay }));
60
+ }
61
+ async press(key, modifiers = []) {
62
+ await guard("other", async () => (await desktop(this.#live)).press([...modifiers, key].map(keyOf).join("+")));
63
+ }
64
+ /** A combination such as "ctrl+c" or "ctrl+shift+t". */
65
+ async hotkey(keys) {
66
+ await guard("other", async () => (await desktop(this.#live)).press(keys.split("+").map(keyOf).join("+")));
67
+ }
68
+ }
69
+ /** `sandbox.computerUse.screenshot`: the whole screen, PNG or JPEG. */
70
+ export class Screenshot {
71
+ #live;
72
+ /** Use sandbox.computerUse.screenshot. */
73
+ constructor(live) {
74
+ this.#live = live;
75
+ }
76
+ takeFullScreen(showCursor = false) {
77
+ return this.takeCompressed({ showCursor, format: "png" });
78
+ }
79
+ takeRegion(_region, _showCursor) {
80
+ return Promise.reject(region());
81
+ }
82
+ async takeCompressed(options = {}) {
83
+ if (options.showCursor)
84
+ throw new NotSupportedError("Drawing the cursor into a screenshot (showCursor)", "Take it without; sandbox.computerUse.mouse.getPosition() says where the cursor is.");
85
+ if (options.scale !== undefined && options.scale !== 1)
86
+ throw new NotSupportedError("Scaling a screenshot", "Take it at full size and scale it yourself, or start the desktop smaller: sandbox.withruntime.desktop.start({ width, height }).");
87
+ const format = (options.format ?? "png").toLowerCase();
88
+ if (format !== "png" && format !== "jpeg" && format !== "jpg")
89
+ throw new NotSupportedError(`Screenshots as ${format}`, 'Use format "png" or "jpeg".');
90
+ const bytes = await guard("other", async () => (await desktop(this.#live)).screenshot(format === "png"
91
+ ? {}
92
+ : {
93
+ format: "jpeg",
94
+ ...(options.quality === undefined ? {} : { quality: options.quality }),
95
+ }));
96
+ return { screenshot: Buffer.from(bytes).toString("base64"), sizeBytes: bytes.byteLength };
97
+ }
98
+ takeCompressedRegion(_region, _options) {
99
+ return Promise.reject(region());
100
+ }
101
+ }
102
+ /** `sandbox.computerUse.display`. */
103
+ export class Display {
104
+ #live;
105
+ /** Use sandbox.computerUse.display. */
106
+ constructor(live) {
107
+ this.#live = live;
108
+ }
109
+ /** The one screen, its size read from a screenshot. */
110
+ async getInfo() {
111
+ const png = await guard("other", async () => (await desktop(this.#live)).screenshot());
112
+ const { width, height } = pngSize(png);
113
+ return { displays: [{ id: 0, x: 0, y: 0, width, height, isActive: true }] };
114
+ }
115
+ async getWindows() {
116
+ const windows = await guard("other", async () => (await desktop(this.#live)).windows());
117
+ return {
118
+ windows: windows.map((one) => ({
119
+ id: Number(one.id),
120
+ title: one.title,
121
+ x: one.x,
122
+ y: one.y,
123
+ width: one.width,
124
+ height: one.height,
125
+ })),
126
+ };
127
+ }
128
+ }
129
+ /** `sandbox.computerUse.recording`: screen recordings to MP4. */
130
+ export class RecordingService {
131
+ #live;
132
+ /** Use sandbox.computerUse.recording. */
133
+ constructor(live) {
134
+ this.#live = live;
135
+ }
136
+ /** Starts recording. Runtime names the file itself; `label` is not used. */
137
+ async start(_label) {
138
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.start()));
139
+ }
140
+ async stop(id) {
141
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.stop(id)));
142
+ }
143
+ async list() {
144
+ const all = await guard("other", async () => (await desktop(this.#live)).recordings.list());
145
+ return { recordings: all.map(recordingOf) };
146
+ }
147
+ async get(id) {
148
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.get(id)));
149
+ }
150
+ async delete(id) {
151
+ await guard("other", async () => (await desktop(this.#live)).recordings.delete(id));
152
+ }
153
+ /** Saves the MP4 to `localPath`, making its directory. */
154
+ async download(id, localPath) {
155
+ const bytes = await guard("other", async () => (await desktop(this.#live)).recordings.download(id));
156
+ await mkdir(dirname(localPath), { recursive: true });
157
+ await writeFile(localPath, bytes);
158
+ }
159
+ }
160
+ /** `sandbox.computerUse.accessibility`: Runtime's desktop has no
161
+ * accessibility tree; every method throws NotSupportedError. */
162
+ export class Accessibility {
163
+ #refuse() {
164
+ return Promise.reject(new NotSupportedError("The desktop's accessibility tree", "Find things on screen with a screenshot (sandbox.computerUse.screenshot.takeFullScreen()) and act with the mouse and keyboard."));
165
+ }
166
+ getTree(_options) {
167
+ return this.#refuse();
168
+ }
169
+ findNodes(_options) {
170
+ return this.#refuse();
171
+ }
172
+ focusNode(_id) {
173
+ return this.#refuse();
174
+ }
175
+ invokeNode(_id, _action) {
176
+ return this.#refuse();
177
+ }
178
+ setNodeValue(_id, _value) {
179
+ return this.#refuse();
180
+ }
181
+ }
182
+ /** `sandbox.computerUse`: a Linux desktop driven like a person would. */
183
+ export class ComputerUse {
184
+ mouse;
185
+ keyboard;
186
+ screenshot;
187
+ display;
188
+ recording;
189
+ accessibility;
190
+ #live;
191
+ /** Use sandbox.computerUse. */
192
+ constructor(live) {
193
+ this.#live = live;
194
+ this.mouse = new Mouse(live);
195
+ this.keyboard = new Keyboard(live);
196
+ this.screenshot = new Screenshot(live);
197
+ this.display = new Display(live);
198
+ this.recording = new RecordingService(live);
199
+ this.accessibility = new Accessibility();
200
+ }
201
+ /** Starts the desktop. A sandbox's first start installs it, which takes a
202
+ * minute or two; this waits for that. */
203
+ async start() {
204
+ await guard("other", async () => (await desktop(this.#live)).start());
205
+ return {
206
+ message: "Computer use processes started successfully",
207
+ status: { desktop: { running: true } },
208
+ };
209
+ }
210
+ async stop() {
211
+ await guard("other", async () => (await desktop(this.#live)).stop());
212
+ return {
213
+ message: "Computer use processes stopped successfully",
214
+ status: { desktop: { running: false } },
215
+ };
216
+ }
217
+ /** "active" while the desktop runs, else "inactive". */
218
+ async getStatus() {
219
+ try {
220
+ await (await desktop(this.#live)).cursor();
221
+ return { status: "active" };
222
+ }
223
+ catch (error) {
224
+ if (error instanceof RuntimeError &&
225
+ (error.code === "desktop_not_running" || error.code === "desktop_not_installed"))
226
+ return { status: "inactive" };
227
+ return guard("other", () => {
228
+ throw error;
229
+ });
230
+ }
231
+ }
232
+ getProcessStatus(_processName) {
233
+ return processes();
234
+ }
235
+ restartProcess(_processName) {
236
+ return processes();
237
+ }
238
+ getProcessLogs(_processName) {
239
+ return processes();
240
+ }
241
+ getProcessErrors(_processName) {
242
+ return processes();
243
+ }
244
+ }
245
+ function processes() {
246
+ return Promise.reject(new NotSupportedError("Managing the desktop's processes one by one", `Use computerUse.getStatus(), stop() and start(). ${ALTERNATIVE_STREAM}`));
247
+ }
248
+ function region() {
249
+ return new NotSupportedError("Screenshots of part of the screen", "Take the whole screen with takeFullScreen() or takeCompressed() and crop it yourself.");
250
+ }
251
+ function buttonOf(button) {
252
+ if (button === "left" || button === "middle" || button === "right")
253
+ return button;
254
+ throw new RangeError(`button is "left", "middle" or "right", not ${JSON.stringify(button)}.`);
255
+ }
256
+ /** Daytona's key names as xdotool's; anything else passes as given. */
257
+ const KEYS = {
258
+ enter: "Return",
259
+ return: "Return",
260
+ esc: "Escape",
261
+ escape: "Escape",
262
+ backspace: "BackSpace",
263
+ delete: "Delete",
264
+ del: "Delete",
265
+ tab: "Tab",
266
+ space: "space",
267
+ up: "Up",
268
+ down: "Down",
269
+ left: "Left",
270
+ right: "Right",
271
+ home: "Home",
272
+ end: "End",
273
+ pageup: "Prior",
274
+ pagedown: "Next",
275
+ insert: "Insert",
276
+ capslock: "Caps_Lock",
277
+ control: "ctrl",
278
+ ctrl: "ctrl",
279
+ shift: "shift",
280
+ alt: "alt",
281
+ cmd: "super",
282
+ command: "super",
283
+ meta: "super",
284
+ win: "super",
285
+ super: "super",
286
+ };
287
+ function keyOf(key) {
288
+ const name = key.trim();
289
+ const lower = name.toLowerCase();
290
+ if (KEYS[lower])
291
+ return KEYS[lower];
292
+ if (/^f([1-9]|1[0-2])$/.test(lower))
293
+ return lower.toUpperCase();
294
+ return name;
295
+ }
296
+ const STATUS = {
297
+ starting: "recording",
298
+ installing: "recording",
299
+ recording: "recording",
300
+ finished: "completed",
301
+ failed: "failed",
302
+ };
303
+ function recordingOf(recording) {
304
+ const start = recording.startedAt ?? null;
305
+ return {
306
+ id: recording.id,
307
+ fileName: basename(recording.path),
308
+ filePath: recording.path,
309
+ startTime: start === null ? "" : new Date(start).toISOString(),
310
+ ...(start !== null && recording.seconds !== undefined && STATUS[recording.state] !== "recording"
311
+ ? { endTime: new Date(start + recording.seconds * 1000).toISOString() }
312
+ : {}),
313
+ status: STATUS[recording.state] ?? recording.state,
314
+ sizeBytes: recording.bytes,
315
+ ...(recording.seconds === undefined ? {} : { durationSeconds: recording.seconds }),
316
+ };
317
+ }
318
+ /** Width and height from a PNG's header. */
319
+ function pngSize(png) {
320
+ const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
321
+ if (png.byteLength < 24 || view.getUint32(12) !== 0x49484452)
322
+ throw new RangeError("The desktop's screenshot was not a PNG.");
323
+ return { width: view.getUint32(16), height: view.getUint32(20) };
324
+ }