@sandboxrouter/sdk 0.1.0 → 0.3.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 (97) hide show
  1. package/README.md +271 -62
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +232 -3
  4. package/dist/catalog.js +569 -14
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +149 -12
  8. package/dist/computers.d.ts +55 -13
  9. package/dist/computers.js +132 -6
  10. package/dist/conformance.d.ts +46 -0
  11. package/dist/conformance.js +65 -0
  12. package/dist/cost.d.ts +67 -0
  13. package/dist/cost.js +153 -0
  14. package/dist/gc.d.ts +29 -0
  15. package/dist/gc.js +54 -0
  16. package/dist/index.d.ts +34 -2
  17. package/dist/index.js +34 -2
  18. package/dist/leaderboard.d.ts +6 -0
  19. package/dist/leaderboard.js +5 -4
  20. package/dist/ledger.d.ts +38 -0
  21. package/dist/ledger.js +89 -0
  22. package/dist/providers/agent37.d.ts +40 -0
  23. package/dist/providers/agent37.js +224 -0
  24. package/dist/providers/agentcore.d.ts +71 -0
  25. package/dist/providers/agentcore.js +446 -0
  26. package/dist/providers/alibaba.d.ts +18 -0
  27. package/dist/providers/alibaba.js +106 -0
  28. package/dist/providers/azure.d.ts +43 -0
  29. package/dist/providers/azure.js +330 -0
  30. package/dist/providers/beam.d.ts +11 -0
  31. package/dist/providers/beam.js +179 -0
  32. package/dist/providers/blaxel.d.ts +3 -1
  33. package/dist/providers/blaxel.js +32 -22
  34. package/dist/providers/boat.d.ts +25 -0
  35. package/dist/providers/boat.js +263 -0
  36. package/dist/providers/cloudflare.d.ts +30 -0
  37. package/dist/providers/cloudflare.js +152 -0
  38. package/dist/providers/codesandbox.d.ts +59 -0
  39. package/dist/providers/codesandbox.js +287 -0
  40. package/dist/providers/daytona.d.ts +25 -4
  41. package/dist/providers/daytona.js +449 -28
  42. package/dist/providers/dedalus.d.ts +12 -0
  43. package/dist/providers/dedalus.js +244 -0
  44. package/dist/providers/deno.d.ts +50 -0
  45. package/dist/providers/deno.js +170 -0
  46. package/dist/providers/docker.d.ts +17 -0
  47. package/dist/providers/docker.js +154 -0
  48. package/dist/providers/e2b.d.ts +70 -3
  49. package/dist/providers/e2b.js +376 -8
  50. package/dist/providers/exedev.d.ts +23 -0
  51. package/dist/providers/exedev.js +290 -0
  52. package/dist/providers/freestyle.d.ts +49 -0
  53. package/dist/providers/freestyle.js +261 -0
  54. package/dist/providers/hopx.d.ts +15 -0
  55. package/dist/providers/hopx.js +177 -0
  56. package/dist/providers/instabox.d.ts +76 -0
  57. package/dist/providers/instabox.js +360 -0
  58. package/dist/providers/koyeb.d.ts +71 -0
  59. package/dist/providers/koyeb.js +379 -0
  60. package/dist/providers/lambda.d.ts +51 -0
  61. package/dist/providers/lambda.js +467 -0
  62. package/dist/providers/langsmith.d.ts +16 -0
  63. package/dist/providers/langsmith.js +149 -0
  64. package/dist/providers/limrun.d.ts +77 -0
  65. package/dist/providers/limrun.js +626 -0
  66. package/dist/providers/maritime.d.ts +5 -1
  67. package/dist/providers/maritime.js +39 -8
  68. package/dist/providers/mobilerun.d.ts +44 -0
  69. package/dist/providers/mobilerun.js +417 -0
  70. package/dist/providers/modal.js +12 -4
  71. package/dist/providers/morph.js +12 -4
  72. package/dist/providers/northflank.d.ts +21 -0
  73. package/dist/providers/northflank.js +259 -0
  74. package/dist/providers/novita.d.ts +12 -0
  75. package/dist/providers/novita.js +81 -0
  76. package/dist/providers/openscale.d.ts +17 -0
  77. package/dist/providers/openscale.js +87 -0
  78. package/dist/providers/orgo.d.ts +45 -0
  79. package/dist/providers/orgo.js +414 -0
  80. package/dist/providers/sail.d.ts +32 -0
  81. package/dist/providers/sail.js +372 -0
  82. package/dist/providers/superserve.d.ts +25 -0
  83. package/dist/providers/superserve.js +337 -0
  84. package/dist/providers/tensorlake.js +17 -14
  85. package/dist/providers/upstash.d.ts +30 -0
  86. package/dist/providers/upstash.js +258 -0
  87. package/dist/providers/vercel.js +14 -3
  88. package/dist/providers/workspaces.d.ts +60 -0
  89. package/dist/providers/workspaces.js +662 -0
  90. package/dist/router.d.ts +36 -5
  91. package/dist/router.js +174 -25
  92. package/dist/server.d.ts +1 -1
  93. package/dist/server.js +22 -3
  94. package/dist/types.d.ts +18 -6
  95. package/dist/util.d.ts +28 -1
  96. package/dist/util.js +56 -7
  97. package/package.json +106 -17
@@ -1,13 +1,24 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { ProviderError } from "../types.js";
3
- import { optionalImport, toBytes, wrapCommand } from "../util.js";
4
- /** Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image. */
3
+ import { DEFAULT_EXEC_TIMEOUT_S, optionalImport, timeoutError, toBytes, wrapCommand } from "../util.js";
4
+ /**
5
+ * Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image.
6
+ *
7
+ * Computers are Linux container sandboxes from a default snapshot (they ship Xvfb, xfce4,
8
+ * x11vnc and noVNC) with the desktop started through `computerUse.start()`. Windows sandboxes
9
+ * exist but need a Tier 3 organization plus a support request, so they aren't routed here.
10
+ * Pass `computerSnapshot` (e.g. "daytona-medium") for a bigger desktop than the 1 vCPU / 1 GiB default.
11
+ * Docs: https://www.daytona.io/docs/en/computer-use
12
+ */
5
13
  export class DaytonaProvider {
6
14
  apiKey;
15
+ computerSnapshot;
7
16
  id = "daytona";
17
+ computerOs = ["linux"];
8
18
  client;
9
- constructor(apiKey = process.env.DAYTONA_API_KEY) {
19
+ constructor(apiKey = process.env.DAYTONA_API_KEY, computerSnapshot) {
10
20
  this.apiKey = apiKey;
21
+ this.computerSnapshot = computerSnapshot;
11
22
  }
12
23
  configured() {
13
24
  return !!this.apiKey;
@@ -21,29 +32,93 @@ export class DaytonaProvider {
21
32
  }
22
33
  async create(spec) {
23
34
  const d = await this.daytona();
35
+ // A per-create tag, so a sandbox Daytona made but couldn't start can be found and deleted: the
36
+ // SDK throws without handing it back, and it would otherwise sit in "error" state.
37
+ const tag = randomUUID();
24
38
  const base = {
25
39
  envVars: spec.env,
26
- labels: spec.metadata,
27
- // Minutes of inactivity before Daytona stops it; delete right after stop.
40
+ labels: { ...spec.metadata, "sandboxrouter-create": tag },
41
+ // Minutes of inactivity before Daytona stops it; deleted a minute after it stops. Not 0:
42
+ // that makes the sandbox ephemeral, which is fixed at creation and rules out pause.
28
43
  autoStopInterval: Math.max(1, Math.round(spec.timeoutMinutes ?? 30)),
29
- autoDeleteInterval: 0,
44
+ autoDeleteInterval: 1,
30
45
  };
31
- const sbx = spec.image?.daytona
32
- ? await d.create({
33
- ...base,
34
- image: spec.image.daytona,
35
- resources: { cpu: spec.vcpu, memory: spec.memoryGb },
36
- })
37
- : await d.create(base);
38
- return new DaytonaBox(sbx);
46
+ try {
47
+ const sbx = spec.image?.daytona
48
+ ? await d.create({
49
+ ...base,
50
+ image: spec.image.daytona,
51
+ resources: { cpu: spec.vcpu, memory: spec.memoryGb },
52
+ })
53
+ : await d.create(base);
54
+ return new DaytonaBox(sbx);
55
+ }
56
+ catch (err) {
57
+ await this.discardTagged(d, tag);
58
+ throw err;
59
+ }
60
+ }
61
+ async discardTagged(d, tag) {
62
+ try {
63
+ for await (const sbx of d.list({ labels: { "sandboxrouter-create": tag } }))
64
+ await sbx.delete().catch(() => { });
65
+ }
66
+ catch {
67
+ /* best effort: the create error is the one worth reporting */
68
+ }
39
69
  }
40
- /** Reattach; a paused (stopped) sandbox is started again. */
70
+ /**
71
+ * Reattach. Daytona stops, archives or pauses idle sandboxes on its own schedule (and pause()
72
+ * stops them), so whatever state the sandbox is in, it is brought back to started first.
73
+ */
41
74
  async connect(nativeId) {
42
75
  const sbx = await (await this.daytona()).get(nativeId);
43
- if (sbx.state === "stopped")
44
- await sbx.start();
76
+ await ensureStarted(sbx);
45
77
  return new DaytonaBox(sbx);
46
78
  }
79
+ /** A new Linux desktop. `externalUserId` is stored as a label only: every call creates a new sandbox. */
80
+ async createComputer(spec) {
81
+ const os = spec.os ?? "linux";
82
+ if (os !== "linux")
83
+ throw new ProviderError(this.id, `no ${os} computers (Windows needs a Tier 3 org and a support request)`);
84
+ const labels = { "sandboxrouter.kind": "computer" };
85
+ if (spec.externalUserId)
86
+ labels.externalUserId = spec.externalUserId;
87
+ const envVars = {};
88
+ if (spec.screen) {
89
+ const { width, height } = spec.screen;
90
+ // Daytona silently falls back to 1024x768 on a bad value, so refuse it here instead.
91
+ if (!(width >= 640 && width <= 7680 && height >= 480 && height <= 4320)) {
92
+ throw new ProviderError(this.id, `screen ${width}x${height} is outside 640-7680 x 480-4320`);
93
+ }
94
+ envVars.VNC_RESOLUTION = `${Math.round(width)}x${Math.round(height)}`;
95
+ }
96
+ const d = await this.daytona();
97
+ const sbx = await d.create({
98
+ name: spec.name,
99
+ snapshot: this.computerSnapshot,
100
+ labels,
101
+ envVars: Object.keys(envVars).length ? envVars : undefined,
102
+ autoStopInterval: COMPUTER_IDLE_MINUTES,
103
+ // Keep the disk when it stops (idle or sleep()), so files and logins survive.
104
+ autoDeleteInterval: -1,
105
+ });
106
+ try {
107
+ await startDesktop(sbx);
108
+ }
109
+ catch (err) {
110
+ await sbx.delete().catch(() => { });
111
+ throw new ProviderError(this.id, `desktop did not start: ${err.message}`);
112
+ }
113
+ return new DaytonaComputer(sbx);
114
+ }
115
+ /** Reattach; a stopped, paused or archived computer is started again, desktop included. */
116
+ async connectComputer(nativeId) {
117
+ const sbx = await (await this.daytona()).get(nativeId);
118
+ await ensureStarted(sbx);
119
+ await startDesktop(sbx);
120
+ return new DaytonaComputer(sbx);
121
+ }
47
122
  }
48
123
  class DaytonaBox {
49
124
  sbx;
@@ -52,22 +127,45 @@ class DaytonaBox {
52
127
  this.sbx = sbx;
53
128
  this.nativeId = sbx.id;
54
129
  }
130
+ /**
131
+ * executeCommand merges stderr into stdout, so stderr goes to a temp file that is read back.
132
+ * Daytona enforces the timeout itself: the command is stopped and the SDK throws a 408
133
+ * (DaytonaProcessExecutionTimeoutError). Streaming goes through a throwaway session instead,
134
+ * whose logs keep the two apart and whose deadline is enforced here: deleting the session ends
135
+ * the command. Sessions aren't used for every command because their logs end output that has no
136
+ * trailing newline with one (`printf hi` reads back as "hi\n").
137
+ */
55
138
  async exec(cmd, opts = {}) {
56
139
  if (opts.onStdout || opts.onStderr)
57
- return this.execStreaming(cmd, opts);
140
+ return this.execSession(cmd, opts);
58
141
  const t0 = Date.now();
142
+ const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
143
+ const errFile = `/tmp/sbr-stderr-${randomUUID()}`;
144
+ // cwd and env go in the command, so a missing cwd stops it from running, as in the session path.
145
+ const command = `{\n${wrapCommand(`(\n${cmd}\n)`, { cwd: opts.cwd, env: opts.env })}\n} 2>${errFile}`;
59
146
  try {
60
- const r = await this.sbx.process.executeCommand(cmd, opts.cwd, opts.env, opts.timeoutSeconds ?? 60);
61
- // Daytona merges stdout and stderr into `result` here.
62
- return { exitCode: r.exitCode, stdout: r.result, stderr: "", durationMs: Date.now() - t0 };
147
+ const r = await this.sbx.process.executeCommand(command, undefined, undefined, seconds);
148
+ let stderr = "";
149
+ try {
150
+ stderr = Buffer.from(await this.sbx.fs.downloadFile(errFile)).toString("utf8");
151
+ }
152
+ catch {
153
+ // No stderr file (e.g. the command removed /tmp): report none.
154
+ }
155
+ this.sbx.process.executeCommand(`rm -f ${errFile}`).catch(() => { });
156
+ return { exitCode: r.exitCode, stdout: r.result, stderr, durationMs: Date.now() - t0 };
63
157
  }
64
158
  catch (err) {
159
+ if (err?.name === "DaytonaProcessExecutionTimeoutError" || err?.constructor?.name === "DaytonaProcessExecutionTimeoutError" || err?.statusCode === 408) {
160
+ this.sbx.process.executeCommand(`rm -f ${errFile}`).catch(() => { });
161
+ throw timeoutError("daytona", seconds);
162
+ }
65
163
  throw new ProviderError("daytona", err?.message ?? String(err));
66
164
  }
67
165
  }
68
- /** Streaming goes through a throwaway session, which also keeps stdout and stderr apart. */
69
- async execStreaming(cmd, opts) {
166
+ async execSession(cmd, opts) {
70
167
  const t0 = Date.now();
168
+ const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
71
169
  const session = `sbr-${randomUUID()}`;
72
170
  const proc = this.sbx.process;
73
171
  let stdout = "";
@@ -75,7 +173,8 @@ class DaytonaBox {
75
173
  try {
76
174
  await proc.createSession(session);
77
175
  const { cmdId } = await proc.executeSessionCommand(session, {
78
- command: wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }),
176
+ // A subshell, so `exit` in the command ends it rather than the session's shell.
177
+ command: wrapCommand(`(\n${cmd}\n)`, { cwd: opts.cwd, env: opts.env }),
79
178
  runAsync: true,
80
179
  });
81
180
  const logs = proc.getSessionCommandLogs(session, cmdId, (c) => {
@@ -85,10 +184,11 @@ class DaytonaBox {
85
184
  stderr += c;
86
185
  opts.onStderr?.(c);
87
186
  });
88
- const limit = (opts.timeoutSeconds ?? 60) * 1000;
89
- const timedOut = await Promise.race([logs.then(() => false), sleep(limit).then(() => true)]);
90
- if (timedOut)
91
- throw new ProviderError("daytona", `command timed out after ${limit / 1000}s`);
187
+ const timedOut = await Promise.race([logs.then(() => false), sleep(seconds * 1000).then(() => true)]);
188
+ if (timedOut) {
189
+ logs.catch(() => { });
190
+ throw timeoutError("daytona", seconds);
191
+ }
92
192
  const done = await proc.getSessionCommand(session, cmdId);
93
193
  return { exitCode: done.exitCode ?? -1, stdout, stderr, durationMs: Date.now() - t0 };
94
194
  }
@@ -116,6 +216,327 @@ class DaytonaBox {
116
216
  await this.sbx.delete();
117
217
  }
118
218
  }
219
+ /** How long start() may take from stopped or paused (seconds). */
220
+ const START_TIMEOUT_S = 60;
221
+ /** Starting an archived sandbox restores its disk from storage first, which grows with its size. */
222
+ const ARCHIVED_START_TIMEOUT_S = 600;
223
+ /** States Daytona moves through on its own; the sandbox is polled until it leaves them. */
224
+ const SETTLING = new Set(["stopping", "pausing", "archiving", "snapshotting", "resizing", "forking"]);
225
+ /** States on the way to started: someone else already woke it, so wait for that rather than start twice. */
226
+ const WAKING = new Set(["starting", "resuming", "restoring", "pulling_snapshot", "creating"]);
227
+ /**
228
+ * Bring a sandbox to "started" from any state Daytona can leave it in: stopped and paused get a
229
+ * plain start, archived gets a long one, transitional states are waited out, and anything that
230
+ * cannot be started (destroyed, error, build failures) is an error up front.
231
+ */
232
+ async function ensureStarted(sbx) {
233
+ const deadline = Date.now() + ARCHIVED_START_TIMEOUT_S * 1000;
234
+ while (SETTLING.has(String(sbx.state))) {
235
+ if (Date.now() > deadline)
236
+ throw new ProviderError("daytona", `sandbox ${sbx.id} is still ${sbx.state} after ${ARCHIVED_START_TIMEOUT_S}s`);
237
+ await sleep(1000);
238
+ await sbx.refreshData();
239
+ }
240
+ const state = String(sbx.state);
241
+ if (state === "started")
242
+ return;
243
+ if (WAKING.has(state))
244
+ return sbx.waitUntilStarted(ARCHIVED_START_TIMEOUT_S);
245
+ if (state === "archived")
246
+ return sbx.start(ARCHIVED_START_TIMEOUT_S);
247
+ if (state === "stopped" || state === "paused")
248
+ return sbx.start(START_TIMEOUT_S);
249
+ throw new ProviderError("daytona", `sandbox ${sbx.id} is in state "${state}" and cannot be started`);
250
+ }
251
+ /** Minutes without activity before Daytona stops a computer (disk kept, billing stops). */
252
+ const COMPUTER_IDLE_MINUTES = 30;
253
+ /** Where the default snapshots serve noVNC. */
254
+ const NOVNC_PORT = 6080;
255
+ async function startDesktop(sbx) {
256
+ const cu = sbx.computerUse;
257
+ if ((await cu.getStatus().catch(() => undefined))?.status === "active")
258
+ return;
259
+ await cu.start();
260
+ for (let i = 0; i < 30; i++) {
261
+ if ((await cu.getStatus())?.status === "active")
262
+ return;
263
+ await sleep(500);
264
+ }
265
+ throw new Error("computer use processes are not active after 15s");
266
+ }
267
+ /** Width and height from a PNG's IHDR chunk. */
268
+ function pngSize(b64) {
269
+ const head = Buffer.from(b64.slice(0, 44), "base64");
270
+ if (head.length < 24 || head.readUInt32BE(0) !== 0x89504e47)
271
+ return {};
272
+ return { width: head.readUInt32BE(16), height: head.readUInt32BE(20) };
273
+ }
274
+ /** xdotool key names to Daytona's (which already lowercases and maps Return, control, meta, win, option). */
275
+ const KEY_ALIASES = {
276
+ return: "enter",
277
+ kp_enter: "num_enter",
278
+ esc: "escape",
279
+ back_space: "backspace",
280
+ prior: "pageup",
281
+ page_up: "pageup",
282
+ next: "pagedown",
283
+ page_down: "pagedown",
284
+ caps_lock: "capslock",
285
+ num_lock: "num_lock",
286
+ super: "cmd",
287
+ super_l: "cmd",
288
+ super_r: "cmd",
289
+ meta_l: "cmd",
290
+ meta_r: "cmd",
291
+ control_l: "ctrl",
292
+ control_r: "ctrl",
293
+ ctrl_l: "ctrl",
294
+ ctrl_r: "ctrl",
295
+ alt_l: "alt",
296
+ alt_r: "alt",
297
+ shift_l: "shift",
298
+ shift_r: "shift",
299
+ kp_add: "num_plus",
300
+ kp_subtract: "num_minus",
301
+ kp_multiply: "num_asterisk",
302
+ kp_divide: "num_slash",
303
+ kp_decimal: "num_decimal",
304
+ kp_equal: "num_equal",
305
+ minus: "-",
306
+ equal: "=",
307
+ bracketleft: "[",
308
+ bracketright: "]",
309
+ backslash: "\\",
310
+ semicolon: ";",
311
+ apostrophe: "'",
312
+ grave: "`",
313
+ comma: ",",
314
+ period: ".",
315
+ slash: "/",
316
+ };
317
+ /** @internal "ctrl+Return" -> ["ctrl", "enter"]. */
318
+ export function daytonaKeys(chord) {
319
+ return chord
320
+ .split("+")
321
+ .map((k) => k.trim())
322
+ .filter(Boolean)
323
+ .map((k) => {
324
+ const l = k.toLowerCase();
325
+ if (KEY_ALIASES[l])
326
+ return KEY_ALIASES[l];
327
+ const kp = /^kp_(\d)$/.exec(l);
328
+ return kp ? `num${kp[1]}` : l;
329
+ });
330
+ }
331
+ const MOUSE_BUTTON = {
332
+ left_click: { button: "left", clicks: 1 },
333
+ right_click: { button: "right", clicks: 1 },
334
+ middle_click: { button: "middle", clicks: 1 },
335
+ double_click: { button: "left", clicks: 2 },
336
+ triple_click: { button: "left", clicks: 3 },
337
+ };
338
+ /** A Daytona desktop driven through `sandbox.computerUse`. Coordinates are real screen pixels. */
339
+ class DaytonaComputer {
340
+ sbx;
341
+ nativeId;
342
+ frame = 0;
343
+ constructor(sbx) {
344
+ this.sbx = sbx;
345
+ this.nativeId = sbx.id;
346
+ }
347
+ async info() {
348
+ await this.sbx.refreshData();
349
+ const state = String(this.sbx.state ?? "unknown");
350
+ const status = state === "started" ? "ready" : ["stopped", "archived", "paused"].includes(state) ? "sleeping" : state;
351
+ const info = { id: this.nativeId, os: "linux", status, externalUserId: this.sbx.labels?.externalUserId ?? null };
352
+ if (state === "started") {
353
+ const d = (await this.sbx.computerUse.display.getInfo().catch(() => undefined))?.displays?.[0];
354
+ if (d?.width && d.height)
355
+ info.screen = { width: d.width, height: d.height };
356
+ }
357
+ return info;
358
+ }
359
+ async act(a) {
360
+ try {
361
+ const extra = await this.perform(a);
362
+ if (a.action === "zoom" || a.noScreenshot)
363
+ return { action: a.action, ok: true, ...extra };
364
+ return { action: a.action, ok: true, ...extra, ...(await this.frameResult()) };
365
+ }
366
+ catch (err) {
367
+ return { action: a.action, ok: false, error: err?.message ?? String(err) };
368
+ }
369
+ }
370
+ /** Runs in order and stops at the first error; only the last action returns a screenshot. */
371
+ async actBatch(actions) {
372
+ const out = [];
373
+ for (const [i, a] of actions.entries()) {
374
+ const r = await this.act(i === actions.length - 1 ? a : { ...a, noScreenshot: a.action === "zoom" ? a.noScreenshot : true });
375
+ out.push(r);
376
+ if (!r.ok)
377
+ break;
378
+ }
379
+ return out;
380
+ }
381
+ async frameResult() {
382
+ const shot = await this.sbx.computerUse.screenshot.takeFullScreen();
383
+ if (!shot.screenshot)
384
+ throw new Error("screenshot came back empty");
385
+ return { frameId: ++this.frame, imageB64: shot.screenshot, mime: "image/png", ...pngSize(shot.screenshot) };
386
+ }
387
+ async here(c) {
388
+ if (c)
389
+ return c;
390
+ const p = await this.sbx.computerUse.mouse.getPosition();
391
+ return [p.x ?? 0, p.y ?? 0];
392
+ }
393
+ /** One canonical action to SDK calls. Returns extra result fields. */
394
+ async perform(a) {
395
+ const { mouse, keyboard, screenshot } = this.sbx.computerUse;
396
+ const mods = a.modifier ? daytonaKeys(a.modifier) : undefined;
397
+ const click = MOUSE_BUTTON[a.action];
398
+ if (click) {
399
+ const [x, y] = await this.here(a.coordinate);
400
+ await mouse.click(x, y, click.button, false, click.clicks, mods);
401
+ return {};
402
+ }
403
+ switch (a.action) {
404
+ case "screenshot":
405
+ return {};
406
+ case "mouse_move": {
407
+ if (!a.coordinate)
408
+ throw new Error("mouse_move needs coordinate");
409
+ await mouse.move(...a.coordinate);
410
+ return {};
411
+ }
412
+ case "left_click_drag": {
413
+ if (!a.coordinate)
414
+ throw new Error("left_click_drag needs coordinate");
415
+ const [sx, sy] = await this.here(a.startCoordinate);
416
+ await mouse.drag(sx, sy, a.coordinate[0], a.coordinate[1], "left", mods);
417
+ return {};
418
+ }
419
+ case "left_mouse_down":
420
+ await mouse.down(a.coordinate?.[0], a.coordinate?.[1], "left");
421
+ return {};
422
+ case "left_mouse_up":
423
+ await mouse.up(a.coordinate?.[0], a.coordinate?.[1], "left");
424
+ return {};
425
+ case "scroll": {
426
+ if (!a.scrollDirection)
427
+ throw new Error("scroll needs scrollDirection");
428
+ const [x, y] = await this.here(a.coordinate);
429
+ await mouse.scroll(x, y, a.scrollDirection, a.scrollAmount ?? 3, mods);
430
+ return {};
431
+ }
432
+ case "type":
433
+ if (!a.text)
434
+ throw new Error("type needs text");
435
+ await keyboard.type(a.text);
436
+ return {};
437
+ case "key": {
438
+ if (!a.text)
439
+ throw new Error("key needs text");
440
+ // xdotool accepts several space-separated chords: "ctrl+a Delete".
441
+ const chords = a.text.trim().split(/\s+/).map(daytonaKeys);
442
+ for (let n = 0; n < Math.max(1, a.repeat ?? 1); n++) {
443
+ for (const keys of chords)
444
+ await keyboard.press(keys[keys.length - 1], keys.slice(0, -1));
445
+ }
446
+ return {};
447
+ }
448
+ case "hold_key": {
449
+ if (!a.text)
450
+ throw new Error("hold_key needs text");
451
+ const keys = daytonaKeys(a.text);
452
+ const held = [];
453
+ try {
454
+ for (const k of keys) {
455
+ await keyboard.down(k);
456
+ held.push(k);
457
+ }
458
+ await sleep(seconds(a.duration ?? 1) * 1000);
459
+ }
460
+ finally {
461
+ for (const k of held.reverse())
462
+ await keyboard.up(k).catch(() => { });
463
+ }
464
+ return {};
465
+ }
466
+ case "wait":
467
+ await sleep(seconds(a.duration ?? 1) * 1000);
468
+ return {};
469
+ case "zoom": {
470
+ if (!a.region)
471
+ throw new Error("zoom needs region [x0, y0, x1, y1]");
472
+ const [x0, y0, x1, y1] = a.region;
473
+ if (x1 <= x0 || y1 <= y0)
474
+ throw new Error("zoom region must have x1 > x0 and y1 > y0");
475
+ const shot = await screenshot.takeRegion({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
476
+ if (!shot.screenshot)
477
+ throw new Error("screenshot came back empty");
478
+ // Region crop at native resolution; the image is the result.
479
+ return { frameId: ++this.frame, imageB64: shot.screenshot, mime: "image/png", ...pngSize(shot.screenshot) };
480
+ }
481
+ case "cursor_position": {
482
+ const p = await mouse.getPosition();
483
+ return { coordinate: [p.x ?? 0, p.y ?? 0] };
484
+ }
485
+ default:
486
+ throw new Error(`daytona does not support action "${a.action}"`);
487
+ }
488
+ }
489
+ async screenshot(opts = {}) {
490
+ const s = this.sbx.computerUse.screenshot;
491
+ const shot = opts.format === "jpeg" ? await s.takeCompressed({ format: "jpeg", quality: opts.quality }) : await s.takeFullScreen();
492
+ if (!shot.screenshot)
493
+ throw new ProviderError("daytona", "screenshot came back empty");
494
+ return new Uint8Array(Buffer.from(shot.screenshot, "base64"));
495
+ }
496
+ /** Daytona merges stderr into stdout here. */
497
+ async exec(command, timeoutSeconds = 60) {
498
+ try {
499
+ const r = await this.sbx.process.executeCommand(command, undefined, undefined, timeoutSeconds);
500
+ return { exitCode: r.exitCode, stdout: r.result, stderr: "" };
501
+ }
502
+ catch (err) {
503
+ throw new ProviderError("daytona", err?.message ?? String(err));
504
+ }
505
+ }
506
+ async readFile(path) {
507
+ return new Uint8Array(await this.sbx.fs.downloadFile(path));
508
+ }
509
+ async writeFile(path, content) {
510
+ await this.sbx.fs.uploadFile(Buffer.from(toBytes(content)), path);
511
+ }
512
+ /**
513
+ * noVNC through a signed preview URL (token in the URL, no headers needed). `watch` sets noVNC's
514
+ * view_only flag, which is enforced by the viewer page only: anyone holding the link can drop it.
515
+ */
516
+ async viewer(mode = "watch", opts = {}) {
517
+ const ttl = Math.max(60, Math.round(opts.ttlSeconds ?? 3600));
518
+ const signed = await this.sbx.getSignedPreviewUrl(NOVNC_PORT, ttl);
519
+ const url = new URL(signed.url);
520
+ url.pathname = "/vnc.html";
521
+ url.searchParams.set("autoconnect", "true");
522
+ url.searchParams.set("resize", "scale");
523
+ url.searchParams.set("view_only", mode === "watch" ? "true" : "false");
524
+ return { url: url.toString(), expiresAt: new Date(Date.now() + ttl * 1000).toISOString() };
525
+ }
526
+ /** Stop: billing ends, the disk is kept. connectComputer starts it and its desktop again. */
527
+ async sleep() {
528
+ await this.sbx.setAutoDeleteInterval(-1);
529
+ await this.sbx.stop();
530
+ }
531
+ async delete() {
532
+ await this.sbx.delete();
533
+ }
534
+ }
535
+ function seconds(n) {
536
+ if (!(n >= 0 && n <= 100))
537
+ throw new Error("duration must be 0 to 100 seconds");
538
+ return n;
539
+ }
119
540
  function sleep(ms) {
120
541
  return new Promise((r) => setTimeout(r, ms).unref());
121
542
  }
@@ -0,0 +1,12 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ export declare class DedalusProvider implements Provider {
3
+ private apiKey;
4
+ private baseUrl;
5
+ readonly id: "dedalus";
6
+ constructor(apiKey?: string | undefined, baseUrl?: string);
7
+ configured(): boolean;
8
+ /** @internal */
9
+ api<T>(method: string, path: string, json?: unknown): Promise<T>;
10
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
11
+ connect(nativeId: string): Promise<ProviderSandbox>;
12
+ }