withruntime 0.8.4 → 0.9.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 (91) hide show
  1. package/COMPATIBILITY.md +145 -0
  2. package/E2B.md +23 -13
  3. package/README.md +8 -5
  4. package/README.src.md +8 -5
  5. package/compatibility-lock.json +281 -0
  6. package/dist/ai-harness/index.js +2 -2
  7. package/dist/api-defaults.d.ts +17 -0
  8. package/dist/api-defaults.js +22 -0
  9. package/dist/blaxel/preview.d.ts +1 -1
  10. package/dist/blaxel/preview.js +1 -1
  11. package/dist/blaxel/process.d.ts +1 -1
  12. package/dist/blaxel/process.js +11 -2
  13. package/dist/cli-words.d.ts +27 -0
  14. package/dist/cli-words.js +135 -0
  15. package/dist/cli.js +162 -31
  16. package/dist/client.d.ts +4 -1
  17. package/dist/client.js +8 -2
  18. package/dist/cloudflare/index.d.ts +294 -0
  19. package/dist/cloudflare/index.js +554 -0
  20. package/dist/cloudflare/worker.d.ts +415 -0
  21. package/dist/cloudflare/worker.js +297 -0
  22. package/dist/codesandbox/guest-service.d.ts +31 -0
  23. package/dist/codesandbox/guest-service.js +453 -0
  24. package/dist/codesandbox/index.d.ts +386 -0
  25. package/dist/codesandbox/index.js +1313 -0
  26. package/dist/compat/core.d.ts +39 -0
  27. package/dist/compat/core.js +199 -0
  28. package/dist/compat/files.d.ts +26 -0
  29. package/dist/compat/files.js +115 -0
  30. package/dist/compat/shell.d.ts +4 -0
  31. package/dist/compat/shell.js +165 -0
  32. package/dist/daytona/context.js +2 -1
  33. package/dist/daytona/process.d.ts +1 -1
  34. package/dist/daytona/process.js +27 -4
  35. package/dist/e2b/code-interpreter.d.ts +4 -5
  36. package/dist/e2b/code-interpreter.js +54 -56
  37. package/dist/e2b/commands.d.ts +5 -4
  38. package/dist/e2b/commands.js +37 -29
  39. package/dist/e2b/errors.js +1 -1
  40. package/dist/e2b/filesystem.d.ts +8 -6
  41. package/dist/e2b/filesystem.js +127 -28
  42. package/dist/e2b/sandbox.d.ts +4 -3
  43. package/dist/e2b/sandbox.js +10 -10
  44. package/dist/errors.d.ts +8 -0
  45. package/dist/errors.js +15 -2
  46. package/dist/freestyle/index.d.ts +301 -0
  47. package/dist/freestyle/index.js +310 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/mcp.js +2 -2
  50. package/dist/modal/index.d.ts +205 -0
  51. package/dist/modal/index.js +485 -0
  52. package/dist/network-cli.js +1 -1
  53. package/dist/openai-agents/index.js +9 -26
  54. package/dist/products/audit.d.ts +3 -0
  55. package/dist/products/index.d.ts +2 -0
  56. package/dist/products/index.js +2 -0
  57. package/dist/products/interpreter.d.ts +11 -4
  58. package/dist/products/interpreter.js +33 -9
  59. package/dist/products/jobs.js +3 -2
  60. package/dist/products/limits.d.ts +10 -0
  61. package/dist/products/previews.d.ts +10 -2
  62. package/dist/products/previews.js +3 -2
  63. package/dist/products/tailscale.d.ts +57 -0
  64. package/dist/products/tailscale.js +17 -0
  65. package/dist/products/watch.d.ts +25 -4
  66. package/dist/products/watch.js +75 -14
  67. package/dist/runloop/index.d.ts +340 -0
  68. package/dist/runloop/index.js +438 -0
  69. package/dist/runloop/pagination.d.ts +46 -0
  70. package/dist/runloop/pagination.js +82 -0
  71. package/dist/runloop/snapshots.d.ts +52 -0
  72. package/dist/runloop/snapshots.js +244 -0
  73. package/dist/sandbox.d.ts +40 -9
  74. package/dist/sandbox.js +273 -76
  75. package/dist/snapshots.d.ts +11 -1
  76. package/dist/snapshots.js +11 -1
  77. package/dist/sprites/index.d.ts +251 -0
  78. package/dist/sprites/index.js +552 -0
  79. package/dist/tar.d.ts +8 -1
  80. package/dist/tar.js +185 -35
  81. package/dist/tools/index.js +2 -1
  82. package/dist/transport.d.ts +20 -2
  83. package/dist/transport.js +48 -11
  84. package/dist/types.d.ts +52 -3
  85. package/dist/vercel/command.js +12 -1
  86. package/dist/vercel/errors.d.ts +2 -2
  87. package/dist/vercel/errors.js +12 -1
  88. package/dist/vercel/filesystem.d.ts +21 -18
  89. package/dist/vercel/filesystem.js +95 -48
  90. package/dist/vercel/sandbox.js +5 -5
  91. package/package.json +45 -3
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { Sandbox as SandboxHandle } from "./sandbox.js";
10
10
  import { NotFoundError, RuntimeError } from "./errors.js";
11
11
  import { NETWORK_PRODUCTS, networkProductCommand } from "./network-cli.js";
12
12
  import { billingCommand } from "./billing-cli.js";
13
+ import { cliRefusal, describeRecord, newestCpu, topicHelp } from "./cli-words.js";
13
14
  import { envFetch } from "./proxy.js";
14
15
  import { VERSION } from "./transport.js";
15
16
  const HELP = `Runtime Cloud CLI ${VERSION}
@@ -40,7 +41,9 @@ Account
40
41
  [--allow '[GET,HEAD] /path/*']... Store a secret sandboxes use without seeing it; the
41
42
  value is read from standard input, never an argument.
42
43
  --allow (paid): only these methods and paths get it
43
- secrets ls | get <NAME> Names, hosts and placeholders, never values
44
+ secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
45
+ (job create --secret NAME); with --host, both
46
+ secrets ls | get <NAME> Names, hosts, placeholders and jobs versions, never values
44
47
  secrets rotate <NAME> A new version of the jobs copy, from standard input
45
48
  secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
46
49
  never readable, by anyone
@@ -86,6 +89,7 @@ Products
86
89
  snapshot Saved sandboxes, disk and memory, to start or fork from
87
90
  image Custom images from a Dockerfile, any public or private image, or a package list
88
91
  volume Persistent disks to attach to sandboxes
92
+ job A command in a fresh sandbox, once or on a cron schedule, with its logs
89
93
  domain Your own hostname for a sandbox's port, with HTTPS
90
94
  port A public TCP port to a sandbox: databases, game servers
91
95
  address A dedicated outbound address, for allow-lists
@@ -103,7 +107,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
103
107
  run [--keep] [create options] -- <command> A fresh sandbox runs one command, then stops
104
108
  ssh <id|name> [-- <command...>] | ssh config [--install] Log in over SSH, also from VS Code and JetBrains
105
109
  port-forward <id|name> <port|local:remote>... Forward local ports to ports in it
106
- create [--name n] [--label k=v]... [--vcpu 2] [--memory 4096] [--disk 4096]
110
+ create [--name n] [--label k=v]... [--env K=V]... [--vcpu 2] [--memory 4096] [--disk 4096]
107
111
  [--cpu shared|reserved] [--cpu-floor <thousandths>] [--max-cost <usd>] [--max-total-cost <usd>]
108
112
  [--timeout <seconds>] [--on-timeout pause|stop] [--trial|--paid] [--no-wait]
109
113
  [--no-internet] [--allow <host>]... [--deny <host>]... [--connect <host:port>]...
@@ -122,10 +126,15 @@ const SANDBOX_HELP = `runtime sandbox <command>
122
126
  cp <src> <dst> Copy files or directories; name a sandbox
123
127
  path as <id>:/path, e.g. cp ./app sbx:/workspace/app
124
128
  cat <id> <path> Print a file
125
- files <id> [path] [--depth 2] [--glob '**/*.py']
129
+ files <id> [path] [--depth 1] [--glob '**/*.py']
126
130
  stop <id> | pause <id> | wake <id> | restart <id>
131
+ rm <id> Delete it for good: stops it, deletes its disk and
132
+ paused memory, revokes its previews; snapshots stay
133
+ switch-image <id> <image> --keep-workspace Move it to another image, keeping /workspace (your home),
134
+ volumes, env and previews; processes restart, the rest of
135
+ its disk is lost (snapshot it first to keep everything)
127
136
  extend <id> <seconds> More time before the lease ends
128
- update <id> [--name n] [--label k=v]... [--idle-pause <seconds>] [--auto-wake on|off]
137
+ update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
129
138
  [--persistent on|off] [--max-total-cost <usd>|none]
130
139
  Change its settings; persistent keeps it running
131
140
  snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
@@ -133,6 +142,9 @@ const SANDBOX_HELP = `runtime sandbox <command>
133
142
  [--endpoint https://...] [--account-id id] [--read-only]
134
143
  Your bucket as a directory; the proxy signs, the sandbox never sees the key
135
144
  mounts <id> | unmount <id> <path> Its bucket mounts; unmount one
145
+ tailscale up <id> --auth-key-secret NAME [--hostname h] [--tag tag:x]...
146
+ Join your tailnet (paid); the key is a secret set with --jobs
147
+ tailscale status <id> | tailscale down <id> Its tailnet address; take it off the tailnet
136
148
  fork <id> [--count 3] [--name n] [--keep-snapshot] [--trial|--paid]
137
149
  Copies of it as it is now, running; prints their ids
138
150
  run-code <id> <file|-> [--lang python|javascript|typescript|r|java|bash|go] [--context c] [--out-dir .]
@@ -385,6 +397,9 @@ const SANDBOX_VERBS = [
385
397
  "ls",
386
398
  "get",
387
399
  "stop",
400
+ "rm",
401
+ "delete",
402
+ "switch-image",
388
403
  "pause",
389
404
  "wake",
390
405
  "restart",
@@ -392,6 +407,7 @@ const SANDBOX_VERBS = [
392
407
  "mount",
393
408
  "mounts",
394
409
  "unmount",
410
+ "tailscale",
395
411
  "fork",
396
412
  "extend",
397
413
  "update",
@@ -484,6 +500,12 @@ function describe(info) {
484
500
  ["funding", info.funding],
485
501
  ["region", info.region],
486
502
  ["expires", info.expiresAt ?? "-"],
503
+ ["at lease end", info.onLeaseEnd ?? "-"],
504
+ // Why a sandbox is no longer running (troubleshooting.md, "A sandbox
505
+ // stopped on its own"); `get` left both out until 30 September 2026.
506
+ ...(info.pausedAt ? [["paused at", info.pausedAt]] : []),
507
+ ...(info.endedAt ? [["ended", info.endedAt]] : []),
508
+ ...(info.stopReason ? [["stop reason", info.stopReason]] : []),
487
509
  ["auto wake", info.autoWake === false ? "off" : "on"],
488
510
  [
489
511
  "idle pause",
@@ -523,6 +545,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
523
545
  // `runtime sandbox create --help` is how an agent asks what a command takes:
524
546
  // answer with the product's help, never by running the command.
525
547
  const cut = remaining.indexOf("--");
548
+ const asked = (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h") ||
549
+ (remaining.length === 1 && remaining[0] === "help");
550
+ // A command without a help of its own answers `help` with its lines of the
551
+ // top-level help, and never takes the word as its input.
552
+ if (first && first !== "help" && asked && !nested && !OWN_HELP.includes(first)) {
553
+ const lines = topicHelp(named(HELP), first);
554
+ if (lines) {
555
+ out.write(out.json ? JSON.stringify({ usage: lines }) : lines);
556
+ return 0;
557
+ }
558
+ }
526
559
  if (first &&
527
560
  (cut < 0 ? remaining : remaining.slice(0, cut)).some((arg) => arg === "--help" || arg === "-h"))
528
561
  return run(nested && command === "ssh"
@@ -671,7 +704,8 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
671
704
  }
672
705
  if (command === "limits") {
673
706
  const l = await (await client(env)).limits.get();
674
- const usd = (micros) => `$${(Number(BigInt(micros) / 10000n) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
707
+ // As `usage` shows money: $0.0014 of spend read $0.00 until 30 September 2026.
708
+ const usd = (micros) => dollars(micros);
675
709
  const access = l.access === "read"
676
710
  ? "read only: reads every product, cannot start, change or spend"
677
711
  : l.access === "full"
@@ -687,6 +721,7 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
687
721
  ],
688
722
  ["used, last 24 hours", usd(l.daily.usedMicros)],
689
723
  ...(l.daily.remainingMicros === null ? [] : [["left", usd(l.daily.remainingMicros)]]),
724
+ ...(l.trial ? [["free trial", trialLeft(l.trial)]] : []),
690
725
  ]), l);
691
726
  return 0;
692
727
  }
@@ -975,11 +1010,15 @@ export function usageCsv(u) {
975
1010
  .join(","));
976
1011
  return [head.join(","), ...rows].join("\n");
977
1012
  }
1013
+ /** The free trial's time left, as `runtime usage` and `runtime limits` say it. */
1014
+ function trialLeft(trial) {
1015
+ const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1016
+ return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left${trial.reservedMs > 0 ? `, ${hours(trial.reservedMs)} held by running sandboxes` : ""}`;
1017
+ }
978
1018
  function usageSummary(u) {
979
1019
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
980
1020
  ? BigInt(value)
981
1021
  : 0n;
982
- const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
983
1022
  const takenBack = micros(u.takenBack);
984
1023
  const rows = [
985
1024
  ["available", dollars(micros(u.available))],
@@ -996,10 +1035,7 @@ function usageSummary(u) {
996
1035
  if (micros(u.expired) > 0n)
997
1036
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
998
1037
  if (u.trial)
999
- rows.push([
1000
- "free trial",
1001
- `${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
1002
- ]);
1038
+ rows.push(["free trial", trialLeft(u.trial)]);
1003
1039
  if (u.outbound) {
1004
1040
  const gib = (bytes) => `${(bytes / 1_073_741_824).toLocaleString("en-US", { maximumFractionDigits: 1 })} GiB`;
1005
1041
  const left = Math.max(0, u.outbound.allowanceBytes - u.outbound.freeBytes);
@@ -1357,8 +1393,12 @@ async function sandbox(argv, env, out) {
1357
1393
  ...(args.flags.has("state")
1358
1394
  ? { state: args.flags.get("state") }
1359
1395
  : {}),
1396
+ limit: 100,
1360
1397
  });
1361
- const all = await page.toArray(1000);
1398
+ // Every page, oldest first, so the newest are last on the screen. Until
1399
+ // 30 September 2026 this stopped at 1,000 and silently left out the
1400
+ // newest sandboxes of a busy account.
1401
+ const all = await page.toArray(Infinity);
1362
1402
  // Stopped sandboxes are hidden by default; say so, or an agent that
1363
1403
  // stopped everything reads "No sandboxes" as "never had any".
1364
1404
  const hidden = out.json || args.flags.size
@@ -1392,6 +1432,23 @@ async function sandbox(argv, env, out) {
1392
1432
  print(describe(sbx.info), sbx.info);
1393
1433
  return 0;
1394
1434
  }
1435
+ case "switch-image": {
1436
+ const args = parse(rest, ["keep-workspace"], []);
1437
+ const [id, image] = args.positional;
1438
+ const sbx = await use(need(id, `a sandbox, e.g. ${me} sandbox switch-image <id> app:v2 --keep-workspace`));
1439
+ if (!args.flags.has("keep-workspace"))
1440
+ throw usage(`A switch keeps only /workspace (your home), volumes, env and previews; processes restart and everything else on its disk is lost. Add --keep-workspace to say so, or snapshot it first (${me} sandbox snapshot ${sbx.id}) to keep everything.`);
1441
+ await sbx.switchImage(need(image, "an image, e.g. app:v2"), { keep: "workspace" });
1442
+ print(describe(sbx.info), sbx.info);
1443
+ return 0;
1444
+ }
1445
+ case "rm":
1446
+ case "delete": {
1447
+ const sbx = await use(need(rest[0], `a sandbox, e.g. ${me} sandbox rm <id>`));
1448
+ const gone = await sbx.delete();
1449
+ print(`${gone.id} deleted`, gone);
1450
+ return 0;
1451
+ }
1395
1452
  case "stop":
1396
1453
  case "pause":
1397
1454
  case "wake":
@@ -1445,6 +1502,35 @@ async function sandbox(argv, env, out) {
1445
1502
  print(`${gone.path} unmounted`, gone);
1446
1503
  return 0;
1447
1504
  }
1505
+ case "tailscale": {
1506
+ const action = rest[0];
1507
+ if (action === "up") {
1508
+ const args = parse(rest.slice(1), [], ["auth-key-secret", "hostname", "tag"]);
1509
+ const sbx = await use(args.positional[0]);
1510
+ const joined = await sbx.tailscale.up({
1511
+ authKeySecret: need(flag(args, "auth-key-secret"), "--auth-key-secret, the name of a secret set with --jobs that holds your Tailscale auth key"),
1512
+ ...(flag(args, "hostname") ? { hostname: flag(args, "hostname") } : {}),
1513
+ ...(args.flags.has("tag") ? { tags: args.flags.get("tag") } : {}),
1514
+ });
1515
+ print(`${joined.hostname} ${joined.addresses.join(" ")}${joined.proxy ? ` (reach the tailnet through ${joined.proxy})` : ""}`, joined);
1516
+ return 0;
1517
+ }
1518
+ if (action === "status") {
1519
+ const sbx = await use(rest[1]);
1520
+ const now = await sbx.tailscale.status();
1521
+ print(now.tailnet
1522
+ ? `${now.tailnet.hostname} ${now.running ? now.addresses.join(" ") : (now.state ?? "not running")}`
1523
+ : "Not on a tailnet.", now);
1524
+ return 0;
1525
+ }
1526
+ if (action === "down") {
1527
+ const sbx = await use(rest[1]);
1528
+ const gone = await sbx.tailscale.down();
1529
+ print(gone.left ? "Left the tailnet." : "Not on a tailnet.", gone);
1530
+ return 0;
1531
+ }
1532
+ throw usage("runtime sandbox tailscale up|status|down <id>");
1533
+ }
1448
1534
  case "snapshot": {
1449
1535
  const args = parse(rest, [], ["name", "retention"]);
1450
1536
  const sbx = await get(args.positional[0]);
@@ -1472,7 +1558,16 @@ async function sandbox(argv, env, out) {
1472
1558
  return 0;
1473
1559
  }
1474
1560
  case "update": {
1475
- const args = parse(rest, [], ["name", "label", "idle-pause", "auto-wake", "persistent", "max-total-cost"]);
1561
+ const args = parse(rest, [], [
1562
+ "name",
1563
+ "label",
1564
+ "env",
1565
+ "unset-env",
1566
+ "idle-pause",
1567
+ "auto-wake",
1568
+ "persistent",
1569
+ "max-total-cost",
1570
+ ]);
1476
1571
  const sbx = await get(args.positional[0]);
1477
1572
  const onOff = (name) => {
1478
1573
  const value = flag(args, name);
@@ -1485,9 +1580,14 @@ async function sandbox(argv, env, out) {
1485
1580
  const cost = flag(args, "max-total-cost");
1486
1581
  if (cost !== undefined && cost !== "none" && !/^\d+(\.\d{1,6})?$/.test(cost))
1487
1582
  throw usage("--max-total-cost takes dollars, such as 25 or 2.50, or none.");
1583
+ const env = {
1584
+ ...(args.flags.has("env") ? pairs(args, "env") : {}),
1585
+ ...Object.fromEntries((args.flags.get("unset-env") ?? []).map((name) => [name, null])),
1586
+ };
1488
1587
  const settings = {
1489
1588
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
1490
1589
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
1590
+ ...(Object.keys(env).length ? { env } : {}),
1491
1591
  ...(integer(args, "idle-pause") !== undefined
1492
1592
  ? { idlePauseSeconds: integer(args, "idle-pause") }
1493
1593
  : {}),
@@ -1500,7 +1600,7 @@ async function sandbox(argv, env, out) {
1500
1600
  }),
1501
1601
  };
1502
1602
  if (!Object.keys(settings).length)
1503
- throw usage("Say what to change: --name, --label, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1603
+ throw usage("Say what to change: --name, --label, --env, --unset-env, --idle-pause, --auto-wake, --persistent or --max-total-cost.");
1504
1604
  await sbx.update(settings);
1505
1605
  print(describe(sbx.info), sbx.info);
1506
1606
  return 0;
@@ -1576,8 +1676,21 @@ async function sandbox(argv, env, out) {
1576
1676
  const args = parse(rest, [], ["signal"]);
1577
1677
  const sbx = await use(args.positional[0]);
1578
1678
  const proc = await sbx.processes.get(need(args.positional[1], "a process id"));
1679
+ // A process that has ended has nothing to signal; "Signalled" said it
1680
+ // had been until 30 September 2026.
1681
+ if (proc.info.state === "exited" ||
1682
+ proc.info.state === "killed" ||
1683
+ proc.info.state === "timed_out") {
1684
+ print(`${proc.id} had already ${proc.info.state === "exited" ? "exited" : "ended"}${proc.info.exitCode == null ? "" : ` (exit code ${proc.info.exitCode})`}; nothing to signal.`, {
1685
+ processId: proc.id,
1686
+ state: proc.info.state,
1687
+ exitCode: proc.info.exitCode ?? null,
1688
+ signalled: false,
1689
+ });
1690
+ return 0;
1691
+ }
1579
1692
  await proc.kill((flag(args, "signal") ?? "SIGTERM"));
1580
- print(`Signalled ${proc.id}.`, { processId: proc.id });
1693
+ print(`Signalled ${proc.id}.`, { processId: proc.id, signalled: true });
1581
1694
  return 0;
1582
1695
  }
1583
1696
  case "shell": {
@@ -2256,7 +2369,7 @@ async function snapshot(argv, env, out) {
2256
2369
  ? { sandboxId: await sandboxIdOf(await client(env), flag(args, "sandbox")) }
2257
2370
  : {}),
2258
2371
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
2259
- })).toArray(1000);
2372
+ })).toArray(Infinity);
2260
2373
  print(all.length
2261
2374
  ? table([
2262
2375
  ["ID", "NAME", "STATUS", "FROM", "EXPIRES"],
@@ -2426,7 +2539,7 @@ read = readSecret) {
2426
2539
  case "list":
2427
2540
  case "versions": {
2428
2541
  const name = verb === "versions" ? need(rest[0], "a name") : undefined;
2429
- const all = await (await api.list(name ? { name } : {})).toArray(1000);
2542
+ const all = await (await api.list(name ? { name } : {})).toArray(Infinity);
2430
2543
  print(all.length
2431
2544
  ? table([
2432
2545
  ["ID", "NAME", "VERSION", "TAGS", "STATE", "MIB", "CREATED"],
@@ -2447,7 +2560,7 @@ read = readSecret) {
2447
2560
  }
2448
2561
  case "get": {
2449
2562
  const found = await api.resolve(need(rest[0], "an image"));
2450
- print(JSON.stringify(found, null, 2), found);
2563
+ print(describeRecord(found, table), found);
2451
2564
  return 0;
2452
2565
  }
2453
2566
  case "logs": {
@@ -2672,7 +2785,7 @@ async function jobCommand(argv, env, out) {
2672
2785
  }
2673
2786
  case "ls":
2674
2787
  case "list": {
2675
- const all = await (await api.list({ limit: 100 })).toArray(1000);
2788
+ const all = await (await api.list({ limit: 100 })).toArray(Infinity);
2676
2789
  print(all.length
2677
2790
  ? table([
2678
2791
  ["ID", "NAME", "STATE", "SCHEDULE", "NEXT RUN", "BLOCKED"],
@@ -2681,7 +2794,8 @@ async function jobCommand(argv, env, out) {
2681
2794
  j.name,
2682
2795
  j.state,
2683
2796
  scheduleText(j),
2684
- when(j.nextRunAt),
2797
+ // A canceled or finished job keeps its last nextRunAt, but runs no more.
2798
+ j.state === "active" || j.state === "paused" ? when(j.nextRunAt) : "-",
2685
2799
  j.blockedReason ?? "-",
2686
2800
  ]),
2687
2801
  ])
@@ -2695,7 +2809,7 @@ async function jobCommand(argv, env, out) {
2695
2809
  }
2696
2810
  case "runs": {
2697
2811
  const jobId = need(rest[0], "a job id");
2698
- const all = await (await api.runs(jobId, { limit: 100 })).toArray(1000);
2812
+ const all = await (await api.runs(jobId, { limit: 100 })).toArray(Infinity);
2699
2813
  print(all.length
2700
2814
  ? table([
2701
2815
  ["RUN", "ATTEMPT", "STATE", "EXIT", "SCHEDULED", "STARTED", "ENDED", "REASON"],
@@ -2724,7 +2838,7 @@ async function jobCommand(argv, env, out) {
2724
2838
  if (!(error instanceof NotFoundError))
2725
2839
  throw error;
2726
2840
  // A job's id: its latest run.
2727
- const runs = await (await api.runs(id, { limit: 100 })).toArray(1000);
2841
+ const runs = await (await api.runs(id, { limit: 100 })).toArray(Infinity);
2728
2842
  const latest = runs.at(-1);
2729
2843
  if (!latest) {
2730
2844
  const job = await api.get(id);
@@ -2823,7 +2937,7 @@ async function volume(argv, env, out) {
2823
2937
  }
2824
2938
  case "ls":
2825
2939
  case "list": {
2826
- const all = await (await api.list()).toArray(1000);
2940
+ const all = await (await api.list()).toArray(Infinity);
2827
2941
  print(all.length
2828
2942
  ? table([
2829
2943
  ["ID", "NAME", "STATE", "MIB", "BACKED UP", "ATTACHED"],
@@ -2841,7 +2955,7 @@ async function volume(argv, env, out) {
2841
2955
  }
2842
2956
  case "get": {
2843
2957
  const found = await api.get(need(rest[0], "a volume id"));
2844
- print(JSON.stringify(found, null, 2), found);
2958
+ print(describeRecord(found, table), found);
2845
2959
  return 0;
2846
2960
  }
2847
2961
  case "rm":
@@ -2862,7 +2976,7 @@ async function volume(argv, env, out) {
2862
2976
  }
2863
2977
  case "backups": {
2864
2978
  const volumeId = rest[0];
2865
- const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(1000);
2979
+ const all = await (await api.backups(volumeId ? { volumeId } : {})).toArray(Infinity);
2866
2980
  print(all.length
2867
2981
  ? table([
2868
2982
  ["ID", "VOLUME", "STATE", "TRIGGER", "MIB", "STORED", "TAKEN", "EXPIRES"],
@@ -2966,6 +3080,7 @@ const CREATE_VALUES = [
2966
3080
  "image",
2967
3081
  "snapshot",
2968
3082
  "volume",
3083
+ "env",
2969
3084
  ];
2970
3085
  /** Dollars from the command line as integer microdollars: 25, 2.50 or 0.000001. */
2971
3086
  function usdMicros(args, name) {
@@ -2993,6 +3108,7 @@ function createInput(args) {
2993
3108
  : {}),
2994
3109
  ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
2995
3110
  ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
3111
+ ...(args.flags.has("env") ? { env: pairs(args, "env") } : {}),
2996
3112
  ...(integer(args, "vcpu") ? { vcpu: integer(args, "vcpu") } : {}),
2997
3113
  ...(integer(args, "memory") ? { memoryMiB: integer(args, "memory") } : {}),
2998
3114
  ...(integer(args, "disk") ? { diskMiB: integer(args, "disk") } : {}),
@@ -3071,7 +3187,11 @@ ownInterrupt = true) {
3071
3187
  const lost = extras.lostOutput(result, sbx.id);
3072
3188
  if (lost && !out.json)
3073
3189
  out.error(lost);
3074
- const code = result.timedOut ? 124 : Math.min(255, Math.max(0, result.exitCode ?? 1));
3190
+ /* A command a signal ended comes back as minus the signal (-9 after the
3191
+ out-of-memory killer's SIGKILL); a shell says 128 + the signal. Clamped
3192
+ to 0 until 30 September 2026, a killed command exited 0. */
3193
+ const exit = result.exitCode ?? 1;
3194
+ const code = result.timedOut ? 124 : Math.min(255, exit < 0 ? 128 - exit : exit);
3075
3195
  return lost && code === 0 ? 1 : code;
3076
3196
  }
3077
3197
  async function shell(sbx, command) {
@@ -3119,7 +3239,9 @@ export function describeError(error, json) {
3119
3239
  details: error.details,
3120
3240
  },
3121
3241
  });
3122
- return `Error [${error.code}]: ${error.message}${error.hint ? `\nHint: ${error.hint}` : ""}${error.requestId
3242
+ // In the command line's words: the option, not the request's field.
3243
+ const { message, hint } = cliRefusal(error);
3244
+ return `Error [${error.code}]: ${message}${hint ? `\nHint: ${hint}` : ""}${error.requestId
3123
3245
  ? // A refusal of what was sent is the caller's to fix; only a failure
3124
3246
  // of ours is worth a report.
3125
3247
  error.status >= 400 && error.status < 500
@@ -3160,17 +3282,26 @@ function metricsText(m) {
3160
3282
  const cpu = m.points.map((p) => p.cpuPercent);
3161
3283
  const memory = m.points.map((p) => p.memoryBytes);
3162
3284
  const peak = Math.max(0, ...m.points.map((p) => p.cpuPeakPercent ?? p.cpuPercent ?? 0));
3163
- const latest = m.latest;
3285
+ // `latest` is the newest reading of the last 15 minutes whatever the state,
3286
+ // so a sandbox paused a minute ago still has one from before it slept. Only a
3287
+ // running sandbox has a now.
3288
+ const latest = m.state === "running" ? m.latest : null;
3289
+ const still = m.state === "running" ? null : `none: it is ${m.state}`;
3290
+ const cpuNow = newestCpu(m);
3164
3291
  return [
3165
3292
  table([
3166
3293
  ["state", m.state],
3167
3294
  [
3168
3295
  "cpu now",
3169
- latest?.cpuPercent == null
3170
- ? "no reading in the last 15 minutes"
3171
- : `${latest.cpuPercent}% of ${m.vcpu} vCPU (${latest.cpuCores} cores)`,
3296
+ still ??
3297
+ (cpuNow
3298
+ ? `${cpuNow.percent}% of ${m.vcpu} vCPU${cpuNow.cores == null ? "" : ` (${cpuNow.cores} cores)`}${cpuNow.ago ? `, ${cpuNow.ago}` : ""}`
3299
+ : "no reading in this range"),
3300
+ ],
3301
+ [
3302
+ "memory now",
3303
+ still ?? (latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"),
3172
3304
  ],
3173
- ["memory now", latest ? `${mib(latest.memoryBytes)} of ${mib(m.memoryLimitBytes)}` : "-"],
3174
3305
  [`cpu, ${m.range}`, m.points.length ? `${spark(cpu)} peak ${peak}%` : "no readings"],
3175
3306
  [
3176
3307
  `memory, ${m.range}`,
package/dist/client.d.ts CHANGED
@@ -4,7 +4,7 @@ import { type ClientExtensions } from "./products/index.js";
4
4
  import { Sandbox } from "./sandbox.js";
5
5
  import { Snapshots } from "./snapshots.js";
6
6
  import { Transport, type RequestOptions } from "./transport.js";
7
- import type { CreateSandbox, FeedbackKind, SandboxInfo, Usage } from "./types.js";
7
+ import type { CreateSandbox, DeletedSandbox, FeedbackKind, SandboxInfo, Usage } from "./types.js";
8
8
  export type RuntimeOptions = {
9
9
  /** Default: the RUNTIME_API_KEY environment variable. */
10
10
  apiKey?: string;
@@ -80,6 +80,9 @@ export declare class Sandboxes {
80
80
  wait?: boolean;
81
81
  }, options?: CreateOptions): Promise<Sandbox>;
82
82
  get(id: string, options?: RequestOptions): Promise<Sandbox>;
83
+ /** Deletes a sandbox for good, by id, without reading it first: see
84
+ * `sandbox.delete()`. Deleting it again answers the same. */
85
+ delete(id: string, options?: RequestOptions): Promise<DeletedSandbox>;
83
86
  /** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
84
87
  list(filter?: {
85
88
  state?: SandboxInfo["state"][];
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { WAIT_FOR_TIMEOUT_SECONDS } from "./api-defaults.js";
1
2
  import { RuntimeError } from "./errors.js";
2
3
  import { clientFactories } from "./products/index.js";
3
- import { Sandbox, sandboxPage } from "./sandbox.js";
4
+ import { Sandbox, deleteSandbox, sandboxPage } from "./sandbox.js";
4
5
  import { Snapshots } from "./snapshots.js";
5
6
  import { Transport, missingKey } from "./transport.js";
6
7
  /** The key `runtime login` saved for this machine, found on first use. */
@@ -83,7 +84,7 @@ export class Sandboxes {
83
84
  });
84
85
  const sandbox = new Sandbox(this.t, info);
85
86
  if (wait !== false && info.state !== "running") {
86
- await sandbox.waitFor("running", { timeoutSeconds: 60 });
87
+ await sandbox.waitFor("running", { timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS });
87
88
  if (sandbox.state !== "running")
88
89
  throw new RuntimeError({
89
90
  message: `Sandbox ${info.id} is ${sandbox.state}, not running.`,
@@ -108,6 +109,11 @@ export class Sandboxes {
108
109
  ...options,
109
110
  }));
110
111
  }
112
+ /** Deletes a sandbox for good, by id, without reading it first: see
113
+ * `sandbox.delete()`. Deleting it again answers the same. */
114
+ delete(id, options = {}) {
115
+ return deleteSandbox(this.t, id, options);
116
+ }
111
117
  /** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
112
118
  async list(filter = {}) {
113
119
  const query = {